@prosdevlab/experience-sdk-plugins 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +120 -0
- package/README.md +141 -79
- package/dist/index.d.ts +206 -35
- package/dist/index.js +1229 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.ts +2 -3
- package/src/index.ts +2 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +356 -297
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -0
- package/src/scroll-depth/scroll-depth.test.ts +35 -0
- package/src/scroll-depth/scroll-depth.ts +2 -4
- package/src/time-delay/time-delay.test.ts +2 -2
- package/src/time-delay/time-delay.ts +2 -3
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +4 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { storagePlugin } from '@lytics/sdk-kit-plugins';
|
|
2
2
|
|
|
3
3
|
// src/utils/sanitize.ts
|
|
4
|
-
var ALLOWED_TAGS = ["strong", "em", "a", "br", "span", "b", "i", "p"];
|
|
4
|
+
var ALLOWED_TAGS = ["strong", "em", "a", "br", "span", "b", "i", "p", "div", "ul", "li"];
|
|
5
5
|
var ALLOWED_ATTRIBUTES = {
|
|
6
6
|
a: ["href", "class", "style", "title"],
|
|
7
7
|
span: ["class", "style"],
|
|
8
|
-
p: ["class", "style"]
|
|
8
|
+
p: ["class", "style"],
|
|
9
|
+
div: ["class", "style"],
|
|
10
|
+
ul: ["class", "style"],
|
|
11
|
+
li: ["class", "style"]
|
|
9
12
|
// Other tags have no attributes allowed
|
|
10
13
|
};
|
|
11
14
|
function sanitizeHTML(html) {
|
|
@@ -115,15 +118,15 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
115
118
|
left: 0;
|
|
116
119
|
right: 0;
|
|
117
120
|
width: 100%;
|
|
118
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
119
|
-
font-size: 14px;
|
|
120
|
-
line-height: 1.5;
|
|
121
|
+
font-family: var(--xp-banner-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
122
|
+
font-size: var(--xp-banner-font-size, 14px);
|
|
123
|
+
line-height: var(--xp-banner-line-height, 1.5);
|
|
121
124
|
box-sizing: border-box;
|
|
122
|
-
z-index: 10000;
|
|
123
|
-
background: #ffffff;
|
|
124
|
-
color: #111827;
|
|
125
|
-
border-bottom: 1px solid #e5e7eb;
|
|
126
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
125
|
+
z-index: var(--xp-banner-z-index, 10000);
|
|
126
|
+
background: var(--xp-banner-bg, #ffffff);
|
|
127
|
+
color: var(--xp-banner-color, #111827);
|
|
128
|
+
border-bottom: var(--xp-banner-border-width, 1px) solid var(--xp-banner-border-color, #e5e7eb);
|
|
129
|
+
box-shadow: var(--xp-banner-shadow, 0 1px 3px 0 rgba(0, 0, 0, 0.05));
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
.xp-banner--top {
|
|
@@ -133,17 +136,17 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
133
136
|
.xp-banner--bottom {
|
|
134
137
|
bottom: 0;
|
|
135
138
|
border-bottom: none;
|
|
136
|
-
border-top: 1px solid #e5e7eb;
|
|
137
|
-
box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
139
|
+
border-top: var(--xp-banner-border-width, 1px) solid var(--xp-banner-border-color, #e5e7eb);
|
|
140
|
+
box-shadow: var(--xp-banner-shadow-bottom, 0 -1px 3px 0 rgba(0, 0, 0, 0.05));
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
.xp-banner__container {
|
|
141
144
|
display: flex;
|
|
142
145
|
align-items: center;
|
|
143
|
-
gap: 16px;
|
|
144
|
-
max-width: 1280px;
|
|
146
|
+
gap: var(--xp-banner-gap, 16px);
|
|
147
|
+
max-width: var(--xp-banner-max-width, 1280px);
|
|
145
148
|
margin: 0 auto;
|
|
146
|
-
padding: 14px 24px;
|
|
149
|
+
padding: var(--xp-banner-padding, 14px 24px);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
.xp-banner__content {
|
|
@@ -151,36 +154,37 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
151
154
|
min-width: 0;
|
|
152
155
|
display: flex;
|
|
153
156
|
flex-direction: column;
|
|
154
|
-
gap: 4px;
|
|
157
|
+
gap: var(--xp-banner-content-gap, 4px);
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
.xp-banner__title {
|
|
158
|
-
font-weight: 600;
|
|
161
|
+
font-weight: var(--xp-banner-title-weight, 600);
|
|
159
162
|
margin: 0;
|
|
160
|
-
font-size: 15px;
|
|
161
|
-
line-height: 1.4;
|
|
163
|
+
font-size: var(--xp-banner-title-size, 15px);
|
|
164
|
+
line-height: var(--xp-banner-title-line-height, 1.4);
|
|
165
|
+
color: var(--xp-banner-title-color, inherit);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
.xp-banner__message {
|
|
165
169
|
margin: 0;
|
|
166
|
-
font-size: 14px;
|
|
167
|
-
line-height: 1.5;
|
|
168
|
-
color: #6b7280;
|
|
170
|
+
font-size: var(--xp-banner-message-size, 14px);
|
|
171
|
+
line-height: var(--xp-banner-message-line-height, 1.5);
|
|
172
|
+
color: var(--xp-banner-message-color, #6b7280);
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
.xp-banner__buttons {
|
|
172
176
|
display: flex;
|
|
173
177
|
align-items: center;
|
|
174
|
-
gap: 8px;
|
|
178
|
+
gap: var(--xp-banner-buttons-gap, 8px);
|
|
175
179
|
flex-shrink: 0;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
.xp-banner__button {
|
|
179
|
-
padding: 8px 16px;
|
|
183
|
+
padding: var(--xp-banner-button-padding, 8px 16px);
|
|
180
184
|
border: none;
|
|
181
|
-
border-radius: 6px;
|
|
182
|
-
font-size: 14px;
|
|
183
|
-
font-weight: 500;
|
|
185
|
+
border-radius: var(--xp-banner-button-radius, 6px);
|
|
186
|
+
font-size: var(--xp-banner-button-font-size, 14px);
|
|
187
|
+
font-weight: var(--xp-banner-button-font-weight, 500);
|
|
184
188
|
cursor: pointer;
|
|
185
189
|
transition: all 0.2s;
|
|
186
190
|
text-decoration: none;
|
|
@@ -191,64 +195,64 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
.xp-banner__button--primary {
|
|
194
|
-
background: #2563eb;
|
|
195
|
-
color: #ffffff;
|
|
198
|
+
background: var(--xp-banner-button-primary-bg, #2563eb);
|
|
199
|
+
color: var(--xp-banner-button-primary-color, #ffffff);
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
.xp-banner__button--primary:hover {
|
|
199
|
-
background: #1d4ed8;
|
|
203
|
+
background: var(--xp-banner-button-primary-bg-hover, #1d4ed8);
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
.xp-banner__button--secondary {
|
|
203
|
-
background: #f3f4f6;
|
|
204
|
-
color: #374151;
|
|
205
|
-
border: 1px solid #e5e7eb;
|
|
207
|
+
background: var(--xp-banner-button-secondary-bg, #f3f4f6);
|
|
208
|
+
color: var(--xp-banner-button-secondary-color, #374151);
|
|
209
|
+
border: var(--xp-banner-border-width, 1px) solid var(--xp-banner-button-secondary-border, #e5e7eb);
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
.xp-banner__button--secondary:hover {
|
|
209
|
-
background: #e5e7eb;
|
|
213
|
+
background: var(--xp-banner-button-secondary-bg-hover, #e5e7eb);
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
.xp-banner__button--link {
|
|
213
217
|
background: transparent;
|
|
214
|
-
color: #2563eb;
|
|
215
|
-
padding: 6px 12px;
|
|
216
|
-
font-weight: 400;
|
|
218
|
+
color: var(--xp-banner-button-link-color, #2563eb);
|
|
219
|
+
padding: var(--xp-banner-button-link-padding, 6px 12px);
|
|
220
|
+
font-weight: var(--xp-banner-button-link-font-weight, 400);
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
.xp-banner__button--link:hover {
|
|
220
|
-
background: #f3f4f6;
|
|
224
|
+
background: var(--xp-banner-button-link-bg-hover, #f3f4f6);
|
|
221
225
|
text-decoration: underline;
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
.xp-banner__close {
|
|
225
229
|
background: transparent;
|
|
226
230
|
border: none;
|
|
227
|
-
color: #9ca3af;
|
|
228
|
-
font-size: 20px;
|
|
231
|
+
color: var(--xp-banner-close-color, #9ca3af);
|
|
232
|
+
font-size: var(--xp-banner-close-size, 20px);
|
|
229
233
|
line-height: 1;
|
|
230
234
|
cursor: pointer;
|
|
231
|
-
padding: 4px;
|
|
235
|
+
padding: var(--xp-banner-close-padding, 4px);
|
|
232
236
|
margin: 0;
|
|
233
237
|
transition: color 0.2s;
|
|
234
238
|
flex-shrink: 0;
|
|
235
|
-
width: 28px;
|
|
236
|
-
height: 28px;
|
|
239
|
+
width: var(--xp-banner-close-width, 28px);
|
|
240
|
+
height: var(--xp-banner-close-height, 28px);
|
|
237
241
|
display: flex;
|
|
238
242
|
align-items: center;
|
|
239
243
|
justify-content: center;
|
|
240
|
-
border-radius: 4px;
|
|
244
|
+
border-radius: var(--xp-banner-close-radius, 4px);
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
.xp-banner__close:hover {
|
|
244
|
-
color: #111827;
|
|
245
|
-
background: #f3f4f6;
|
|
248
|
+
color: var(--xp-banner-close-color-hover, #111827);
|
|
249
|
+
background: var(--xp-banner-close-bg-hover, #f3f4f6);
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
@media (max-width: 640px) {
|
|
249
253
|
.xp-banner__container {
|
|
250
254
|
flex-wrap: wrap;
|
|
251
|
-
padding: 14px 16px;
|
|
255
|
+
padding: var(--xp-banner-padding-mobile, 14px 16px);
|
|
252
256
|
position: relative;
|
|
253
257
|
}
|
|
254
258
|
|
|
@@ -273,55 +277,55 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
/* Dark mode support */
|
|
280
|
+
/* Dark mode support - override CSS variables */
|
|
277
281
|
@media (prefers-color-scheme: dark) {
|
|
278
282
|
.xp-banner {
|
|
279
|
-
background: #111827;
|
|
280
|
-
color: #f9fafb;
|
|
281
|
-
border-bottom-color: #1f2937;
|
|
283
|
+
background: var(--xp-banner-bg-dark, #111827);
|
|
284
|
+
color: var(--xp-banner-color-dark, #f9fafb);
|
|
285
|
+
border-bottom-color: var(--xp-banner-border-color-dark, #1f2937);
|
|
282
286
|
}
|
|
283
287
|
|
|
284
288
|
.xp-banner--bottom {
|
|
285
|
-
border-top-color: #1f2937;
|
|
289
|
+
border-top-color: var(--xp-banner-border-color-dark, #1f2937);
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
.xp-banner__message {
|
|
289
|
-
color: #9ca3af;
|
|
293
|
+
color: var(--xp-banner-message-color-dark, #9ca3af);
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
.xp-banner__button--primary {
|
|
293
|
-
background: #3b82f6;
|
|
297
|
+
background: var(--xp-banner-button-primary-bg-dark, #3b82f6);
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
.xp-banner__button--primary:hover {
|
|
297
|
-
background: #2563eb;
|
|
301
|
+
background: var(--xp-banner-button-primary-bg-hover-dark, #2563eb);
|
|
298
302
|
}
|
|
299
303
|
|
|
300
304
|
.xp-banner__button--secondary {
|
|
301
|
-
background: #1f2937;
|
|
302
|
-
color: #f9fafb;
|
|
303
|
-
border-color: #374151;
|
|
305
|
+
background: var(--xp-banner-button-secondary-bg-dark, #1f2937);
|
|
306
|
+
color: var(--xp-banner-button-secondary-color-dark, #f9fafb);
|
|
307
|
+
border-color: var(--xp-banner-button-secondary-border-dark, #374151);
|
|
304
308
|
}
|
|
305
309
|
|
|
306
310
|
.xp-banner__button--secondary:hover {
|
|
307
|
-
background: #374151;
|
|
311
|
+
background: var(--xp-banner-button-secondary-bg-hover-dark, #374151);
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
.xp-banner__button--link {
|
|
311
|
-
color: #60a5fa;
|
|
315
|
+
color: var(--xp-banner-button-link-color-dark, #60a5fa);
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
.xp-banner__button--link:hover {
|
|
315
|
-
background: #1f2937;
|
|
319
|
+
background: var(--xp-banner-button-link-bg-hover-dark, #1f2937);
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
.xp-banner__close {
|
|
319
|
-
color: #6b7280;
|
|
323
|
+
color: var(--xp-banner-close-color-dark, #6b7280);
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
.xp-banner__close:hover {
|
|
323
|
-
color: #f9fafb;
|
|
324
|
-
background: #1f2937;
|
|
327
|
+
color: var(--xp-banner-close-color-hover-dark, #f9fafb);
|
|
328
|
+
background: var(--xp-banner-close-bg-hover-dark, #1f2937);
|
|
325
329
|
}
|
|
326
330
|
}
|
|
327
331
|
`;
|
|
@@ -726,10 +730,9 @@ var exitIntentPlugin = (plugin, instance, config) => {
|
|
|
726
730
|
}
|
|
727
731
|
});
|
|
728
732
|
initialize();
|
|
729
|
-
|
|
733
|
+
instance.on("sdk:destroy", () => {
|
|
730
734
|
cleanup();
|
|
731
|
-
};
|
|
732
|
-
instance.on("destroy", destroyHandler);
|
|
735
|
+
});
|
|
733
736
|
};
|
|
734
737
|
var frequencyPlugin = (plugin, instance, config) => {
|
|
735
738
|
plugin.ns("frequency");
|
|
@@ -858,6 +861,1160 @@ var frequencyPlugin = (plugin, instance, config) => {
|
|
|
858
861
|
});
|
|
859
862
|
}
|
|
860
863
|
};
|
|
864
|
+
|
|
865
|
+
// src/inline/insertion.ts
|
|
866
|
+
function insertContent(selector, content, position, experienceId) {
|
|
867
|
+
const target = document.querySelector(selector);
|
|
868
|
+
if (!target) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const wrapper = document.createElement("div");
|
|
872
|
+
wrapper.className = "xp-inline";
|
|
873
|
+
wrapper.setAttribute("data-xp-id", experienceId);
|
|
874
|
+
wrapper.innerHTML = content;
|
|
875
|
+
switch (position) {
|
|
876
|
+
case "replace":
|
|
877
|
+
target.innerHTML = "";
|
|
878
|
+
target.appendChild(wrapper);
|
|
879
|
+
break;
|
|
880
|
+
case "append":
|
|
881
|
+
target.appendChild(wrapper);
|
|
882
|
+
break;
|
|
883
|
+
case "prepend":
|
|
884
|
+
target.insertBefore(wrapper, target.firstChild);
|
|
885
|
+
break;
|
|
886
|
+
case "before":
|
|
887
|
+
target.parentElement?.insertBefore(wrapper, target);
|
|
888
|
+
break;
|
|
889
|
+
case "after":
|
|
890
|
+
target.parentElement?.insertBefore(wrapper, target.nextSibling);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
return wrapper;
|
|
894
|
+
}
|
|
895
|
+
function removeContent(experienceId) {
|
|
896
|
+
const element = document.querySelector(`[data-xp-id="${experienceId}"]`);
|
|
897
|
+
if (!element) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
element.remove();
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/inline/inline.ts
|
|
905
|
+
var inlinePlugin = (plugin, instance, config) => {
|
|
906
|
+
plugin.ns("experiences.inline");
|
|
907
|
+
plugin.defaults({
|
|
908
|
+
inline: {
|
|
909
|
+
retry: false,
|
|
910
|
+
retryTimeout: 5e3
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
if (!instance.storage) {
|
|
914
|
+
instance.use(storagePlugin);
|
|
915
|
+
}
|
|
916
|
+
const sdkInstance = instance;
|
|
917
|
+
if (typeof document !== "undefined") {
|
|
918
|
+
const styleId = "xp-inline-styles";
|
|
919
|
+
if (!document.getElementById(styleId)) {
|
|
920
|
+
const style = document.createElement("style");
|
|
921
|
+
style.id = styleId;
|
|
922
|
+
style.textContent = getInlineStyles();
|
|
923
|
+
document.head.appendChild(style);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const activeInlines = /* @__PURE__ */ new Map();
|
|
927
|
+
const show = (experience) => {
|
|
928
|
+
const { id, content } = experience;
|
|
929
|
+
if (activeInlines.has(id)) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (content.persist && content.dismissable && sdkInstance.storage) {
|
|
933
|
+
const dismissed = sdkInstance.storage.get(`xp-inline-dismissed-${id}`);
|
|
934
|
+
if (dismissed) {
|
|
935
|
+
instance.emit("experiences:inline:dismissed", {
|
|
936
|
+
experienceId: id,
|
|
937
|
+
reason: "previously-dismissed",
|
|
938
|
+
timestamp: Date.now()
|
|
939
|
+
});
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const element = insertContent(
|
|
944
|
+
content.selector,
|
|
945
|
+
sanitizeHTML(content.message),
|
|
946
|
+
content.position || "replace",
|
|
947
|
+
id
|
|
948
|
+
);
|
|
949
|
+
if (!element) {
|
|
950
|
+
instance.emit("experiences:inline:error", {
|
|
951
|
+
experienceId: id,
|
|
952
|
+
error: "selector-not-found",
|
|
953
|
+
selector: content.selector,
|
|
954
|
+
timestamp: Date.now()
|
|
955
|
+
});
|
|
956
|
+
const retryEnabled = config.get("inline.retry") ?? false;
|
|
957
|
+
const retryTimeout = config.get("inline.retryTimeout") ?? 5e3;
|
|
958
|
+
if (retryEnabled) {
|
|
959
|
+
setTimeout(() => {
|
|
960
|
+
show(experience);
|
|
961
|
+
}, retryTimeout);
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
activeInlines.set(id, element);
|
|
966
|
+
if (content.className) {
|
|
967
|
+
element.classList.add(content.className);
|
|
968
|
+
}
|
|
969
|
+
if (content.style) {
|
|
970
|
+
Object.assign(element.style, content.style);
|
|
971
|
+
}
|
|
972
|
+
if (content.dismissable) {
|
|
973
|
+
const closeBtn = document.createElement("button");
|
|
974
|
+
closeBtn.className = "xp-inline__close";
|
|
975
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
976
|
+
closeBtn.textContent = "\xD7";
|
|
977
|
+
closeBtn.onclick = () => {
|
|
978
|
+
remove(id);
|
|
979
|
+
if (content.persist && sdkInstance.storage) {
|
|
980
|
+
sdkInstance.storage.set(`xp-inline-dismissed-${id}`, true);
|
|
981
|
+
}
|
|
982
|
+
instance.emit("experiences:dismissed", {
|
|
983
|
+
experienceId: id,
|
|
984
|
+
timestamp: Date.now()
|
|
985
|
+
});
|
|
986
|
+
};
|
|
987
|
+
element.prepend(closeBtn);
|
|
988
|
+
}
|
|
989
|
+
instance.emit("experiences:shown", {
|
|
990
|
+
experienceId: id,
|
|
991
|
+
type: "inline",
|
|
992
|
+
selector: content.selector,
|
|
993
|
+
position: content.position || "replace",
|
|
994
|
+
timestamp: Date.now()
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
const remove = (experienceId) => {
|
|
998
|
+
const element = activeInlines.get(experienceId);
|
|
999
|
+
if (!element) return;
|
|
1000
|
+
removeContent(experienceId);
|
|
1001
|
+
activeInlines.delete(experienceId);
|
|
1002
|
+
};
|
|
1003
|
+
const isShowing = (experienceId) => {
|
|
1004
|
+
if (experienceId) {
|
|
1005
|
+
return activeInlines.has(experienceId);
|
|
1006
|
+
}
|
|
1007
|
+
return activeInlines.size > 0;
|
|
1008
|
+
};
|
|
1009
|
+
plugin.expose({
|
|
1010
|
+
inline: {
|
|
1011
|
+
show,
|
|
1012
|
+
remove,
|
|
1013
|
+
isShowing
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
instance.on("experiences:evaluated", (data) => {
|
|
1017
|
+
if (data.decision?.show && data.experience?.type === "inline") {
|
|
1018
|
+
show(data.experience);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
instance.on("sdk:destroy", () => {
|
|
1022
|
+
for (const id of Array.from(activeInlines.keys())) {
|
|
1023
|
+
remove(id);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
function getInlineStyles() {
|
|
1028
|
+
return `
|
|
1029
|
+
:root {
|
|
1030
|
+
--xp-inline-close-size: 24px;
|
|
1031
|
+
--xp-inline-close-color: #666;
|
|
1032
|
+
--xp-inline-close-hover-color: #111;
|
|
1033
|
+
--xp-inline-close-bg: transparent;
|
|
1034
|
+
--xp-inline-close-hover-bg: rgba(0, 0, 0, 0.05);
|
|
1035
|
+
--xp-inline-close-border-radius: 4px;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
@media (prefers-color-scheme: dark) {
|
|
1039
|
+
:root {
|
|
1040
|
+
--xp-inline-close-color: #9ca3af;
|
|
1041
|
+
--xp-inline-close-hover-color: #f9fafb;
|
|
1042
|
+
--xp-inline-close-hover-bg: rgba(255, 255, 255, 0.1);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.xp-inline {
|
|
1047
|
+
position: relative;
|
|
1048
|
+
animation: xp-inline-enter 0.4s ease-out forwards;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
@keyframes xp-inline-enter {
|
|
1052
|
+
from {
|
|
1053
|
+
opacity: 0;
|
|
1054
|
+
transform: translateY(-8px);
|
|
1055
|
+
}
|
|
1056
|
+
to {
|
|
1057
|
+
opacity: 1;
|
|
1058
|
+
transform: translateY(0);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Respect user's motion preferences */
|
|
1063
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1064
|
+
.xp-inline {
|
|
1065
|
+
animation: xp-inline-enter-reduced 0.2s ease-out forwards;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
@keyframes xp-inline-enter-reduced {
|
|
1069
|
+
from {
|
|
1070
|
+
opacity: 0;
|
|
1071
|
+
}
|
|
1072
|
+
to {
|
|
1073
|
+
opacity: 1;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.xp-inline__close {
|
|
1079
|
+
position: absolute;
|
|
1080
|
+
top: 8px;
|
|
1081
|
+
right: 8px;
|
|
1082
|
+
width: var(--xp-inline-close-size);
|
|
1083
|
+
height: var(--xp-inline-close-size);
|
|
1084
|
+
padding: 0;
|
|
1085
|
+
border: none;
|
|
1086
|
+
background: var(--xp-inline-close-bg);
|
|
1087
|
+
color: var(--xp-inline-close-color);
|
|
1088
|
+
font-size: 20px;
|
|
1089
|
+
line-height: 1;
|
|
1090
|
+
cursor: pointer;
|
|
1091
|
+
border-radius: var(--xp-inline-close-border-radius);
|
|
1092
|
+
transition: all 0.2s ease;
|
|
1093
|
+
display: flex;
|
|
1094
|
+
align-items: center;
|
|
1095
|
+
justify-content: center;
|
|
1096
|
+
z-index: 1;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.xp-inline__close:hover {
|
|
1100
|
+
background: var(--xp-inline-close-hover-bg);
|
|
1101
|
+
color: var(--xp-inline-close-hover-color);
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/modal/form-styles.ts
|
|
1107
|
+
function getFormStyles() {
|
|
1108
|
+
return `
|
|
1109
|
+
margin-top: var(--xp-form-spacing, 16px);
|
|
1110
|
+
display: flex;
|
|
1111
|
+
flex-direction: column;
|
|
1112
|
+
gap: var(--xp-form-gap, 16px);
|
|
1113
|
+
`.trim();
|
|
1114
|
+
}
|
|
1115
|
+
function getFieldStyles() {
|
|
1116
|
+
return `
|
|
1117
|
+
display: flex;
|
|
1118
|
+
flex-direction: column;
|
|
1119
|
+
gap: var(--xp-field-gap, 6px);
|
|
1120
|
+
`.trim();
|
|
1121
|
+
}
|
|
1122
|
+
function getLabelStyles() {
|
|
1123
|
+
return `
|
|
1124
|
+
font-size: var(--xp-label-font-size, 14px);
|
|
1125
|
+
font-weight: var(--xp-label-font-weight, 500);
|
|
1126
|
+
color: var(--xp-label-color, #374151);
|
|
1127
|
+
line-height: 1.5;
|
|
1128
|
+
`.trim();
|
|
1129
|
+
}
|
|
1130
|
+
function getRequiredStyles() {
|
|
1131
|
+
return `
|
|
1132
|
+
color: var(--xp-required-color, #ef4444);
|
|
1133
|
+
`.trim();
|
|
1134
|
+
}
|
|
1135
|
+
function getInputStyles() {
|
|
1136
|
+
return `
|
|
1137
|
+
padding: var(--xp-input-padding, 8px 12px);
|
|
1138
|
+
font-size: var(--xp-input-font-size, 14px);
|
|
1139
|
+
line-height: 1.5;
|
|
1140
|
+
color: var(--xp-input-color, #111827);
|
|
1141
|
+
background-color: var(--xp-input-bg, white);
|
|
1142
|
+
border: var(--xp-input-border-width, 1px) solid var(--xp-input-border-color, #d1d5db);
|
|
1143
|
+
border-radius: var(--xp-input-radius, 6px);
|
|
1144
|
+
transition: all 0.15s ease-in-out;
|
|
1145
|
+
outline: none;
|
|
1146
|
+
width: 100%;
|
|
1147
|
+
box-sizing: border-box;
|
|
1148
|
+
`.trim();
|
|
1149
|
+
}
|
|
1150
|
+
function getInputErrorStyles() {
|
|
1151
|
+
return `
|
|
1152
|
+
border-color: var(--xp-input-error-border, #ef4444);
|
|
1153
|
+
`.trim();
|
|
1154
|
+
}
|
|
1155
|
+
function getErrorMessageStyles() {
|
|
1156
|
+
return `
|
|
1157
|
+
font-size: var(--xp-error-font-size, 13px);
|
|
1158
|
+
color: var(--xp-error-color, #ef4444);
|
|
1159
|
+
line-height: 1.4;
|
|
1160
|
+
min-height: 18px;
|
|
1161
|
+
`.trim();
|
|
1162
|
+
}
|
|
1163
|
+
function getSubmitButtonStyles() {
|
|
1164
|
+
return `
|
|
1165
|
+
margin-top: var(--xp-submit-margin-top, 8px);
|
|
1166
|
+
padding: var(--xp-submit-padding, 10px 20px);
|
|
1167
|
+
font-size: var(--xp-submit-font-size, 14px);
|
|
1168
|
+
font-weight: var(--xp-submit-font-weight, 500);
|
|
1169
|
+
color: var(--xp-submit-color, white);
|
|
1170
|
+
background-color: var(--xp-submit-bg, #2563eb);
|
|
1171
|
+
border: none;
|
|
1172
|
+
border-radius: var(--xp-submit-radius, 6px);
|
|
1173
|
+
cursor: pointer;
|
|
1174
|
+
transition: all 0.2s;
|
|
1175
|
+
width: 100%;
|
|
1176
|
+
`.trim();
|
|
1177
|
+
}
|
|
1178
|
+
function getSubmitButtonHoverBg() {
|
|
1179
|
+
return "var(--xp-submit-bg-hover, #1d4ed8)";
|
|
1180
|
+
}
|
|
1181
|
+
function getFormStateStyles() {
|
|
1182
|
+
return `
|
|
1183
|
+
padding: var(--xp-state-padding, 16px);
|
|
1184
|
+
border-radius: var(--xp-state-radius, 8px);
|
|
1185
|
+
text-align: center;
|
|
1186
|
+
`.trim();
|
|
1187
|
+
}
|
|
1188
|
+
function getSuccessStateStyles() {
|
|
1189
|
+
return `
|
|
1190
|
+
background-color: var(--xp-success-bg, #f0fdf4);
|
|
1191
|
+
border: var(--xp-state-border-width, 1px) solid var(--xp-success-border, #86efac);
|
|
1192
|
+
`.trim();
|
|
1193
|
+
}
|
|
1194
|
+
function getErrorStateStyles() {
|
|
1195
|
+
return `
|
|
1196
|
+
background-color: var(--xp-error-bg, #fef2f2);
|
|
1197
|
+
border: var(--xp-state-border-width, 1px) solid var(--xp-error-border, #fca5a5);
|
|
1198
|
+
`.trim();
|
|
1199
|
+
}
|
|
1200
|
+
function getStateTitleStyles() {
|
|
1201
|
+
return `
|
|
1202
|
+
font-size: var(--xp-state-title-font-size, 16px);
|
|
1203
|
+
font-weight: var(--xp-state-title-font-weight, 600);
|
|
1204
|
+
margin: 0 0 var(--xp-state-title-margin-bottom, 8px) 0;
|
|
1205
|
+
color: var(--xp-state-title-color, #111827);
|
|
1206
|
+
`.trim();
|
|
1207
|
+
}
|
|
1208
|
+
function getStateMessageStyles() {
|
|
1209
|
+
return `
|
|
1210
|
+
font-size: var(--xp-state-message-font-size, 14px);
|
|
1211
|
+
line-height: 1.5;
|
|
1212
|
+
color: var(--xp-state-message-color, #374151);
|
|
1213
|
+
margin: 0;
|
|
1214
|
+
`.trim();
|
|
1215
|
+
}
|
|
1216
|
+
function getStateButtonsStyles() {
|
|
1217
|
+
return `
|
|
1218
|
+
margin-top: var(--xp-state-buttons-margin-top, 16px);
|
|
1219
|
+
display: flex;
|
|
1220
|
+
gap: var(--xp-state-buttons-gap, 8px);
|
|
1221
|
+
justify-content: center;
|
|
1222
|
+
flex-wrap: wrap;
|
|
1223
|
+
`.trim();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/modal/form-rendering.ts
|
|
1227
|
+
function renderForm(experienceId, config) {
|
|
1228
|
+
const form = document.createElement("form");
|
|
1229
|
+
form.className = "xp-modal__form";
|
|
1230
|
+
form.style.cssText = getFormStyles();
|
|
1231
|
+
form.dataset.xpExperienceId = experienceId;
|
|
1232
|
+
form.setAttribute("novalidate", "");
|
|
1233
|
+
config.fields.forEach((field) => {
|
|
1234
|
+
const fieldElement = renderFormField(experienceId, field);
|
|
1235
|
+
form.appendChild(fieldElement);
|
|
1236
|
+
});
|
|
1237
|
+
const submitButton = renderSubmitButton(config.submitButton);
|
|
1238
|
+
form.appendChild(submitButton);
|
|
1239
|
+
return form;
|
|
1240
|
+
}
|
|
1241
|
+
function renderFormField(experienceId, field) {
|
|
1242
|
+
const wrapper = document.createElement("div");
|
|
1243
|
+
wrapper.className = "xp-form__field";
|
|
1244
|
+
wrapper.style.cssText = getFieldStyles();
|
|
1245
|
+
if (field.label) {
|
|
1246
|
+
const label = document.createElement("label");
|
|
1247
|
+
label.className = "xp-form__label";
|
|
1248
|
+
label.style.cssText = getLabelStyles();
|
|
1249
|
+
label.htmlFor = `${experienceId}-${field.name}`;
|
|
1250
|
+
label.textContent = field.label;
|
|
1251
|
+
if (field.required) {
|
|
1252
|
+
const required = document.createElement("span");
|
|
1253
|
+
required.className = "xp-form__required";
|
|
1254
|
+
required.style.cssText = getRequiredStyles();
|
|
1255
|
+
required.textContent = " *";
|
|
1256
|
+
required.setAttribute("aria-label", "required");
|
|
1257
|
+
label.appendChild(required);
|
|
1258
|
+
}
|
|
1259
|
+
wrapper.appendChild(label);
|
|
1260
|
+
}
|
|
1261
|
+
const input = field.type === "textarea" ? document.createElement("textarea") : document.createElement("input");
|
|
1262
|
+
input.className = "xp-form__input";
|
|
1263
|
+
input.style.cssText = getInputStyles();
|
|
1264
|
+
input.id = `${experienceId}-${field.name}`;
|
|
1265
|
+
input.name = field.name;
|
|
1266
|
+
if (input instanceof HTMLInputElement) {
|
|
1267
|
+
input.type = field.type;
|
|
1268
|
+
}
|
|
1269
|
+
if (field.placeholder) {
|
|
1270
|
+
input.placeholder = field.placeholder;
|
|
1271
|
+
}
|
|
1272
|
+
if (field.required) {
|
|
1273
|
+
input.required = true;
|
|
1274
|
+
}
|
|
1275
|
+
if (field.pattern && input instanceof HTMLInputElement) {
|
|
1276
|
+
input.setAttribute("pattern", field.pattern);
|
|
1277
|
+
}
|
|
1278
|
+
input.setAttribute("aria-invalid", "false");
|
|
1279
|
+
input.setAttribute("aria-describedby", `${experienceId}-${field.name}-error`);
|
|
1280
|
+
if (field.className) {
|
|
1281
|
+
input.className += ` ${field.className}`;
|
|
1282
|
+
}
|
|
1283
|
+
if (field.style) {
|
|
1284
|
+
Object.assign(input.style, field.style);
|
|
1285
|
+
}
|
|
1286
|
+
wrapper.appendChild(input);
|
|
1287
|
+
const error = document.createElement("div");
|
|
1288
|
+
error.className = "xp-form__error";
|
|
1289
|
+
error.style.cssText = getErrorMessageStyles();
|
|
1290
|
+
error.id = `${experienceId}-${field.name}-error`;
|
|
1291
|
+
error.setAttribute("role", "alert");
|
|
1292
|
+
error.setAttribute("aria-live", "polite");
|
|
1293
|
+
wrapper.appendChild(error);
|
|
1294
|
+
return wrapper;
|
|
1295
|
+
}
|
|
1296
|
+
function renderSubmitButton(buttonConfig) {
|
|
1297
|
+
const button = document.createElement("button");
|
|
1298
|
+
button.type = "submit";
|
|
1299
|
+
button.className = "xp-form__submit xp-modal__button";
|
|
1300
|
+
button.style.cssText = getSubmitButtonStyles();
|
|
1301
|
+
if (buttonConfig.variant) {
|
|
1302
|
+
button.className += ` xp-modal__button--${buttonConfig.variant}`;
|
|
1303
|
+
}
|
|
1304
|
+
if (buttonConfig.className) {
|
|
1305
|
+
button.className += ` ${buttonConfig.className}`;
|
|
1306
|
+
}
|
|
1307
|
+
button.textContent = buttonConfig.text;
|
|
1308
|
+
const hoverBg = getSubmitButtonHoverBg();
|
|
1309
|
+
button.onmouseover = () => {
|
|
1310
|
+
button.style.backgroundColor = hoverBg;
|
|
1311
|
+
};
|
|
1312
|
+
button.onmouseout = () => {
|
|
1313
|
+
button.style.backgroundColor = "";
|
|
1314
|
+
};
|
|
1315
|
+
if (buttonConfig.style) {
|
|
1316
|
+
Object.assign(button.style, buttonConfig.style);
|
|
1317
|
+
}
|
|
1318
|
+
return button;
|
|
1319
|
+
}
|
|
1320
|
+
function renderFormState(state, stateConfig) {
|
|
1321
|
+
const stateEl = document.createElement("div");
|
|
1322
|
+
stateEl.className = `xp-form__state xp-form__state--${state}`;
|
|
1323
|
+
const baseStyles = getFormStateStyles();
|
|
1324
|
+
const stateStyles = state === "success" ? getSuccessStateStyles() : getErrorStateStyles();
|
|
1325
|
+
stateEl.style.cssText = `${baseStyles}; ${stateStyles}`;
|
|
1326
|
+
if (stateConfig.title) {
|
|
1327
|
+
const title = document.createElement("h3");
|
|
1328
|
+
title.className = "xp-form__state-title";
|
|
1329
|
+
title.style.cssText = getStateTitleStyles();
|
|
1330
|
+
title.textContent = stateConfig.title;
|
|
1331
|
+
stateEl.appendChild(title);
|
|
1332
|
+
}
|
|
1333
|
+
const message = document.createElement("div");
|
|
1334
|
+
message.className = "xp-form__state-message";
|
|
1335
|
+
message.style.cssText = getStateMessageStyles();
|
|
1336
|
+
message.textContent = stateConfig.message;
|
|
1337
|
+
stateEl.appendChild(message);
|
|
1338
|
+
if (stateConfig.buttons && stateConfig.buttons.length > 0) {
|
|
1339
|
+
const buttonContainer = document.createElement("div");
|
|
1340
|
+
buttonContainer.className = "xp-form__state-buttons";
|
|
1341
|
+
buttonContainer.style.cssText = getStateButtonsStyles();
|
|
1342
|
+
stateConfig.buttons.forEach((btnConfig) => {
|
|
1343
|
+
const btn = document.createElement("button");
|
|
1344
|
+
btn.type = "button";
|
|
1345
|
+
btn.className = "xp-modal__button";
|
|
1346
|
+
if (btnConfig.variant) {
|
|
1347
|
+
btn.className += ` xp-modal__button--${btnConfig.variant}`;
|
|
1348
|
+
}
|
|
1349
|
+
if (btnConfig.className) {
|
|
1350
|
+
btn.className += ` ${btnConfig.className}`;
|
|
1351
|
+
}
|
|
1352
|
+
btn.textContent = btnConfig.text;
|
|
1353
|
+
if (btnConfig.style) {
|
|
1354
|
+
Object.assign(btn.style, btnConfig.style);
|
|
1355
|
+
}
|
|
1356
|
+
if (btnConfig.action) {
|
|
1357
|
+
btn.dataset.action = btnConfig.action;
|
|
1358
|
+
}
|
|
1359
|
+
if (btnConfig.dismiss) {
|
|
1360
|
+
btn.dataset.dismiss = "true";
|
|
1361
|
+
}
|
|
1362
|
+
buttonContainer.appendChild(btn);
|
|
1363
|
+
});
|
|
1364
|
+
stateEl.appendChild(buttonContainer);
|
|
1365
|
+
}
|
|
1366
|
+
return stateEl;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/modal/form-validation.ts
|
|
1370
|
+
function validateField(field, value) {
|
|
1371
|
+
const errors = {};
|
|
1372
|
+
if (field.required && (!value || value.trim() === "")) {
|
|
1373
|
+
errors[field.name] = field.errorMessage || `${field.label || field.name} is required`;
|
|
1374
|
+
return { valid: false, errors };
|
|
1375
|
+
}
|
|
1376
|
+
if (!value || value.trim() === "") {
|
|
1377
|
+
return { valid: true };
|
|
1378
|
+
}
|
|
1379
|
+
switch (field.type) {
|
|
1380
|
+
case "email": {
|
|
1381
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1382
|
+
if (!emailRegex.test(value)) {
|
|
1383
|
+
errors[field.name] = field.errorMessage || "Please enter a valid email address";
|
|
1384
|
+
}
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
case "url": {
|
|
1388
|
+
try {
|
|
1389
|
+
new URL(value);
|
|
1390
|
+
} catch {
|
|
1391
|
+
errors[field.name] = field.errorMessage || "Please enter a valid URL";
|
|
1392
|
+
}
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
case "tel": {
|
|
1396
|
+
const phoneRegex = /^[\d\s\-()+]+$/;
|
|
1397
|
+
if (!phoneRegex.test(value)) {
|
|
1398
|
+
errors[field.name] = field.errorMessage || "Please enter a valid phone number";
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
case "number": {
|
|
1403
|
+
if (Number.isNaN(Number(value))) {
|
|
1404
|
+
errors[field.name] = field.errorMessage || "Please enter a valid number";
|
|
1405
|
+
}
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (field.pattern && value) {
|
|
1410
|
+
try {
|
|
1411
|
+
const regex = new RegExp(field.pattern);
|
|
1412
|
+
if (!regex.test(value)) {
|
|
1413
|
+
errors[field.name] = field.errorMessage || `Invalid format for ${field.label || field.name}`;
|
|
1414
|
+
}
|
|
1415
|
+
} catch (_error) {
|
|
1416
|
+
console.warn(`Invalid regex pattern for field ${field.name}:`, field.pattern);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
valid: Object.keys(errors).length === 0,
|
|
1421
|
+
errors: Object.keys(errors).length > 0 ? errors : void 0
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function validateForm(config, data) {
|
|
1425
|
+
const errors = {};
|
|
1426
|
+
config.fields.forEach((field) => {
|
|
1427
|
+
const value = data[field.name] || "";
|
|
1428
|
+
const result = validateField(field, value);
|
|
1429
|
+
if (!result.valid && result.errors) {
|
|
1430
|
+
Object.assign(errors, result.errors);
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
if (config.validate) {
|
|
1434
|
+
try {
|
|
1435
|
+
const customResult = config.validate(data);
|
|
1436
|
+
if (!customResult.valid && customResult.errors) {
|
|
1437
|
+
Object.assign(errors, customResult.errors);
|
|
1438
|
+
}
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
console.error("Custom validation function threw an error:", error);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
valid: Object.keys(errors).length === 0,
|
|
1445
|
+
errors: Object.keys(errors).length > 0 ? errors : void 0
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/modal/modal-styles.ts
|
|
1450
|
+
function getBackdropStyles() {
|
|
1451
|
+
return `
|
|
1452
|
+
position: absolute;
|
|
1453
|
+
inset: 0;
|
|
1454
|
+
background-color: var(--xp-modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
|
1455
|
+
`.trim();
|
|
1456
|
+
}
|
|
1457
|
+
function getDialogStyles(params) {
|
|
1458
|
+
return `
|
|
1459
|
+
position: relative;
|
|
1460
|
+
background: var(--xp-modal-dialog-bg, white);
|
|
1461
|
+
border-radius: var(--xp-modal-dialog-radius, ${params.borderRadius});
|
|
1462
|
+
box-shadow: var(--xp-modal-dialog-shadow, 0 4px 6px rgba(0, 0, 0, 0.1));
|
|
1463
|
+
max-width: ${params.width};
|
|
1464
|
+
width: ${params.maxWidth};
|
|
1465
|
+
height: ${params.height};
|
|
1466
|
+
max-height: ${params.maxHeight};
|
|
1467
|
+
overflow-y: auto;
|
|
1468
|
+
padding: ${params.padding};
|
|
1469
|
+
`.trim();
|
|
1470
|
+
}
|
|
1471
|
+
function getHeroImageStyles(params) {
|
|
1472
|
+
return `
|
|
1473
|
+
width: 100%;
|
|
1474
|
+
height: auto;
|
|
1475
|
+
max-height: ${params.maxHeight}px;
|
|
1476
|
+
object-fit: cover;
|
|
1477
|
+
border-radius: ${params.borderRadius};
|
|
1478
|
+
display: block;
|
|
1479
|
+
margin: 0;
|
|
1480
|
+
`.trim();
|
|
1481
|
+
}
|
|
1482
|
+
function getCloseButtonStyles() {
|
|
1483
|
+
return `
|
|
1484
|
+
position: absolute;
|
|
1485
|
+
top: var(--xp-modal-close-top, 16px);
|
|
1486
|
+
right: var(--xp-modal-close-right, 16px);
|
|
1487
|
+
background: none;
|
|
1488
|
+
border: none;
|
|
1489
|
+
font-size: var(--xp-modal-close-size, 24px);
|
|
1490
|
+
line-height: 1;
|
|
1491
|
+
cursor: pointer;
|
|
1492
|
+
padding: var(--xp-modal-close-padding, 4px 8px);
|
|
1493
|
+
color: var(--xp-modal-close-color, #666);
|
|
1494
|
+
opacity: var(--xp-modal-close-opacity, 0.7);
|
|
1495
|
+
transition: opacity 0.2s;
|
|
1496
|
+
`.trim();
|
|
1497
|
+
}
|
|
1498
|
+
function getCloseButtonHoverOpacity() {
|
|
1499
|
+
return "var(--xp-modal-close-hover-opacity, 1)";
|
|
1500
|
+
}
|
|
1501
|
+
function getCloseButtonDefaultOpacity() {
|
|
1502
|
+
return "var(--xp-modal-close-opacity, 0.7)";
|
|
1503
|
+
}
|
|
1504
|
+
function getContentWrapperStyles(padding) {
|
|
1505
|
+
return `padding: ${padding};`;
|
|
1506
|
+
}
|
|
1507
|
+
function getTitleStyles() {
|
|
1508
|
+
return `
|
|
1509
|
+
margin: 0 0 var(--xp-modal-title-margin-bottom, 16px) 0;
|
|
1510
|
+
font-size: var(--xp-modal-title-size, 20px);
|
|
1511
|
+
font-weight: var(--xp-modal-title-weight, 600);
|
|
1512
|
+
color: var(--xp-modal-title-color, #111);
|
|
1513
|
+
`.trim();
|
|
1514
|
+
}
|
|
1515
|
+
function getMessageStyles() {
|
|
1516
|
+
return `
|
|
1517
|
+
margin: 0 0 var(--xp-modal-message-margin-bottom, 20px) 0;
|
|
1518
|
+
font-size: var(--xp-modal-message-size, 14px);
|
|
1519
|
+
line-height: var(--xp-modal-message-line-height, 1.5);
|
|
1520
|
+
color: var(--xp-modal-message-color, #444);
|
|
1521
|
+
`.trim();
|
|
1522
|
+
}
|
|
1523
|
+
function getButtonContainerStyles() {
|
|
1524
|
+
return `
|
|
1525
|
+
display: flex;
|
|
1526
|
+
gap: var(--xp-modal-buttons-gap, 8px);
|
|
1527
|
+
flex-wrap: wrap;
|
|
1528
|
+
`.trim();
|
|
1529
|
+
}
|
|
1530
|
+
function getPrimaryButtonStyles() {
|
|
1531
|
+
return `
|
|
1532
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
1533
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
1534
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
1535
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
1536
|
+
cursor: pointer;
|
|
1537
|
+
transition: all 0.2s;
|
|
1538
|
+
border: none;
|
|
1539
|
+
background: var(--xp-button-primary-bg, #2563eb);
|
|
1540
|
+
color: var(--xp-button-primary-color, white);
|
|
1541
|
+
`.trim();
|
|
1542
|
+
}
|
|
1543
|
+
function getPrimaryButtonHoverBg() {
|
|
1544
|
+
return "var(--xp-button-primary-bg-hover, #1d4ed8)";
|
|
1545
|
+
}
|
|
1546
|
+
function getPrimaryButtonDefaultBg() {
|
|
1547
|
+
return "var(--xp-button-primary-bg, #2563eb)";
|
|
1548
|
+
}
|
|
1549
|
+
function getSecondaryButtonStyles() {
|
|
1550
|
+
return `
|
|
1551
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
1552
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
1553
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
1554
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
1555
|
+
cursor: pointer;
|
|
1556
|
+
transition: all 0.2s;
|
|
1557
|
+
border: none;
|
|
1558
|
+
background: var(--xp-button-secondary-bg, #f3f4f6);
|
|
1559
|
+
color: var(--xp-button-secondary-color, #374151);
|
|
1560
|
+
`.trim();
|
|
1561
|
+
}
|
|
1562
|
+
function getSecondaryButtonHoverBg() {
|
|
1563
|
+
return "var(--xp-button-secondary-bg-hover, #e5e7eb)";
|
|
1564
|
+
}
|
|
1565
|
+
function getSecondaryButtonDefaultBg() {
|
|
1566
|
+
return "var(--xp-button-secondary-bg, #f3f4f6)";
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/modal/modal.ts
|
|
1570
|
+
var modalPlugin = (plugin, instance) => {
|
|
1571
|
+
plugin.ns("experiences.modal");
|
|
1572
|
+
plugin.defaults({
|
|
1573
|
+
modal: {
|
|
1574
|
+
dismissable: true,
|
|
1575
|
+
backdropDismiss: true,
|
|
1576
|
+
zIndex: 10001,
|
|
1577
|
+
size: "md",
|
|
1578
|
+
mobileFullscreen: false,
|
|
1579
|
+
position: "center",
|
|
1580
|
+
animation: "fade",
|
|
1581
|
+
animationDuration: 200
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
const activeModals = /* @__PURE__ */ new Map();
|
|
1585
|
+
const previouslyFocusedElement = /* @__PURE__ */ new Map();
|
|
1586
|
+
const formData = /* @__PURE__ */ new Map();
|
|
1587
|
+
const getFocusableElements = (container) => {
|
|
1588
|
+
const selector = 'a[href], button, textarea, input, select, details, [tabindex]:not([tabindex="-1"])';
|
|
1589
|
+
return Array.from(container.querySelectorAll(selector)).filter(
|
|
1590
|
+
(el) => !el.hasAttribute("disabled")
|
|
1591
|
+
);
|
|
1592
|
+
};
|
|
1593
|
+
const createFocusTrap = (container) => {
|
|
1594
|
+
const focusable = getFocusableElements(container);
|
|
1595
|
+
if (focusable.length === 0) return () => {
|
|
1596
|
+
};
|
|
1597
|
+
const firstFocusable = focusable[0];
|
|
1598
|
+
const lastFocusable = focusable[focusable.length - 1];
|
|
1599
|
+
const handleKeyDown = (e) => {
|
|
1600
|
+
if (e.key !== "Tab") return;
|
|
1601
|
+
if (e.shiftKey) {
|
|
1602
|
+
if (document.activeElement === firstFocusable) {
|
|
1603
|
+
e.preventDefault();
|
|
1604
|
+
lastFocusable.focus();
|
|
1605
|
+
}
|
|
1606
|
+
} else {
|
|
1607
|
+
if (document.activeElement === lastFocusable) {
|
|
1608
|
+
e.preventDefault();
|
|
1609
|
+
firstFocusable.focus();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
1614
|
+
firstFocusable.focus();
|
|
1615
|
+
return () => {
|
|
1616
|
+
container.removeEventListener("keydown", handleKeyDown);
|
|
1617
|
+
};
|
|
1618
|
+
};
|
|
1619
|
+
const getSizeWidth = (size) => {
|
|
1620
|
+
switch (size) {
|
|
1621
|
+
case "sm":
|
|
1622
|
+
return "400px";
|
|
1623
|
+
case "md":
|
|
1624
|
+
return "600px";
|
|
1625
|
+
case "lg":
|
|
1626
|
+
return "800px";
|
|
1627
|
+
case "fullscreen":
|
|
1628
|
+
return "100vw";
|
|
1629
|
+
case "auto":
|
|
1630
|
+
return "auto";
|
|
1631
|
+
default:
|
|
1632
|
+
return "600px";
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
const isMobile = () => {
|
|
1636
|
+
return typeof window !== "undefined" && window.innerWidth < 640;
|
|
1637
|
+
};
|
|
1638
|
+
const renderModal = (experienceId, content) => {
|
|
1639
|
+
const modalConfig = instance.get("modal") || {};
|
|
1640
|
+
const zIndex = modalConfig.zIndex || 10001;
|
|
1641
|
+
const size = modalConfig.size || "md";
|
|
1642
|
+
const position = modalConfig.position || "center";
|
|
1643
|
+
const animation = modalConfig.animation || "fade";
|
|
1644
|
+
const animationDuration = modalConfig.animationDuration || 200;
|
|
1645
|
+
const mobileFullscreen = modalConfig.mobileFullscreen !== void 0 ? modalConfig.mobileFullscreen : size === "lg";
|
|
1646
|
+
const shouldBeFullscreen = size === "fullscreen" || mobileFullscreen && isMobile();
|
|
1647
|
+
const container = document.createElement("div");
|
|
1648
|
+
const sizeClass = shouldBeFullscreen ? "fullscreen" : size;
|
|
1649
|
+
const positionClass = position === "bottom" ? "xp-modal--bottom" : "xp-modal--center";
|
|
1650
|
+
const animationClass = animation !== "none" ? `xp-modal--${animation}` : "";
|
|
1651
|
+
container.className = `xp-modal xp-modal--${sizeClass} ${positionClass} ${animationClass} ${content.className || ""}`.trim();
|
|
1652
|
+
container.setAttribute("data-xp-id", experienceId);
|
|
1653
|
+
container.setAttribute("role", "dialog");
|
|
1654
|
+
container.setAttribute("aria-modal", "true");
|
|
1655
|
+
if (content.title) {
|
|
1656
|
+
container.setAttribute("aria-labelledby", `xp-modal-title-${experienceId}`);
|
|
1657
|
+
}
|
|
1658
|
+
const alignItems = position === "bottom" ? "flex-end" : "center";
|
|
1659
|
+
container.style.cssText = `position: fixed; inset: 0; z-index: ${zIndex}; display: flex; align-items: ${alignItems}; justify-content: center;`;
|
|
1660
|
+
if (animation !== "none") {
|
|
1661
|
+
container.style.opacity = "0";
|
|
1662
|
+
container.style.transition = `opacity ${animationDuration}ms ease-in-out`;
|
|
1663
|
+
if (animation === "slide-up") {
|
|
1664
|
+
container.style.transform = "translateY(100%)";
|
|
1665
|
+
container.style.transition += `, transform ${animationDuration}ms ease-out`;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (content.style) {
|
|
1669
|
+
Object.entries(content.style).forEach(([key, value]) => {
|
|
1670
|
+
container.style.setProperty(key, String(value));
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const backdrop = document.createElement("div");
|
|
1674
|
+
backdrop.className = "xp-modal__backdrop";
|
|
1675
|
+
backdrop.style.cssText = getBackdropStyles();
|
|
1676
|
+
container.appendChild(backdrop);
|
|
1677
|
+
const dialog = document.createElement("div");
|
|
1678
|
+
const dialogWidth = shouldBeFullscreen ? "100%" : size === "auto" ? "none" : getSizeWidth(size);
|
|
1679
|
+
const dialogHeight = shouldBeFullscreen ? "100%" : "auto";
|
|
1680
|
+
const dialogMaxWidth = shouldBeFullscreen ? "100%" : size === "auto" ? "none" : "90%";
|
|
1681
|
+
const dialogBorderRadius = shouldBeFullscreen ? "0" : "8px";
|
|
1682
|
+
const dialogPadding = content.image ? "0" : "24px";
|
|
1683
|
+
dialog.className = `xp-modal__dialog${content.image ? " xp-modal__dialog--has-image" : ""}`;
|
|
1684
|
+
dialog.style.cssText = getDialogStyles({
|
|
1685
|
+
width: dialogWidth,
|
|
1686
|
+
maxWidth: dialogMaxWidth,
|
|
1687
|
+
height: dialogHeight,
|
|
1688
|
+
maxHeight: shouldBeFullscreen ? "100%" : "90vh",
|
|
1689
|
+
borderRadius: dialogBorderRadius,
|
|
1690
|
+
padding: dialogPadding
|
|
1691
|
+
});
|
|
1692
|
+
container.appendChild(dialog);
|
|
1693
|
+
if (content.image) {
|
|
1694
|
+
const img = document.createElement("img");
|
|
1695
|
+
img.className = "xp-modal__hero-image";
|
|
1696
|
+
img.src = content.image.src;
|
|
1697
|
+
img.alt = content.image.alt;
|
|
1698
|
+
img.loading = "lazy";
|
|
1699
|
+
const maxHeight = content.image.maxHeight || (isMobile() ? 200 : 300);
|
|
1700
|
+
img.style.cssText = getHeroImageStyles({
|
|
1701
|
+
maxHeight,
|
|
1702
|
+
borderRadius: shouldBeFullscreen ? "0" : "8px 8px 0 0"
|
|
1703
|
+
});
|
|
1704
|
+
dialog.appendChild(img);
|
|
1705
|
+
}
|
|
1706
|
+
if (modalConfig.dismissable !== false) {
|
|
1707
|
+
const closeButton = document.createElement("button");
|
|
1708
|
+
closeButton.className = "xp-modal__close";
|
|
1709
|
+
closeButton.setAttribute("aria-label", "Close dialog");
|
|
1710
|
+
closeButton.innerHTML = "×";
|
|
1711
|
+
closeButton.style.cssText = getCloseButtonStyles();
|
|
1712
|
+
closeButton.onmouseover = () => {
|
|
1713
|
+
closeButton.style.opacity = getCloseButtonHoverOpacity();
|
|
1714
|
+
};
|
|
1715
|
+
closeButton.onmouseout = () => {
|
|
1716
|
+
closeButton.style.opacity = getCloseButtonDefaultOpacity();
|
|
1717
|
+
};
|
|
1718
|
+
closeButton.onclick = () => removeModal(experienceId);
|
|
1719
|
+
dialog.appendChild(closeButton);
|
|
1720
|
+
}
|
|
1721
|
+
const contentWrapper = document.createElement("div");
|
|
1722
|
+
contentWrapper.className = "xp-modal__content";
|
|
1723
|
+
const contentPadding = content.image ? "24px" : "24px 24px 0 24px";
|
|
1724
|
+
contentWrapper.style.cssText = getContentWrapperStyles(contentPadding);
|
|
1725
|
+
if (content.title) {
|
|
1726
|
+
const title = document.createElement("h2");
|
|
1727
|
+
title.id = `xp-modal-title-${experienceId}`;
|
|
1728
|
+
title.className = "xp-modal__title";
|
|
1729
|
+
title.textContent = content.title;
|
|
1730
|
+
title.style.cssText = getTitleStyles();
|
|
1731
|
+
contentWrapper.appendChild(title);
|
|
1732
|
+
}
|
|
1733
|
+
const message = document.createElement("div");
|
|
1734
|
+
message.className = "xp-modal__message";
|
|
1735
|
+
message.innerHTML = sanitizeHTML(content.message);
|
|
1736
|
+
message.style.cssText = getMessageStyles();
|
|
1737
|
+
contentWrapper.appendChild(message);
|
|
1738
|
+
if (content.form) {
|
|
1739
|
+
const form = renderForm(experienceId, content.form);
|
|
1740
|
+
contentWrapper.appendChild(form);
|
|
1741
|
+
container.__formConfig = content.form;
|
|
1742
|
+
const data = {};
|
|
1743
|
+
content.form.fields.forEach((field) => {
|
|
1744
|
+
data[field.name] = "";
|
|
1745
|
+
});
|
|
1746
|
+
formData.set(experienceId, data);
|
|
1747
|
+
content.form.fields.forEach((field) => {
|
|
1748
|
+
const input = form.querySelector(`#${experienceId}-${field.name}`);
|
|
1749
|
+
const errorEl = form.querySelector(`#${experienceId}-${field.name}-error`);
|
|
1750
|
+
if (!input) return;
|
|
1751
|
+
input.addEventListener("input", () => {
|
|
1752
|
+
const currentData = formData.get(experienceId) || {};
|
|
1753
|
+
currentData[field.name] = input.value;
|
|
1754
|
+
formData.set(experienceId, currentData);
|
|
1755
|
+
instance.emit("experiences:modal:form:change", {
|
|
1756
|
+
experienceId,
|
|
1757
|
+
field: field.name,
|
|
1758
|
+
value: input.value,
|
|
1759
|
+
formData: { ...currentData },
|
|
1760
|
+
timestamp: Date.now()
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
input.addEventListener("blur", () => {
|
|
1764
|
+
const currentData = formData.get(experienceId) || {};
|
|
1765
|
+
const result = validateField(field, currentData[field.name] || "");
|
|
1766
|
+
if (!result.valid && result.errors) {
|
|
1767
|
+
input.style.cssText += `; ${getInputErrorStyles()}`;
|
|
1768
|
+
input.setAttribute("aria-invalid", "true");
|
|
1769
|
+
errorEl.textContent = result.errors[field.name] || "";
|
|
1770
|
+
instance.emit("experiences:modal:form:validation", {
|
|
1771
|
+
experienceId,
|
|
1772
|
+
field: field.name,
|
|
1773
|
+
valid: false,
|
|
1774
|
+
errors: result.errors,
|
|
1775
|
+
timestamp: Date.now()
|
|
1776
|
+
});
|
|
1777
|
+
} else {
|
|
1778
|
+
input.style.cssText = input.style.cssText.replace(getInputErrorStyles(), "");
|
|
1779
|
+
input.setAttribute("aria-invalid", "false");
|
|
1780
|
+
errorEl.textContent = "";
|
|
1781
|
+
instance.emit("experiences:modal:form:validation", {
|
|
1782
|
+
experienceId,
|
|
1783
|
+
field: field.name,
|
|
1784
|
+
valid: true,
|
|
1785
|
+
timestamp: Date.now()
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
form.addEventListener("submit", async (e) => {
|
|
1791
|
+
e.preventDefault();
|
|
1792
|
+
if (!content.form) return;
|
|
1793
|
+
const currentData = formData.get(experienceId) || {};
|
|
1794
|
+
const result = validateForm(content.form, currentData);
|
|
1795
|
+
if (!result.valid && result.errors) {
|
|
1796
|
+
content.form.fields.forEach((field) => {
|
|
1797
|
+
if (result.errors?.[field.name]) {
|
|
1798
|
+
const input = form.querySelector(
|
|
1799
|
+
`#${experienceId}-${field.name}`
|
|
1800
|
+
);
|
|
1801
|
+
const errorEl = form.querySelector(
|
|
1802
|
+
`#${experienceId}-${field.name}-error`
|
|
1803
|
+
);
|
|
1804
|
+
if (input) {
|
|
1805
|
+
input.style.cssText += `; ${getInputErrorStyles()}`;
|
|
1806
|
+
input.setAttribute("aria-invalid", "true");
|
|
1807
|
+
}
|
|
1808
|
+
if (errorEl) {
|
|
1809
|
+
errorEl.textContent = result.errors[field.name] || "";
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
instance.emit("experiences:modal:form:validation", {
|
|
1814
|
+
experienceId,
|
|
1815
|
+
valid: false,
|
|
1816
|
+
errors: result.errors,
|
|
1817
|
+
timestamp: Date.now()
|
|
1818
|
+
});
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const submitButton = form.querySelector('button[type="submit"]');
|
|
1822
|
+
if (submitButton) {
|
|
1823
|
+
submitButton.disabled = true;
|
|
1824
|
+
submitButton.textContent = "Submitting...";
|
|
1825
|
+
}
|
|
1826
|
+
instance.emit("experiences:modal:form:submit", {
|
|
1827
|
+
experienceId,
|
|
1828
|
+
formData: { ...currentData },
|
|
1829
|
+
timestamp: Date.now()
|
|
1830
|
+
});
|
|
1831
|
+
});
|
|
1832
|
+
} else if (content.buttons && content.buttons.length > 0) {
|
|
1833
|
+
const buttonContainer = document.createElement("div");
|
|
1834
|
+
buttonContainer.className = "xp-modal__buttons";
|
|
1835
|
+
buttonContainer.style.cssText = getButtonContainerStyles();
|
|
1836
|
+
content.buttons.forEach((button) => {
|
|
1837
|
+
const btn = document.createElement("button");
|
|
1838
|
+
btn.className = `xp-modal__button xp-modal__button--${button.variant || "secondary"}`;
|
|
1839
|
+
btn.textContent = button.text;
|
|
1840
|
+
if (button.variant === "primary") {
|
|
1841
|
+
btn.style.cssText = getPrimaryButtonStyles();
|
|
1842
|
+
btn.onmouseover = () => {
|
|
1843
|
+
btn.style.background = getPrimaryButtonHoverBg();
|
|
1844
|
+
};
|
|
1845
|
+
btn.onmouseout = () => {
|
|
1846
|
+
btn.style.background = getPrimaryButtonDefaultBg();
|
|
1847
|
+
};
|
|
1848
|
+
} else {
|
|
1849
|
+
btn.style.cssText = getSecondaryButtonStyles();
|
|
1850
|
+
btn.onmouseover = () => {
|
|
1851
|
+
btn.style.background = getSecondaryButtonHoverBg();
|
|
1852
|
+
};
|
|
1853
|
+
btn.onmouseout = () => {
|
|
1854
|
+
btn.style.background = getSecondaryButtonDefaultBg();
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
btn.onclick = () => {
|
|
1858
|
+
instance.emit("experiences:action", {
|
|
1859
|
+
experienceId,
|
|
1860
|
+
action: button.action,
|
|
1861
|
+
button,
|
|
1862
|
+
timestamp: Date.now()
|
|
1863
|
+
});
|
|
1864
|
+
if (button.dismiss) {
|
|
1865
|
+
removeModal(experienceId);
|
|
1866
|
+
}
|
|
1867
|
+
if (button.url) {
|
|
1868
|
+
window.location.href = button.url;
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
buttonContainer.appendChild(btn);
|
|
1872
|
+
});
|
|
1873
|
+
contentWrapper.appendChild(buttonContainer);
|
|
1874
|
+
}
|
|
1875
|
+
dialog.appendChild(contentWrapper);
|
|
1876
|
+
if (modalConfig.backdropDismiss !== false) {
|
|
1877
|
+
backdrop.onclick = () => removeModal(experienceId);
|
|
1878
|
+
}
|
|
1879
|
+
const handleEscape = (e) => {
|
|
1880
|
+
if (e.key === "Escape" && modalConfig.dismissable !== false) {
|
|
1881
|
+
removeModal(experienceId);
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
document.addEventListener("keydown", handleEscape);
|
|
1885
|
+
container.__cleanupEscape = () => {
|
|
1886
|
+
document.removeEventListener("keydown", handleEscape);
|
|
1887
|
+
};
|
|
1888
|
+
return container;
|
|
1889
|
+
};
|
|
1890
|
+
const showModal = (experience) => {
|
|
1891
|
+
const experienceId = experience.id;
|
|
1892
|
+
if (activeModals.has(experienceId)) return;
|
|
1893
|
+
if (activeModals.size > 0) {
|
|
1894
|
+
const existingIds = Array.from(activeModals.keys());
|
|
1895
|
+
for (const id of existingIds) {
|
|
1896
|
+
removeModal(id);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
previouslyFocusedElement.set(experienceId, document.activeElement);
|
|
1900
|
+
const modal = renderModal(experienceId, experience.content);
|
|
1901
|
+
document.body.appendChild(modal);
|
|
1902
|
+
activeModals.set(experienceId, modal);
|
|
1903
|
+
const modalConfig = instance.get("modal") || {};
|
|
1904
|
+
const animation = modalConfig.animation || "fade";
|
|
1905
|
+
if (animation !== "none") {
|
|
1906
|
+
requestAnimationFrame(() => {
|
|
1907
|
+
modal.style.opacity = "1";
|
|
1908
|
+
if (animation === "slide-up") {
|
|
1909
|
+
modal.style.transform = "translateY(0)";
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
const cleanupFocusTrap = createFocusTrap(modal);
|
|
1914
|
+
modal.__cleanupFocusTrap = cleanupFocusTrap;
|
|
1915
|
+
instance.emit("experiences:shown", {
|
|
1916
|
+
experienceId,
|
|
1917
|
+
timestamp: Date.now()
|
|
1918
|
+
});
|
|
1919
|
+
instance.emit("trigger:modal", {
|
|
1920
|
+
experienceId,
|
|
1921
|
+
timestamp: Date.now(),
|
|
1922
|
+
shown: true
|
|
1923
|
+
});
|
|
1924
|
+
};
|
|
1925
|
+
const removeModal = (experienceId) => {
|
|
1926
|
+
const modal = activeModals.get(experienceId);
|
|
1927
|
+
if (!modal) return;
|
|
1928
|
+
if (modal.__cleanupFocusTrap) {
|
|
1929
|
+
modal.__cleanupFocusTrap();
|
|
1930
|
+
}
|
|
1931
|
+
if (modal.__cleanupEscape) {
|
|
1932
|
+
modal.__cleanupEscape();
|
|
1933
|
+
}
|
|
1934
|
+
const previousElement = previouslyFocusedElement.get(experienceId);
|
|
1935
|
+
if (previousElement && document.body.contains(previousElement)) {
|
|
1936
|
+
previousElement.focus();
|
|
1937
|
+
}
|
|
1938
|
+
previouslyFocusedElement.delete(experienceId);
|
|
1939
|
+
modal.remove();
|
|
1940
|
+
activeModals.delete(experienceId);
|
|
1941
|
+
instance.emit("experiences:dismissed", {
|
|
1942
|
+
experienceId,
|
|
1943
|
+
timestamp: Date.now()
|
|
1944
|
+
});
|
|
1945
|
+
};
|
|
1946
|
+
const isShowing = (experienceId) => {
|
|
1947
|
+
if (experienceId) {
|
|
1948
|
+
return activeModals.has(experienceId);
|
|
1949
|
+
}
|
|
1950
|
+
return activeModals.size > 0;
|
|
1951
|
+
};
|
|
1952
|
+
const showFormState = (experienceId, state) => {
|
|
1953
|
+
const modal = activeModals.get(experienceId);
|
|
1954
|
+
if (!modal) return;
|
|
1955
|
+
const form = modal.querySelector(".xp-modal__form");
|
|
1956
|
+
if (!form) return;
|
|
1957
|
+
const formConfig = modal.__formConfig;
|
|
1958
|
+
if (!formConfig) return;
|
|
1959
|
+
const stateConfig = state === "success" ? formConfig.successState : formConfig.errorState;
|
|
1960
|
+
if (!stateConfig) return;
|
|
1961
|
+
const stateEl = renderFormState(state, stateConfig);
|
|
1962
|
+
form.replaceWith(stateEl);
|
|
1963
|
+
instance.emit("experiences:modal:form:state", {
|
|
1964
|
+
experienceId,
|
|
1965
|
+
state,
|
|
1966
|
+
timestamp: Date.now()
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1969
|
+
const resetForm = (experienceId) => {
|
|
1970
|
+
const modal = activeModals.get(experienceId);
|
|
1971
|
+
if (!modal) return;
|
|
1972
|
+
const form = modal.querySelector(".xp-modal__form");
|
|
1973
|
+
if (!form) return;
|
|
1974
|
+
form.reset();
|
|
1975
|
+
const data = formData.get(experienceId);
|
|
1976
|
+
if (data) {
|
|
1977
|
+
Object.keys(data).forEach((key) => {
|
|
1978
|
+
data[key] = "";
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
const errors = form.querySelectorAll(".xp-form__error");
|
|
1982
|
+
errors.forEach((error) => {
|
|
1983
|
+
error.textContent = "";
|
|
1984
|
+
});
|
|
1985
|
+
const inputs = form.querySelectorAll(".xp-form__input");
|
|
1986
|
+
inputs.forEach((input) => {
|
|
1987
|
+
input.setAttribute("aria-invalid", "false");
|
|
1988
|
+
input.style.cssText = input.style.cssText.replace(getInputErrorStyles(), "");
|
|
1989
|
+
});
|
|
1990
|
+
};
|
|
1991
|
+
const getFormData = (experienceId) => {
|
|
1992
|
+
return formData.get(experienceId) || null;
|
|
1993
|
+
};
|
|
1994
|
+
plugin.expose({
|
|
1995
|
+
modal: {
|
|
1996
|
+
show: showModal,
|
|
1997
|
+
remove: removeModal,
|
|
1998
|
+
isShowing,
|
|
1999
|
+
showFormState,
|
|
2000
|
+
resetForm,
|
|
2001
|
+
getFormData
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
instance.on("experiences:evaluated", (data) => {
|
|
2005
|
+
const { decision, experience } = data;
|
|
2006
|
+
if (decision.show && decision.experienceId && experience) {
|
|
2007
|
+
if (experience.type === "modal") {
|
|
2008
|
+
showModal(experience);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
instance.on("sdk:destroy", () => {
|
|
2013
|
+
activeModals.forEach((_, id) => {
|
|
2014
|
+
removeModal(id);
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
};
|
|
861
2018
|
function respectsDNT() {
|
|
862
2019
|
if (typeof navigator === "undefined") return false;
|
|
863
2020
|
return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
|
|
@@ -1175,10 +2332,9 @@ var scrollDepthPlugin = (plugin, instance, config) => {
|
|
|
1175
2332
|
window.removeEventListener("scroll", throttledScrollHandler);
|
|
1176
2333
|
window.removeEventListener("resize", throttledResizeHandler);
|
|
1177
2334
|
}
|
|
1178
|
-
|
|
2335
|
+
instance.on("sdk:destroy", () => {
|
|
1179
2336
|
cleanup();
|
|
1180
|
-
};
|
|
1181
|
-
instance.on("destroy", destroyHandler);
|
|
2337
|
+
});
|
|
1182
2338
|
plugin.expose({
|
|
1183
2339
|
scrollDepth: {
|
|
1184
2340
|
/**
|
|
@@ -1229,7 +2385,6 @@ var scrollDepthPlugin = (plugin, instance, config) => {
|
|
|
1229
2385
|
}
|
|
1230
2386
|
return () => {
|
|
1231
2387
|
cleanup();
|
|
1232
|
-
instance.off("destroy", destroyHandler);
|
|
1233
2388
|
};
|
|
1234
2389
|
};
|
|
1235
2390
|
|
|
@@ -1373,12 +2528,11 @@ var timeDelayPlugin = (plugin, instance, config) => {
|
|
|
1373
2528
|
}
|
|
1374
2529
|
});
|
|
1375
2530
|
initialize();
|
|
1376
|
-
|
|
2531
|
+
instance.on("sdk:destroy", () => {
|
|
1377
2532
|
cleanup();
|
|
1378
|
-
};
|
|
1379
|
-
instance.on("destroy", destroyHandler);
|
|
2533
|
+
});
|
|
1380
2534
|
};
|
|
1381
2535
|
|
|
1382
|
-
export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin };
|
|
2536
|
+
export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };
|
|
1383
2537
|
//# sourceMappingURL=index.js.map
|
|
1384
2538
|
//# sourceMappingURL=index.js.map
|