nodebb-plugin-ezoic-infinite 1.7.84 → 1.7.85

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.7.84",
3
+ "version": "1.7.85",
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
@@ -76,16 +76,22 @@
76
76
  const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
77
77
  const A_CREATED = 'data-ezoic-created'; // timestamp création ms
78
78
  const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
79
- const A_FILLED = 'data-ezoic-filled'; // timestamp premier fill réel ms
79
+ const A_FILLED = 'data-ezoic-filled'; // timestamp premier fill réel ms
80
+ const A_LAST_W = 'data-ezoic-lastw'; // dernière largeur pub connue
81
+ const A_LAST_H = 'data-ezoic-lasth'; // dernière hauteur pub connue
80
82
 
81
83
  const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
84
+ const EMPTY_RECHECK_2_MS = EMPTY_CHECK_MS + 5_000;
85
+ const EMPTY_RECHECK_3_MS = EMPTY_CHECK_MS + 15_000;
86
+ const MIN_LIVE_AFTER_SHOW_MS = 15_000;
87
+ const MIN_LIVE_AFTER_FILL_MS = 25_000;
88
+ const KEEP_SHELL_AFTER_UNUSED_MS = 90_000;
89
+ const MIN_SHELL_HEIGHT = 120;
82
90
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
83
91
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
84
92
  const MAX_INFLIGHT = 4; // max showAds() simultanés
85
93
  const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
86
94
  const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
87
- const MIN_LIVE_AFTER_SHOW_MS = 15_000; // grâce post-show avant drop/recycle
88
- const MIN_LIVE_AFTER_FILL_MS = 25_000; // grâce post-fill réel avant drop/recycle
89
95
 
90
96
  // Marges IO larges et fixes — observer créé une seule fois au boot
91
97
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -130,8 +136,7 @@
130
136
  pending: [], // ids en attente de slot inflight
131
137
  pendingSet: new Set(),
132
138
  wrapByKey: new Map(), // anchorKey → wrap DOM node
133
- emptyChecks: new Map(), // id -> timeout ids for empty checks
134
- fillObs: new Map(), // id -> MutationObserver (late fill uncollapse + responsive fit)
139
+ emptyChecks: new Map(), // id -> timeout ids
135
140
  runQueued: false,
136
141
  burstActive: false,
137
142
  burstDeadline: 0,
@@ -147,12 +152,16 @@
147
152
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
148
153
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
149
154
 
155
+ function mutate(fn) {
156
+ S.mutGuard++;
157
+ try { fn(); } finally { S.mutGuard--; }
158
+ }
159
+
150
160
  function clearEmptyChecks(id) {
151
161
  const timers = S.emptyChecks.get(id);
152
- if (timers) {
153
- for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
154
- S.emptyChecks.delete(id);
155
- }
162
+ if (!timers) return;
163
+ for (const t of timers) clearTimeout(t);
164
+ S.emptyChecks.delete(id);
156
165
  }
157
166
 
158
167
  function queueEmptyCheck(id, timerId) {
@@ -160,28 +169,28 @@
160
169
  S.emptyChecks.get(id).push(timerId);
161
170
  }
162
171
 
172
+ function hasEverFilled(wrap) { return !!wrap?.getAttribute?.(A_FILLED); }
173
+ function getFilledTs(wrap) { return parseInt(wrap?.getAttribute?.(A_FILLED) || '0', 10) || 0; }
174
+ function shouldKeepShellAfterUnused(wrap) {
175
+ const t = getFilledTs(wrap);
176
+ return !!t && (Date.now() - t) < KEEP_SHELL_AFTER_UNUSED_MS;
177
+ }
163
178
  function markFilledOnce(ph) {
164
179
  try {
165
180
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
166
181
  if (!wrap) return;
167
- if (!wrap.getAttribute(A_FILLED) && isFilled(ph)) {
168
- wrap.setAttribute(A_FILLED, String(ts()));
169
- }
182
+ if (!wrap.getAttribute(A_FILLED) && isFilled(ph)) wrap.setAttribute(A_FILLED, String(Date.now()));
170
183
  } catch (_) {}
171
184
  }
