@prosdevlab/experience-sdk-plugins 0.1.4 → 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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +608 -1
- package/dist/index.js +692 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/exit-intent/exit-intent.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +372 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +5 -0
- package/src/integration.test.ts +362 -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 +545 -0
- package/src/scroll-depth/scroll-depth.ts +400 -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 +297 -0
- package/src/time-delay/types.ts +89 -0
- package/src/utils/sanitize.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ function sanitizeHTML(html) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
const attrString = attrs.length > 0 ?
|
|
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);
|
|
@@ -561,6 +561,176 @@ var debugPlugin = (plugin, instance, config) => {
|
|
|
561
561
|
});
|
|
562
562
|
}
|
|
563
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
|
+
};
|
|
564
734
|
var frequencyPlugin = (plugin, instance, config) => {
|
|
565
735
|
plugin.ns("frequency");
|
|
566
736
|
plugin.defaults({
|
|
@@ -688,7 +858,527 @@ var frequencyPlugin = (plugin, instance, config) => {
|
|
|
688
858
|
});
|
|
689
859
|
}
|
|
690
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
|
+
};
|
|
691
1381
|
|
|
692
|
-
export { bannerPlugin, debugPlugin, frequencyPlugin };
|
|
1382
|
+
export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin };
|
|
693
1383
|
//# sourceMappingURL=index.js.map
|
|
694
1384
|
//# sourceMappingURL=index.js.map
|