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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +113 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.79",
3
+ "version": "1.5.80",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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
- const id = pickIdFromAll(allIds, cursorKey);
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 = insertAfter(el, id, kindClass, afterPos);
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();