@prosdevlab/experience-sdk-plugins 0.1.4 → 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 +150 -0
- package/README.md +141 -79
- package/dist/index.d.ts +813 -35
- package/dist/index.js +1910 -66
- 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.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +371 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +7 -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 +421 -0
- 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/page-visits/index.ts +6 -0
- package/src/page-visits/page-visits.test.ts +562 -0
- package/src/page-visits/page-visits.ts +314 -0
- package/src/page-visits/types.ts +119 -0
- package/src/scroll-depth/index.ts +6 -0
- package/src/scroll-depth/scroll-depth.test.ts +580 -0
- package/src/scroll-depth/scroll-depth.ts +398 -0
- package/src/scroll-depth/types.ts +122 -0
- package/src/time-delay/index.ts +6 -0
- package/src/time-delay/time-delay.test.ts +477 -0
- package/src/time-delay/time-delay.ts +296 -0
- package/src/time-delay/types.ts +89 -0
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +5 -2
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) {
|
|
@@ -42,7 +45,7 @@ function sanitizeHTML(html) {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
const attrString = attrs.length > 0 ?
|
|
48
|
+
const attrString = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
46
49
|
let innerHTML = "";
|
|
47
50
|
for (const child of Array.from(element.childNodes)) {
|
|
48
51
|
innerHTML += sanitizeNode(child);
|
|
@@ -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
|
`;
|
|
@@ -561,6 +565,175 @@ var debugPlugin = (plugin, instance, config) => {
|
|
|
561
565
|
});
|
|
562
566
|
}
|
|
563
567
|
};
|
|
568
|
+
|
|
569
|
+
// src/exit-intent/exit-intent.ts
|
|
570
|
+
function isMobileDevice(userAgent) {
|
|
571
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
572
|
+
}
|
|
573
|
+
function hasMinTimeElapsed(pageLoadTime, minTime, currentTime) {
|
|
574
|
+
return currentTime - pageLoadTime >= minTime;
|
|
575
|
+
}
|
|
576
|
+
function addPositionToHistory(positions, newPosition, maxSize) {
|
|
577
|
+
const updated = [...positions, newPosition];
|
|
578
|
+
if (updated.length > maxSize) {
|
|
579
|
+
return updated.slice(1);
|
|
580
|
+
}
|
|
581
|
+
return updated;
|
|
582
|
+
}
|
|
583
|
+
function calculateVelocity(lastY, previousY) {
|
|
584
|
+
return Math.abs(lastY - previousY);
|
|
585
|
+
}
|
|
586
|
+
function shouldTriggerExitIntent(positions, sensitivity, relatedTarget) {
|
|
587
|
+
if (positions.length < 2) {
|
|
588
|
+
return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
|
|
589
|
+
}
|
|
590
|
+
if (relatedTarget && relatedTarget.nodeName !== "HTML") {
|
|
591
|
+
return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
|
|
592
|
+
}
|
|
593
|
+
const lastY = positions[positions.length - 1].y;
|
|
594
|
+
const previousY = positions[positions.length - 2].y;
|
|
595
|
+
const velocity = calculateVelocity(lastY, previousY);
|
|
596
|
+
const isMovingUp = lastY < previousY;
|
|
597
|
+
const isNearTop = lastY - velocity <= sensitivity;
|
|
598
|
+
return {
|
|
599
|
+
shouldTrigger: isMovingUp && isNearTop,
|
|
600
|
+
lastY,
|
|
601
|
+
previousY,
|
|
602
|
+
velocity
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function createExitIntentEvent(lastY, previousY, velocity, pageLoadTime, timestamp) {
|
|
606
|
+
return {
|
|
607
|
+
timestamp,
|
|
608
|
+
lastY,
|
|
609
|
+
previousY,
|
|
610
|
+
velocity,
|
|
611
|
+
timeOnPage: timestamp - pageLoadTime
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
var exitIntentPlugin = (plugin, instance, config) => {
|
|
615
|
+
plugin.ns("experiences.exitIntent");
|
|
616
|
+
plugin.defaults({
|
|
617
|
+
exitIntent: {
|
|
618
|
+
sensitivity: 50,
|
|
619
|
+
minTimeOnPage: 2e3,
|
|
620
|
+
delay: 0,
|
|
621
|
+
positionHistorySize: 30,
|
|
622
|
+
disableOnMobile: true
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
const exitIntentConfig = config.get("exitIntent");
|
|
626
|
+
if (!exitIntentConfig) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
let positions = [];
|
|
630
|
+
let triggered = false;
|
|
631
|
+
const pageLoadTime = Date.now();
|
|
632
|
+
let mouseMoveListener = null;
|
|
633
|
+
let mouseOutListener = null;
|
|
634
|
+
function shouldDisable() {
|
|
635
|
+
if (!exitIntentConfig?.disableOnMobile) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return isMobileDevice(navigator.userAgent);
|
|
639
|
+
}
|
|
640
|
+
function trackPosition(e) {
|
|
641
|
+
const newPosition = { x: e.clientX, y: e.clientY };
|
|
642
|
+
const maxSize = exitIntentConfig?.positionHistorySize ?? 30;
|
|
643
|
+
positions = addPositionToHistory(positions, newPosition, maxSize);
|
|
644
|
+
}
|
|
645
|
+
function handleExitIntent(e) {
|
|
646
|
+
if (triggered) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const minTime = exitIntentConfig?.minTimeOnPage ?? 2e3;
|
|
650
|
+
if (!hasMinTimeElapsed(pageLoadTime, minTime, Date.now())) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const sensitivity = exitIntentConfig?.sensitivity ?? 50;
|
|
654
|
+
const relatedTarget = e.relatedTarget || e.toElement;
|
|
655
|
+
const result = shouldTriggerExitIntent(positions, sensitivity, relatedTarget);
|
|
656
|
+
if (result.shouldTrigger) {
|
|
657
|
+
triggered = true;
|
|
658
|
+
const eventPayload = createExitIntentEvent(
|
|
659
|
+
result.lastY,
|
|
660
|
+
result.previousY,
|
|
661
|
+
result.velocity,
|
|
662
|
+
pageLoadTime,
|
|
663
|
+
Date.now()
|
|
664
|
+
);
|
|
665
|
+
const delay = exitIntentConfig?.delay ?? 0;
|
|
666
|
+
if (delay > 0) {
|
|
667
|
+
setTimeout(() => {
|
|
668
|
+
instance.emit("trigger:exitIntent", eventPayload);
|
|
669
|
+
}, delay);
|
|
670
|
+
} else {
|
|
671
|
+
instance.emit("trigger:exitIntent", eventPayload);
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
sessionStorage.setItem("xp:exitIntent:triggered", Date.now().toString());
|
|
675
|
+
} catch (_e) {
|
|
676
|
+
}
|
|
677
|
+
cleanup();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function cleanup() {
|
|
681
|
+
if (mouseMoveListener) {
|
|
682
|
+
document.removeEventListener("mousemove", mouseMoveListener);
|
|
683
|
+
mouseMoveListener = null;
|
|
684
|
+
}
|
|
685
|
+
if (mouseOutListener) {
|
|
686
|
+
document.removeEventListener("mouseout", mouseOutListener);
|
|
687
|
+
mouseOutListener = null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function initialize() {
|
|
691
|
+
if (shouldDisable()) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const storedTrigger = sessionStorage.getItem("xp:exitIntent:triggered");
|
|
696
|
+
if (storedTrigger) {
|
|
697
|
+
triggered = true;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
} catch (_e) {
|
|
701
|
+
}
|
|
702
|
+
mouseMoveListener = trackPosition;
|
|
703
|
+
mouseOutListener = handleExitIntent;
|
|
704
|
+
document.addEventListener("mousemove", mouseMoveListener);
|
|
705
|
+
document.addEventListener("mouseout", mouseOutListener);
|
|
706
|
+
}
|
|
707
|
+
plugin.expose({
|
|
708
|
+
exitIntent: {
|
|
709
|
+
/**
|
|
710
|
+
* Check if exit intent has been triggered
|
|
711
|
+
*/
|
|
712
|
+
isTriggered: () => triggered,
|
|
713
|
+
/**
|
|
714
|
+
* Reset exit intent state (useful for testing)
|
|
715
|
+
*/
|
|
716
|
+
reset: () => {
|
|
717
|
+
triggered = false;
|
|
718
|
+
positions = [];
|
|
719
|
+
try {
|
|
720
|
+
sessionStorage.removeItem("xp:exitIntent:triggered");
|
|
721
|
+
} catch (_e) {
|
|
722
|
+
}
|
|
723
|
+
cleanup();
|
|
724
|
+
initialize();
|
|
725
|
+
},
|
|
726
|
+
/**
|
|
727
|
+
* Get current position history
|
|
728
|
+
*/
|
|
729
|
+
getPositions: () => [...positions]
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
initialize();
|
|
733
|
+
instance.on("sdk:destroy", () => {
|
|
734
|
+
cleanup();
|
|
735
|
+
});
|
|
736
|
+
};
|
|
564
737
|
var frequencyPlugin = (plugin, instance, config) => {
|
|
565
738
|
plugin.ns("frequency");
|
|
566
739
|
plugin.defaults({
|
|
@@ -689,6 +862,1677 @@ var frequencyPlugin = (plugin, instance, config) => {
|
|
|
689
862
|
}
|
|
690
863
|
};
|
|
691
864
|
|
|
692
|
-
|
|
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
|
+
};
|
|
2018
|
+
function respectsDNT() {
|
|
2019
|
+
if (typeof navigator === "undefined") return false;
|
|
2020
|
+
return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
|
|
2021
|
+
}
|
|
2022
|
+
function createVisitsEvent(isFirstVisit, totalVisits, sessionVisits, firstVisitTime, lastVisitTime, timestamp) {
|
|
2023
|
+
return {
|
|
2024
|
+
isFirstVisit,
|
|
2025
|
+
totalVisits,
|
|
2026
|
+
sessionVisits,
|
|
2027
|
+
firstVisitTime,
|
|
2028
|
+
lastVisitTime,
|
|
2029
|
+
timestamp
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
var pageVisitsPlugin = (plugin, instance, config) => {
|
|
2033
|
+
plugin.ns("pageVisits");
|
|
2034
|
+
plugin.defaults({
|
|
2035
|
+
pageVisits: {
|
|
2036
|
+
enabled: true,
|
|
2037
|
+
respectDNT: true,
|
|
2038
|
+
sessionKey: "pageVisits:session",
|
|
2039
|
+
totalKey: "pageVisits:total",
|
|
2040
|
+
ttl: void 0,
|
|
2041
|
+
autoIncrement: true
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
if (!instance.storage) {
|
|
2045
|
+
console.warn("[PageVisits] Storage plugin not found, auto-loading...");
|
|
2046
|
+
instance.use(storagePlugin);
|
|
2047
|
+
}
|
|
2048
|
+
const sdkInstance = instance;
|
|
2049
|
+
let sessionCount = 0;
|
|
2050
|
+
let totalCount = 0;
|
|
2051
|
+
let firstVisitTime;
|
|
2052
|
+
let lastVisitTime;
|
|
2053
|
+
let isFirstVisitFlag = false;
|
|
2054
|
+
let initialized = false;
|
|
2055
|
+
function loadData() {
|
|
2056
|
+
const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
|
|
2057
|
+
const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
|
|
2058
|
+
const storedSession = sdkInstance.storage.get(sessionKey, {
|
|
2059
|
+
backend: "sessionStorage"
|
|
2060
|
+
});
|
|
2061
|
+
sessionCount = storedSession ?? 0;
|
|
2062
|
+
const storedTotal = sdkInstance.storage.get(totalKey, {
|
|
2063
|
+
backend: "localStorage"
|
|
2064
|
+
});
|
|
2065
|
+
if (storedTotal) {
|
|
2066
|
+
totalCount = storedTotal.count ?? 0;
|
|
2067
|
+
firstVisitTime = storedTotal.first;
|
|
2068
|
+
lastVisitTime = storedTotal.last;
|
|
2069
|
+
isFirstVisitFlag = false;
|
|
2070
|
+
} else {
|
|
2071
|
+
totalCount = 0;
|
|
2072
|
+
firstVisitTime = void 0;
|
|
2073
|
+
lastVisitTime = void 0;
|
|
2074
|
+
isFirstVisitFlag = true;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
function saveData() {
|
|
2078
|
+
const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
|
|
2079
|
+
const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
|
|
2080
|
+
const ttl = config.get("pageVisits.ttl");
|
|
2081
|
+
sdkInstance.storage.set(sessionKey, sessionCount, {
|
|
2082
|
+
backend: "sessionStorage"
|
|
2083
|
+
});
|
|
2084
|
+
const totalData = {
|
|
2085
|
+
count: totalCount,
|
|
2086
|
+
first: firstVisitTime ?? Date.now(),
|
|
2087
|
+
last: lastVisitTime ?? Date.now()
|
|
2088
|
+
};
|
|
2089
|
+
sdkInstance.storage.set(totalKey, totalData, {
|
|
2090
|
+
backend: "localStorage",
|
|
2091
|
+
...ttl && { ttl }
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
function increment() {
|
|
2095
|
+
if (!initialized) {
|
|
2096
|
+
loadData();
|
|
2097
|
+
initialized = true;
|
|
2098
|
+
}
|
|
2099
|
+
sessionCount += 1;
|
|
2100
|
+
totalCount += 1;
|
|
2101
|
+
const now = Date.now();
|
|
2102
|
+
if (isFirstVisitFlag) {
|
|
2103
|
+
firstVisitTime = now;
|
|
2104
|
+
}
|
|
2105
|
+
lastVisitTime = now;
|
|
2106
|
+
saveData();
|
|
2107
|
+
const event = createVisitsEvent(
|
|
2108
|
+
isFirstVisitFlag,
|
|
2109
|
+
totalCount,
|
|
2110
|
+
sessionCount,
|
|
2111
|
+
firstVisitTime,
|
|
2112
|
+
lastVisitTime,
|
|
2113
|
+
now
|
|
2114
|
+
);
|
|
2115
|
+
plugin.emit("pageVisits:incremented", event);
|
|
2116
|
+
if (isFirstVisitFlag) {
|
|
2117
|
+
isFirstVisitFlag = false;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function reset() {
|
|
2121
|
+
const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
|
|
2122
|
+
const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
|
|
2123
|
+
sdkInstance.storage.remove(sessionKey, { backend: "sessionStorage" });
|
|
2124
|
+
sdkInstance.storage.remove(totalKey, { backend: "localStorage" });
|
|
2125
|
+
sessionCount = 0;
|
|
2126
|
+
totalCount = 0;
|
|
2127
|
+
firstVisitTime = void 0;
|
|
2128
|
+
lastVisitTime = void 0;
|
|
2129
|
+
isFirstVisitFlag = false;
|
|
2130
|
+
initialized = false;
|
|
2131
|
+
plugin.emit("pageVisits:reset");
|
|
2132
|
+
}
|
|
2133
|
+
function getState() {
|
|
2134
|
+
return createVisitsEvent(
|
|
2135
|
+
isFirstVisitFlag,
|
|
2136
|
+
totalCount,
|
|
2137
|
+
sessionCount,
|
|
2138
|
+
firstVisitTime,
|
|
2139
|
+
lastVisitTime,
|
|
2140
|
+
Date.now()
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
function initialize() {
|
|
2144
|
+
const enabled = config.get("pageVisits.enabled") ?? true;
|
|
2145
|
+
const respectDNTConfig = config.get("pageVisits.respectDNT") ?? true;
|
|
2146
|
+
const autoIncrement = config.get("pageVisits.autoIncrement") ?? true;
|
|
2147
|
+
if (respectDNTConfig && respectsDNT()) {
|
|
2148
|
+
plugin.emit("pageVisits:disabled", { reason: "dnt" });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (!enabled) {
|
|
2152
|
+
plugin.emit("pageVisits:disabled", { reason: "config" });
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (autoIncrement) {
|
|
2156
|
+
increment();
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
instance.on("sdk:ready", initialize);
|
|
2160
|
+
plugin.expose({
|
|
2161
|
+
pageVisits: {
|
|
2162
|
+
getTotalCount: () => totalCount,
|
|
2163
|
+
getSessionCount: () => sessionCount,
|
|
2164
|
+
isFirstVisit: () => isFirstVisitFlag,
|
|
2165
|
+
getFirstVisitTime: () => firstVisitTime,
|
|
2166
|
+
getLastVisitTime: () => lastVisitTime,
|
|
2167
|
+
increment,
|
|
2168
|
+
reset,
|
|
2169
|
+
getState
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
// src/scroll-depth/scroll-depth.ts
|
|
2175
|
+
function detectDevice() {
|
|
2176
|
+
if (typeof window === "undefined") return "desktop";
|
|
2177
|
+
const ua = navigator.userAgent;
|
|
2178
|
+
const isMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
2179
|
+
const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
|
|
2180
|
+
const width = window.innerWidth;
|
|
2181
|
+
if (width < 768) return "mobile";
|
|
2182
|
+
if (width < 1024) return "tablet";
|
|
2183
|
+
if (isMobile) return "mobile";
|
|
2184
|
+
if (isTablet) return "tablet";
|
|
2185
|
+
return "desktop";
|
|
2186
|
+
}
|
|
2187
|
+
function throttle(func, wait) {
|
|
2188
|
+
let timeout = null;
|
|
2189
|
+
let previous = 0;
|
|
2190
|
+
return function throttled(...args) {
|
|
2191
|
+
const now = Date.now();
|
|
2192
|
+
const remaining = wait - (now - previous);
|
|
2193
|
+
if (remaining <= 0 || remaining > wait) {
|
|
2194
|
+
if (timeout) {
|
|
2195
|
+
clearTimeout(timeout);
|
|
2196
|
+
timeout = null;
|
|
2197
|
+
}
|
|
2198
|
+
previous = now;
|
|
2199
|
+
func(...args);
|
|
2200
|
+
} else if (!timeout) {
|
|
2201
|
+
timeout = setTimeout(() => {
|
|
2202
|
+
previous = Date.now();
|
|
2203
|
+
timeout = null;
|
|
2204
|
+
func(...args);
|
|
2205
|
+
}, remaining);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
function calculateScrollPercent(includeViewportHeight) {
|
|
2210
|
+
if (typeof document === "undefined") return 0;
|
|
2211
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
2212
|
+
const scrollTop = scrollingElement.scrollTop;
|
|
2213
|
+
const scrollHeight = scrollingElement.scrollHeight;
|
|
2214
|
+
const clientHeight = scrollingElement.clientHeight;
|
|
2215
|
+
if (scrollHeight <= clientHeight) {
|
|
2216
|
+
return 100;
|
|
2217
|
+
}
|
|
2218
|
+
if (includeViewportHeight) {
|
|
2219
|
+
return Math.min((scrollTop + clientHeight) / scrollHeight * 100, 100);
|
|
2220
|
+
}
|
|
2221
|
+
return Math.min(scrollTop / (scrollHeight - clientHeight) * 100, 100);
|
|
2222
|
+
}
|
|
2223
|
+
function calculateEngagementScore(velocity, fastScrollThreshold, directionChanges, timeScrollingUp, totalTime) {
|
|
2224
|
+
const velocityScore = Math.min(velocity / fastScrollThreshold * 50, 50);
|
|
2225
|
+
const directionScore = Math.min(directionChanges / 5 * 30, 30);
|
|
2226
|
+
const seekingScore = Math.min(timeScrollingUp / totalTime * 20, 20);
|
|
2227
|
+
return Math.max(0, 100 - (velocityScore + directionScore + seekingScore));
|
|
2228
|
+
}
|
|
2229
|
+
var scrollDepthPlugin = (plugin, instance, config) => {
|
|
2230
|
+
plugin.ns("experiences.scrollDepth");
|
|
2231
|
+
plugin.defaults({
|
|
2232
|
+
scrollDepth: {
|
|
2233
|
+
thresholds: [25, 50, 75, 100],
|
|
2234
|
+
throttle: 100,
|
|
2235
|
+
includeViewportHeight: true,
|
|
2236
|
+
recalculateOnResize: true,
|
|
2237
|
+
trackAdvancedMetrics: false,
|
|
2238
|
+
fastScrollVelocityThreshold: 3,
|
|
2239
|
+
disableOnMobile: false
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
const scrollConfig = config.get("scrollDepth");
|
|
2243
|
+
if (!scrollConfig) return;
|
|
2244
|
+
const cfg = scrollConfig;
|
|
2245
|
+
const device = detectDevice();
|
|
2246
|
+
if (cfg.disableOnMobile && device === "mobile") {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
let maxScrollPercent = 0;
|
|
2250
|
+
const triggeredThresholds = /* @__PURE__ */ new Set();
|
|
2251
|
+
const pageLoadTime = Date.now();
|
|
2252
|
+
let lastScrollPosition = 0;
|
|
2253
|
+
let lastScrollTime = Date.now();
|
|
2254
|
+
let lastScrollDirection = null;
|
|
2255
|
+
let directionChangesSinceLastThreshold = 0;
|
|
2256
|
+
let timeScrollingUp = 0;
|
|
2257
|
+
const thresholdTimes = /* @__PURE__ */ new Map();
|
|
2258
|
+
function handleScroll() {
|
|
2259
|
+
const currentPercent = calculateScrollPercent(cfg.includeViewportHeight ?? true);
|
|
2260
|
+
const now = Date.now();
|
|
2261
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
2262
|
+
const currentPosition = scrollingElement.scrollTop;
|
|
2263
|
+
let velocity = 0;
|
|
2264
|
+
if (cfg.trackAdvancedMetrics) {
|
|
2265
|
+
const timeDelta = now - lastScrollTime;
|
|
2266
|
+
const positionDelta = currentPosition - lastScrollPosition;
|
|
2267
|
+
velocity = timeDelta > 0 ? Math.abs(positionDelta) / timeDelta : 0;
|
|
2268
|
+
const currentDirection = positionDelta > 0 ? "down" : positionDelta < 0 ? "up" : lastScrollDirection;
|
|
2269
|
+
if (currentDirection && lastScrollDirection && currentDirection !== lastScrollDirection) {
|
|
2270
|
+
directionChangesSinceLastThreshold++;
|
|
2271
|
+
}
|
|
2272
|
+
if (currentDirection === "up" && timeDelta > 0) {
|
|
2273
|
+
timeScrollingUp += timeDelta;
|
|
2274
|
+
}
|
|
2275
|
+
lastScrollDirection = currentDirection;
|
|
2276
|
+
lastScrollPosition = currentPosition;
|
|
2277
|
+
lastScrollTime = now;
|
|
2278
|
+
}
|
|
2279
|
+
maxScrollPercent = Math.max(maxScrollPercent, currentPercent);
|
|
2280
|
+
for (const threshold of cfg.thresholds || []) {
|
|
2281
|
+
if (currentPercent >= threshold && !triggeredThresholds.has(threshold)) {
|
|
2282
|
+
triggeredThresholds.add(threshold);
|
|
2283
|
+
if (cfg.trackAdvancedMetrics) {
|
|
2284
|
+
thresholdTimes.set(threshold, now - pageLoadTime);
|
|
2285
|
+
}
|
|
2286
|
+
const eventPayload = {
|
|
2287
|
+
triggered: true,
|
|
2288
|
+
timestamp: now,
|
|
2289
|
+
percent: Math.round(currentPercent * 100) / 100,
|
|
2290
|
+
maxPercent: Math.round(maxScrollPercent * 100) / 100,
|
|
2291
|
+
threshold,
|
|
2292
|
+
thresholdsCrossed: Array.from(triggeredThresholds).sort((a, b) => a - b),
|
|
2293
|
+
device
|
|
2294
|
+
};
|
|
2295
|
+
if (cfg.trackAdvancedMetrics) {
|
|
2296
|
+
const fastScrollThreshold = cfg.fastScrollVelocityThreshold || 3;
|
|
2297
|
+
const isFastScrolling = velocity > fastScrollThreshold;
|
|
2298
|
+
const engagementScore = calculateEngagementScore(
|
|
2299
|
+
velocity,
|
|
2300
|
+
fastScrollThreshold,
|
|
2301
|
+
directionChangesSinceLastThreshold,
|
|
2302
|
+
timeScrollingUp,
|
|
2303
|
+
now - pageLoadTime
|
|
2304
|
+
);
|
|
2305
|
+
eventPayload.advanced = {
|
|
2306
|
+
timeToThreshold: now - pageLoadTime,
|
|
2307
|
+
velocity: Math.round(velocity * 1e3) / 1e3,
|
|
2308
|
+
// Round to 3 decimals
|
|
2309
|
+
isFastScrolling,
|
|
2310
|
+
directionChanges: directionChangesSinceLastThreshold,
|
|
2311
|
+
timeScrollingUp,
|
|
2312
|
+
engagementScore: Math.round(engagementScore)
|
|
2313
|
+
};
|
|
2314
|
+
directionChangesSinceLastThreshold = 0;
|
|
2315
|
+
}
|
|
2316
|
+
instance.emit("trigger:scrollDepth", eventPayload);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const throttledScrollHandler = throttle(handleScroll, cfg.throttle || 100);
|
|
2321
|
+
const throttledResizeHandler = throttle(handleScroll, cfg.throttle || 100);
|
|
2322
|
+
function initialize() {
|
|
2323
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
window.addEventListener("scroll", throttledScrollHandler, { passive: true });
|
|
2327
|
+
if (cfg.recalculateOnResize) {
|
|
2328
|
+
window.addEventListener("resize", throttledResizeHandler, { passive: true });
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function cleanup() {
|
|
2332
|
+
window.removeEventListener("scroll", throttledScrollHandler);
|
|
2333
|
+
window.removeEventListener("resize", throttledResizeHandler);
|
|
2334
|
+
}
|
|
2335
|
+
instance.on("sdk:destroy", () => {
|
|
2336
|
+
cleanup();
|
|
2337
|
+
});
|
|
2338
|
+
plugin.expose({
|
|
2339
|
+
scrollDepth: {
|
|
2340
|
+
/**
|
|
2341
|
+
* Get the maximum scroll percentage reached during the session
|
|
2342
|
+
*/
|
|
2343
|
+
getMaxPercent: () => maxScrollPercent,
|
|
2344
|
+
/**
|
|
2345
|
+
* Get the current scroll percentage
|
|
2346
|
+
*/
|
|
2347
|
+
getCurrentPercent: () => calculateScrollPercent(cfg.includeViewportHeight ?? true),
|
|
2348
|
+
/**
|
|
2349
|
+
* Get all thresholds that have been crossed
|
|
2350
|
+
*/
|
|
2351
|
+
getThresholdsCrossed: () => Array.from(triggeredThresholds).sort((a, b) => a - b),
|
|
2352
|
+
/**
|
|
2353
|
+
* Get the detected device type
|
|
2354
|
+
*/
|
|
2355
|
+
getDevice: () => device,
|
|
2356
|
+
/**
|
|
2357
|
+
* Get advanced metrics (only available when trackAdvancedMetrics is enabled)
|
|
2358
|
+
*/
|
|
2359
|
+
getAdvancedMetrics: () => {
|
|
2360
|
+
if (!cfg.trackAdvancedMetrics) return null;
|
|
2361
|
+
const now = Date.now();
|
|
2362
|
+
return {
|
|
2363
|
+
timeOnPage: now - pageLoadTime,
|
|
2364
|
+
directionChanges: directionChangesSinceLastThreshold,
|
|
2365
|
+
timeScrollingUp,
|
|
2366
|
+
thresholdTimes: Object.fromEntries(thresholdTimes)
|
|
2367
|
+
};
|
|
2368
|
+
},
|
|
2369
|
+
/**
|
|
2370
|
+
* Reset scroll depth tracking
|
|
2371
|
+
* Clears all triggered thresholds, max scroll, and advanced metrics
|
|
2372
|
+
*/
|
|
2373
|
+
reset: () => {
|
|
2374
|
+
maxScrollPercent = 0;
|
|
2375
|
+
triggeredThresholds.clear();
|
|
2376
|
+
directionChangesSinceLastThreshold = 0;
|
|
2377
|
+
timeScrollingUp = 0;
|
|
2378
|
+
thresholdTimes.clear();
|
|
2379
|
+
lastScrollDirection = null;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
if (typeof window !== "undefined") {
|
|
2384
|
+
setTimeout(initialize, 0);
|
|
2385
|
+
}
|
|
2386
|
+
return () => {
|
|
2387
|
+
cleanup();
|
|
2388
|
+
};
|
|
2389
|
+
};
|
|
2390
|
+
|
|
2391
|
+
// src/time-delay/time-delay.ts
|
|
2392
|
+
function calculateElapsed(startTime, pausedDuration) {
|
|
2393
|
+
return Date.now() - startTime - pausedDuration;
|
|
2394
|
+
}
|
|
2395
|
+
function isDocumentHidden() {
|
|
2396
|
+
if (typeof document === "undefined") return false;
|
|
2397
|
+
return document.hidden || false;
|
|
2398
|
+
}
|
|
2399
|
+
function createTimeDelayEvent(startTime, pausedDuration, wasPaused, visibilityChanges) {
|
|
2400
|
+
const timestamp = Date.now();
|
|
2401
|
+
const elapsed = timestamp - startTime;
|
|
2402
|
+
const activeElapsed = elapsed - pausedDuration;
|
|
2403
|
+
return {
|
|
2404
|
+
timestamp,
|
|
2405
|
+
elapsed,
|
|
2406
|
+
activeElapsed,
|
|
2407
|
+
wasPaused,
|
|
2408
|
+
visibilityChanges
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
var timeDelayPlugin = (plugin, instance, config) => {
|
|
2412
|
+
plugin.ns("experiences.timeDelay");
|
|
2413
|
+
plugin.defaults({
|
|
2414
|
+
timeDelay: {
|
|
2415
|
+
delay: 0,
|
|
2416
|
+
pauseWhenHidden: true
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
const timeDelayConfig = config.get("timeDelay");
|
|
2420
|
+
if (!timeDelayConfig) return;
|
|
2421
|
+
const delay = timeDelayConfig.delay ?? 0;
|
|
2422
|
+
const pauseWhenHidden = timeDelayConfig.pauseWhenHidden ?? true;
|
|
2423
|
+
if (delay <= 0) return;
|
|
2424
|
+
const startTime = Date.now();
|
|
2425
|
+
let triggered = false;
|
|
2426
|
+
let paused = false;
|
|
2427
|
+
let pausedDuration = 0;
|
|
2428
|
+
let lastPauseTime = 0;
|
|
2429
|
+
let visibilityChanges = 0;
|
|
2430
|
+
let timer = null;
|
|
2431
|
+
let visibilityListener = null;
|
|
2432
|
+
function trigger() {
|
|
2433
|
+
if (triggered) return;
|
|
2434
|
+
triggered = true;
|
|
2435
|
+
const eventPayload = createTimeDelayEvent(
|
|
2436
|
+
startTime,
|
|
2437
|
+
pausedDuration,
|
|
2438
|
+
visibilityChanges > 0,
|
|
2439
|
+
visibilityChanges
|
|
2440
|
+
);
|
|
2441
|
+
instance.emit("trigger:timeDelay", eventPayload);
|
|
2442
|
+
cleanup();
|
|
2443
|
+
}
|
|
2444
|
+
function scheduleTimer(remainingDelay) {
|
|
2445
|
+
if (timer) {
|
|
2446
|
+
clearTimeout(timer);
|
|
2447
|
+
}
|
|
2448
|
+
timer = setTimeout(() => {
|
|
2449
|
+
trigger();
|
|
2450
|
+
}, remainingDelay);
|
|
2451
|
+
}
|
|
2452
|
+
function handleVisibilityChange() {
|
|
2453
|
+
const hidden = isDocumentHidden();
|
|
2454
|
+
if (hidden && !paused) {
|
|
2455
|
+
paused = true;
|
|
2456
|
+
lastPauseTime = Date.now();
|
|
2457
|
+
visibilityChanges++;
|
|
2458
|
+
if (timer) {
|
|
2459
|
+
clearTimeout(timer);
|
|
2460
|
+
timer = null;
|
|
2461
|
+
}
|
|
2462
|
+
} else if (!hidden && paused) {
|
|
2463
|
+
paused = false;
|
|
2464
|
+
const pauseDuration = Date.now() - lastPauseTime;
|
|
2465
|
+
pausedDuration += pauseDuration;
|
|
2466
|
+
visibilityChanges++;
|
|
2467
|
+
const elapsed = calculateElapsed(startTime, pausedDuration);
|
|
2468
|
+
const remaining = delay - elapsed;
|
|
2469
|
+
if (remaining > 0) {
|
|
2470
|
+
scheduleTimer(remaining);
|
|
2471
|
+
} else {
|
|
2472
|
+
trigger();
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
function cleanup() {
|
|
2477
|
+
if (timer) {
|
|
2478
|
+
clearTimeout(timer);
|
|
2479
|
+
timer = null;
|
|
2480
|
+
}
|
|
2481
|
+
if (visibilityListener && typeof document !== "undefined") {
|
|
2482
|
+
document.removeEventListener("visibilitychange", visibilityListener);
|
|
2483
|
+
visibilityListener = null;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
function initialize() {
|
|
2487
|
+
if (pauseWhenHidden && isDocumentHidden()) {
|
|
2488
|
+
paused = true;
|
|
2489
|
+
lastPauseTime = Date.now();
|
|
2490
|
+
visibilityChanges++;
|
|
2491
|
+
} else {
|
|
2492
|
+
scheduleTimer(delay);
|
|
2493
|
+
}
|
|
2494
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
2495
|
+
visibilityListener = handleVisibilityChange;
|
|
2496
|
+
document.addEventListener("visibilitychange", visibilityListener);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
plugin.expose({
|
|
2500
|
+
timeDelay: {
|
|
2501
|
+
getElapsed: () => {
|
|
2502
|
+
return Date.now() - startTime;
|
|
2503
|
+
},
|
|
2504
|
+
getActiveElapsed: () => {
|
|
2505
|
+
let currentPausedDuration = pausedDuration;
|
|
2506
|
+
if (paused) {
|
|
2507
|
+
currentPausedDuration += Date.now() - lastPauseTime;
|
|
2508
|
+
}
|
|
2509
|
+
return calculateElapsed(startTime, currentPausedDuration);
|
|
2510
|
+
},
|
|
2511
|
+
getRemaining: () => {
|
|
2512
|
+
if (triggered) return 0;
|
|
2513
|
+
const elapsed = calculateElapsed(startTime, pausedDuration);
|
|
2514
|
+
const remaining = delay - elapsed;
|
|
2515
|
+
return Math.max(0, remaining);
|
|
2516
|
+
},
|
|
2517
|
+
isPaused: () => paused,
|
|
2518
|
+
isTriggered: () => triggered,
|
|
2519
|
+
reset: () => {
|
|
2520
|
+
triggered = false;
|
|
2521
|
+
paused = false;
|
|
2522
|
+
pausedDuration = 0;
|
|
2523
|
+
lastPauseTime = 0;
|
|
2524
|
+
visibilityChanges = 0;
|
|
2525
|
+
cleanup();
|
|
2526
|
+
initialize();
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
initialize();
|
|
2531
|
+
instance.on("sdk:destroy", () => {
|
|
2532
|
+
cleanup();
|
|
2533
|
+
});
|
|
2534
|
+
};
|
|
2535
|
+
|
|
2536
|
+
export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };
|
|
693
2537
|
//# sourceMappingURL=index.js.map
|
|
694
2538
|
//# sourceMappingURL=index.js.map
|