172
-
173
- function isProtectedFromDrop(wrap) {
185
+ function rememberAdSize(ph, w, h) {
174
186
  try {
175
- if (!wrap) return false;
176
- const now = ts();
177
- const shownTs = parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) || 0;
178
- const filledTs = parseInt(wrap.getAttribute(A_FILLED) || '0', 10) || 0;
179
- if (shownTs && (now - shownTs) < MIN_LIVE_AFTER_SHOW_MS) return true;
180
- if (filledTs && (now - filledTs) < MIN_LIVE_AFTER_FILL_MS) return true;
181
- return false;
182
- } catch (_) { return false; }
187
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
188
+ if (!wrap) return;
189
+ const ww = Math.round(w || 0), hh = Math.round(h || 0);
190
+ if (ww > 0) wrap.setAttribute(A_LAST_W, String(ww));
191
+ if (hh > 0) wrap.setAttribute(A_LAST_H, String(hh));
192
+ } catch (_) {}
183
193
  }
184
-
185
194
  function getAdIntrinsicSize(ph) {
186
195
  if (!ph) return null;
187
196
  const iframe = ph.querySelector('iframe');
@@ -189,45 +198,42 @@
189
198
  const wAttr = parseInt(iframe.getAttribute('width') || iframe.getAttribute('ezaw') || '0', 10);
190
199
  const hAttr = parseInt(iframe.getAttribute('height') || iframe.getAttribute('ezah') || '0', 10);
191
200
  const cs = window.getComputedStyle(iframe);
192
- const wCss = parseFloat(cs.width) || 0;
193
- const hCss = parseFloat(cs.height) || 0;
194
- const w = wAttr || Math.round(wCss);
195
- const h = hAttr || Math.round(hCss);
201
+ const w = wAttr || Math.round(parseFloat(cs.width) || iframe.offsetWidth || 0);
202
+ const h = hAttr || Math.round(parseFloat(cs.height) || iframe.offsetHeight || 0);
196
203
  if (w > 0 && h > 0) return { w, h };
197
204
  }
198
- const gpt = ph.querySelector('[id$="__container__"]');
199
- if (gpt) {
200
- const cs = window.getComputedStyle(gpt);
201
- const w = Math.round(parseFloat(cs.width) || gpt.offsetWidth || 0);
202
- const h = Math.round(parseFloat(cs.height) || gpt.offsetHeight || 0);
203
- if (w > 0 && h > 0) return { w, h };
204
- }
205
- const ez = ph.querySelector('.ezoic-ad');
206
- if (ez) {
207
- const cs = window.getComputedStyle(ez);
208
- const w = Math.round(parseFloat(cs.width) || ez.offsetWidth || 0);
209
- const h = Math.round(parseFloat(cs.height) || ez.offsetHeight || 0);
205
+ const c = ph.querySelector('[id$="__container__"]') || ph.querySelector('.ezoic-ad');
206
+ if (c) {
207
+ const cs = window.getComputedStyle(c);
208
+ const w = Math.round(parseFloat(cs.width) || c.offsetWidth || 0);
209
+ const h = Math.round(parseFloat(cs.height) || c.offsetHeight || 0);
210
210
  if (w > 0 && h > 0) return { w, h };
211
211
  }
212
212
  return null;
213
213
  }
