nodebb-plugin-ezoic-infinite 1.7.82 → 1.7.84

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.82",
3
+ "version": "1.7.84",
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,6 +76,7 @@
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
80
 
80
81
  const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
81
82
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
@@ -83,6 +84,8 @@
83
84
  const MAX_INFLIGHT = 4; // max showAds() simultanés
84
85
  const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
85
86
  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
86
89
 
87
90
  // Marges IO larges et fixes — observer créé une seule fois au boot
88
91
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -127,6 +130,8 @@
127
130
  pending: [], // ids en attente de slot inflight
128
131
  pendingSet: new Set(),
129
132
  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)
130
135
  runQueued: false,
131
136
  burstActive: false,
132
137
  burstDeadline: 0,
@@ -142,6 +147,154 @@
142
147
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
143
148
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
144
149
 
150
+ function clearEmptyChecks(id) {
151
+ 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
+ }
156
+ }
157
+
158
+ function queueEmptyCheck(id, timerId) {
159
+ if (!S.emptyChecks.has(id)) S.emptyChecks.set(id, []);
160
+ S.emptyChecks.get(id).push(timerId);
161
+ }
162
+
163
+ function markFilledOnce(ph) {
164
+ try {
165
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
166
+ if (!wrap) return;
167
+ if (!wrap.getAttribute(A_FILLED) && isFilled(ph)) {
168
+ wrap.setAttribute(A_FILLED, String(ts()));
169
+ }
170
+ } catch (_) {}
171
+ }
172
+
173
+ function isProtectedFromDrop(wrap) {
174
+ 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; }
183
+ }
184
+
185
+ function getAdIntrinsicSize(ph) {
186
+ if (!ph) return null;
187
+ const iframe = ph.querySelector('iframe');
188
+ if (iframe) {
189
+ const wAttr = parseInt(iframe.getAttribute('width') || iframe.getAttribute('ezaw') || '0', 10);
190
+ const hAttr = parseInt(iframe.getAttribute('height') || iframe.getAttribute('ezah') || '0', 10);
191
+ 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);
196
+ if (w > 0 && h > 0) return { w, h };
197
+ }
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);
210
+ if (w > 0 && h > 0) return { w, h };
211
+ }
212
+ return null;
213
+ }
214
+
215
+ function ensureScaleBox(ph) {
216
+ let box = ph.querySelector(':scope > .nbb-ez-scale-box');
217
+ 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;
224
+ box = document.createElement('div');
225
+ box.className = 'nbb-ez-scale-box';
226
+ while (ph.firstChild) box.appendChild(ph.firstChild);
227
+ ph.appendChild(box);
228
+ return box;
229
+ }
230
+
231
+ function clearAdScale(ph) {
232
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
233
+ if (!wrap) return;
234
+ wrap.classList.remove('is-scaled');
235
+ wrap.style.removeProperty('height');
236
+ const box = ph.querySelector(':scope > .nbb-ez-scale-box');
237
+ if (box) {
238
+ box.style.removeProperty('transform');
239
+ box.style.removeProperty('width');
240
+ box.style.removeProperty('height');
241
+ }
242
+ ph.style.removeProperty('height');
243
+ }
244
+
245
+ function fitWideAd(ph) {
246
+ try {
247
+ if (!ph?.isConnected) return;
248
+ const wrap = ph.closest?.(`.${WRAP_CLASS}`);
249
+ if (!wrap) return;
250
+ if (!isFilled(ph)) { clearAdScale(ph); return; }
251
+ const size = getAdIntrinsicSize(ph);
252
+ if (!size?.w || !size?.h) return;
253
+ const wrapWidth = Math.max(0, Math.floor(wrap.clientWidth || wrap.getBoundingClientRect().width || 0));
254
+ if (!wrapWidth) return;
255
+ if (size.w <= wrapWidth + 2) { clearAdScale(ph); return; }
256
+ const scale = Math.max(0.1, Math.min(1, wrapWidth / size.w));
257
+ const scaledH = Math.ceil(size.h * scale);
258
+ const box = ensureScaleBox(ph);
259
+ if (!box) return;
260
+ wrap.classList.add('is-scaled');
261
+ box.style.width = `${size.w}px`;
262
+ box.style.height = `${size.h}px`;
263
+ box.style.transform = `scale(${scale})`;
264
+ ph.style.height = `${scaledH}px`;
265
+ wrap.style.height = `${scaledH}px`;
266
+ } catch (_) {}
267
+ }
268
+
269
+ function uncollapseIfFilled(ph) {
270
+ try {
271
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
272
+ if (!wrap) return;
273
+ if (isFilled(ph)) {
274
+ wrap.classList.remove('is-empty');
275
+ markFilledOnce(ph);
276
+ fitWideAd(ph);
277
+ }
278
+ } catch (_) {}
279
+ }
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
+ 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 (_) {} });
292
+ try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (_) { return; }
293
+ S.fillObs.set(id, obs);
294
+ uncollapseIfFilled(ph);
295
+ try { fitWideAd(ph); } catch (_) {}
296
+ }
297
+
145
298
  function mutate(fn) {
146
299
  S.mutGuard++;
147
300
  try { fn(); } finally { S.mutGuard--; }
@@ -322,6 +475,7 @@
322
475
 
323
476
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
324
477
  try {
478
+ if (isProtectedFromDrop(wrap)) return;
325
479
  const rect = wrap.getBoundingClientRect();
326
480
  if (rect.bottom > threshold) return;
327
481
  if (!isFilled(wrap)) {
@@ -340,14 +494,16 @@
340
494
  const oldKey = best.getAttribute(A_ANCHOR);
341
495
  // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
342
496
  // parasite si le nœud était encore dans la zone IO_MARGIN.
343
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
497
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { S.io?.unobserve(ph); clearAdScale(ph); unwatchPlaceholderFill(ph); } } catch (_) {}
498
+ clearEmptyChecks(id);
344
499
  mutate(() => {
345
500
  best.setAttribute(A_ANCHOR, newKey);
346
501
  best.setAttribute(A_CREATED, String(ts()));
347
502
  best.setAttribute(A_SHOWN, '0');
503
+ best.setAttribute(A_FILLED, '0');
348
504
  best.classList.remove('is-empty');
349
505
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
350
- if (ph) ph.innerHTML = '';
506
+ if (ph) { ph.innerHTML = ''; ph.style.removeProperty('height'); }
351
507
  targetEl.insertAdjacentElement('afterend', best);
352
508
  });
353
509
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
@@ -359,6 +515,7 @@
359
515
  const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
360
516
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
361
517
 
518
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { observePh(id); watchPlaceholderFill(ph); } } catch (_) {}
362
519
  return { id, wrap: best };
363
520
  }
