nodebb-plugin-ezoic-infinite 1.7.19 → 1.7.21
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 +33 -149
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v28
|
|
3
3
|
*
|
|
4
4
|
* Historique des corrections majeures
|
|
5
5
|
* ────────────────────────────────────
|
|
6
6
|
* v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
|
|
7
|
-
* Suppression du recyclage de wraps
|
|
7
|
+
* Suppression du recyclage de wraps. Cleanup complet navigation.
|
|
8
8
|
*
|
|
9
9
|
* v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
|
|
10
10
|
* la position dans le batch courant.
|
|
@@ -16,17 +16,23 @@
|
|
|
16
16
|
* Fix unobserve(null) → corruption IO → pubads error au scroll retour.
|
|
17
17
|
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
18
18
|
*
|
|
19
|
-
* v25
|
|
20
|
-
*
|
|
21
|
-
* – pruneOrphans : PRUNE_STABLE_MS = 45 s, isFilled guard en premier.
|
|
19
|
+
* v25 Table KIND unifiée avec baseTag + ordinalAttr.
|
|
20
|
+
* Fix scroll-up / virtualisation NodeBB :
|
|
22
21
|
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* Tentative recyclage d'id (v25) → cause exactement le même bug (wraps
|
|
23
|
+
* déplacés laissent les positions originales libres → réinjection en haut).
|
|
24
|
+
*
|
|
25
|
+
* v26 Suppression définitive du recyclage d'id.
|
|
26
|
+
* KIND simplifié.
|
|
27
|
+
*
|
|
28
|
+
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB).
|
|
29
|
+
*
|
|
30
|
+
* v28 decluster supprimé.
|
|
31
|
+
* Analyse : decluster appelait dropWrap() qui faisait S.mountedIds.delete(id).
|
|
32
|
+
* Un wrap vide adjacent à un autre wrap → supprimé → id libéré → réinjecté
|
|
33
|
+
* en haut au prochain scroll. Exactement le bug observé.
|
|
34
|
+
* Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
|
|
35
|
+
* maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
|
|
30
36
|
*/
|
|
31
37
|
(function () {
|
|
32
38
|
'use strict';
|
|
@@ -40,10 +46,7 @@
|
|
|
40
46
|
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
41
47
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
42
48
|
|
|
43
|
-
const PRUNE_STABLE_MS = 45_000; // délai avant pruning (évite faux-orphelins scroll-up)
|
|
44
|
-
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
45
49
|
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
46
|
-
const RECYCLE_THRESHOLD = 6; // nb de viewports au-dessus du seuil de recyclage
|
|
47
50
|
const MAX_INSERTS_RUN = 6;
|
|
48
51
|
const MAX_INFLIGHT = 4;
|
|
49
52
|
const SHOW_THROTTLE_MS = 900;
|
|
@@ -69,13 +72,11 @@
|
|
|
69
72
|
* data-pid posts / data-index topics / data-cid catégories
|
|
70
73
|
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
71
74
|
* null → fallback positionnel (catégories)
|
|
72
|
-
* recyclable : autoriser le recyclage d'id quand le pool est épuisé
|
|
73
|
-
* false pour ezoic-ad-message (sauts visuels indésirables)
|
|
74
75
|
*/
|
|
75
76
|
const KIND = {
|
|
76
|
-
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index'
|
|
77
|
-
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index'
|
|
78
|
-
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null
|
|
77
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
78
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
79
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
@@ -98,8 +99,6 @@
|
|
|
98
99
|
burstDeadline: 0,
|
|
99
100
|
burstCount: 0,
|
|
100
101
|
lastBurstTs: 0,
|
|
101
|
-
scrollDir: 1, // 1 = down, -1 = up
|
|
102
|
-
lastScrollY: 0,
|
|
103
102
|
};
|
|
104
103
|
|
|
105
104
|
let blockedUntil = 0;
|
|
@@ -223,49 +222,6 @@
|
|
|
223
222
|
return null;
|
|
224
223
|
}
|
|
225
224
|
|
|
226
|
-
// ── Recyclage d'id ─────────────────────────────────────────────────────────
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Sélectionne le wrap vide le plus éloigné au-dessus du viewport.
|
|
230
|
-
* Conditions : kindClass.recyclable = true, scroll vers le bas,
|
|
231
|
-
* wrap vide (non filled), rect.bottom < -(RECYCLE_THRESHOLD × vh).
|
|
232
|
-
*/
|
|
233
|
-
function pickRecyclableWrap(klass) {
|
|
234
|
-
if (!KIND[klass]?.recyclable) return null;
|
|
235
|
-
if (S.scrollDir < 0) return null;
|
|
236
|
-
|
|
237
|
-
const vh = Math.max(300, window.innerHeight || 800);
|
|
238
|
-
const threshold = -(vh * RECYCLE_THRESHOLD);
|
|
239
|
-
let best = null, bestBottom = Infinity;
|
|
240
|
-
|
|
241
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
242
|
-
if (!w.isConnected || isFilled(w)) continue;
|
|
243
|
-
try {
|
|
244
|
-
const rect = w.getBoundingClientRect();
|
|
245
|
-
if (rect.bottom < threshold && rect.bottom < bestBottom) {
|
|
246
|
-
bestBottom = rect.bottom;
|
|
247
|
-
best = w;
|
|
248
|
-
}
|
|
249
|
-
} catch (_) {}
|
|
250
|
-
}
|
|
251
|
-
return best;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Déplace un wrap recyclé vers sa nouvelle ancre el.
|
|
256
|
-
* Réinitialise A_ANCHOR, A_CREATED, supprime A_SHOWN.
|
|
257
|
-
*/
|
|
258
|
-
function moveWrapAfter(el, wrap, newKey) {
|
|
259
|
-
try {
|
|
260
|
-
if (!el || !wrap?.isConnected) return null;
|
|
261
|
-
wrap.setAttribute(A_ANCHOR, newKey);
|
|
262
|
-
wrap.setAttribute(A_CREATED, String(ts()));
|
|
263
|
-
wrap.removeAttribute(A_SHOWN);
|
|
264
|
-
mutate(() => el.insertAdjacentElement('afterend', wrap));
|
|
265
|
-
return wrap;
|
|
266
|
-
} catch (_) { return null; }
|
|
267
|
-
}
|
|
268
|
-
|
|
269
225
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
270
226
|
|
|
271
227
|
function makeWrap(id, klass, key) {
|
|
@@ -303,66 +259,16 @@
|
|
|
303
259
|
} catch (_) {}
|
|
304
260
|
}
|
|
305
261
|
|
|
306
|
-
// ── Prune
|
|
262
|
+
// ── Prune : désactivé ─────────────────────────────────────────────────────
|
|
263
|
+
//
|
|
264
|
+
// pruneOrphans() a été supprimé car il causait le bug "pubs en haut".
|
|
265
|
+
// NodeBB virtualise les posts hors viewport → les ancres disparaissent du DOM
|
|
266
|
+
// temporairement → pruneOrphans supprimait les wraps → scroll retour → les
|
|
267
|
+
// ancres revenaient → injectBetween réinjectait tout en haut.
|
|
268
|
+
//
|
|
269
|
+
// Les wraps ne sont supprimés que par cleanup() à chaque navigation ajaxify.
|
|
270
|
+
// decluster() et pruneOrphans() sont désactivés — voir v28.
|
|
307
271
|
|
|
308
|
-
/**
|
|
309
|
-
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
310
|
-
*
|
|
311
|
-
* isFilled en premier : un wrap rempli n'est JAMAIS supprimé.
|
|
312
|
-
* PRUNE_STABLE_MS (45 s) : pendant cette fenêtre une ancre absente est
|
|
313
|
-
* considérée comme virtualisée par NodeBB (scroll up), pas comme orphelin réel.
|
|
314
|
-
*/
|
|
315
|
-
function pruneOrphans(klass) {
|
|
316
|
-
const meta = KIND[klass];
|
|
317
|
-
if (!meta) return;
|
|
318
|
-
|
|
319
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
320
|
-
if (isFilled(w)) continue;
|
|
321
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
322
|
-
|
|
323
|
-
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
324
|
-
const sid = key.slice(klass.length + 1);
|
|
325
|
-
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
326
|
-
|
|
327
|
-
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
328
|
-
const anchorEl = document.querySelector(sel);
|
|
329
|
-
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Deux wraps adjacents → supprimer le courant s'il est vide et hors grâce.
|
|
337
|
-
* Guards dans l'ordre :
|
|
338
|
-
* 1. isFilled(w) → jamais toucher un wrap rempli
|
|
339
|
-
* 2. A_CREATED < FILL_GRACE → wrap trop récent (pas encore showAds'd)
|
|
340
|
-
* 3. A_SHOWN grace → fill en cours
|
|
341
|
-
* 4. isFilled(prev) → voisin rempli, intouchable → break
|
|
342
|
-
* 5. A_CREATED prev grace → voisin trop récent → break
|
|
343
|
-
* 6. A_SHOWN prev grace → break
|
|
344
|
-
* → les deux vides et hors grâce : supprimer le courant
|
|
345
|
-
*/
|
|
346
|
-
function decluster(klass) {
|
|
347
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
348
|
-
if (isFilled(w)) continue;
|
|
349
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
350
|
-
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
351
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
352
|
-
|
|
353
|
-
let prev = w.previousElementSibling, steps = 0;
|
|
354
|
-
while (prev && steps++ < 3) {
|
|
355
|
-
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
356
|
-
if (isFilled(prev)) break;
|
|
357
|
-
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
358
|
-
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
359
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
360
|
-
|
|
361
|
-
mutate(() => dropWrap(w));
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
272
|
|
|
367
273
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
368
274
|
|
|
@@ -401,23 +307,11 @@
|
|
|
401
307
|
const key = anchorKey(klass, el);
|
|
402
308
|
if (findWrap(key)) continue;
|
|
403
309
|
|
|
404
|
-
// 1. Tentative pool normal
|
|
405
310
|
const id = pickId(poolKey);
|
|
406
|
-
if (id)
|
|
407
|
-
const w = insertAfter(el, id, klass, key);
|
|
408
|
-
if (w) { observePh(id); inserted++; }
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
311
|
+
if (!id) continue; // pool épuisé : tous les ids sont montés, on passe au suivant
|
|
411
312
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (recyclable) {
|
|
415
|
-
const rid = parseInt(recyclable.getAttribute(A_WRAPID), 10);
|
|
416
|
-
const w = moveWrapAfter(el, recyclable, key);
|
|
417
|
-
if (w && Number.isFinite(rid)) { observePh(rid); inserted++; }
|
|
418
|
-
}
|
|
419
|
-
// Pool épuisé et pas de recyclage : on continue (items suivants peuvent
|
|
420
|
-
// avoir un wrap existant via findWrap, on ne break pas)
|
|
313
|
+
const w = insertAfter(el, id, klass, key);
|
|
314
|
+
if (w) { observePh(id); inserted++; }
|
|
421
315
|
}
|
|
422
316
|
return inserted;
|
|
423
317
|
}
|
|
@@ -558,9 +452,7 @@
|
|
|
558
452
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
559
453
|
if (!normBool(cfgEnable)) return 0;
|
|
560
454
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
561
|
-
pruneOrphans(klass);
|
|
562
455
|
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
563
|
-
if (n) decluster(klass);
|
|
564
456
|
return n;
|
|
565
457
|
};
|
|
566
458
|
|
|
@@ -743,13 +635,6 @@
|
|
|
743
635
|
function bindScroll() {
|
|
744
636
|
let ticking = false;
|
|
745
637
|
window.addEventListener('scroll', () => {
|
|
746
|
-
// Suivi direction du scroll (nécessaire pour le recyclage conditionnel)
|
|
747
|
-
try {
|
|
748
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
749
|
-
const d = y - S.lastScrollY;
|
|
750
|
-
if (Math.abs(d) > 4) { S.scrollDir = d > 0 ? 1 : -1; S.lastScrollY = y; }
|
|
751
|
-
} catch (_) {}
|
|
752
|
-
|
|
753
638
|
if (ticking) return;
|
|
754
639
|
ticking = true;
|
|
755
640
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
@@ -759,7 +644,6 @@
|
|
|
759
644
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
760
645
|
|
|
761
646
|
S.pageKey = pageKey();
|
|
762
|
-
try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
|
|
763
647
|
muteConsole();
|
|
764
648
|
ensureTcfLocator();
|
|
765
649
|
warmNetwork();
|