214
-
214
+ function rememberCurrentAdSize(ph) {
215
+ try {
216
+ const size = getAdIntrinsicSize(ph);
217
+ if (size?.w > 0 && size?.h > 0) return rememberAdSize(ph, size.w, size.h);
218
+ const r = ph.getBoundingClientRect?.();
219
+ if (r && (r.width > 0 || r.height > 0)) rememberAdSize(ph, r.width, r.height);
220
+ } catch (_) {}
221
+ }
215
222
  function ensureScaleBox(ph) {
216
223
  let box = ph.querySelector(':scope > .nbb-ez-scale-box');
217
224
  if (box) return box;
218
- const kids = Array.from(ph.childNodes).filter(n => {
219
- if (n.nodeType === 1) return true;
220
- if (n.nodeType === 3) return (n.textContent || '').trim().length > 0;
221
- return false;
222
- });
223
- if (!kids.length) return null;
225
+ let hasChild = false;
226
+ for (const n of Array.from(ph.childNodes)) {
227
+ if (n.nodeType === 1) { hasChild = true; break; }
228
+ if (n.nodeType === 3 && (n.textContent || '').trim()) { hasChild = true; break; }
229
+ }
230
+ if (!hasChild) return null;
224
231
  box = document.createElement('div');
225
232
  box.className = 'nbb-ez-scale-box';
226
233
  while (ph.firstChild) box.appendChild(ph.firstChild);
227
234
  ph.appendChild(box);
228
235
  return box;
229
236
  }
230
-
231
237
  function clearAdScale(ph) {
232
238
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
233
239
  if (!wrap) return;
@@ -241,18 +247,18 @@
241
247
  }
242
248
  ph.style.removeProperty('height');
243
249
  }
244
-
245
250
  function fitWideAd(ph) {
246
251
  try {
247
252
  if (!ph?.isConnected) return;
248
253
  const wrap = ph.closest?.(`.${WRAP_CLASS}`);
249
254
  if (!wrap) return;
250
- if (!isFilled(ph)) { clearAdScale(ph); return; }
255
+ if (!isFilled(ph)) return clearAdScale(ph);
251
256
  const size = getAdIntrinsicSize(ph);
252
257
  if (!size?.w || !size?.h) return;
258
+ rememberAdSize(ph, size.w, size.h);
253
259
  const wrapWidth = Math.max(0, Math.floor(wrap.clientWidth || wrap.getBoundingClientRect().width || 0));
254
260
  if (!wrapWidth) return;
255
- if (size.w <= wrapWidth + 2) { clearAdScale(ph); return; }
261
+ if (size.w <= wrapWidth + 2) return clearAdScale(ph);
256
262
  const scale = Math.max(0.1, Math.min(1, wrapWidth / size.w));
257
263
  const scaledH = Math.ceil(size.h * scale);
258
264
  const box = ensureScaleBox(ph);
@@ -265,39 +271,66 @@
265
271
  wrap.style.height = `${scaledH}px`;
266
272
  } catch (_) {}
267
273
  }
268
-
274
+ function applyUnusedShell(wrap, ph) {
275
+ try {
276
+ if (!wrap) return;
277
+ const lastH = parseInt(wrap.getAttribute(A_LAST_H) || '0', 10) || 0;
278
+ const h = Math.max(MIN_SHELL_HEIGHT, lastH || 0);
279
+ wrap.classList.remove('is-empty');
280
+ wrap.classList.add('is-unused-shell');
281
+ wrap.style.minHeight = `${h}px`;
282
+ if (ph) ph.style.minHeight = `${h}px`;
283
+ } catch (_) {}
284
+ }
285
+ function clearUnusedShell(wrap, ph) {
286
+ try {
287
+ wrap?.classList?.remove('is-unused-shell');
288
+ wrap?.style?.removeProperty('min-height');
289
+ ph?.style?.removeProperty('min-height');
290
+ } catch (_) {}
291
+ }
292
+ function isProtectedFromDrop(wrap) {
293
+ try {
294
+ const now = Date.now();
295
+ const shownTs = parseInt(wrap?.getAttribute?.(A_SHOWN) || '0', 10) || 0;
296
+ const filledTs = parseInt(wrap?.getAttribute?.(A_FILLED) || '0', 10) || 0;
297
+ if (shownTs && (now - shownTs) < MIN_LIVE_AFTER_SHOW_MS) return true;
298
+ if (filledTs && (now - filledTs) < MIN_LIVE_AFTER_FILL_MS) return true;
299
+ if (shouldKeepShellAfterUnused(wrap)) return true;
300
+ } catch (_) {}
301
+ return false;
302
+ }
269
303
  function uncollapseIfFilled(ph) {
270
304
  try {
271
305
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
272
306
  if (!wrap) return;
273
307
  if (isFilled(ph)) {
274
308
  wrap.classList.remove('is-empty');
309
+ clearUnusedShell(wrap, ph);
275
310
  markFilledOnce(ph);
311
+ rememberCurrentAdSize(ph);
276
312
  fitWideAd(ph);
277
313
  }
278
314
  } catch (_) {}
279
315
  }
