nodebb-plugin-ezoic-infinite 1.8.26 → 1.8.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.26",
3
+ "version": "1.8.28",
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
@@ -83,7 +83,7 @@
83
83
  const MAX_INFLIGHT = 4; // max showAds() simultanés
84
84
  const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
85
85
  const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
86
- const MIN_PLACEHOLDER_HEIGHT = 50; // réservation minimale perçue
86
+ const MIN_PLACEHOLDER_HEIGHT = 1; // placeholder minimal (test stabilité scroll)
87
87
 
88
88
  // Marges IO larges et fixes — observer créé une seule fois au boot
89
89
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -144,13 +144,51 @@
144
144
  const isBlocked = () => ts() < blockedUntil;
145
145
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
146
146
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
147
- const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
147
+ const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
148
+ const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
148
149
 
149
150
  function mutate(fn) {
150
151
  S.mutGuard++;
151
152
  try { fn(); } finally { S.mutGuard--; }
152
153
  }
153
154
 
155
+ function clearEmptyIfFilled(wrap) {
156
+ try {
157
+ if (!wrap?.isConnected) return false;
158
+ const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
159
+ if (!ph) return false;
160
+ if (!isFilled(ph)) return false;
161
+ wrap.classList.remove('is-empty');
162
+ return true;
163
+ } catch (_) { return false; }
164
+ }
165
+
166
+ function scheduleUncollapseChecksForWrap(wrap) {
167
+ if (!wrap) return;
168
+ for (const ms of [500, 1500, 3000, 7000, 15000]) {
169
+ setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
170
+ }
171
+ }
172
+
173
+
174
+ function clearEmptyIfFilled(wrap) {
175
+ try {
176
+ if (!wrap?.isConnected) return false;
177
+ const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
178
+ if (!ph) return false;
179
+ if (!isFilled(ph)) return false;
180
+ wrap.classList.remove('is-empty');
181
+ return true;
182
+ } catch (_) { return false; }
183
+ }
184
+
185
+ function scheduleUncollapseChecksForWrap(wrap) {
186
+ if (!wrap) return;
187
+ for (const ms of [500, 1500, 3000, 7000, 15000]) {
188
+ setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
189
+ }
190
+ }
191
+
154
192
  // ── Config ─────────────────────────────────────────────────────────────────
155
193
 
156
194
  async function fetchConfig() {
@@ -370,17 +408,30 @@
370
408
  best.setAttribute(A_CREATED, String(ts()));
371
409
  best.setAttribute(A_SHOWN, '0');
372
410
  best.classList.remove('is-empty');
373
- const ph = best.querySelector(`#${PH_PREFIX}${id}`);
374
- if (ph) ph.innerHTML = '';
411
+ const oldPh = best.querySelector(`#${PH_PREFIX}${id}`);
412
+ if (oldPh) {
413
+ const fresh = document.createElement('div');
414
+ fresh.id = `${PH_PREFIX}${id}`;
415
+ fresh.setAttribute('data-ezoic-id', String(id));
416
+ fresh.style.minHeight = `${MIN_PLACEHOLDER_HEIGHT}px`;
417
+ oldPh.replaceWith(fresh);
418
+ }
375
419
  targetEl.insertAdjacentElement('afterend', best);
376
420
  });
377
421
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
378
422
  S.wrapByKey.set(newKey, best);
379
423
 
380
- // Délais requis : destroyPlaceholders est asynchrone en interne
381
- const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
382
- const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
383
- const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
424
+ // Recyclage Ezoic : détruire l'ancien placeholder avant de réutiliser le même ID.
425
+ // Puis ré-observer + re-show (batché via patchShowAds) sur le placeholder recréé.
426
+ const doDestroy = () => {
427
+ try { ez.destroyPlaceholders(id); } catch (_) {
428
+ try { ez.destroyPlaceholders([id]); } catch (_) {}
429
+ }
430
+ setTimeout(() => {
431
+ try { observePh(id); } catch (_) {}
432
+ try { enqueueShow(id); } catch (_) {}
433
+ }, 450);
434
+ };
384
435
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
385
436
 
386
437
  return { id, wrap: best };
@@ -585,7 +636,10 @@
585
636
  window.ezstandalone = window.ezstandalone || {};
586
637
  const ez = window.ezstandalone;
587
638
  const doShow = () => {
639
+ let wrap = null;
640
+ try { wrap = ph.closest?.(`.${WRAP_CLASS}`) || null; } catch (_) {}
588
641
  try { ez.showAds(id); } catch (_) {}
642
+ if (wrap) scheduleUncollapseChecksForWrap(wrap);
589
643
  scheduleEmptyCheck(id, t);
590
644
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
591
645
  };
@@ -601,7 +655,8 @@
601
655
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
602
656
  if (!wrap || !ph?.isConnected) return;
