nodebb-plugin-ezoic-infinite 1.8.11 → 1.8.13

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/library.js CHANGED
@@ -71,9 +71,8 @@ async function getSettings() {
71
71
 
72
72
  async function isUserExcluded(uid, excludedGroups) {
73
73
  if (!uid || !excludedGroups.length) return false;
74
- const excluded = new Set((excludedGroups || []).map(String));
75
74
  const userGroups = await groups.getUserGroups([uid]);
76
- return (userGroups[0] || []).some(g => excluded.has(String(g.name)));
75
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
77
76
  }
78
77
 
79
78
  // ── Scripts Ezoic ──────────────────────────────────────────────────────────
@@ -114,34 +113,13 @@ plugin.injectEzoicHead = async (data) => {
114
113
  const uid = data.req?.uid ?? 0;
115
114
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
116
115
  if (!excluded) {
117
- const html = data.templateData.customHTML || '';
118
- if (!html.includes('data-nbb-ezoic-head="1"')) {
119
- data.templateData.customHTML = `<meta data-nbb-ezoic-head="1">` + EZOIC_SCRIPTS + html;
120
- }
116
+ // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
+ data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
121
118
  }
122
119
  } catch (_) {}
123
120
  return data;
124
121
  };
125
122
 
126
-
127
- function publicConfigPayload(settings, excluded) {
128
- return {
129
- excluded,
130
- enableBetweenAds: settings.enableBetweenAds,
131
- showFirstTopicAd: settings.showFirstTopicAd,
132
- placeholderIds: settings.placeholderIds,
133
- intervalPosts: settings.intervalPosts,
134
- enableCategoryAds: settings.enableCategoryAds,
135
- showFirstCategoryAd: settings.showFirstCategoryAd,
136
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
137
- intervalCategories: settings.intervalCategories,
138
- enableMessageAds: settings.enableMessageAds,
139
- showFirstMessageAd: settings.showFirstMessageAd,
140
- messagePlaceholderIds: settings.messagePlaceholderIds,
141
- messageIntervalPosts: settings.messageIntervalPosts,
142
- };
143
- }
144
-
145
123
  plugin.init = async ({ router, middleware }) => {
146
124
  async function render(req, res) {
147
125
  const settings = await getSettings();
@@ -161,7 +139,21 @@ plugin.init = async ({ router, middleware }) => {
161
139
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
162
140
  const settings = await getSettings();
163
141
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
164
- res.json(publicConfigPayload(settings, excluded));
142
+ res.json({
143
+ excluded,
144
+ enableBetweenAds: settings.enableBetweenAds,
145
+ showFirstTopicAd: settings.showFirstTopicAd,
146
+ placeholderIds: settings.placeholderIds,
147
+ intervalPosts: settings.intervalPosts,
148
+ enableCategoryAds: settings.enableCategoryAds,
149
+ showFirstCategoryAd: settings.showFirstCategoryAd,
150
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
151
+ intervalCategories: settings.intervalCategories,
152
+ enableMessageAds: settings.enableMessageAds,
153
+ showFirstMessageAd: settings.showFirstMessageAd,
154
+ messagePlaceholderIds: settings.messagePlaceholderIds,
155
+ messageIntervalPosts: settings.messageIntervalPosts,
156
+ });
165
157
  });
166
158
  };
167
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.11",
3
+ "version": "1.8.13",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -18,4 +18,4 @@
18
18
  "compatibility": "^4.0.0"
19
19
  },
20
20
  "private": false
21
- }
21
+ }
package/public/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v52
2
+ * NodeBB Ezoic Infinite Ads — client.js v36
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
@@ -32,8 +32,6 @@
32
32
  *
33
33
  * v34 moveDistantWrap — voir v38.
34
34
  *
35
- * v52 Clean architecture interne (modules logiques, cache DOM, bridge Ezoic).
36
- *
37
35
  * v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
