nodebb-plugin-ezoic-infinite 1.8.9 → 1.8.11

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,8 +71,9 @@ 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));
74
75
  const userGroups = await groups.getUserGroups([uid]);
75
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
76
+ return (userGroups[0] || []).some(g => excluded.has(String(g.name)));
76
77
  }
77
78
 
78
79
  // ── Scripts Ezoic ──────────────────────────────────────────────────────────
@@ -113,13 +114,34 @@ plugin.injectEzoicHead = async (data) => {
113
114
  const uid = data.req?.uid ?? 0;
114
115
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
116
  if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
- data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
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
+ }
118
121
  }
119
122
  } catch (_) {}
120
123
  return data;
121
124
  };
122
125
 
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
+
123
145
  plugin.init = async ({ router, middleware }) => {
124
146
  async function render(req, res) {
125
147
  const settings = await getSettings();
@@ -139,21 +161,7 @@ plugin.init = async ({ router, middleware }) => {
139
161
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
140
162
  const settings = await getSettings();
141
163
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
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
- });
164
+ res.json(publicConfigPayload(settings, excluded));
157
165
  });
158
166
  };
159
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.9",
3
+ "version": "1.8.11",
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 v36
2
+ * NodeBB Ezoic Infinite Ads — client.js v52
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
@@ -32,6 +32,8 @@
32
32
  *
33
33
  * v34 moveDistantWrap — voir v38.
34
34
  *
35
+ * v52 Clean architecture interne (modules logiques, cache DOM, bridge Ezoic).
36
+ *
35
37
  * v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
36
38
  * après login — filter:middleware.renderHeader re-évalue l'exclusion au
37
39
  * rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
@@ -89,7 +91,6 @@
89
91
  const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
90
92
  const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
91
93
  const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
92
- const MIN_RECYCLE_AGE_DESKTOP_MS = 3000; // évite le flash show→recycle trop rapide sur PC
93
94
 
94
95
  // Marges IO larges et fixes — observer créé une seule fois au boot
95
96
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -150,6 +151,9 @@
150
151
  burstDeadline: 0,
151
152
  burstCount: 0,
152
153
  lastBurstTs: 0,
154
+ ioViewportMode: isMobile() ? 'm' : 'd',
155
+ domRev: 0,
156
+ domCache: new Map(),
153
157
  };
154
158
 
155
159
  let blockedUntil = 0;
@@ -158,7 +162,61 @@
158
162
  const isBlocked = () => ts() < blockedUntil;
159
163
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
160
164
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
161
- const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
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
+ };
162
220
 
