nodebb-plugin-ezoic-infinite 1.5.79 → 1.5.81
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/package.json +1 -1
- package/public/client.js +135 -6
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// Track scroll direction to avoid aggressive recycling when the user scrolls upward.
|
|
5
|
+
// Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
|
|
6
|
+
let lastScrollY = 0;
|
|
7
|
+
let scrollDir = 1; // 1 = down, -1 = up
|
|
8
|
+
try {
|
|
9
|
+
lastScrollY = window.scrollY || 0;
|
|
10
|
+
window.addEventListener(
|
|
11
|
+
'scroll',
|
|
12
|
+
() => {
|
|
13
|
+
const y = window.scrollY || 0;
|
|
14
|
+
const d = y - lastScrollY;
|
|
15
|
+
if (Math.abs(d) > 4) {
|
|
16
|
+
scrollDir = d > 0 ? 1 : -1;
|
|
17
|
+
lastScrollY = y;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{ passive: true }
|
|
21
|
+
);
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
|
|
4
24
|
// NodeBB client context
|
|
5
25
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
6
26
|
|
|
@@ -83,6 +103,22 @@
|
|
|
83
103
|
} catch (e) {}
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
// Some CMP/TCF stubs rely on a hidden iframe named `__tcfapiLocator` to route postMessage calls.
|
|
107
|
+
// In SPA/Ajaxify navigations, that iframe can be removed/replaced unexpectedly, causing noisy
|
|
108
|
+
// "postMessage" / "addtlConsent" null errors. Ensuring it's present makes the environment stable.
|
|
109
|
+
function ensureTcfApiLocator() {
|
|
110
|
+
try {
|
|
111
|
+
// If a CMP is not present, do nothing.
|
|
112
|
+
if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
|
|
113
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
114
|
+
const f = document.createElement('iframe');
|
|
115
|
+
f.style.display = 'none';
|
|
116
|
+
f.id = '__tcfapiLocator';
|
|
117
|
+
f.name = '__tcfapiLocator';
|
|
118
|
+
(document.body || document.documentElement).appendChild(f);
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
86
122
|
|
|
87
123
|
|
|
88
124
|
function isFilledNode(node) {
|
|
@@ -668,9 +704,10 @@ function globalGapFixInit() {
|
|
|
668
704
|
}
|
|
669
705
|
|
|
670
706
|
function pruneOrphanWraps(kindClass, items) {
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
|
|
707
|
+
// Topic pages can be virtualized (posts removed from DOM as you scroll).
|
|
708
|
+
// When that happens, previously-inserted ad wraps may become "orphan" nodes with no
|
|
709
|
+
// nearby post containers, which leads to ads clustering together when scrolling back up.
|
|
710
|
+
// We prune only *true* orphans that are far offscreen to keep the UI stable.
|
|
674
711
|
if (!items || !items.length) return 0;
|
|
675
712
|
const itemSet = new Set(items);
|
|
676
713
|
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
@@ -701,7 +738,10 @@ function globalGapFixInit() {
|
|
|
701
738
|
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
702
739
|
} catch (e) {}
|
|
703
740
|
|
|
704
|
-
|
|
741
|
+
// For message/topic pages we may prune filled or empty orphans if they are far away,
|
|
742
|
+
// otherwise consecutive "stacks" can appear when posts are virtualized.
|
|
743
|
+
const isMessage = (kindClass === 'ezoic-ad-message');
|
|
744
|
+
if (!isMessage && isFilled(wrap)) return; // never prune filled ads for non-message lists
|
|
705
745
|
|
|
706
746
|
// Never prune a fresh wrap: it may fill late.
|
|
707
747
|
try {
|
|
@@ -711,6 +751,20 @@ function globalGapFixInit() {
|
|
|
711
751
|
|
|
712
752
|
if (hasNearbyItem(wrap)) return;
|
|
713
753
|
|
|
754
|
+
// For message ads: only prune if far offscreen to avoid perceived "vanishing".
|
|
755
|
+
if (isMessage) {
|
|
756
|
+
try {
|
|
757
|
+
const r = wrap.getBoundingClientRect();
|
|
758
|
+
const vh = Math.max(1, window.innerHeight || 1);
|
|
759
|
+
const farAbove = r.bottom < -vh * 2;
|
|
760
|
+
const farBelow = r.top > vh * 4;
|
|
761
|
+
if (!farAbove && !farBelow) return;
|
|
762
|
+
} catch (e) {
|
|
763
|
+
// If we can't measure, be conservative.
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
714
768
|
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
715
769
|
removed++;
|
|
716
770
|
});
|
|
@@ -1003,12 +1057,31 @@ function globalGapFixInit() {
|
|
|
1003
1057
|
if (isAdjacentAd(el)) continue;
|
|
1004
1058
|
if (findWrap(kindClass, afterPos)) continue;
|
|
1005
1059
|
|
|
1006
|
-
|
|
1060
|
+
let id = pickIdFromAll(allIds, cursorKey);
|
|
1061
|
+
let recycledWrap = null;
|
|
1062
|
+
|
|
1063
|
+
// If the pool is exhausted (all placeholder ids already mounted), recycle a wrap that is far
|
|
1064
|
+
// above the viewport by moving it to the new target instead of creating a new placeholder.
|
|
1065
|
+
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1066
|
+
// appearing on very long infinite scroll sessions.
|
|
1067
|
+
if (!id) {
|
|
1068
|
+
// Only recycle while scrolling down. Recycling while scrolling up tends to
|
|
1069
|
+
// cause perceived instability (ads bunching/disappearing).
|
|
1070
|
+
recycledWrap = scrollDir > 0 ? pickRecyclableWrap(kindClass) : null;
|
|
1071
|
+
if (recycledWrap) {
|
|
1072
|
+
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1007
1076
|
if (!id) break;
|
|
1008
1077
|
|
|
1009
|
-
const wrap =
|
|
1078
|
+
const wrap = recycledWrap
|
|
1079
|
+
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1080
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
1010
1081
|
if (!wrap) continue;
|
|
1011
1082
|
|
|
1083
|
+
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
1084
|
+
// after ajaxify/infinite scroll mutations.
|
|
1012
1085
|
observePlaceholder(id);
|
|
1013
1086
|
inserted++;
|
|
1014
1087
|
}
|
|
@@ -1016,6 +1089,60 @@ function globalGapFixInit() {
|
|
|
1016
1089
|
return inserted;
|
|
1017
1090
|
}
|
|
1018
1091
|
|
|
1092
|
+
function pickRecyclableWrap(kindClass) {
|
|
1093
|
+
// Only recycle wrappers that are well above the viewport to avoid visible "disappearing".
|
|
1094
|
+
// With very small id pools (e.g. 6 ids), recycling is the only way to keep ads appearing
|
|
1095
|
+
// on long topics without redefining placeholders.
|
|
1096
|
+
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
1097
|
+
if (!wraps || !wraps.length) return null;
|
|
1098
|
+
|
|
1099
|
+
const vh = Math.max(300, window.innerHeight || 800);
|
|
1100
|
+
// Recycle only when the wrapper is far above the viewport.
|
|
1101
|
+
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1102
|
+
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1103
|
+
|
|
1104
|
+
let best = null;
|
|
1105
|
+
let bestBottom = Infinity;
|
|
1106
|
+
for (const w of wraps) {
|
|
1107
|
+
if (!w || !w.isConnected) continue;
|
|
1108
|
+
const rect = w.getBoundingClientRect();
|
|
1109
|
+
if (rect.bottom < threshold) {
|
|
1110
|
+
if (rect.bottom < bestBottom) {
|
|
1111
|
+
bestBottom = rect.bottom;
|
|
1112
|
+
best = w;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return best;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
1120
|
+
try {
|
|
1121
|
+
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
1122
|
+
|
|
1123
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
1124
|
+
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
1125
|
+
|
|
1126
|
+
// Ensure minimal layout impact.
|
|
1127
|
+
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
1128
|
+
try { tightenStickyIn(wrap); } catch (e) {}
|
|
1129
|
+
|
|
1130
|
+
const id = wrap.getAttribute('data-ezoic-wrapid');
|
|
1131
|
+
if (id) {
|
|
1132
|
+
setTimeout(() => {
|
|
1133
|
+
try {
|
|
1134
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
1135
|
+
window.ezstandalone.showAds(id);
|
|
1136
|
+
}
|
|
1137
|
+
} catch (e) {}
|
|
1138
|
+
}, 0);
|
|
1139
|
+
}
|
|
1140
|
+
return wrap;
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1019
1146
|
async function runCore() {
|
|
1020
1147
|
if (isBlocked()) return 0;
|
|
1021
1148
|
|
|
@@ -1268,6 +1395,7 @@ function globalGapFixInit() {
|
|
|
1268
1395
|
blockedUntil = 0;
|
|
1269
1396
|
|
|
1270
1397
|
muteNoisyConsole();
|
|
1398
|
+
ensureTcfApiLocator();
|
|
1271
1399
|
warmUpNetwork();
|
|
1272
1400
|
patchShowAds();
|
|
1273
1401
|
globalGapFixInit();
|
|
@@ -1339,6 +1467,7 @@ function globalGapFixInit() {
|
|
|
1339
1467
|
|
|
1340
1468
|
state.pageKey = getPageKey();
|
|
1341
1469
|
muteNoisyConsole();
|
|
1470
|
+
ensureTcfApiLocator();
|
|
1342
1471
|
warmUpNetwork();
|
|
1343
1472
|
patchShowAds();
|
|
1344
1473
|
ensurePreloadObserver();
|