nodebb-plugin-ezoic-infinite 1.7.1 → 1.7.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +96 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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
@@ -261,11 +261,45 @@
261
261
  return w;
262
262
  }
263
263
 
264
+ /**
265
+ * Retire proprement un wrap du DOM.
266
+ *
267
+ * Sans précaution, retirer un wrap contenant un player vidéo Ezoic (wyvern.js)
268
+ * déclenche des erreurs async sur des nœuds détachés :
269
+ * "Cannot read properties of null (reading 'paused')"
270
+ * "Cannot read properties of null (reading 'offsetWidth')"
271
+ * "Invalid target for null#trigger / null#on"
272
+ *
273
+ * On pause les media et on tente de notifier l'API wyvern avant remove().
274
+ */
264
275
  function dropWrap(w) {
265
276
  try {
277
+ // 1. Pause tous les media actifs avant détachement
278
+ try {
279
+ w.querySelectorAll('video, audio').forEach(m => {
280
+ try { if (!m.paused) m.pause(); } catch (_) {}
281
+ });
282
+ } catch (_) {}
283
+
284
+ // 2. Notifier l'API wyvern si disponible
285
+ try {
286
+ if (window.wyvern && typeof window.wyvern === 'object') {
287
+ w.querySelectorAll('[id^="ezoic-"],[class*="wyvern"],[class*="ezoic-video"]')
288
+ .forEach(n => { try { window.wyvern.destroy?.(n); } catch (_) {} });
289
+ }
290
+ } catch (_) {}
291
+
292
+ // 3. Unobserve (guard instanceof Element — unobserve(null) corrompt l'IO)
293
+ try {
294
+ const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
295
+ if (ph instanceof Element) S.io?.unobserve(ph);
296
+ } catch (_) {}
297
+
298
+ // 4. Libérer l'id du pool
266
299
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
267
300
  if (Number.isFinite(id)) S.mountedIds.delete(id);
268
- try { S.io?.unobserve(w.querySelector(`[id^="${PH_PREFIX}"]`)); } catch (_) {}
301
+
302
+ // 5. Retrait DOM
269
303
  w.remove();
270
304
  } catch (_) {}
271
305
  }
@@ -283,17 +317,31 @@
283
317
  *
284
318
  * On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
285
319
  */