38
36
  * après login — filter:middleware.renderHeader re-évalue l'exclusion au
39
37
  * rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
@@ -151,9 +149,6 @@
151
149
  burstDeadline: 0,
152
150
  burstCount: 0,
153
151
  lastBurstTs: 0,
154
- ioViewportMode: isMobile() ? 'm' : 'd',
155
- domRev: 0,
156
- domCache: new Map(),
157
152
  };
158
153
 
159
154
  let blockedUntil = 0;
@@ -162,61 +157,7 @@
162
157
  const isBlocked = () => ts() < blockedUntil;
163
158
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
164
159
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
165
- const DEBUG = (() => { try { return localStorage.getItem('nbbEzDebug') === '1'; } catch (_) { return false; } })();
166
- const dbg = (...a) => { if (DEBUG) { try { console.debug('[nbb-ezoic]', ...a); } catch (_) {} } };
167
- const isFilled = n => {
168
- try {
169
- if (!n?.querySelector) return false;
170
- if (n.querySelector('iframe, ins, img, video, [data-google-container-id], .ezoic-ad, [id$="__container__"]')) return true;
171
- const r = n.getBoundingClientRect?.();
172
- return !!(r && r.height > 8);
173
- } catch (_) { return false; }
174
- };
175
-
176
- const Arch = {
177
- cache: {
178
- invalidate(reason) {
179
- S.domRev++;
180
- S.domCache.clear();
181
- dbg('dom-cache.invalidate', reason || '');
182
- },
183
- queryAll(key, selector, mapFn) {
184
- const cacheKey = `${S.domRev}:${key}`;
185
- if (S.domCache.has(cacheKey)) return S.domCache.get(cacheKey);
186
- const arr = Array.from(document.querySelectorAll(selector));
187
- const out = typeof mapFn === 'function' ? arr.filter(Boolean).filter(mapFn) : arr;
188
- S.domCache.set(cacheKey, out);
189
- return out;
190
- },
191
- get(key, buildFn) {
192
- const cacheKey = `${S.domRev}:${key}`;
193
- if (S.domCache.has(cacheKey)) return S.domCache.get(cacheKey);
194
- const v = buildFn();
195
- S.domCache.set(cacheKey, v);
196
- return v;
197
- },
198
- },
199
- ezoic: {
200
- ensure() {
201
- window.ezstandalone = window.ezstandalone || {};
202
- return window.ezstandalone;
203
- },
204
- run(fn) {
205
- const ez = this.ensure();
206
- try { return (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(fn) : fn(); } catch (_) {}
207
- },
208
- call(method, ...args) {
209
- const ez = this.ensure();
210
- const fn = ez?.[method];
211
- if (typeof fn !== 'function') return;
212
- try { return fn.apply(ez, args); } catch (_) {}
213
- },
214
- },
215
- page: {
216
- kind() { return getKind(); },
217
- key() { return pageKey(); },
218
- },
219
- };
160
+ const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
220
161
 
221
162
  function healFalseEmpty(root = document) {
222
163
  try {
@@ -274,8 +215,7 @@
274
215
 
275
216
  function mutate(fn) {
276
217
  S.mutGuard++;
277
- try { fn(); }
278
- finally { S.mutGuard--; Arch.cache.invalidate('mutate'); }
218
+ try { fn(); } finally { S.mutGuard--; }
279
219
  }
280
220
  function scheduleDestroyFlush() {
281
221
  if (S.destroyBatchTimer) return;
@@ -295,7 +235,11 @@ function flushDestroyBatch() {
295
235
  ids.push(id);
296
236
  }
297
237
  if (ids.length) {
298
- try { Arch.ezoic.run(() => Arch.ezoic.call('destroyPlaceholders', ids)); } catch (_) {}
238
+ try {
239
+ const ez = window.ezstandalone;
240
+ const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
241
+ try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
242
+ } catch (_) {}
299
243
  }
300
244
  if (S.destroyPending.length) scheduleDestroyFlush();
301
245
  }
@@ -311,28 +255,20 @@ function destroyEzoicId(id) {
311
255
  scheduleDestroyFlush();
312
256
  }
313
257
 
314
- function queueDestroyIds(ids) {
315
- for (const raw of (ids || [])) {
316
- const id = parseInt(raw, 10);
317
- if (!Number.isFinite(id) || id <= 0) continue;
318
- if (!S.ezActiveIds.has(id) && !S.ezShownSinceDestroy.has(id)) continue;
319
- if (S.destroyPendingSet.has(id)) continue;
320
- S.destroyPending.push(id);
321
- S.destroyPendingSet.add(id);
322
- S.ezActiveIds.delete(id);
323
- S.ezShownSinceDestroy.delete(id);
324
- }
325
- if (S.destroyPending.length) scheduleDestroyFlush();
326
- }
327
-
328
- function prepareIdsForShow(ids) {
258
+ function destroyBeforeReuse(ids) {
329
259
  const out = [];
260
+ const toDestroy = [];
330
261
  const seen = new Set();
331
262
  for (const raw of (ids || [])) {
332
263
  const id = parseInt(raw, 10);
333
264
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
334
265
  seen.add(id);
335
266
  out.push(id);
267
+ if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
268
+ }
269
+ if (toDestroy.length) {
270
+ try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
271
+ for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
336
272
  }
337
273
  return out;
338
274
  }
@@ -378,7 +314,6 @@ function prepareIdsForShow(ids) {
378
314
  }
379
315
 
380
316
  function getKind() {
381
- return Arch.cache.get('kind', () => {
382
317
  const p = location.pathname;
383
318
  if (/^\/topic\//.test(p)) return 'topic';
384
319
  if (/^\/category\//.test(p)) return 'categoryTopics';
@@ -387,13 +322,12 @@ function prepareIdsForShow(ids) {
387
322
  if (document.querySelector(SEL.post)) return 'topic';
388
323
  if (document.querySelector(SEL.topic)) return 'categoryTopics';
389
324
  return 'other';
390
- });
391
325
  }
392
326
 
393
327
  // ── Items DOM ──────────────────────────────────────────────────────────────
394
328
 
395
329
  function getPosts() {
396
- return Arch.cache.queryAll('posts', SEL.post, el => {
330
+ return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
397
331
  if (!el.isConnected) return false;
398
332
  if (!el.querySelector('[component="post/content"]')) return false;
399
333
  const p = el.parentElement?.closest(SEL.post);
@@ -402,8 +336,8 @@ function prepareIdsForShow(ids) {
402
336
  });
403
337
  }
404
338
 
405
- const getTopics = () => Arch.cache.queryAll('topics', SEL.topic);
406
- const getCategories = () => Arch.cache.queryAll('categories', SEL.category);
339
+ const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
340
+ const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
407
341
 
408
342
  // ── Wraps — détection ──────────────────────────────────────────────────────
409
343
 
@@ -504,7 +438,6 @@ function prepareIdsForShow(ids) {
504
438
  S.mountedIds.delete(id);
505
439
  S.pendingSet.delete(id);
506
440
  S.lastShow.delete(id);
507
- queueDestroyIds([id]);
508
441
  S.ezActiveIds.delete(id);
509
442
  }
510
443
  }
@@ -521,7 +454,7 @@ function prepareIdsForShow(ids) {
521
454
  * Priorité : wraps vides d'abord, remplis si nécessaire.
522
455
  */
523
456
  function recycleAndMove(klass, targetEl, newKey) {
524
- const ez = Arch.ezoic.ensure();
457
+ const ez = window.ezstandalone;
525
458
  if (typeof ez?.destroyPlaceholders !== 'function' ||
526
459
  typeof ez?.define !== 'function' ||
527
460
  typeof ez?.displayMore !== 'function') return null;
@@ -593,7 +526,7 @@ function recycleAndMove(klass, targetEl, newKey) {
593
526
  };
594
527
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
595
528
  const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
596
- try { Arch.ezoic.run(doDestroy); } catch (_) {}
529
+ try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
597
530
 
598
531
  return { id, wrap: best };
599
532
  }
@@ -633,7 +566,7 @@ function recycleAndMove(klass, targetEl, newKey) {
633
566
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
634
567
  if (ph instanceof Element) S.io?.unobserve(ph);
635
568
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
636
- if (Number.isFinite(id)) { queueDestroyIds([id]); S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
569
+ if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
637
570
  const key = w.getAttribute(A_ANCHOR);
638
571
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
639
572
  w.remove();
@@ -656,18 +589,19 @@ function recycleAndMove(klass, targetEl, newKey) {
656
589
  // supprimerait les wraps, et provoquerait une réinjection en haut.
657
590
 
658
591
  function pruneOrphansBetween() {
659
- if (S.burstActive || (S.scrollSpeed || 0) > 1800) return;
660
592
  const klass = 'ezoic-ad-between';
661
593
  const cfg = KIND[klass];
662
- const liveAnchors = new Set(Arch.cache.queryAll('prune-between-anchors', cfg.sel).map(el => el.getAttribute(cfg.anchorAttr)).filter(Boolean));
663
594
 
664
595
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
665
596
  const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
666
- if (ts() - created < MIN_PRUNE_AGE_MS) return;
597
+ if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
598
+
667
599
  const key = w.getAttribute(A_ANCHOR) ?? '';
668
- const sid = key.slice(klass.length + 1);
600
+ const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
669
601
  if (!sid) { mutate(() => dropWrap(w)); return; }
670
- if (!liveAnchors.has(sid)) mutate(() => dropWrap(w));
602
+
603
+ const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
604
+ if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
671
605
  });
672
606
  }
673
607
 
@@ -713,8 +647,6 @@ function recycleAndMove(klass, targetEl, newKey) {
713
647
  const w = insertAfter(el, id, klass, key);
714
648
  if (w) { observePh(id); inserted++; }
715
649
  } else {
716
- // Recyclage agressif = source fréquente de churn visuel (surtout sticky/fixed)
717
- if (klass === 'ezoic-ad-message') break;
718
650
  const recycled = recycleAndMove(klass, el, key);
719
651
  if (!recycled) break;
720
652
  inserted++;
@@ -725,31 +657,15 @@ function recycleAndMove(klass, targetEl, newKey) {
725
657
 
726
658
  // ── IntersectionObserver & Show ────────────────────────────────────────────
727
659
 
728
- function ensureIOViewportMode() {
729
- const mode = isMobile() ? 'm' : 'd';
730
- if (S.io && S.ioViewportMode !== mode) {
731
- try { S.io.disconnect(); } catch (_) {}
732
- S.io = null;
733
- }
734
- S.ioViewportMode = mode;
735
- }
736
-
737
660
  function getIO() {
738
- ensureIOViewportMode();
739
661
  if (S.io) return S.io;
740
662
  try {
741
663
  S.io = new IntersectionObserver(entries => {
742
664
  for (const e of entries) {
743
665
  if (!e.isIntersecting) continue;
744
- const target = e.target instanceof Element ? e.target : null;
745
- const id = parseInt(target?.getAttribute('data-ezoic-id'), 10);
746
- if (!Number.isFinite(id) || id <= 0) continue;
747
- const accepted = enqueueShow(id);
748
- if (accepted) {
749
- try { S.io?.unobserve(target); } catch (_) {}
750
- } else if (target?.isConnected) {
751
- setTimeout(() => { try { if (target.isConnected) S.io?.observe(target); } catch (_) {} }, 300);
752
- }
666
+ if (e.target instanceof Element) S.io?.unobserve(e.target);
667
+ const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
668
+ if (Number.isFinite(id) && id > 0) enqueueShow(id);
753
669
  }
754
670
  }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
755
671
  } catch (_) { S.io = null; }
@@ -770,15 +686,12 @@ function recycleAndMove(klass, targetEl, newKey) {
770
686
  }
771
687
 
772
688
  function enqueueShow(id) {
773
- if (!id || isBlocked()) return false;
689
+ if (!id || isBlocked()) return;
774
690
  const n = parseInt(id, 10);
775
- if (!Number.isFinite(n) || n <= 0) return false;
776
- if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
777
- const ph = phEl(n);
778
- if (!ph?.isConnected || isFilled(ph) || !hasSinglePlaceholder(n)) return false;
691
+ if (!Number.isFinite(n) || n <= 0) return;
692
+ if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
779
693
  if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
780
694
  scheduleDrainQueue();
781
- return true;
782
695
  }
783
696
 
784
697
  function scheduleDrainQueue() {
@@ -844,18 +757,19 @@ function startShowBatch(ids) {
844
757
 
845
758
  if (!valid.length) { clearTimeout(timer); return release(); }
846
759
 
847
- const ez = Arch.ezoic.ensure();
760
+ window.ezstandalone = window.ezstandalone || {};
761
+ const ez = window.ezstandalone;
848
762
  const doShow = () => {
849
- const prepared = prepareIdsForShow(valid);
763
+ const prepared = destroyBeforeReuse(valid);
850
764
  if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
851
- try { ez.showAds(...prepared); } catch (_) { dbg('showAds failed', prepared); }
765
+ try { ez.showAds(...prepared); } catch (_) {}
852
766
  for (const id of prepared) {
853
767
  S.ezActiveIds.add(id);
854
768
  S.ezShownSinceDestroy.add(id);
855
769
  }
856
770
  setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
857
771
  };
858
- Arch.ezoic.run(doShow);
772
+ Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
859
773
  } catch (_) { clearTimeout(timer); release(); }
860
774
  });
861
775
  }
@@ -870,7 +784,8 @@ function startShowBatch(ids) {
870
784
  function patchShowAds() {
871
785
  const apply = () => {
872
786
  try {
873
- const ez = Arch.ezoic.ensure();
787
+ window.ezstandalone = window.ezstandalone || {};
788
+ const ez = window.ezstandalone;
874
789
  if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
875
790
  window.__nbbEzPatched = true;
876
791
  const orig = ez.showAds.bind(ez);
@@ -897,7 +812,8 @@ function startShowBatch(ids) {
897
812
  };
898
813
  apply();
899
814
  if (!window.__nbbEzPatched) {
900
- Arch.ezoic.run(apply);
815
+ window.ezstandalone = window.ezstandalone || {};
816
+ (window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
901
817
  }
902
818
  }
903
819
 
@@ -1006,7 +922,6 @@ function startShowBatch(ids) {
1006
922
  S.scrollSpeed = 0;
1007
923
  S.lastScrollY = 0;
1008
924
  S.lastScrollTs = 0;
1009
- Arch.cache.invalidate('cleanup');
1010
925
  }
1011
926
 
1012
927
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -1024,14 +939,14 @@ function startShowBatch(ids) {
1024
939
  sawWrapRemoval = true;
1025
940
  }
1026
941
  }
1027
- if (sawWrapRemoval) { Arch.cache.invalidate('wrap-removed'); queueSweepDeadWraps(); }
942
+ if (sawWrapRemoval) queueSweepDeadWraps();
1028
943
  for (const n of m.addedNodes) {
1029
944
  if (n.nodeType !== 1) continue;
1030
945
  try { healFalseEmpty(n); } catch (_) {}
1031
946
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
1032
947
  if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
1033
948
  allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
1034
- Arch.cache.invalidate('dom-added'); requestBurst(); return;
949
+ requestBurst(); return;
1035
950
  }
1036
951
  }
1037
952
  }
@@ -1110,8 +1025,7 @@ function startShowBatch(ids) {
1110
1025
  $(window).off('.nbbEzoic');
1111
1026
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
1112
1027
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
1113
- S.pageKey = Arch.page.key();
1114
- Arch.cache.invalidate('ajaxify.end');
1028
+ S.pageKey = pageKey();
1115
1029
  blockedUntil = 0;
1116
1030
  muteConsole(); ensureTcfLocator(); warmNetwork();
1117
1031
  patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
@@ -1134,17 +1048,6 @@ function startShowBatch(ids) {
1134
1048
  } catch (_) {}
1135
1049
  }
1136
1050
 
1137
- function bindResize() {
1138
- let rT = 0;
1139
- window.addEventListener('resize', () => {
1140
- if (rT) clearTimeout(rT);
1141
- rT = setTimeout(() => {
1142
- ensureIOViewportMode();
1143
- requestBurst();
1144
- }, 120);
1145
- }, { passive: true });
1146
- }
1147
-
1148
1051
  function bindScroll() {
1149
1052
  let ticking = false;
1150
1053
  try {
@@ -1171,29 +1074,16 @@ function startShowBatch(ids) {
1171
1074
 
1172
1075
  // ── Boot ───────────────────────────────────────────────────────────────────
1173
1076
 
1174
- const App = {
1175
- modules: {
1176
- infra: { muteConsole, ensureTcfLocator, warmNetwork, patchShowAds },
1177
- lifecycle: { ensureDomObserver, bindNodeBB, cleanup },
1178
- viewport: { getIO, bindResize, bindScroll },
1179
- scheduler: { requestBurst },
1180
- },
1181
- boot() {
1182
- this.modules.infra.muteConsole();
1183
- this.modules.infra.ensureTcfLocator();
1184
- this.modules.infra.warmNetwork();
1185
- this.modules.infra.patchShowAds();
1186
- this.modules.viewport.getIO();
1187
- this.modules.lifecycle.ensureDomObserver();
1188
- this.modules.lifecycle.bindNodeBB();
1189
- this.modules.viewport.bindResize();
1190
- this.modules.viewport.bindScroll();
1191
- blockedUntil = 0;
1192
- this.modules.scheduler.requestBurst();
1193
- },
1194
- };
1195
-
1196
- S.pageKey = Arch.page.key();
1197
- App.boot();
1077
+ S.pageKey = pageKey();
1078
+ muteConsole();
1079
+ ensureTcfLocator();
1080
+ warmNetwork();
1081
+ patchShowAds();
1082
+ getIO();
1083
+ ensureDomObserver();
1084
+ bindNodeBB();
1085
+ bindScroll();
1086
+ blockedUntil = 0;
1087
+ requestBurst();
1198
1088
 
1199
1089
  })();
package/public/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * NodeBB Ezoic Infinite Ads — style.css (v21)
2
+ * NodeBB Ezoic Infinite Ads — style.css (v20)
3
3
  */
4
4
 
5
5
  /* ── Wrapper ──────────────────────────────────────────────────────────────── */
@@ -8,9 +8,8 @@
8
8
  width: 100%;
9
9
  margin: 0 !important;
10
10
  padding: 0 !important;
11
- overflow: visible;
12
- contain: none;
13
- position: relative;
11
+ overflow: hidden;
12
+ contain: layout style;
14
13
  }
15
14
 
16
15
  /* Placeholder : 1px minimum pour rester visible par l'IntersectionObserver */
@@ -51,7 +50,11 @@
51
50
  position: absolute !important;
52
51
  }
53
52
 
54
- /* Compat sticky/fixed Ezoic : ne pas neutraliser globalement ici. */
53
+ /* Neutralise sticky dans nos wraps (évite l'effet "gliding") */
54
+ .nodebb-ezoic-wrap .ezads-sticky-intradiv {
55
+ position: static !important;
56
+ top: auto !important;
57
+ }
55
58
 
56
59
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
57
60
  .ezoic-ad {