280
-
281
- function unwatchPlaceholderFill(phOrId) {
282
- const id = typeof phOrId === 'number' ? phOrId : parseInt(phOrId?.getAttribute?.('data-ezoic-id'), 10);
283
- if (!Number.isFinite(id)) return;
284
- const obs = S.fillObs.get(id);
285
- if (obs) { try { obs.disconnect(); } catch (_) {} S.fillObs.delete(id); }
286
- }
287
-
288
316
  function watchPlaceholderFill(ph) {
289
- const id = parseInt(ph?.getAttribute?.('data-ezoic-id'), 10);
290
- if (!ph || !Number.isFinite(id) || S.fillObs.has(id)) { try { if (ph) uncollapseIfFilled(ph); } catch (_) {} return; }
291
- const obs = new MutationObserver(() => { uncollapseIfFilled(ph); try { fitWideAd(ph); } catch (_) {} });
317
+ if (!ph || ph.__nbbEzFillObs) return;
318
+ const obs = new MutationObserver(() => { uncollapseIfFilled(ph); });
292
319
  try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (_) { return; }
293
- S.fillObs.set(id, obs);
320
+ ph.__nbbEzFillObs = obs;
294
321
  uncollapseIfFilled(ph);
295
- try { fitWideAd(ph); } catch (_) {}
296
322
  }
297
-
298
- function mutate(fn) {
299
- S.mutGuard++;
300
- try { fn(); } finally { S.mutGuard--; }
323
+ function unwatchPlaceholderFill(ph) {
324
+ try { ph?.__nbbEzFillObs?.disconnect?.(); } catch (_) {}
325
+ try { delete ph.__nbbEzFillObs; } catch (_) {}
326
+ }
327
+ let fitRaf = 0;
328
+ function scheduleRefitAll() {
329
+ if (fitRaf) return;
330
+ fitRaf = requestAnimationFrame(() => {
331
+ fitRaf = 0;
332
+ document.querySelectorAll(`[id^="${PH_PREFIX}"]`).forEach(ph => { try { fitWideAd(ph); } catch (_) {} });
333
+ });
301
334
  }
302
335
 
303
336
  // ── Config ─────────────────────────────────────────────────────────────────
@@ -475,7 +508,6 @@
475
508
 
476
509
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
477
510
  try {
478
- if (isProtectedFromDrop(wrap)) return;
479
511
  const rect = wrap.getBoundingClientRect();
480
512
  if (rect.bottom > threshold) return;
481
513
  if (!isFilled(wrap)) {
@@ -488,26 +520,27 @@
488
520
 
489
521
  const best = bestEmpty ?? bestFilled;
490
522
  if (!best) return null;
523
+ if (isProtectedFromDrop(best)) return null;
491
524
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
492
525
  if (!Number.isFinite(id)) return null;
493
526
 
494
527
  const oldKey = best.getAttribute(A_ANCHOR);
495
528
  // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
496
529
  // parasite si le nœud était encore dans la zone IO_MARGIN.
497
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { S.io?.unobserve(ph); clearAdScale(ph); unwatchPlaceholderFill(ph); } } catch (_) {}
498
- clearEmptyChecks(id);
530
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { S.io?.unobserve(ph); unwatchPlaceholderFill(ph); clearAdScale(ph); clearUnusedShell(best, ph); } } catch (_) {}
499
531
  mutate(() => {
500
532
  best.setAttribute(A_ANCHOR, newKey);
501
533
  best.setAttribute(A_CREATED, String(ts()));
502
534
  best.setAttribute(A_SHOWN, '0');
503
- best.setAttribute(A_FILLED, '0');
504
- best.classList.remove('is-empty');
535
+ best.classList.remove('is-empty', 'is-unused-shell');
505
536
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
506
- if (ph) { ph.innerHTML = ''; ph.style.removeProperty('height'); }
537
+ if (ph) { ph.innerHTML = ''; ph.style.removeProperty('min-height'); ph.style.removeProperty('height'); }
538
+ best.removeAttribute(A_FILLED); best.removeAttribute(A_LAST_W); best.removeAttribute(A_LAST_H);
507
539
  targetEl.insertAdjacentElement('afterend', best);
508
540
  });
509
541
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
510
542
  S.wrapByKey.set(newKey, best);
543
+ try { const ph2 = best.querySelector(`#${PH_PREFIX}${id}`); if (ph2) watchPlaceholderFill(ph2); } catch (_) {}
511
544
 
512
545
  // Délais requis : destroyPlaceholders est asynchrone en interne
513
546
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
@@ -515,7 +548,6 @@
515
548
  const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
516
549
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
517
550
 
518
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { observePh(id); watchPlaceholderFill(ph); } } catch (_) {}
519
551
  return { id, wrap: best };