320
+ /**
321
+ * Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
322
+ *
323
+ * Règle stricte : on ne supprime JAMAIS un wrap rempli (filled).
324
+ * - Il peut contenir un player wyvern actif → .remove() déclenche des
325
+ * erreurs async ("Cannot read 'paused'", "offsetWidth", "getChild"…).
326
+ * - Le post-ancre peut être temporairement virtualisé par NodeBB puis
327
+ * revenir — dans ce cas le wrap filled doit rester en place.
328
+ *
329
+ * Seuls les wraps VIDES dont l'ancre a disparu sont supprimés.
330
+ */
286
331
  function pruneOrphans(klass) {
287
332
  const meta = KIND[klass];
288
333
  if (!meta) return;
289
334
 
290
- const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
335
+ const baseTag = meta.sel.split('[')[0];
291
336
 
292
337
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
293
338
  if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
294
339
 
340
+ // Ne jamais retirer un wrap qui contient du contenu (player potentiellement actif)
341
+ if (isFilled(w)) return;
342
+
295
343
  const key = w.getAttribute(A_ANCHOR) ?? '';
296
- const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
344
+ const sid = key.slice(klass.length + 1);
297
345
  if (!sid) { mutate(() => dropWrap(w)); return; }
298
346
 
299
347
  const anchorEl = document.querySelector(
@@ -308,11 +356,12 @@
308
356
  /**
309
357
  * Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
310
358
  * Priorité : filled > en grâce (fill en cours) > vide.
311
- * Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
359
+ *
360
+ * Règle de sécurité wyvern : on ne supprime JAMAIS un wrap rempli.
361
+ * Si les deux wraps adjacents sont remplis, on laisse en place (edge case rare).
312
362
  */
313
363
  function decluster(klass) {
314
364
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
315
- // Grace sur le wrap courant : on le saute entièrement
316
365
  const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
317
366
  if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
318
367
 
@@ -321,10 +370,16 @@
321
370
  if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
322
371
 
323
372
  const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
324
- if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
373
+ if (pShown && ts() - pShown < FILL_GRACE_MS) break;
374
+
375
+ const wFilled = isFilled(w);
376
+ const pFilled = isFilled(prev);
325
377
 
326
- if (!isFilled(w)) mutate(() => dropWrap(w));
327
- else if (!isFilled(prev)) mutate(() => dropWrap(prev));
378
+ // Ne jamais retirer un wrap rempli (player actif potentiel)
379
+ if (!wFilled && !pFilled) mutate(() => dropWrap(w)); // les deux vides → retirer le courant
380
+ else if (!wFilled && pFilled) mutate(() => dropWrap(w)); // courant vide, précédent rempli → retirer le courant
381
+ else if ( wFilled && !pFilled) mutate(() => dropWrap(prev)); // courant rempli, précédent vide → retirer le précédent
382
+ // les deux remplis → rien (on ne touche pas)
328
383
  break;
329
384
  }
330
385
  }
@@ -389,7 +444,7 @@
389
444
  S.io = new IntersectionObserver(entries => {
390
445
  for (const e of entries) {
391
446
  if (!e.isIntersecting) continue;
392
- S.io?.unobserve(e.target);
447
+ if (e.target instanceof Element) S.io?.unobserve(e.target);
393
448
  const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
394
449
  if (Number.isFinite(id) && id > 0) enqueueShow(id);
395
450
  }
@@ -586,6 +641,16 @@
586
641
 
587
642
  function cleanup() {
588
643
  blockedUntil = ts() + 1500;
644
+
645
+ // Pause tous les media dans nos wraps AVANT de les retirer du DOM.
646
+ // Empêche wyvern.js de continuer d'exécuter ses callbacks async sur des
647
+ // nœuds détachés (erreurs "Cannot read 'paused'", "offsetWidth"…).
648
+ try {
649
+ document.querySelectorAll(`.${WRAP_CLASS} video, .${WRAP_CLASS} audio`).forEach(m => {
650
+ try { if (!m.paused) m.pause(); } catch (_) {}
651
+ });
652
+ } catch (_) {}
653
+
589
654
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
590
655
  S.cfg = null;
591
656
  S.pools = { topics: [], posts: [], categories: [] };
@@ -637,12 +702,30 @@
637
702
  }
638
703
 
639
704
  function ensureTcfLocator() {
705
+ // Le CMP utilise une iframe nommée __tcfapiLocator pour router les
706
+ // postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
707
+ // iframe du DOM (vidage partiel du body), ce qui provoque :
708
+ // "Cannot read properties of null (reading 'postMessage')"
709
+ // "Cannot set properties of null (setting 'addtlConsent')"
710
+ // Solution : la recrée immédiatement si elle disparaît, via un observer.
640
711
  try {
641
712
  if (!window.__tcfapi && !window.__cmp) return;
642
- if (document.getElementById('__tcfapiLocator')) return;
643
- const f = document.createElement('iframe');
644
- f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
645
- (document.body || document.documentElement).appendChild(f);
713
+
714
+ const inject = () => {
715
+ if (document.getElementById('__tcfapiLocator')) return;
716
+ const f = document.createElement('iframe');
717
+ f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
718
+ (document.body || document.documentElement).appendChild(f);
719
+ };
720
+
721
+ inject();
722
+
723
+ // Observer dédié — si quelqu'un retire l'iframe, on la remet.
724
+ if (!window.__nbbTcfObs) {
725
+ window.__nbbTcfObs = new MutationObserver(() => inject());
726
+ window.__nbbTcfObs.observe(document.documentElement,
727
+ { childList: true, subtree: true });
728
+ }
646
729
  } catch (_) {}
647
730
  }
648
731