nodebb-plugin-ezoic-infinite 1.5.78 → 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 +142 -10
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
|
|
|
@@ -22,9 +42,11 @@
|
|
|
22
42
|
|
|
23
43
|
// Preload margins
|
|
24
44
|
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
45
|
+
// Mobile: larger preload window so ad fill requests start earlier and
|
|
46
|
+
// users don't scroll past empty placeholders.
|
|
47
|
+
const PRELOAD_MARGIN_MOBILE = '3200px 0px 3200px 0px';
|
|
48
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
|
|
49
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '5200px 0px 5200px 0px';
|
|
28
50
|
|
|
29
51
|
const BOOST_DURATION_MS = 2500;
|
|
30
52
|
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
@@ -81,6 +103,22 @@
|
|
|
81
103
|
} catch (e) {}
|
|
82
104
|
}
|
|
83
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
|
+
|
|
84
122
|
|
|
85
123
|
|
|
86
124
|
function isFilledNode(node) {
|
|
@@ -751,11 +789,24 @@ function globalGapFixInit() {
|
|
|
751
789
|
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
752
790
|
} catch (e) {}
|
|
753
791
|
|
|
754
|
-
//
|
|
755
|
-
//
|
|
792
|
+
// Never remove a wrap that is already filled; otherwise it looks like
|
|
793
|
+
// ads "disappear" while scrolling. Only remove the empty neighbour.
|
|
756
794
|
const prevFilled = isFilled(prev);
|
|
757
795
|
const curFilled = isFilled(w);
|
|
758
|
-
|
|
796
|
+
|
|
797
|
+
if (curFilled) {
|
|
798
|
+
// If the previous one is empty (and not fresh), drop the previous instead.
|
|
799
|
+
if (!prevFilled && !isFresh(prev)) {
|
|
800
|
+
withInternalDomChange(() => releaseWrapNode(prev));
|
|
801
|
+
removed++;
|
|
802
|
+
}
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Current is empty.
|
|
807
|
+
// Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
|
|
808
|
+
// Only decluster when previous is filled, or when current is stale.
|
|
809
|
+
if (prevFilled || !isFresh(w)) {
|
|
759
810
|
withInternalDomChange(() => releaseWrapNode(w));
|
|
760
811
|
removed++;
|
|
761
812
|
}
|
|
@@ -817,10 +868,16 @@ function globalGapFixInit() {
|
|
|
817
868
|
try { io && io.observe(ph); } catch (e) {}
|
|
818
869
|
|
|
819
870
|
// If already near viewport, fire immediately.
|
|
871
|
+
// Mobile tends to scroll faster + has slower auctions, so we fire earlier.
|
|
820
872
|
try {
|
|
821
873
|
const r = ph.getBoundingClientRect();
|
|
822
|
-
const
|
|
823
|
-
const
|
|
874
|
+
const mobile = isMobile();
|
|
875
|
+
const screens = isBoosted()
|
|
876
|
+
? (mobile ? 9.0 : 5.0)
|
|
877
|
+
: (mobile ? 6.0 : 3.0);
|
|
878
|
+
const minBottom = isBoosted()
|
|
879
|
+
? (mobile ? -2600 : -1500)
|
|
880
|
+
: (mobile ? -1400 : -800);
|
|
824
881
|
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
825
882
|
} catch (e) {}
|
|
826
883
|
}
|
|
@@ -982,12 +1039,31 @@ function globalGapFixInit() {
|
|
|
982
1039
|
if (isAdjacentAd(el)) continue;
|
|
983
1040
|
if (findWrap(kindClass, afterPos)) continue;
|
|
984
1041
|
|
|
985
|
-
|
|
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
|
+
|
|
986
1058
|
if (!id) break;
|
|
987
1059
|
|
|
988
|
-
const wrap =
|
|
1060
|
+
const wrap = recycledWrap
|
|
1061
|
+
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1062
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
989
1063
|
if (!wrap) continue;
|
|
990
1064
|
|
|
1065
|
+
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
1066
|
+
// after ajaxify/infinite scroll mutations.
|
|
991
1067
|
observePlaceholder(id);
|
|
992
1068
|
inserted++;
|
|
993
1069
|
}
|
|
@@ -995,6 +1071,60 @@ function globalGapFixInit() {
|
|
|
995
1071
|
return inserted;
|
|
996
1072
|
}
|
|
997
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
|
+
|
|
998
1128
|
async function runCore() {
|
|
999
1129
|
if (isBlocked()) return 0;
|
|
1000
1130
|
|
|
@@ -1247,6 +1377,7 @@ function globalGapFixInit() {
|
|
|
1247
1377
|
blockedUntil = 0;
|
|
1248
1378
|
|
|
1249
1379
|
muteNoisyConsole();
|
|
1380
|
+
ensureTcfApiLocator();
|
|
1250
1381
|
warmUpNetwork();
|
|
1251
1382
|
patchShowAds();
|
|
1252
1383
|
globalGapFixInit();
|
|
@@ -1318,6 +1449,7 @@ function globalGapFixInit() {
|
|
|
1318
1449
|
|
|
1319
1450
|
state.pageKey = getPageKey();
|
|
1320
1451
|
muteNoisyConsole();
|
|
1452
|
+
ensureTcfApiLocator();
|
|
1321
1453
|
warmUpNetwork();
|
|
1322
1454
|
patchShowAds();
|
|
1323
1455
|
ensurePreloadObserver();
|