nodebb-plugin-ezoic-infinite 1.5.79 → 1.5.80
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 +113 -2
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) {
|
|
@@ -1003,12 +1039,31 @@ function globalGapFixInit() {
|
|
|
1003
1039
|
if (isAdjacentAd(el)) continue;
|
|
1004
1040
|
if (findWrap(kindClass, afterPos)) continue;
|
|
1005
1041
|
|
|
1006
|
-
|
|
1042
|
+
let id = pickIdFromAll(allIds, cursorKey);
|
|
1043
|
+
let recycledWrap = null;
|
|
1044
|
+
|
|
1045
|
+
// If the pool is exhausted (all placeholder ids already mounted), recycle a wrap that is far
|
|
1046
|
+
// above the viewport by moving it to the new target instead of creating a new placeholder.
|
|
1047
|
+
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1048
|
+
// appearing on very long infinite scroll sessions.
|
|
1049
|
+
if (!id) {
|
|
1050
|
+
// Only recycle while scrolling down. Recycling while scrolling up tends to
|
|
1051
|
+
// cause perceived instability (ads bunching/disappearing).
|
|
1052
|
+
recycledWrap = scrollDir > 0 ? pickRecyclableWrap(kindClass) : null;
|
|
1053
|
+
if (recycledWrap) {
|
|
1054
|
+
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1007
1058
|
if (!id) break;
|
|
1008
1059
|
|
|
1009
|
-
const wrap =
|
|
1060
|
+
const wrap = recycledWrap
|
|
1061
|
+
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1062
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
1010
1063
|
if (!wrap) continue;
|
|
1011
1064
|
|
|
1065
|
+
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
1066
|
+
// after ajaxify/infinite scroll mutations.
|
|
1012
1067
|
observePlaceholder(id);
|
|
1013
1068
|
inserted++;
|
|
1014
1069
|
}
|
|
@@ -1016,6 +1071,60 @@ function globalGapFixInit() {
|
|
|
1016
1071
|
return inserted;
|
|
1017
1072
|
}
|
|
1018
1073
|
|
|
1074
|
+
function pickRecyclableWrap(kindClass) {
|
|
1075
|
+
// Only recycle wrappers that are well above the viewport to avoid visible "disappearing".
|
|
1076
|
+
// With very small id pools (e.g. 6 ids), recycling is the only way to keep ads appearing
|
|
1077
|
+
// on long topics without redefining placeholders.
|
|
1078
|
+
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
1079
|
+
if (!wraps || !wraps.length) return null;
|
|
1080
|
+
|
|
1081
|
+
const vh = Math.max(300, window.innerHeight || 800);
|
|
1082
|
+
// Recycle only when the wrapper is far above the viewport.
|
|
1083
|
+
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1084
|
+
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1085
|
+
|
|
1086
|
+
let best = null;
|
|
1087
|
+
let bestBottom = Infinity;
|
|
1088
|
+
for (const w of wraps) {
|
|
1089
|
+
if (!w || !w.isConnected) continue;
|
|
1090
|
+
const rect = w.getBoundingClientRect();
|
|
1091
|
+
if (rect.bottom < threshold) {
|
|
1092
|
+
if (rect.bottom < bestBottom) {
|
|
1093
|
+
bestBottom = rect.bottom;
|
|
1094
|
+
best = w;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return best;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
1102
|
+
try {
|
|
1103
|
+
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
1104
|
+
|
|
1105
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
1106
|
+
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
1107
|
+
|
|
1108
|
+
// Ensure minimal layout impact.
|
|
1109
|
+
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
1110
|
+
try { tightenStickyIn(wrap); } catch (e) {}
|
|
1111
|
+
|
|
1112
|
+
const id = wrap.getAttribute('data-ezoic-wrapid');
|
|
1113
|
+
if (id) {
|
|
1114
|
+
setTimeout(() => {
|
|
1115
|
+
try {
|
|
1116
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
1117
|
+
window.ezstandalone.showAds(id);
|
|
1118
|
+
}
|
|
1119
|
+
} catch (e) {}
|
|
1120
|
+
}, 0);
|
|
1121
|
+
}
|
|
1122
|
+
return wrap;
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1019
1128
|
async function runCore() {
|
|
1020
1129
|
if (isBlocked()) return 0;
|
|
1021
1130
|
|
|
@@ -1268,6 +1377,7 @@ function globalGapFixInit() {
|
|
|
1268
1377
|
blockedUntil = 0;
|
|
1269
1378
|
|
|
1270
1379
|
muteNoisyConsole();
|
|
1380
|
+
ensureTcfApiLocator();
|
|
1271
1381
|
warmUpNetwork();
|
|
1272
1382
|
patchShowAds();
|
|
1273
1383
|
globalGapFixInit();
|
|
@@ -1339,6 +1449,7 @@ function globalGapFixInit() {
|
|
|
1339
1449
|
|
|
1340
1450
|
state.pageKey = getPageKey();
|
|
1341
1451
|
muteNoisyConsole();
|
|
1452
|
+
ensureTcfApiLocator();
|
|
1342
1453
|
warmUpNetwork();
|
|
1343
1454
|
patchShowAds();
|
|
1344
1455
|
ensurePreloadObserver();
|