nodebb-plugin-ezoic-infinite 1.7.2 → 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.
- package/package.json +1 -1
- package/public/client.js +110 -47
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -60,19 +60,25 @@
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Table centrale
|
|
63
|
+
* Table centrale par kindClass :
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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 ───────────────────────────────────────────────────────────────────
|
|
@@ -261,17 +267,45 @@
|
|
|
261
267
|
return w;
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Retire proprement un wrap du DOM.
|
|
272
|
+
*
|
|
273
|
+
* Sans précaution, retirer un wrap contenant un player vidéo Ezoic (wyvern.js)
|
|
274
|
+
* déclenche des erreurs async sur des nœuds détachés :
|
|
275
|
+
* "Cannot read properties of null (reading 'paused')"
|
|
276
|
+
* "Cannot read properties of null (reading 'offsetWidth')"
|
|
277
|
+
* "Invalid target for null#trigger / null#on"
|
|
278
|
+
*
|
|
279
|
+
* On pause les media et on tente de notifier l'API wyvern avant remove().
|
|
280
|
+
*/
|
|
264
281
|
function dropWrap(w) {
|
|
265
282
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
// 1. Pause tous les media actifs avant détachement
|
|
284
|
+
try {
|
|
285
|
+
w.querySelectorAll('video, audio').forEach(m => {
|
|
286
|
+
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
287
|
+
});
|
|
288
|
+
} catch (_) {}
|
|
289
|
+
|
|
290
|
+
// 2. Notifier l'API wyvern si disponible
|
|
291
|
+
try {
|
|
292
|
+
if (window.wyvern && typeof window.wyvern === 'object') {
|
|
293
|
+
w.querySelectorAll('[id^="ezoic-"],[class*="wyvern"],[class*="ezoic-video"]')
|
|
294
|
+
.forEach(n => { try { window.wyvern.destroy?.(n); } catch (_) {} });
|
|
295
|
+
}
|
|
296
|
+
} catch (_) {}
|
|
297
|
+
|
|
298
|
+
// 3. Unobserve (guard instanceof Element — unobserve(null) corrompt l'IO)
|
|
271
299
|
try {
|
|
272
300
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
301
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
274
302
|
} catch (_) {}
|
|
303
|
+
|
|
304
|
+
// 4. Libérer l'id du pool
|
|
305
|
+
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
306
|
+
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
307
|
+
|
|
308
|
+
// 5. Retrait DOM
|
|
275
309
|
w.remove();
|
|
276
310
|
} catch (_) {}
|
|
277
311
|
}
|
|
@@ -279,32 +313,33 @@
|
|
|
279
313
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
314
|
|
|
281
315
|
/**
|
|
282
|
-
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
283
|
-
*
|
|
284
|
-
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
285
|
-
* Exemples :
|
|
286
|
-
* ezoic-ad-message → cherche [data-pid="123"]
|
|
287
|
-
* ezoic-ad-between → cherche [data-index="5"]
|
|
288
|
-
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
316
|
+
* Supprime les wraps VIDES dont l'élément-ancre n'est plus dans le DOM.
|
|
289
317
|
*
|
|
290
|
-
*
|
|
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"]
|
|
291
325
|
*/
|
|
292
326
|
function pruneOrphans(klass) {
|
|
293
327
|
const meta = KIND[klass];
|
|
294
328
|
if (!meta) return;
|
|
295
329
|
|
|
296
|
-
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
297
|
-
|
|
298
330
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
299
331
|
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
332
|
+
if (isFilled(w)) return; // jamais supprimer un wrap rempli (wyvern)
|
|
300
333
|
|
|
301
334
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
-
const sid = key.slice(klass.length + 1);
|
|
335
|
+
const sid = key.slice(klass.length + 1);
|
|
303
336
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
304
337
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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);
|
|
308
343
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
344
|
});
|
|
310
345
|
}
|
|
@@ -314,11 +349,12 @@
|
|
|
314
349
|
/**
|
|
315
350
|
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
316
351
|
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
317
|
-
*
|
|
352
|
+
*
|
|
353
|
+
* Règle de sécurité wyvern : on ne supprime JAMAIS un wrap rempli.
|
|
354
|
+
* Si les deux wraps adjacents sont remplis, on laisse en place (edge case rare).
|
|
318
355
|
*/
|
|
319
356
|
function decluster(klass) {
|
|
320
357
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
-
// Grace sur le wrap courant : on le saute entièrement
|
|
322
358
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
359
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
360
|
|
|
@@ -327,10 +363,16 @@
|
|
|
327
363
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
364
|
|
|
329
365
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
366
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
331
367
|
|
|
332
|
-
|
|
333
|
-
|
|
368
|
+
const wFilled = isFilled(w);
|
|
369
|
+
const pFilled = isFilled(prev);
|
|
370
|
+
|
|
371
|
+
// Ne jamais retirer un wrap rempli (player actif potentiel)
|
|
372
|
+
if (!wFilled && !pFilled) mutate(() => dropWrap(w)); // les deux vides → retirer le courant
|
|
373
|
+
else if (!wFilled && pFilled) mutate(() => dropWrap(w)); // courant vide, précédent rempli → retirer le courant
|
|
374
|
+
else if ( wFilled && !pFilled) mutate(() => dropWrap(prev)); // courant rempli, précédent vide → retirer le précédent
|
|
375
|
+
// les deux remplis → rien (on ne touche pas)
|
|
334
376
|
break;
|
|
335
377
|
}
|
|
336
378
|
}
|
|
@@ -339,24 +381,35 @@
|
|
|
339
381
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
340
382
|
|
|
341
383
|
/**
|
|
342
|
-
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
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).
|
|
345
391
|
*/
|
|
346
392
|
function ordinal(klass, el) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
//
|
|
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
|
|
350
402
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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++;
|
|
358
410
|
}
|
|
359
411
|
} catch (_) {}
|
|
412
|
+
|
|
360
413
|
return 0;
|
|
361
414
|
}
|
|
362
415
|
|
|
@@ -592,6 +645,16 @@
|
|
|
592
645
|
|
|
593
646
|
function cleanup() {
|
|
594
647
|
blockedUntil = ts() + 1500;
|
|
648
|
+
|
|
649
|
+
// Pause tous les media dans nos wraps AVANT de les retirer du DOM.
|
|
650
|
+
// Empêche wyvern.js de continuer d'exécuter ses callbacks async sur des
|
|
651
|
+
// nœuds détachés (erreurs "Cannot read 'paused'", "offsetWidth"…).
|
|
652
|
+
try {
|
|
653
|
+
document.querySelectorAll(`.${WRAP_CLASS} video, .${WRAP_CLASS} audio`).forEach(m => {
|
|
654
|
+
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
655
|
+
});
|
|
656
|
+
} catch (_) {}
|
|
657
|
+
|
|
595
658
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
596
659
|
S.cfg = null;
|
|
597
660
|
S.pools = { topics: [], posts: [], categories: [] };
|