520
552
  }
521
553
 
@@ -528,7 +560,6 @@
528
560
  w.setAttribute(A_WRAPID, String(id));
529
561
  w.setAttribute(A_CREATED, String(ts()));
530
562
  w.setAttribute(A_SHOWN, '0');
531
- w.setAttribute(A_FILLED, '0');
532
563
  w.style.cssText = 'width:100%;display:block;';
533
564
  const ph = document.createElement('div');
534
565
  ph.id = `${PH_PREFIX}${id}`;
@@ -546,21 +577,26 @@
546
577
  mutate(() => el.insertAdjacentElement('afterend', w));
547
578
  S.mountedIds.add(id);
548
579
  S.wrapByKey.set(key, w);
549
- try { const ph = w.querySelector(`#${PH_PREFIX}${id}`); if (ph) watchPlaceholderFill(ph); } catch (_) {}
550
580
  return w;
551
581
  }
552
582
 
553
583
  function dropWrap(w) {
554
584
  try {
555
- if (isProtectedFromDrop(w)) return;
585
+ if (isProtectedFromDrop(w)) {
586
+ const keepPh = w.querySelector(`[id^="${PH_PREFIX}"]`);
587
+ if (shouldKeepShellAfterUnused(w)) applyUnusedShell(w, keepPh);
588
+ return false;
589
+ }
556
590
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
557
- if (ph instanceof Element) { S.io?.unobserve(ph); clearAdScale(ph); unwatchPlaceholderFill(ph); }
591
+ if (ph instanceof Element) { S.io?.unobserve(ph); unwatchPlaceholderFill(ph); clearAdScale(ph); clearUnusedShell(w, ph); }
558
592
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
559
593
  if (Number.isFinite(id)) { S.mountedIds.delete(id); clearEmptyChecks(id); }
560
594
  const key = w.getAttribute(A_ANCHOR);
561
595
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
562
596
  w.remove();
597
+ return true;
563
598
  } catch (_) {}
599
+ return false;
564
600
  }
565
601
 
566
602
  // ── Prune (topics de catégorie uniquement) ────────────────────────────────
@@ -663,10 +699,9 @@
663
699
 
