nodebb-plugin-ezoic-infinite 1.7.83 → 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.83",
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,12 +130,13 @@
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,
133
138
  burstCount: 0,
134
139
  lastBurstTs: 0,
135
- emptyChecks: new Map(), // id -> timeout ids[]
136
140
  };
137
141
 
138
142
  let blockedUntil = 0;
@@ -145,40 +149,150 @@
145
149
 
146
150
  function clearEmptyChecks(id) {
147
151
  const timers = S.emptyChecks.get(id);
148
- if (!timers) return;
149
- for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
150
- S.emptyChecks.delete(id);
152
+ if (timers) {
153
+ for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
154
+ S.emptyChecks.delete(id);
155
+ }
151
156
  }
152
157
 
153
158
  function queueEmptyCheck(id, timerId) {
154
- const arr = S.emptyChecks.get(id) || [];
155
- arr.push(timerId);
156
- S.emptyChecks.set(id, arr);
159
+ if (!S.emptyChecks.has(id)) S.emptyChecks.set(id, []);
160
+ S.emptyChecks.get(id).push(timerId);
157
161
  }
158
162
 
159
- function uncollapseIfFilled(ph) {
163
+ function markFilledOnce(ph) {
160
164
  try {
161
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 {
162
175
  if (!wrap) return false;
163
- if (!isFilled(ph)) return false;
164
- wrap.classList.remove('is-empty');
165
- return true;
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;
166
182
  } catch (_) { return false; }
167
183
  }
168
184
 
169
- function watchPlaceholderFill(ph) {
170
- if (!ph || ph.__nbbFillObs) return;
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) {
171
246
  try {
172
- const obs = new MutationObserver(() => { if (uncollapseIfFilled(ph)) return; });
173
- obs.observe(ph, { childList: true, subtree: true, attributes: true });
174
- ph.__nbbFillObs = obs;
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`;
175
266
  } catch (_) {}
176
- uncollapseIfFilled(ph);
177
267
  }
178
268
 
179
- function unwatchPlaceholderFill(ph) {
180
- try { ph?.__nbbFillObs?.disconnect?.(); } catch (_) {}
181
- try { if (ph) delete ph.__nbbFillObs; } catch (_) {}
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 (_) {}
182
296
  }
183
297
 
184
298
  function mutate(fn) {
@@ -361,6 +475,7 @@
361
475
 
362
476
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
363
477
  try {
478
+ if (isProtectedFromDrop(wrap)) return;
364
479
  const rect = wrap.getBoundingClientRect();
365
480
  if (rect.bottom > threshold) return;
366
481
  if (!isFilled(wrap)) {
@@ -379,15 +494,16 @@
379
494
  const oldKey = best.getAttribute(A_ANCHOR);
380
495
  // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
381
496
  // parasite si le nœud était encore dans la zone IO_MARGIN.
382
- 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 (_) {}
383
498
  clearEmptyChecks(id);
384
499
  mutate(() => {
385
500
  best.setAttribute(A_ANCHOR, newKey);
386
501
  best.setAttribute(A_CREATED, String(ts()));
387
502
  best.setAttribute(A_SHOWN, '0');
503
+ best.setAttribute(A_FILLED, '0');
388
504
  best.classList.remove('is-empty');
389
505
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
390
- if (ph) { ph.innerHTML = ''; watchPlaceholderFill(ph); }
506
+ if (ph) { ph.innerHTML = ''; ph.style.removeProperty('height'); }
391
507
  targetEl.insertAdjacentElement('afterend', best);
392
508
  });
393
509
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
@@ -399,6 +515,7 @@
399
515
  const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
400
516
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
401
517
 
518
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { observePh(id); watchPlaceholderFill(ph); } } catch (_) {}
402
519
  return { id, wrap: best };
403
520
  }
404
521
 
@@ -411,6 +528,7 @@
411
528
  w.setAttribute(A_WRAPID, String(id));
412
529
  w.setAttribute(A_CREATED, String(ts()));
413
530
  w.setAttribute(A_SHOWN, '0');
531
+ w.setAttribute(A_FILLED, '0');
414
532
  w.style.cssText = 'width:100%;display:block;';
415
533
  const ph = document.createElement('div');
416
534
  ph.id = `${PH_PREFIX}${id}`;
@@ -428,14 +546,15 @@
428
546
  mutate(() => el.insertAdjacentElement('afterend', w));
429
547
  S.mountedIds.add(id);
430
548
  S.wrapByKey.set(key, w);
431
- try { watchPlaceholderFill(w.querySelector(`#${PH_PREFIX}${id}`)); } catch (_) {}
549
+ try { const ph = w.querySelector(`#${PH_PREFIX}${id}`); if (ph) watchPlaceholderFill(ph); } catch (_) {}
432
550
  return w;
433
551
  }
434
552
 
435
553
  function dropWrap(w) {
436
554
  try {
555
+ if (isProtectedFromDrop(w)) return;
437
556
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
438
- if (ph instanceof Element) { S.io?.unobserve(ph); unwatchPlaceholderFill(ph); }
557
+ if (ph instanceof Element) { S.io?.unobserve(ph); clearAdScale(ph); unwatchPlaceholderFill(ph); }
439
558
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
440
559
  if (Number.isFinite(id)) { S.mountedIds.delete(id); clearEmptyChecks(id); }
441
560
  const key = w.getAttribute(A_ANCHOR);
@@ -544,9 +663,10 @@
544
663
 
545
664
  function observePh(id) {
546
665
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
547
- if (!ph?.isConnected) return;
548
- watchPlaceholderFill(ph);
549
- try { getIO()?.observe(ph); } catch (_) {}
666
+ if (ph?.isConnected) {
667
+ try { getIO()?.observe(ph); } catch (_) {}
668
+ try { watchPlaceholderFill(ph); } catch (_) {}
669
+ }
550
670
  }
551
671
 
552
672
  function enqueueShow(id) {
@@ -584,21 +704,29 @@
584
704
  try {
585
705
  if (isBlocked()) { clearTimeout(timer); return release(); }
586
706
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
587
- if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
588
- clearEmptyChecks(id);
589
- try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
707
+ if (!ph?.isConnected || isFilled(ph)) { try { if (ph?.isConnected) fitWideAd(ph); } catch (_) {} clearTimeout(timer); return release(); }
590
708
 
591
709
  const t = ts();
592
710
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
593
711
  S.lastShow.set(id, t);
594
712
 
595
- 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);
596
722
 
597
723
  window.ezstandalone = window.ezstandalone || {};
598
724
  const ez = window.ezstandalone;
599
725
  const doShow = () => {
600
726
  try { ez.showAds(id); } catch (_) {}
601
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);
602
730
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
603
731
  };
604
732
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
@@ -608,18 +736,25 @@
608
736
 
609
737
  function scheduleEmptyCheck(id, showTs) {
610
738
  clearEmptyChecks(id);
611
- const delays = [EMPTY_CHECK_MS, EMPTY_CHECK_MS + 5000, EMPTY_CHECK_MS + 15000];
612
739
  const runCheck = () => {
613
740
  try {
614
741
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
615
742
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
616
743
  if (!wrap || !ph?.isConnected) return;
617
744
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
618
- if (uncollapseIfFilled(ph)) return;
619
- wrap.classList.add('is-empty');
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
+ }
750
+ wrap.classList.toggle('is-empty', !isFilled(ph));
751
+ if (isFilled(ph)) fitWideAd(ph);
620
752
  } catch (_) {}
621
753
  };
622
- for (const d of delays) queueEmptyCheck(id, setTimeout(runCheck, d));
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
+ });
623
758
  }
624
759
 
625
760
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -734,6 +869,17 @@
734
869
  step();
735
870
  }
736
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
+
737
883
  // ── Cleanup navigation ─────────────────────────────────────────────────────
738
884
 
739
885
  function cleanup() {
@@ -746,13 +892,16 @@
746
892
  S.mountedIds.clear();
747
893
  S.lastShow.clear();
748
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();
749
899
  S.inflight = 0;
750
900
  S.pending = [];
751
901
  S.pendingSet.clear();
752
- for (const timers of S.emptyChecks.values()) for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
753
- S.emptyChecks.clear();
754
902
  S.burstActive = false;
755
903
  S.runQueued = false;
904
+ if (fitRaf) { try { cancelAnimationFrame(fitRaf); } catch (_) {} fitRaf = 0; }
756
905
  }
757
906
 
758
907
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -890,6 +1039,7 @@
890
1039
  ensureDomObserver();
891
1040
  bindNodeBB();
892
1041
  bindScroll();
1042
+ window.addEventListener('resize', scheduleRefitAll, { passive: true });
893
1043
  blockedUntil = 0;
894
1044
  requestBurst();
895
1045
 
package/public/style.css CHANGED
@@ -78,10 +78,56 @@
78
78
  }
79
79
 
80
80
 
81
- /* Filet de sécurité : si Ezoic a rempli le wrap, annuler le collapse même si .is-empty est resté */
81
+ /* Si le wrap contient une pub, ne jamais le laisser en mode vide */
82
82
  .nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
83
83
  height: auto !important;
84
84
  min-height: 1px !important;
85
85
  max-height: none !important;
86
86
  overflow: visible !important;
87
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
+ }