364
521
 
@@ -371,6 +528,7 @@
371
528
  w.setAttribute(A_WRAPID, String(id));
372
529
  w.setAttribute(A_CREATED, String(ts()));
373
530
  w.setAttribute(A_SHOWN, '0');
531
+ w.setAttribute(A_FILLED, '0');
374
532
  w.style.cssText = 'width:100%;display:block;';
375
533
  const ph = document.createElement('div');
376
534
  ph.id = `${PH_PREFIX}${id}`;
@@ -388,15 +546,17 @@
388
546
  mutate(() => el.insertAdjacentElement('afterend', w));
389
547
  S.mountedIds.add(id);
390
548
  S.wrapByKey.set(key, w);
549
+ try { const ph = w.querySelector(`#${PH_PREFIX}${id}`); if (ph) watchPlaceholderFill(ph); } catch (_) {}
391
550
  return w;
392
551
  }
393
552
 
394
553
  function dropWrap(w) {
395
554
  try {
555
+ if (isProtectedFromDrop(w)) return;
396
556
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
397
- if (ph instanceof Element) S.io?.unobserve(ph);
557
+ if (ph instanceof Element) { S.io?.unobserve(ph); clearAdScale(ph); unwatchPlaceholderFill(ph); }
398
558
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
399
- if (Number.isFinite(id)) S.mountedIds.delete(id);
559
+ if (Number.isFinite(id)) { S.mountedIds.delete(id); clearEmptyChecks(id); }
400
560
  const key = w.getAttribute(A_ANCHOR);
401
561
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
402
562
  w.remove();
@@ -503,7 +663,10 @@
503
663
 
504
664
  function observePh(id) {
505
665
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
506
- if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
666
+ if (ph?.isConnected) {
667
+ try { getIO()?.observe(ph); } catch (_) {}
668
+ try { watchPlaceholderFill(ph); } catch (_) {}
669
+ }
507
670
  }
508
671
 
509
672
  function enqueueShow(id) {
@@ -541,19 +704,29 @@
541
704
  try {
542
705
  if (isBlocked()) { clearTimeout(timer); return release(); }
543
706
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
544
- if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
707
+ if (!ph?.isConnected || isFilled(ph)) { try { if (ph?.isConnected) fitWideAd(ph); } catch (_) {} clearTimeout(timer); return release(); }
545
708
 
546
709
  const t = ts();
547
710
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
548
711
  S.lastShow.set(id, t);
549
712
 
550
- try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
713
+ try {
714
+ const wrap = ph.closest?.(`.${WRAP_CLASS}`);
715
+ 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');
720
+ } catch (_) {}
721
+ clearEmptyChecks(id);
551
722
 
552
723
  window.ezstandalone = window.ezstandalone || {};
553
724
  const ez = window.ezstandalone;
554
725
  const doShow = () => {
555
726
  try { ez.showAds(id); } catch (_) {}
556
727
  scheduleEmptyCheck(id, t);
728
+ setTimeout(() => { try { const p = document.getElementById(`${PH_PREFIX}${id}`); if (p) fitWideAd(p); } catch (_) {} }, 500);
729
+ setTimeout(() => { try { const p = document.getElementById(`${PH_PREFIX}${id}`); if (p) fitWideAd(p); } catch (_) {} }, 2500);
557
730
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
558
731
  };
559
732
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
@@ -562,15 +735,26 @@
562
735
  }
563
736
 
564
737
  function scheduleEmptyCheck(id, showTs) {
565
- setTimeout(() => {
738
+ clearEmptyChecks(id);
739
+ const runCheck = () => {
566
740
  try {
567
741
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
568
742
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
569
743
  if (!wrap || !ph?.isConnected) return;
570
744
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
745
+ if ((parseInt(wrap.getAttribute(A_FILLED) || '0', 10) || 0) > 0) {
746
+ wrap.classList.remove('is-empty');
747
+ if (isFilled(ph)) fitWideAd(ph);
748
+ return;
749
+ }
571
750
  wrap.classList.toggle('is-empty', !isFilled(ph));
751
+ if (isFilled(ph)) fitWideAd(ph);
572
752
  } catch (_) {}
573
- }, EMPTY_CHECK_MS);
753
+ };
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
+ });
574
758
  }
575
759
 
576
760
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -685,6 +869,17 @@
685
869
  step();
686
870
  }