664
700
  function observePh(id) {
665
701
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
666
- if (ph?.isConnected) {
667
- try { getIO()?.observe(ph); } catch (_) {}
668
- try { watchPlaceholderFill(ph); } catch (_) {}
669
- }
702
+ if (!ph?.isConnected) return;
703
+ try { watchPlaceholderFill(ph); } catch (_) {}
704
+ try { getIO()?.observe(ph); } catch (_) {}
670
705
  }
671
706
 
672
707
  function enqueueShow(id) {
@@ -704,7 +739,7 @@
704
739
  try {
705
740
  if (isBlocked()) { clearTimeout(timer); return release(); }
706
741
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
707
- if (!ph?.isConnected || isFilled(ph)) { try { if (ph?.isConnected) fitWideAd(ph); } catch (_) {} clearTimeout(timer); return release(); }
742
+ if (!ph?.isConnected || isFilled(ph)) { try { if (ph) uncollapseIfFilled(ph); } catch (_) {} clearTimeout(timer); return release(); }
708
743
 
709
744
  const t = ts();
710
745
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
@@ -713,12 +748,10 @@
713
748
  try {
714
749
  const wrap = ph.closest?.(`.${WRAP_CLASS}`);
715
750
  wrap?.setAttribute(A_SHOWN, String(t));
716
- wrap?.setAttribute(A_FILLED, '0');
717
- wrap?.classList.remove('is-empty');
718
- if (wrap) wrap.style.removeProperty('height');
719
- ph.style.removeProperty('height');
751
+ wrap?.classList?.remove('is-empty');
752
+ clearUnusedShell(wrap, ph);
753
+ clearEmptyChecks(id);
720
754
  } catch (_) {}
721
- clearEmptyChecks(id);
722
755
 
723
756
  window.ezstandalone = window.ezstandalone || {};
724
757
  const ez = window.ezstandalone;
@@ -736,25 +769,37 @@
736
769
 
737
770
  function scheduleEmptyCheck(id, showTs) {
738
771
  clearEmptyChecks(id);
772
+
739
773
  const runCheck = () => {
740
774
  try {
741
775
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
742
776
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
743
777
  if (!wrap || !ph?.isConnected) return;
744
778
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
745
- if ((parseInt(wrap.getAttribute(A_FILLED) || '0', 10) || 0) > 0) {
779
+
780
+ if (isFilled(ph)) {
746
781
  wrap.classList.remove('is-empty');
747
- if (isFilled(ph)) fitWideAd(ph);
782
+ clearUnusedShell(wrap, ph);
783
+ markFilledOnce(ph);
784
+ rememberCurrentAdSize(ph);
785
+ fitWideAd(ph);
786
+ return;
787
+ }
788
+
789
+ if (hasEverFilled(wrap) || shouldKeepShellAfterUnused(wrap)) {
790
+ applyUnusedShell(wrap, ph);
748
791
  return;
749
792
  }
750
- wrap.classList.toggle('is-empty', !isFilled(ph));
751
- if (isFilled(ph)) fitWideAd(ph);
793
+
794
+ clearUnusedShell(wrap, ph);
795
+ clearAdScale(ph);
796
+ wrap.classList.add('is-empty');
752
797
  } catch (_) {}
753
798
  };
754
- [EMPTY_CHECK_MS, EMPTY_CHECK_MS + 5000, EMPTY_CHECK_MS + 15000].forEach(delay => {
755
- const t = setTimeout(runCheck, delay);
756
- queueEmptyCheck(id, t);
757
- });
799
+
800
+ queueEmptyCheck(id, setTimeout(runCheck, EMPTY_CHECK_MS));
801
+ queueEmptyCheck(id, setTimeout(runCheck, EMPTY_RECHECK_2_MS));
802
+ queueEmptyCheck(id, setTimeout(runCheck, EMPTY_RECHECK_3_MS));
758
803
  }
759
804
 
760
805
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -869,17 +914,6 @@
869
914
  step();
870
915
  }
871
916
 
872
- let fitRaf = 0;
873
- function scheduleRefitAll() {
874
- if (fitRaf) return;
875
- fitRaf = requestAnimationFrame(() => {
876
- fitRaf = 0;
877
- document.querySelectorAll(`[id^="${PH_PREFIX}"]`).forEach(ph => {
878
- try { fitWideAd(ph); } catch (_) {}
879
- });
880
- });
881
- }
882
-
883
917
  // ── Cleanup navigation ─────────────────────────────────────────────────────
884
918
 
885
919
  function cleanup() {
@@ -892,16 +926,13 @@
892
926
  S.mountedIds.clear();
893
927
  S.lastShow.clear();
894
928
  S.wrapByKey.clear();
895
- for (const [, timers] of S.emptyChecks) { for (const t of timers) { try { clearTimeout(t); } catch (_) {} } }
929
+ for (const timers of S.emptyChecks.values()) for (const t of timers) clearTimeout(t);
896
930
  S.emptyChecks.clear();
897
- for (const [, obs] of S.fillObs) { try { obs.disconnect(); } catch (_) {} }
898
- S.fillObs.clear();
899
931
  S.inflight = 0;
900
932
  S.pending = [];
901
933
  S.pendingSet.clear();
902
934
  S.burstActive = false;
903
935
  S.runQueued = false;
904
- if (fitRaf) { try { cancelAnimationFrame(fitRaf); } catch (_) {} fitRaf = 0; }
905
936
  }
906
937
 
907
938
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -1028,6 +1059,10 @@
1028
1059
  }, { passive: true });
