nodebb-plugin-ezoic-infinite 1.7.3 → 1.7.4

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 +54 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
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
@@ -60,19 +60,25 @@
60
60
  };
61
61
 
62
62
  /**
63
- * Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
63
+ * Table centrale par kindClass :
64
64
  *
65
- * L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
66
- * quelle que soit la page ou la virtualisation :
67
- * posts → data-pid (id du message, unique et permanent)
68
- * topics → data-index (position 0-based dans la liste, fourni par NodeBB)
69
- * catégories data-cid (id de la catégorie, unique et permanent)
70
- * C'était le bug v19 : on cherchait data-index ici
65
+ * sel : sélecteur CSS complet de l'élément cible
66
+ * baseTag : tag CSS à préfixer dans les querySelector de recherche
67
+ * (déduit manuellement évite le fragile split('[')[0])
68
+ * anchorAttr : attribut DOM STABLE qui identifie l'élément de façon unique
69
+ * et permanente, utilisé comme clé d'ancre du wrap.
70
+ * data-pid pour posts (id message, immuable)
71
+ * → data-index pour topics (position dans la liste)
72
+ * → data-cid pour catégories (id catégorie, immuable)
73
+ * ordinalAttr: attribut utilisé pour calculer l'intervalle d'injection.
74
+ * Doit être un entier 0-based fourni par NodeBB.
75
+ * Pour posts ET topics : data-index (0-based, toujours présent).
76
+ * Pour catégories : pas d'infinite scroll → fallback positionnel.
71
77
  */
72
78
  const KIND = {
73
- 'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid' },
74
- 'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index' },
75
- 'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid' },
79
+ 'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
80
+ 'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
81
+ 'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
76
82
  };
77
83
 
78
84
  // ── État ───────────────────────────────────────────────────────────────────
@@ -307,46 +313,33 @@
307
313
  // ── Prune ──────────────────────────────────────────────────────────────────
308
314
 
309
315
  /**
310
- * Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
316
+ * Supprime les wraps VIDES dont l'élément-ancre n'est plus dans le DOM.
311
317
  *
312
- * L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
313
- * Exemples :
314
- * ezoic-ad-message → cherche [data-pid="123"]
315
- * ezoic-ad-between → cherche [data-index="5"]
316
- * ezoic-ad-categories cherche [data-cid="7"] ← fix v20
317
- *
318
- * On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
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.
318
+ * Règles :
319
+ * 1. Jamais avant MIN_PRUNE_AGE_MS (DOM post-batch pas encore stabilisé).
320
+ * 2. Jamais un wrap rempli (player wyvern potentiellement actif).
321
+ * 3. L'ancre est retrouvée via KIND[klass].anchorAttr (stable par design) :
322
+ * ezoic-ad-message → [data-pid="123"]
323
+ * ezoic-ad-between → li[data-index="5"]
324
+ * ezoic-ad-categories li[data-cid="7"]
330
325
  */
331
326
  function pruneOrphans(klass) {
332
327
  const meta = KIND[klass];
333
328
  if (!meta) return;
334
329
 
335
- const baseTag = meta.sel.split('[')[0];
336
-
337
330
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
338
331
  if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
339
-
340
- // Ne jamais retirer un wrap qui contient du contenu (player potentiellement actif)
341
- if (isFilled(w)) return;
332
+ if (isFilled(w)) return; // jamais supprimer un wrap rempli (wyvern)
342
333
 
343
334
  const key = w.getAttribute(A_ANCHOR) ?? '';
344
335
  const sid = key.slice(klass.length + 1);
345
336
  if (!sid) { mutate(() => dropWrap(w)); return; }
346
337
 
347
- const anchorEl = document.querySelector(
348
- `${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
349
- );
338
+ // Construire le sélecteur avec baseTag explicite depuis KIND
339
+ // baseTag='' pour posts → sélecteur : [data-pid="123"] (correct, sans tag ambigu)
340
+ // baseTag='li' pour topics/categories → li[data-index="5"], li[data-cid="7"]
341
+ const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
342
+ const anchorEl = document.querySelector(sel);
350
343
  if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
351
344
  });
352
345
  }
@@ -388,24 +381,35 @@
388
381
  // ── Injection ──────────────────────────────────────────────────────────────
389
382
 
390
383
  /**
391
- * Ordinal 0-based pour le calcul de l'intervalle.
392
- * Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
393
- * Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
384
+ * Ordinal 0-based d'un élément pour le calcul de l'intervalle d'injection.
385
+ *
386
+ * Utilise KIND[klass].ordinalAttr en priorité (data-index pour posts et topics,
387
+ * null pour catégories). Si absent, fallback positionnel dans le parent.
388
+ *
389
+ * Pour les posts : KIND.baseTag='' donc le fallback itère sur les enfants directs
390
+ * du parent en filtrant par sélecteur complet (pas juste le tag).
394
391
  */
395
392
  function ordinal(klass, el) {
396
- const di = el.getAttribute('data-index');
397
- if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
398
- // Fallback positionnel
393
+ const meta = KIND[klass];
394
+
395
+ // 1. Attribut ordinal explicite (data-index sur posts et topics)
396
+ if (meta?.ordinalAttr) {
397
+ const v = el.getAttribute(meta.ordinalAttr);
398
+ if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
399
+ }
400
+
401
+ // 2. Fallback positionnel — compte parmi les frères qui matchent le même sélecteur
399
402
  try {
400
- const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
401
- if (tag) {
402
- let i = 0;
403
- for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
404
- if (n === el) return i;
405
- i++;
406
- }
403
+ let i = 0;
404
+ const siblings = el.parentElement?.children ?? [];
405
+ const fullSel = meta?.sel ?? '';
406
+ for (const s of siblings) {
407
+ if (s === el) return i;
408
+ // Compter uniquement les éléments du même type (pas les wraps ou autres)
409
+ if (!fullSel || s.matches?.(fullSel)) i++;
407
410
  }
408
411
  } catch (_) {}
412
+
409
413
  return 0;
410
414
  }
411
415