@prosdevlab/experience-sdk-plugins 0.1.3 → 0.2.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/dist/index.js CHANGED
@@ -42,7 +42,7 @@ function sanitizeHTML(html) {
42
42
  }
43
43
  }
44
44
  }
45
- const attrString = attrs.length > 0 ? " " + attrs.join(" ") : "";
45
+ const attrString = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
46
46
  let innerHTML = "";
47
47
  for (const child of Array.from(element.childNodes)) {
48
48
  innerHTML += sanitizeNode(child);
@@ -115,16 +115,12 @@ var bannerPlugin = (plugin, instance, config) => {
115
115
  left: 0;
116
116
  right: 0;
117
117
  width: 100%;
118
- padding: 16px 20px;
119
118
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
120
119
  font-size: 14px;
121
120
  line-height: 1.5;
122
- display: flex;
123
- align-items: center;
124
- justify-content: space-between;
125
121
  box-sizing: border-box;
126
122
  z-index: 10000;
127
- background: #f9fafb;
123
+ background: #ffffff;
128
124
  color: #111827;
129
125
  border-bottom: 1px solid #e5e7eb;
130
126
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
@@ -144,33 +140,38 @@ var bannerPlugin = (plugin, instance, config) => {
144
140
  .xp-banner__container {
145
141
  display: flex;
146
142
  align-items: center;
147
- justify-content: space-between;
148
- gap: 20px;
149
- width: 100%;
143
+ gap: 16px;
144
+ max-width: 1280px;
145
+ margin: 0 auto;
146
+ padding: 14px 24px;
150
147
  }
151
148
 
152
149
  .xp-banner__content {
153
150
  flex: 1;
154
151
  min-width: 0;
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 4px;
155
155
  }
156
156
 
157
157
  .xp-banner__title {
158
158
  font-weight: 600;
159
- margin-bottom: 4px;
160
- margin-top: 0;
161
- font-size: 14px;
159
+ margin: 0;
160
+ font-size: 15px;
161
+ line-height: 1.4;
162
162
  }
163
163
 
164
164
  .xp-banner__message {
165
165
  margin: 0;
166
166
  font-size: 14px;
167
+ line-height: 1.5;
168
+ color: #6b7280;
167
169
  }
168
170
 
169
171
  .xp-banner__buttons {
170
172
  display: flex;
171
173
  align-items: center;
172
- gap: 12px;
173
- flex-wrap: wrap;
174
+ gap: 8px;
174
175
  flex-shrink: 0;
175
176
  }
176
177
 
@@ -183,6 +184,10 @@ var bannerPlugin = (plugin, instance, config) => {
183
184
  cursor: pointer;
184
185
  transition: all 0.2s;
185
186
  text-decoration: none;
187
+ display: inline-flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ white-space: nowrap;
186
191
  }
187
192
 
188
193
  .xp-banner__button--primary {
@@ -195,71 +200,93 @@ var bannerPlugin = (plugin, instance, config) => {
195
200
  }
196
201
 
197
202
  .xp-banner__button--secondary {
198
- background: #ffffff;
203
+ background: #f3f4f6;
199
204
  color: #374151;
200
- border: 1px solid #d1d5db;
205
+ border: 1px solid #e5e7eb;
201
206
  }
202
207
 
203
208
  .xp-banner__button--secondary:hover {
204
- background: #f9fafb;
209
+ background: #e5e7eb;
205
210
  }
206
211
 
207
212
  .xp-banner__button--link {
208
213
  background: transparent;
209
214
  color: #2563eb;
210
- padding: 4px 8px;
215
+ padding: 6px 12px;
211
216
  font-weight: 400;
212
- text-decoration: underline;
213
217
  }
214
218
 
215
219
  .xp-banner__button--link:hover {
216
- background: rgba(0, 0, 0, 0.05);
220
+ background: #f3f4f6;
221
+ text-decoration: underline;
217
222
  }
218
223
 
219
224
  .xp-banner__close {
220
225
  background: transparent;
221
226
  border: none;
222
- color: #6b7280;
223
- font-size: 24px;
227
+ color: #9ca3af;
228
+ font-size: 20px;
224
229
  line-height: 1;
225
230
  cursor: pointer;
226
- padding: 0;
231
+ padding: 4px;
227
232
  margin: 0;
228
- opacity: 0.7;
229
- transition: opacity 0.2s;
233
+ transition: color 0.2s;
230
234
  flex-shrink: 0;
235
+ width: 28px;
236
+ height: 28px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ border-radius: 4px;
231
241
  }
232
242
 
233
243
  .xp-banner__close:hover {
234
- opacity: 1;
244
+ color: #111827;
245
+ background: #f3f4f6;
235
246
  }
236
247
 
237
248
  @media (max-width: 640px) {
238
249
  .xp-banner__container {
239
- flex-direction: column;
240
- align-items: stretch;
250
+ flex-wrap: wrap;
251
+ padding: 14px 16px;
252
+ position: relative;
253
+ }
254
+
255
+ .xp-banner__content {
256
+ flex: 1 1 100%;
257
+ padding-right: 32px;
241
258
  }
242
259
 
243
260
  .xp-banner__buttons {
261
+ flex: 1 1 auto;
244
262
  width: 100%;
245
- flex-direction: column;
246
263
  }
247
264
 
248
265
  .xp-banner__button {
249
- width: 100%;
266
+ flex: 1;
267
+ }
268
+
269
+ .xp-banner__close {
270
+ position: absolute;
271
+ top: 12px;
272
+ right: 12px;
250
273
  }
251
274
  }
252
275
 
253
276
  /* Dark mode support */
254
277
  @media (prefers-color-scheme: dark) {
255
278
  .xp-banner {
256
- background: #1f2937;
257
- color: #f3f4f6;
258
- border-bottom-color: #374151;
279
+ background: #111827;
280
+ color: #f9fafb;
281
+ border-bottom-color: #1f2937;
259
282
  }
260
283
 
261
284
  .xp-banner--bottom {
262
- border-top-color: #374151;
285
+ border-top-color: #1f2937;
286
+ }
287
+
288
+ .xp-banner__message {
289
+ color: #9ca3af;
263
290
  }
264
291
 
265
292
  .xp-banner__button--primary {
@@ -271,21 +298,30 @@ var bannerPlugin = (plugin, instance, config) => {
271
298
  }
272
299
 
273
300
  .xp-banner__button--secondary {
274
- background: #374151;
275
- color: #f3f4f6;
276
- border-color: #4b5563;
301
+ background: #1f2937;
302
+ color: #f9fafb;
303
+ border-color: #374151;
277
304
  }
278
305
 
279
306
  .xp-banner__button--secondary:hover {
280
- background: #4b5563;
307
+ background: #374151;
281
308
  }
282
309
 
283
310
  .xp-banner__button--link {
284
- color: #93c5fd;
311
+ color: #60a5fa;
312
+ }
313
+
314
+ .xp-banner__button--link:hover {
315
+ background: #1f2937;
285
316
  }
286
317
 
287
318
  .xp-banner__close {
288
- color: #9ca3af;
319
+ color: #6b7280;
320
+ }
321
+
322
+ .xp-banner__close:hover {
323
+ color: #f9fafb;
324
+ background: #1f2937;
289
325
  }
290
326
  }
291
327
  `;
@@ -326,14 +362,6 @@ var bannerPlugin = (plugin, instance, config) => {
326
362
  message.innerHTML = sanitizeHTML(content.message);
327
363
  contentDiv.appendChild(message);
328
364
  container.appendChild(contentDiv);
329
- banner.appendChild(contentDiv);
330
- const buttonContainer = document.createElement("div");
331
- buttonContainer.style.cssText = `
332
- display: flex;
333
- align-items: center;
334
- gap: 12px;
335
- flex-wrap: wrap;
336
- `;
337
365
  const buttonsDiv = document.createElement("div");
338
366
  buttonsDiv.className = "xp-banner__buttons";
339
367
  function createButton(buttonConfig) {
@@ -387,6 +415,31 @@ var bannerPlugin = (plugin, instance, config) => {
387
415
  container.appendChild(buttonsDiv);
388
416
  return banner;
389
417
  }
418
+ function applyPushDown(banner, position) {
419
+ const pushDownSelector = config.get("banner.pushDown");
420
+ if (!pushDownSelector || position !== "top") {
421
+ return;
422
+ }
423
+ const targetElement = document.querySelector(pushDownSelector);
424
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
425
+ return;
426
+ }
427
+ const height = banner.offsetHeight;
428
+ targetElement.style.transition = "margin-top 0.3s ease";
429
+ targetElement.style.marginTop = `${height}px`;
430
+ }
431
+ function removePushDown() {
432
+ const pushDownSelector = config.get("banner.pushDown");
433
+ if (!pushDownSelector) {
434
+ return;
435
+ }
436
+ const targetElement = document.querySelector(pushDownSelector);
437
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
438
+ return;
439
+ }
440
+ targetElement.style.transition = "margin-top 0.3s ease";
441
+ targetElement.style.marginTop = "0";
442
+ }
390
443
  function show(experience) {
391
444
  if (activeBanners.has(experience.id)) {
392
445
  return;
@@ -397,6 +450,9 @@ var bannerPlugin = (plugin, instance, config) => {
397
450
  const banner = createBannerElement(experience);
398
451
  document.body.appendChild(banner);
399
452
  activeBanners.set(experience.id, banner);
453
+ const content = experience.content;
454
+ const position = content.position ?? config.get("banner.position") ?? "top";
455
+ applyPushDown(banner, position);
400
456
  instance.emit("experiences:shown", {
401
457
  experienceId: experience.id,
402
458
  type: "banner",
@@ -410,6 +466,9 @@ var bannerPlugin = (plugin, instance, config) => {
410
466
  banner.parentNode.removeChild(banner);
411
467
  }
412
468
  activeBanners.delete(experienceId);
469
+ if (activeBanners.size === 0) {
470
+ removePushDown();
471
+ }
413
472
  } else {
414
473
  for (const [id, banner] of activeBanners.entries()) {
415
474
  if (banner?.parentNode) {
@@ -417,6 +476,7 @@ var bannerPlugin = (plugin, instance, config) => {
417
476
  }
418
477
  activeBanners.delete(id);
419
478
  }
479
+ removePushDown();
420
480
  }
421
481
  }
422
482
  function isShowing() {
@@ -501,6 +561,176 @@ var debugPlugin = (plugin, instance, config) => {
501
561
  });
502
562
  }
503
563
  };
564
+
565
+ // src/exit-intent/exit-intent.ts
566
+ function isMobileDevice(userAgent) {
567
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
568
+ }
569
+ function hasMinTimeElapsed(pageLoadTime, minTime, currentTime) {
570
+ return currentTime - pageLoadTime >= minTime;
571
+ }
572
+ function addPositionToHistory(positions, newPosition, maxSize) {
573
+ const updated = [...positions, newPosition];
574
+ if (updated.length > maxSize) {
575
+ return updated.slice(1);
576
+ }
577
+ return updated;
578
+ }
579
+ function calculateVelocity(lastY, previousY) {
580
+ return Math.abs(lastY - previousY);
581
+ }
582
+ function shouldTriggerExitIntent(positions, sensitivity, relatedTarget) {
583
+ if (positions.length < 2) {
584
+ return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
585
+ }
586
+ if (relatedTarget && relatedTarget.nodeName !== "HTML") {
587
+ return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
588
+ }
589
+ const lastY = positions[positions.length - 1].y;
590
+ const previousY = positions[positions.length - 2].y;
591
+ const velocity = calculateVelocity(lastY, previousY);
592
+ const isMovingUp = lastY < previousY;
593
+ const isNearTop = lastY - velocity <= sensitivity;
594
+ return {
595
+ shouldTrigger: isMovingUp && isNearTop,
596
+ lastY,
597
+ previousY,
598
+ velocity
599
+ };
600
+ }
601
+ function createExitIntentEvent(lastY, previousY, velocity, pageLoadTime, timestamp) {
602
+ return {
603
+ timestamp,
604
+ lastY,
605
+ previousY,
606
+ velocity,
607
+ timeOnPage: timestamp - pageLoadTime
608
+ };
609
+ }
610
+ var exitIntentPlugin = (plugin, instance, config) => {
611
+ plugin.ns("experiences.exitIntent");
612
+ plugin.defaults({
613
+ exitIntent: {
614
+ sensitivity: 50,
615
+ minTimeOnPage: 2e3,
616
+ delay: 0,
617
+ positionHistorySize: 30,
618
+ disableOnMobile: true
619
+ }
620
+ });
621
+ const exitIntentConfig = config.get("exitIntent");
622
+ if (!exitIntentConfig) {
623
+ return;
624
+ }
625
+ let positions = [];
626
+ let triggered = false;
627
+ const pageLoadTime = Date.now();
628
+ let mouseMoveListener = null;
629
+ let mouseOutListener = null;
630
+ function shouldDisable() {
631
+ if (!exitIntentConfig?.disableOnMobile) {
632
+ return false;
633
+ }
634
+ return isMobileDevice(navigator.userAgent);
635
+ }
636
+ function trackPosition(e) {
637
+ const newPosition = { x: e.clientX, y: e.clientY };
638
+ const maxSize = exitIntentConfig?.positionHistorySize ?? 30;
639
+ positions = addPositionToHistory(positions, newPosition, maxSize);
640
+ }
641
+ function handleExitIntent(e) {
642
+ if (triggered) {
643
+ return;
644
+ }
645
+ const minTime = exitIntentConfig?.minTimeOnPage ?? 2e3;
646
+ if (!hasMinTimeElapsed(pageLoadTime, minTime, Date.now())) {
647
+ return;
648
+ }
649
+ const sensitivity = exitIntentConfig?.sensitivity ?? 50;
650
+ const relatedTarget = e.relatedTarget || e.toElement;
651
+ const result = shouldTriggerExitIntent(positions, sensitivity, relatedTarget);
652
+ if (result.shouldTrigger) {
653
+ triggered = true;
654
+ const eventPayload = createExitIntentEvent(
655
+ result.lastY,
656
+ result.previousY,
657
+ result.velocity,
658
+ pageLoadTime,
659
+ Date.now()
660
+ );
661
+ const delay = exitIntentConfig?.delay ?? 0;
662
+ if (delay > 0) {
663
+ setTimeout(() => {
664
+ instance.emit("trigger:exitIntent", eventPayload);
665
+ }, delay);
666
+ } else {
667
+ instance.emit("trigger:exitIntent", eventPayload);
668
+ }
669
+ try {
670
+ sessionStorage.setItem("xp:exitIntent:triggered", Date.now().toString());
671
+ } catch (_e) {
672
+ }
673
+ cleanup();
674
+ }
675
+ }
676
+ function cleanup() {
677
+ if (mouseMoveListener) {
678
+ document.removeEventListener("mousemove", mouseMoveListener);
679
+ mouseMoveListener = null;
680
+ }
681
+ if (mouseOutListener) {
682
+ document.removeEventListener("mouseout", mouseOutListener);
683
+ mouseOutListener = null;
684
+ }
685
+ }
686
+ function initialize() {
687
+ if (shouldDisable()) {
688
+ return;
689
+ }
690
+ try {
691
+ const storedTrigger = sessionStorage.getItem("xp:exitIntent:triggered");
692
+ if (storedTrigger) {
693
+ triggered = true;
694
+ return;
695
+ }
696
+ } catch (_e) {
697
+ }
698
+ mouseMoveListener = trackPosition;
699
+ mouseOutListener = handleExitIntent;
700
+ document.addEventListener("mousemove", mouseMoveListener);
701
+ document.addEventListener("mouseout", mouseOutListener);
702
+ }
703
+ plugin.expose({
704
+ exitIntent: {
705
+ /**
706
+ * Check if exit intent has been triggered
707
+ */
708
+ isTriggered: () => triggered,
709
+ /**
710
+ * Reset exit intent state (useful for testing)
711
+ */
712
+ reset: () => {
713
+ triggered = false;
714
+ positions = [];
715
+ try {
716
+ sessionStorage.removeItem("xp:exitIntent:triggered");
717
+ } catch (_e) {
718
+ }
719
+ cleanup();
720
+ initialize();
721
+ },
722
+ /**
723
+ * Get current position history
724
+ */
725
+ getPositions: () => [...positions]
726
+ }
727
+ });
728
+ initialize();
729
+ const destroyHandler = () => {
730
+ cleanup();
731
+ };
732
+ instance.on("destroy", destroyHandler);
733
+ };
504
734
  var frequencyPlugin = (plugin, instance, config) => {
505
735
  plugin.ns("frequency");
506
736
  plugin.defaults({
@@ -628,7 +858,527 @@ var frequencyPlugin = (plugin, instance, config) => {
628
858
  });
629
859
  }
630
860
  };
861
+ function respectsDNT() {
862
+ if (typeof navigator === "undefined") return false;
863
+ return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
864
+ }
865
+ function createVisitsEvent(isFirstVisit, totalVisits, sessionVisits, firstVisitTime, lastVisitTime, timestamp) {
866
+ return {
867
+ isFirstVisit,
868
+ totalVisits,
869
+ sessionVisits,
870
+ firstVisitTime,
871
+ lastVisitTime,
872
+ timestamp
873
+ };
874
+ }
875
+ var pageVisitsPlugin = (plugin, instance, config) => {
876
+ plugin.ns("pageVisits");
877
+ plugin.defaults({
878
+ pageVisits: {
879
+ enabled: true,
880
+ respectDNT: true,
881
+ sessionKey: "pageVisits:session",
882
+ totalKey: "pageVisits:total",
883
+ ttl: void 0,
884
+ autoIncrement: true
885
+ }
886
+ });
887
+ if (!instance.storage) {
888
+ console.warn("[PageVisits] Storage plugin not found, auto-loading...");
889
+ instance.use(storagePlugin);
890
+ }
891
+ const sdkInstance = instance;
892
+ let sessionCount = 0;
893
+ let totalCount = 0;
894
+ let firstVisitTime;
895
+ let lastVisitTime;
896
+ let isFirstVisitFlag = false;
897
+ let initialized = false;
898
+ function loadData() {
899
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
900
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
901
+ const storedSession = sdkInstance.storage.get(sessionKey, {
902
+ backend: "sessionStorage"
903
+ });
904
+ sessionCount = storedSession ?? 0;
905
+ const storedTotal = sdkInstance.storage.get(totalKey, {
906
+ backend: "localStorage"
907
+ });
908
+ if (storedTotal) {
909
+ totalCount = storedTotal.count ?? 0;
910
+ firstVisitTime = storedTotal.first;
911
+ lastVisitTime = storedTotal.last;
912
+ isFirstVisitFlag = false;
913
+ } else {
914
+ totalCount = 0;
915
+ firstVisitTime = void 0;
916
+ lastVisitTime = void 0;
917
+ isFirstVisitFlag = true;
918
+ }
919
+ }
920
+ function saveData() {
921
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
922
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
923
+ const ttl = config.get("pageVisits.ttl");
924
+ sdkInstance.storage.set(sessionKey, sessionCount, {
925
+ backend: "sessionStorage"
926
+ });
927
+ const totalData = {
928
+ count: totalCount,
929
+ first: firstVisitTime ?? Date.now(),
930
+ last: lastVisitTime ?? Date.now()
931
+ };
932
+ sdkInstance.storage.set(totalKey, totalData, {
933
+ backend: "localStorage",
934
+ ...ttl && { ttl }
935
+ });
936
+ }
937
+ function increment() {
938
+ if (!initialized) {
939
+ loadData();
940
+ initialized = true;
941
+ }
942
+ sessionCount += 1;
943
+ totalCount += 1;
944
+ const now = Date.now();
945
+ if (isFirstVisitFlag) {
946
+ firstVisitTime = now;
947
+ }
948
+ lastVisitTime = now;
949
+ saveData();
950
+ const event = createVisitsEvent(
951
+ isFirstVisitFlag,
952
+ totalCount,
953
+ sessionCount,
954
+ firstVisitTime,
955
+ lastVisitTime,
956
+ now
957
+ );
958
+ plugin.emit("pageVisits:incremented", event);
959
+ if (isFirstVisitFlag) {
960
+ isFirstVisitFlag = false;
961
+ }
962
+ }
963
+ function reset() {
964
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
965
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
966
+ sdkInstance.storage.remove(sessionKey, { backend: "sessionStorage" });
967
+ sdkInstance.storage.remove(totalKey, { backend: "localStorage" });
968
+ sessionCount = 0;
969
+ totalCount = 0;
970
+ firstVisitTime = void 0;
971
+ lastVisitTime = void 0;
972
+ isFirstVisitFlag = false;
973
+ initialized = false;
974
+ plugin.emit("pageVisits:reset");
975
+ }
976
+ function getState() {
977
+ return createVisitsEvent(
978
+ isFirstVisitFlag,
979
+ totalCount,
980
+ sessionCount,
981
+ firstVisitTime,
982
+ lastVisitTime,
983
+ Date.now()
984
+ );
985
+ }
986
+ function initialize() {
987
+ const enabled = config.get("pageVisits.enabled") ?? true;
988
+ const respectDNTConfig = config.get("pageVisits.respectDNT") ?? true;
989
+ const autoIncrement = config.get("pageVisits.autoIncrement") ?? true;
990
+ if (respectDNTConfig && respectsDNT()) {
991
+ plugin.emit("pageVisits:disabled", { reason: "dnt" });
992
+ return;
993
+ }
994
+ if (!enabled) {
995
+ plugin.emit("pageVisits:disabled", { reason: "config" });
996
+ return;
997
+ }
998
+ if (autoIncrement) {
999
+ increment();
1000
+ }
1001
+ }
1002
+ instance.on("sdk:ready", initialize);
1003
+ plugin.expose({
1004
+ pageVisits: {
1005
+ getTotalCount: () => totalCount,
1006
+ getSessionCount: () => sessionCount,
1007
+ isFirstVisit: () => isFirstVisitFlag,
1008
+ getFirstVisitTime: () => firstVisitTime,
1009
+ getLastVisitTime: () => lastVisitTime,
1010
+ increment,
1011
+ reset,
1012
+ getState
1013
+ }
1014
+ });
1015
+ };
1016
+
1017
+ // src/scroll-depth/scroll-depth.ts
1018
+ function detectDevice() {
1019
+ if (typeof window === "undefined") return "desktop";
1020
+ const ua = navigator.userAgent;
1021
+ const isMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
1022
+ const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
1023
+ const width = window.innerWidth;
1024
+ if (width < 768) return "mobile";
1025
+ if (width < 1024) return "tablet";
1026
+ if (isMobile) return "mobile";
1027
+ if (isTablet) return "tablet";
1028
+ return "desktop";
1029
+ }
1030
+ function throttle(func, wait) {
1031
+ let timeout = null;
1032
+ let previous = 0;
1033
+ return function throttled(...args) {
1034
+ const now = Date.now();
1035
+ const remaining = wait - (now - previous);
1036
+ if (remaining <= 0 || remaining > wait) {
1037
+ if (timeout) {
1038
+ clearTimeout(timeout);
1039
+ timeout = null;
1040
+ }
1041
+ previous = now;
1042
+ func(...args);
1043
+ } else if (!timeout) {
1044
+ timeout = setTimeout(() => {
1045
+ previous = Date.now();
1046
+ timeout = null;
1047
+ func(...args);
1048
+ }, remaining);
1049
+ }
1050
+ };
1051
+ }
1052
+ function calculateScrollPercent(includeViewportHeight) {
1053
+ if (typeof document === "undefined") return 0;
1054
+ const scrollingElement = document.scrollingElement || document.documentElement;
1055
+ const scrollTop = scrollingElement.scrollTop;
1056
+ const scrollHeight = scrollingElement.scrollHeight;
1057
+ const clientHeight = scrollingElement.clientHeight;
1058
+ if (scrollHeight <= clientHeight) {
1059
+ return 100;
1060
+ }
1061
+ if (includeViewportHeight) {
1062
+ return Math.min((scrollTop + clientHeight) / scrollHeight * 100, 100);
1063
+ }
1064
+ return Math.min(scrollTop / (scrollHeight - clientHeight) * 100, 100);
1065
+ }
1066
+ function calculateEngagementScore(velocity, fastScrollThreshold, directionChanges, timeScrollingUp, totalTime) {
1067
+ const velocityScore = Math.min(velocity / fastScrollThreshold * 50, 50);
1068
+ const directionScore = Math.min(directionChanges / 5 * 30, 30);
1069
+ const seekingScore = Math.min(timeScrollingUp / totalTime * 20, 20);
1070
+ return Math.max(0, 100 - (velocityScore + directionScore + seekingScore));
1071
+ }
1072
+ var scrollDepthPlugin = (plugin, instance, config) => {
1073
+ plugin.ns("experiences.scrollDepth");
1074
+ plugin.defaults({
1075
+ scrollDepth: {
1076
+ thresholds: [25, 50, 75, 100],
1077
+ throttle: 100,
1078
+ includeViewportHeight: true,
1079
+ recalculateOnResize: true,
1080
+ trackAdvancedMetrics: false,
1081
+ fastScrollVelocityThreshold: 3,
1082
+ disableOnMobile: false
1083
+ }
1084
+ });
1085
+ const scrollConfig = config.get("scrollDepth");
1086
+ if (!scrollConfig) return;
1087
+ const cfg = scrollConfig;
1088
+ const device = detectDevice();
1089
+ if (cfg.disableOnMobile && device === "mobile") {
1090
+ return;
1091
+ }
1092
+ let maxScrollPercent = 0;
1093
+ const triggeredThresholds = /* @__PURE__ */ new Set();
1094
+ const pageLoadTime = Date.now();
1095
+ let lastScrollPosition = 0;
1096
+ let lastScrollTime = Date.now();
1097
+ let lastScrollDirection = null;
1098
+ let directionChangesSinceLastThreshold = 0;
1099
+ let timeScrollingUp = 0;
1100
+ const thresholdTimes = /* @__PURE__ */ new Map();
1101
+ function handleScroll() {
1102
+ const currentPercent = calculateScrollPercent(cfg.includeViewportHeight ?? true);
1103
+ const now = Date.now();
1104
+ const scrollingElement = document.scrollingElement || document.documentElement;
1105
+ const currentPosition = scrollingElement.scrollTop;
1106
+ let velocity = 0;
1107
+ if (cfg.trackAdvancedMetrics) {
1108
+ const timeDelta = now - lastScrollTime;
1109
+ const positionDelta = currentPosition - lastScrollPosition;
1110
+ velocity = timeDelta > 0 ? Math.abs(positionDelta) / timeDelta : 0;
1111
+ const currentDirection = positionDelta > 0 ? "down" : positionDelta < 0 ? "up" : lastScrollDirection;
1112
+ if (currentDirection && lastScrollDirection && currentDirection !== lastScrollDirection) {
1113
+ directionChangesSinceLastThreshold++;
1114
+ }
1115
+ if (currentDirection === "up" && timeDelta > 0) {
1116
+ timeScrollingUp += timeDelta;
1117
+ }
1118
+ lastScrollDirection = currentDirection;
1119
+ lastScrollPosition = currentPosition;
1120
+ lastScrollTime = now;
1121
+ }
1122
+ maxScrollPercent = Math.max(maxScrollPercent, currentPercent);
1123
+ for (const threshold of cfg.thresholds || []) {
1124
+ if (currentPercent >= threshold && !triggeredThresholds.has(threshold)) {
1125
+ triggeredThresholds.add(threshold);
1126
+ if (cfg.trackAdvancedMetrics) {
1127
+ thresholdTimes.set(threshold, now - pageLoadTime);
1128
+ }
1129
+ const eventPayload = {
1130
+ triggered: true,
1131
+ timestamp: now,
1132
+ percent: Math.round(currentPercent * 100) / 100,
1133
+ maxPercent: Math.round(maxScrollPercent * 100) / 100,
1134
+ threshold,
1135
+ thresholdsCrossed: Array.from(triggeredThresholds).sort((a, b) => a - b),
1136
+ device
1137
+ };
1138
+ if (cfg.trackAdvancedMetrics) {
1139
+ const fastScrollThreshold = cfg.fastScrollVelocityThreshold || 3;
1140
+ const isFastScrolling = velocity > fastScrollThreshold;
1141
+ const engagementScore = calculateEngagementScore(
1142
+ velocity,
1143
+ fastScrollThreshold,
1144
+ directionChangesSinceLastThreshold,
1145
+ timeScrollingUp,
1146
+ now - pageLoadTime
1147
+ );
1148
+ eventPayload.advanced = {
1149
+ timeToThreshold: now - pageLoadTime,
1150
+ velocity: Math.round(velocity * 1e3) / 1e3,
1151
+ // Round to 3 decimals
1152
+ isFastScrolling,
1153
+ directionChanges: directionChangesSinceLastThreshold,
1154
+ timeScrollingUp,
1155
+ engagementScore: Math.round(engagementScore)
1156
+ };
1157
+ directionChangesSinceLastThreshold = 0;
1158
+ }
1159
+ instance.emit("trigger:scrollDepth", eventPayload);
1160
+ }
1161
+ }
1162
+ }
1163
+ const throttledScrollHandler = throttle(handleScroll, cfg.throttle || 100);
1164
+ const throttledResizeHandler = throttle(handleScroll, cfg.throttle || 100);
1165
+ function initialize() {
1166
+ if (typeof window === "undefined" || typeof document === "undefined") {
1167
+ return;
1168
+ }
1169
+ window.addEventListener("scroll", throttledScrollHandler, { passive: true });
1170
+ if (cfg.recalculateOnResize) {
1171
+ window.addEventListener("resize", throttledResizeHandler, { passive: true });
1172
+ }
1173
+ }
1174
+ function cleanup() {
1175
+ window.removeEventListener("scroll", throttledScrollHandler);
1176
+ window.removeEventListener("resize", throttledResizeHandler);
1177
+ }
1178
+ const destroyHandler = () => {
1179
+ cleanup();
1180
+ };
1181
+ instance.on("destroy", destroyHandler);
1182
+ plugin.expose({
1183
+ scrollDepth: {
1184
+ /**
1185
+ * Get the maximum scroll percentage reached during the session
1186
+ */
1187
+ getMaxPercent: () => maxScrollPercent,
1188
+ /**
1189
+ * Get the current scroll percentage
1190
+ */
1191
+ getCurrentPercent: () => calculateScrollPercent(cfg.includeViewportHeight ?? true),
1192
+ /**
1193
+ * Get all thresholds that have been crossed
1194
+ */
1195
+ getThresholdsCrossed: () => Array.from(triggeredThresholds).sort((a, b) => a - b),
1196
+ /**
1197
+ * Get the detected device type
1198
+ */
1199
+ getDevice: () => device,
1200
+ /**
1201
+ * Get advanced metrics (only available when trackAdvancedMetrics is enabled)
1202
+ */
1203
+ getAdvancedMetrics: () => {
1204
+ if (!cfg.trackAdvancedMetrics) return null;
1205
+ const now = Date.now();
1206
+ return {
1207
+ timeOnPage: now - pageLoadTime,
1208
+ directionChanges: directionChangesSinceLastThreshold,
1209
+ timeScrollingUp,
1210
+ thresholdTimes: Object.fromEntries(thresholdTimes)
1211
+ };
1212
+ },
1213
+ /**
1214
+ * Reset scroll depth tracking
1215
+ * Clears all triggered thresholds, max scroll, and advanced metrics
1216
+ */
1217
+ reset: () => {
1218
+ maxScrollPercent = 0;
1219
+ triggeredThresholds.clear();
1220
+ directionChangesSinceLastThreshold = 0;
1221
+ timeScrollingUp = 0;
1222
+ thresholdTimes.clear();
1223
+ lastScrollDirection = null;
1224
+ }
1225
+ }
1226
+ });
1227
+ if (typeof window !== "undefined") {
1228
+ setTimeout(initialize, 0);
1229
+ }
1230
+ return () => {
1231
+ cleanup();
1232
+ instance.off("destroy", destroyHandler);
1233
+ };
1234
+ };
1235
+
1236
+ // src/time-delay/time-delay.ts
1237
+ function calculateElapsed(startTime, pausedDuration) {
1238
+ return Date.now() - startTime - pausedDuration;
1239
+ }
1240
+ function isDocumentHidden() {
1241
+ if (typeof document === "undefined") return false;
1242
+ return document.hidden || false;
1243
+ }
1244
+ function createTimeDelayEvent(startTime, pausedDuration, wasPaused, visibilityChanges) {
1245
+ const timestamp = Date.now();
1246
+ const elapsed = timestamp - startTime;
1247
+ const activeElapsed = elapsed - pausedDuration;
1248
+ return {
1249
+ timestamp,
1250
+ elapsed,
1251
+ activeElapsed,
1252
+ wasPaused,
1253
+ visibilityChanges
1254
+ };
1255
+ }
1256
+ var timeDelayPlugin = (plugin, instance, config) => {
1257
+ plugin.ns("experiences.timeDelay");
1258
+ plugin.defaults({
1259
+ timeDelay: {
1260
+ delay: 0,
1261
+ pauseWhenHidden: true
1262
+ }
1263
+ });
1264
+ const timeDelayConfig = config.get("timeDelay");
1265
+ if (!timeDelayConfig) return;
1266
+ const delay = timeDelayConfig.delay ?? 0;
1267
+ const pauseWhenHidden = timeDelayConfig.pauseWhenHidden ?? true;
1268
+ if (delay <= 0) return;
1269
+ const startTime = Date.now();
1270
+ let triggered = false;
1271
+ let paused = false;
1272
+ let pausedDuration = 0;
1273
+ let lastPauseTime = 0;
1274
+ let visibilityChanges = 0;
1275
+ let timer = null;
1276
+ let visibilityListener = null;
1277
+ function trigger() {
1278
+ if (triggered) return;
1279
+ triggered = true;
1280
+ const eventPayload = createTimeDelayEvent(
1281
+ startTime,
1282
+ pausedDuration,
1283
+ visibilityChanges > 0,
1284
+ visibilityChanges
1285
+ );
1286
+ instance.emit("trigger:timeDelay", eventPayload);
1287
+ cleanup();
1288
+ }
1289
+ function scheduleTimer(remainingDelay) {
1290
+ if (timer) {
1291
+ clearTimeout(timer);
1292
+ }
1293
+ timer = setTimeout(() => {
1294
+ trigger();
1295
+ }, remainingDelay);
1296
+ }
1297
+ function handleVisibilityChange() {
1298
+ const hidden = isDocumentHidden();
1299
+ if (hidden && !paused) {
1300
+ paused = true;
1301
+ lastPauseTime = Date.now();
1302
+ visibilityChanges++;
1303
+ if (timer) {
1304
+ clearTimeout(timer);
1305
+ timer = null;
1306
+ }
1307
+ } else if (!hidden && paused) {
1308
+ paused = false;
1309
+ const pauseDuration = Date.now() - lastPauseTime;
1310
+ pausedDuration += pauseDuration;
1311
+ visibilityChanges++;
1312
+ const elapsed = calculateElapsed(startTime, pausedDuration);
1313
+ const remaining = delay - elapsed;
1314
+ if (remaining > 0) {
1315
+ scheduleTimer(remaining);
1316
+ } else {
1317
+ trigger();
1318
+ }
1319
+ }
1320
+ }
1321
+ function cleanup() {
1322
+ if (timer) {
1323
+ clearTimeout(timer);
1324
+ timer = null;
1325
+ }
1326
+ if (visibilityListener && typeof document !== "undefined") {
1327
+ document.removeEventListener("visibilitychange", visibilityListener);
1328
+ visibilityListener = null;
1329
+ }
1330
+ }
1331
+ function initialize() {
1332
+ if (pauseWhenHidden && isDocumentHidden()) {
1333
+ paused = true;
1334
+ lastPauseTime = Date.now();
1335
+ visibilityChanges++;
1336
+ } else {
1337
+ scheduleTimer(delay);
1338
+ }
1339
+ if (pauseWhenHidden && typeof document !== "undefined") {
1340
+ visibilityListener = handleVisibilityChange;
1341
+ document.addEventListener("visibilitychange", visibilityListener);
1342
+ }
1343
+ }
1344
+ plugin.expose({
1345
+ timeDelay: {
1346
+ getElapsed: () => {
1347
+ return Date.now() - startTime;
1348
+ },
1349
+ getActiveElapsed: () => {
1350
+ let currentPausedDuration = pausedDuration;
1351
+ if (paused) {
1352
+ currentPausedDuration += Date.now() - lastPauseTime;
1353
+ }
1354
+ return calculateElapsed(startTime, currentPausedDuration);
1355
+ },
1356
+ getRemaining: () => {
1357
+ if (triggered) return 0;
1358
+ const elapsed = calculateElapsed(startTime, pausedDuration);
1359
+ const remaining = delay - elapsed;
1360
+ return Math.max(0, remaining);
1361
+ },
1362
+ isPaused: () => paused,
1363
+ isTriggered: () => triggered,
1364
+ reset: () => {
1365
+ triggered = false;
1366
+ paused = false;
1367
+ pausedDuration = 0;
1368
+ lastPauseTime = 0;
1369
+ visibilityChanges = 0;
1370
+ cleanup();
1371
+ initialize();
1372
+ }
1373
+ }
1374
+ });
1375
+ initialize();
1376
+ const destroyHandler = () => {
1377
+ cleanup();
1378
+ };
1379
+ instance.on("destroy", destroyHandler);
1380
+ };
631
1381
 
632
- export { bannerPlugin, debugPlugin, frequencyPlugin };
1382
+ export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin };
633
1383
  //# sourceMappingURL=index.js.map
634
1384
  //# sourceMappingURL=index.js.map