1029
1060
  }
1030
1061
 
1062
+ function bindResize() {
1063
+ window.addEventListener('resize', scheduleRefitAll, { passive: true });
1064
+ }
1065
+
1031
1066
  // ── Boot ───────────────────────────────────────────────────────────────────
1032
1067
 
1033
1068
  S.pageKey = pageKey();
@@ -1039,7 +1074,7 @@
1039
1074
  ensureDomObserver();
1040
1075
  bindNodeBB();
1041
1076
  bindScroll();
1042
- window.addEventListener('resize', scheduleRefitAll, { passive: true });
1077
+ bindResize();
1043
1078
  blockedUntil = 0;
1044
1079
  requestBurst();
1045
1080
 
package/public/style.css CHANGED
@@ -77,8 +77,7 @@
77
77
  padding: 0 !important;
78
78
  }
79
79
 
80
-
81
- /* Si le wrap contient une pub, ne jamais le laisser en mode vide */
80
+ /* ── Hardening async fill + responsive creatives ───────────────────────── */
82
81
  .nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
83
82
  height: auto !important;
84
83
  min-height: 1px !important;
@@ -86,7 +85,6 @@
86
85
  overflow: visible !important;
87
86
  }
88
87
 
89
- /* ── Responsive hardening Ezoic ─────────────────────────────────────────── */
90
88
  .nodebb-ezoic-wrap {
91
89
  max-width: 100%;
92
90
  overflow-x: clip;
@@ -113,7 +111,6 @@
113
111
  }
114
112
 
115
113
  .nodebb-ezoic-wrap iframe {
116
- display: block !important;
117
114
  margin-left: auto !important;
118
115
  margin-right: auto !important;
119
116
  max-width: none !important;
@@ -128,6 +125,11 @@
128
125
  will-change: transform;
129
126
  }
130
127
 
131
- @supports not (overflow: clip) {
132
- .nodebb-ezoic-wrap { overflow-x: hidden; }
128
+ .nodebb-ezoic-wrap.is-unused-shell {
129
+ overflow: hidden !important;
130
+ }
131
+
132
+ .nodebb-ezoic-wrap.is-unused-shell > [id^="ezoic-pub-ad-placeholder-"] {
133
+ display: block;
134
+ width: 100%;
133
135
  }