687
871
 
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
+
688
883
  // ── Cleanup navigation ─────────────────────────────────────────────────────
689
884
 
690
885
  function cleanup() {
@@ -697,11 +892,16 @@
697
892
  S.mountedIds.clear();
698
893
  S.lastShow.clear();
699
894
  S.wrapByKey.clear();
895
+ for (const [, timers] of S.emptyChecks) { for (const t of timers) { try { clearTimeout(t); } catch (_) {} } }
896
+ S.emptyChecks.clear();
897
+ for (const [, obs] of S.fillObs) { try { obs.disconnect(); } catch (_) {} }
898
+ S.fillObs.clear();
700
899
  S.inflight = 0;
701
900
  S.pending = [];
702
901
  S.pendingSet.clear();
703
902
  S.burstActive = false;
704
903
  S.runQueued = false;
904
+ if (fitRaf) { try { cancelAnimationFrame(fitRaf); } catch (_) {} fitRaf = 0; }
705
905
  }
706
906
 
707
907
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -839,6 +1039,7 @@
839
1039
  ensureDomObserver();
840
1040
  bindNodeBB();
841
1041
  bindScroll();
1042
+ window.addEventListener('resize', scheduleRefitAll, { passive: true });
842
1043
  blockedUntil = 0;
843
1044
  requestBurst();
844
1045
 
package/public/style.css CHANGED
@@ -76,3 +76,58 @@
76
76
  margin: 0 !important;
77
77
  padding: 0 !important;
78
78
  }
79
+
80
+
81
+ /* Si le wrap contient une pub, ne jamais le laisser en mode vide */
82
+ .nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
83
+ height: auto !important;
84
+ min-height: 1px !important;
85
+ max-height: none !important;
86
+ overflow: visible !important;
87
+ }
88
+
89
+ /* ── Responsive hardening Ezoic ─────────────────────────────────────────── */
90
+ .nodebb-ezoic-wrap {
91
+ max-width: 100%;
92
+ overflow-x: clip;
93
+ overflow-y: visible;
94
+ }
95
+
96
+ .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
97
+ max-width: 100%;
98
+ }
99
+
100
+ .nodebb-ezoic-wrap .ezoic-ad,
101
+ .nodebb-ezoic-wrap span.ezoic-ad {
102
+ max-width: 100% !important;
103
+ min-width: 0 !important;
104
+ box-sizing: border-box !important;
105
+ }
106
+
107
+ .nodebb-ezoic-wrap [id^="google_ads_iframe_"][id$="__container__"],
108
+ .nodebb-ezoic-wrap div[id$="__container__"] {
109
+ max-width: 100% !important;
110
+ margin-left: auto !important;
111
+ margin-right: auto !important;
112
+ overflow: visible !important;
113
+ }
114
+
115
+ .nodebb-ezoic-wrap iframe {
116
+ display: block !important;
117
+ margin-left: auto !important;
118
+ margin-right: auto !important;
119
+ max-width: none !important;
120
+ }
121
+
122
+ .nodebb-ezoic-wrap.is-scaled {
123
+ overflow: hidden !important;
124
+ }
125
+
126
+ .nodebb-ezoic-wrap .nbb-ez-scale-box {
127
+ transform-origin: top center;
128
+ will-change: transform;
129
+ }
130
+
131
+ @supports not (overflow: clip) {
132
+ .nodebb-ezoic-wrap { overflow-x: hidden; }
133
+ }