163
221
  function healFalseEmpty(root = document) {
164
222
  try {
@@ -216,7 +274,8 @@
216
274
 
217
275
  function mutate(fn) {
218
276
  S.mutGuard++;
219
- try { fn(); } finally { S.mutGuard--; }
277
+ try { fn(); }
278
+ finally { S.mutGuard--; Arch.cache.invalidate('mutate'); }
220
279
  }
221
280
  function scheduleDestroyFlush() {
222
281
  if (S.destroyBatchTimer) return;
@@ -236,11 +295,7 @@ function flushDestroyBatch() {
236
295
  ids.push(id);
237
296
  }
238
297
  if (ids.length) {
239
- try {
240
- const ez = window.ezstandalone;
241
- const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
242
- try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
243
- } catch (_) {}
298
+ try { Arch.ezoic.run(() => Arch.ezoic.call('destroyPlaceholders', ids)); } catch (_) {}
244
299
  }
245
300
  if (S.destroyPending.length) scheduleDestroyFlush();
246
301
  }
@@ -256,20 +311,28 @@ function destroyEzoicId(id) {
256
311
  scheduleDestroyFlush();
257
312
  }
258
313
 
259
- function destroyBeforeReuse(ids) {
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) {
260
329
  const out = [];
261
- const toDestroy = [];
262
330
  const seen = new Set();
263
331
  for (const raw of (ids || [])) {
264
332
  const id = parseInt(raw, 10);
265
333
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
266
334
  seen.add(id);
267
335
  out.push(id);
268
- if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
269
- }
270
- if (toDestroy.length) {
271
- try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
272
- for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
273
336
  }
274
337
  return out;
275
338
  }
@@ -315,6 +378,7 @@ function destroyBeforeReuse(ids) {
315
378
  }
316
379
 
317
380
  function getKind() {
381
+ return Arch.cache.get('kind', () => {
318
382
  const p = location.pathname;
319
383
  if (/^\/topic\//.test(p)) return 'topic';
320
384
  if (/^\/category\//.test(p)) return 'categoryTopics';
@@ -323,12 +387,13 @@ function destroyBeforeReuse(ids) {
323
387
  if (document.querySelector(SEL.post)) return 'topic';
324
388
  if (document.querySelector(SEL.topic)) return 'categoryTopics';
325
389
  return 'other';
390
+ });
326
391
  }
327
392
 
328
393
  // ── Items DOM ──────────────────────────────────────────────────────────────
329
394
 
330
395
  function getPosts() {
331
- return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
396
+ return Arch.cache.queryAll('posts', SEL.post, el => {
332
397
  if (!el.isConnected) return false;
333
398
  if (!el.querySelector('[component="post/content"]')) return false;
334
399
  const p = el.parentElement?.closest(SEL.post);
@@ -337,8 +402,8 @@ function destroyBeforeReuse(ids) {
337
402
  });
338
403
  }
339
404
 
340
- const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
341
- const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
405
+ const getTopics = () => Arch.cache.queryAll('topics', SEL.topic);
406
+ const getCategories = () => Arch.cache.queryAll('categories', SEL.category);
342
407
 
343
408
  // ── Wraps — détection ──────────────────────────────────────────────────────
344
409
 
@@ -439,6 +504,7 @@ function destroyBeforeReuse(ids) {
439
504
  S.mountedIds.delete(id);
440
505
  S.pendingSet.delete(id);
441
506
  S.lastShow.delete(id);
507
+ queueDestroyIds([id]);
442
508
  S.ezActiveIds.delete(id);
443
509
  }
444
510
  }
@@ -455,7 +521,7 @@ function destroyBeforeReuse(ids) {
455
521
  * Priorité : wraps vides d'abord, remplis si nécessaire.
456
522
  */
457
523
  function recycleAndMove(klass, targetEl, newKey) {
458
- const ez = window.ezstandalone;
524
+ const ez = Arch.ezoic.ensure();
459
525
  if (typeof ez?.destroyPlaceholders !== 'function' ||
460
526
  typeof ez?.define !== 'function' ||
461
527
  typeof ez?.displayMore !== 'function') return null;
@@ -473,10 +539,6 @@ function recycleAndMove(klass, targetEl, newKey) {
473
539
  for (const wrap of S.wrapByKey.values()) {
474
540
  if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
475
541
  try {
476
- if (!isMobile()) {
477
- const shownTs = parseInt(wrap.getAttribute(A_SHOWN) || '0', 10);
478
- if (shownTs > 0 && (ts() - shownTs) < MIN_RECYCLE_AGE_DESKTOP_MS) continue;
479
- }
480
542
  const rect = wrap.getBoundingClientRect();
481
543
  const isAbove = rect.bottom <= farAbove;
482
544
  const isBelow = rect.top >= farBelow;
@@ -531,7 +593,7 @@ function recycleAndMove(klass, targetEl, newKey) {
531
593
  };
532
594
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
533
595
  const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
534
- try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
596
+ try { Arch.ezoic.run(doDestroy); } catch (_) {}
535
597
 
536
598
  return { id, wrap: best };
537
599
  }
@@ -571,7 +633,7 @@ function recycleAndMove(klass, targetEl, newKey) {
571
633
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
572
634
  if (ph instanceof Element) S.io?.unobserve(ph);
573
635
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
574
- if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
636
+ if (Number.isFinite(id)) { queueDestroyIds([id]); S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
575
637
  const key = w.getAttribute(A_ANCHOR);
576
638
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
577
639
  w.remove();
@@ -594,19 +656,18 @@ function recycleAndMove(klass, targetEl, newKey) {
594
656
  // supprimerait les wraps, et provoquerait une réinjection en haut.
595
657
 
596
658
  function pruneOrphansBetween() {
659
+ if (S.burstActive || (S.scrollSpeed || 0) > 1800) return;
597
660
  const klass = 'ezoic-ad-between';
598
661
  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));
599
663
 
600
664
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
601
665
  const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
602
- if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
603
-
666
+ if (ts() - created < MIN_PRUNE_AGE_MS) return;
604
667
  const key = w.getAttribute(A_ANCHOR) ?? '';
605
- const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
668
+ const sid = key.slice(klass.length + 1);
606
669
  if (!sid) { mutate(() => dropWrap(w)); return; }
607
-
608
- const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
609
- if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
670
+ if (!liveAnchors.has(sid)) mutate(() => dropWrap(w));
610
671
  });
611
672
  }
612
673
 
@@ -652,6 +713,8 @@ function recycleAndMove(klass, targetEl, newKey) {
652
713
  const w = insertAfter(el, id, klass, key);
653
714
  if (w) { observePh(id); inserted++; }
654
715
  } else {
716
+ // Recyclage agressif = source fréquente de churn visuel (surtout sticky/fixed)
717
+ if (klass === 'ezoic-ad-message') break;
655
718
  const recycled = recycleAndMove(klass, el, key);
656
719
  if (!recycled) break;
657
720
  inserted++;
@@ -662,15 +725,31 @@ function recycleAndMove(klass, targetEl, newKey) {
662
725
 
663
726
  // ── IntersectionObserver & Show ────────────────────────────────────────────
664
727
 
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
+
665
737
  function getIO() {
738
+ ensureIOViewportMode();
666
739
  if (S.io) return S.io;
667
740
  try {
668
741
  S.io = new IntersectionObserver(entries => {
669
742
  for (const e of entries) {
670
743
  if (!e.isIntersecting) continue;
671
- if (e.target instanceof Element) S.io?.unobserve(e.target);
672
- const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
673
- if (Number.isFinite(id) && id > 0) enqueueShow(id);
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
+ }
674
753
  }
675
754
  }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
676
755
  } catch (_) { S.io = null; }
@@ -691,12 +770,15 @@ function recycleAndMove(klass, targetEl, newKey) {
691
770
  }
692
771
 
693
772
  function enqueueShow(id) {
694
- if (!id || isBlocked()) return;
773
+ if (!id || isBlocked()) return false;
695
774
  const n = parseInt(id, 10);
696
- if (!Number.isFinite(n) || n <= 0) return;
697
- if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
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;
698
779
  if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
699
780
  scheduleDrainQueue();
781
+ return true;
700
782
  }
701
783
 
702
784
  function scheduleDrainQueue() {
@@ -762,19 +844,18 @@ function startShowBatch(ids) {
762
844
 
763
845
  if (!valid.length) { clearTimeout(timer); return release(); }
764
846
 
765
- window.ezstandalone = window.ezstandalone || {};
766
- const ez = window.ezstandalone;
847
+ const ez = Arch.ezoic.ensure();
767
848
  const doShow = () => {
768
- const prepared = destroyBeforeReuse(valid);
849
+ const prepared = prepareIdsForShow(valid);
769
850
  if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
770
- try { ez.showAds(...prepared); } catch (_) {}
851
+ try { ez.showAds(...prepared); } catch (_) { dbg('showAds failed', prepared); }
771
852
  for (const id of prepared) {
772
853
  S.ezActiveIds.add(id);
773
854
  S.ezShownSinceDestroy.add(id);
774
855
  }
775
856
  setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
776
857
  };
777
- Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
858
+ Arch.ezoic.run(doShow);
778
859
  } catch (_) { clearTimeout(timer); release(); }
779
860
  });
780
861
  }
@@ -789,8 +870,7 @@ function startShowBatch(ids) {
789
870
  function patchShowAds() {
790
871
  const apply = () => {
791
872
  try {
792
- window.ezstandalone = window.ezstandalone || {};
793
- const ez = window.ezstandalone;
873
+ const ez = Arch.ezoic.ensure();
794
874
  if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
795
875
  window.__nbbEzPatched = true;
796
876
  const orig = ez.showAds.bind(ez);
@@ -817,8 +897,7 @@ function startShowBatch(ids) {
817
897
  };
818
898
  apply();
819
899
  if (!window.__nbbEzPatched) {
820
- window.ezstandalone = window.ezstandalone || {};
821
- (window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
900
+ Arch.ezoic.run(apply);
822
901
  }
823
902
  }
824
903
 
@@ -927,6 +1006,7 @@ function startShowBatch(ids) {
927
1006
  S.scrollSpeed = 0;
928
1007
  S.lastScrollY = 0;
929
1008
  S.lastScrollTs = 0;
1009
+ Arch.cache.invalidate('cleanup');
930
1010
  }
931
1011
 
932
1012
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -944,14 +1024,14 @@ function startShowBatch(ids) {
944
1024
  sawWrapRemoval = true;
945
1025
  }
946
1026
  }
947
- if (sawWrapRemoval) queueSweepDeadWraps();
1027
+ if (sawWrapRemoval) { Arch.cache.invalidate('wrap-removed'); queueSweepDeadWraps(); }
948
1028
  for (const n of m.addedNodes) {
949
1029
  if (n.nodeType !== 1) continue;
950
1030
  try { healFalseEmpty(n); } catch (_) {}
951
1031
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
952
1032
  if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
953
1033
  allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
954
- requestBurst(); return;
1034
+ Arch.cache.invalidate('dom-added'); requestBurst(); return;
955
1035
  }
956
1036
  }
957
1037
  }
@@ -1030,7 +1110,8 @@ function startShowBatch(ids) {
1030
1110
  $(window).off('.nbbEzoic');
1031
1111
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
1032
1112
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
1033
- S.pageKey = pageKey();
1113
+ S.pageKey = Arch.page.key();
1114
+ Arch.cache.invalidate('ajaxify.end');
1034
1115
  blockedUntil = 0;
1035
1116
  muteConsole(); ensureTcfLocator(); warmNetwork();
1036
1117
  patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
@@ -1053,6 +1134,17 @@ function startShowBatch(ids) {
1053
1134
  } catch (_) {}
1054
1135
  }
1055
1136
 
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
+
1056
1148
  function bindScroll() {
1057
1149
  let ticking = false;
1058
1150
  try {
@@ -1079,16 +1171,29 @@ function startShowBatch(ids) {
1079
1171
 
1080
1172
  // ── Boot ───────────────────────────────────────────────────────────────────
1081
1173
 
1082
- S.pageKey = pageKey();
1083
- muteConsole();
1084
- ensureTcfLocator();
1085
- warmNetwork();
1086
- patchShowAds();
1087
- getIO();
1088
- ensureDomObserver();
1089
- bindNodeBB();
1090
- bindScroll();
1091
- blockedUntil = 0;
1092
- requestBurst();
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();
1093
1198
 
1094
1199
  })();
package/public/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * NodeBB Ezoic Infinite Ads — style.css (v20)
2
+ * NodeBB Ezoic Infinite Ads — style.css (v21)
3
3
  */
4
4
 
5
5
  /* ── Wrapper ──────────────────────────────────────────────────────────────── */
@@ -8,8 +8,9 @@
8
8
  width: 100%;
9
9
  margin: 0 !important;
10
10
  padding: 0 !important;
11
- overflow: hidden;
12
- contain: layout style;
11
+ overflow: visible;
12
+ contain: none;
13
+ position: relative;
13
14
  }
14
15
 
15
16
  /* Placeholder : 1px minimum pour rester visible par l'IntersectionObserver */
@@ -50,11 +51,7 @@
50
51
  position: absolute !important;
51
52
  }
52
53
 
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
- }
54
+ /* Compat sticky/fixed Ezoic : ne pas neutraliser globalement ici. */
58
55
 
59
56
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
60
57
  .ezoic-ad {