603
657
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
604
- wrap.classList.toggle('is-empty', !isFilled(ph));
658
+ if (clearEmptyIfFilled(wrap)) return;
659
+ wrap.classList.add('is-empty');
605
660
  } catch (_) {}
606
661
  }, EMPTY_CHECK_MS);
607
662
  }
@@ -620,17 +675,34 @@
620
675
  if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
621
676
  window.__nbbEzPatched = true;
622
677
  const orig = ez.showAds.bind(ez);
678
+ const q = new Set();
679
+ let flushTimer = null;
680
+ const BATCH_SIZE = 3;
681
+ const FLUSH_MS = 80;
682
+ const flush = () => {
683
+ flushTimer = null;
684
+ if (isBlocked() || !q.size) return;
685
+ const ids = Array.from(q).sort((a, b) => a - b);
686
+ q.clear();
687
+ const valid = ids.filter(id => document.getElementById(`${PH_PREFIX}${id}`)?.isConnected);
688
+ for (let i = 0; i < valid.length; i += BATCH_SIZE) {
689
+ const chunk = valid.slice(i, i + BATCH_SIZE);
690
+ try { orig(...chunk); } catch (_) {
691
+ for (const id of chunk) { try { orig(id); } catch (_) {} }
692
+ }
693
+ }
694
+ };
623
695
  ez.showAds = function (...args) {
624
696
  if (isBlocked()) return;
625
- const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
626
- const seen = new Set();
697
+ const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
627
698
  for (const v of ids) {
628
699
  const id = parseInt(v, 10);
629
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
700
+ if (!Number.isFinite(id) || id <= 0) continue;
630
701
  if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
631
- seen.add(id);
632
- try { orig(id); } catch (_) {}
702
+ q.add(id);
633
703
  }
704
+ if (!q.size) return;
705
+ if (!flushTimer) flushTimer = setTimeout(flush, FLUSH_MS);
634
706
  };
635
707
  } catch (_) {}
636
708
  };
@@ -746,6 +818,21 @@
746
818
  if (S.domObs) return;
747
819
  S.domObs = new MutationObserver(muts => {
748
820
  if (S.mutGuard > 0 || isBlocked()) return;
821
+ for (const m of muts) {
822
+ if (m.type !== 'childList') continue;
823
+ for (const node of m.addedNodes) {
824
+ if (!(node instanceof Element)) continue;
825
+ let hasAd = false;
826
+ try {
827
+ hasAd = !!(node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL));
828
+ } catch (_) {}
829
+ if (!hasAd) continue;
830
+ try {
831
+ const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) || m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
832
+ if (wrap) clearEmptyIfFilled(wrap);
833
+ } catch (_) {}
834
+ }
835
+ }
749
836
  const relevant = (() => {
750
837
  const k = getKind();
751
838
  if (k === 'topic') return [SEL.post];
package/public/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * NodeBB Ezoic Infinite Ads — style.css (v20.1)
2
+ * NodeBB Ezoic Infinite Ads — style.css (v20)
3
3
  */
4
4
 
5
5
  /* ── Wrapper ──────────────────────────────────────────────────────────────── */
@@ -39,9 +39,9 @@
39
39
  /* Ne pas écraser la hauteur réelle calculée par Ezoic */
40
40
  .nodebb-ezoic-wrap .ezoic-ad,
41
41
  .nodebb-ezoic-wrap span.ezoic-ad {
42
- display: block !important;
43
42
  margin: 0 !important;
44
43
  padding: 0 !important;
44
+ display: block !important;
45
45
  height: auto !important;
46
46
  min-height: unset !important;
47
47
  max-height: none !important;
@@ -60,8 +60,8 @@
60
60
 
61
61
  /* ── État vide ────────────────────────────────────────────────────────────── */
62
62
  /*
63
- Ajouté après le délai d'empty-check si aucun fill détecté.
64
- Collapse à 1px : réserve minimale, reste observable par l'IO.
63
+ Ajouté 20s après showAds si aucun fill détecté.
64
+ Collapse à 1px : réserve minimale demandée, reste observable par l'IO.
65
65
  */
66
66
  .nodebb-ezoic-wrap.is-empty {
67
67
  display: block !important;
@@ -77,4 +77,15 @@
77
77
  .ezoic-ad {
78
78
  margin: 0 !important;
79
79
  padding: 0 !important;
80
- }
80
+ }
81
+
82
+
83
+ /* Anti-faux-empty : si une pub est présente, annule le collapse */
84
+ .nodebb-ezoic-wrap.is-empty:has(iframe),
85
+ .nodebb-ezoic-wrap.is-empty:has([data-google-container-id]),
86
+ .nodebb-ezoic-wrap.is-empty:has(div[id$="__container__"]) {
87
+ height: auto !important;
88
+ min-height: 0 !important;
89
+ max-height: none !important;
90
+ overflow: visible !important;
91
+ }