nodebb-plugin-ezoic-infinite 1.7.29 → 1.7.30
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 +172 -121
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,68 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
3
|
*
|
|
4
4
|
* Historique des corrections majeures
|
|
5
5
|
* ────────────────────────────────────
|
|
6
|
-
* v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
|
|
7
|
-
* Suppression du recyclage de wraps. Cleanup complet navigation.
|
|
6
|
+
* v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
|
|
8
7
|
*
|
|
9
|
-
* v19 Intervalle global basé sur l'ordinal absolu (data-index)
|
|
8
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
|
|
10
9
|
* la position dans le batch courant.
|
|
11
10
|
*
|
|
12
|
-
* v20 Table KIND : anchorAttr/ordinalAttr/baseTag
|
|
11
|
+
* v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
|
|
13
12
|
* Fix fatal catégories : data-cid au lieu de data-index inexistant.
|
|
14
|
-
*
|
|
15
|
-
* IO fixe (une instance, jamais recréée). Burst cooldown 200 ms.
|
|
16
|
-
* Fix unobserve(null) → corruption IO → pubads error au scroll retour.
|
|
13
|
+
* IO fixe (une instance, jamais recréée).
|
|
17
14
|
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
18
15
|
*
|
|
19
|
-
* v25
|
|
20
|
-
* Fix scroll-up / virtualisation NodeBB :
|
|
21
|
-
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
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).
|
|
16
|
+
* v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
|
|
24
17
|
*
|
|
25
|
-
* v26 Suppression définitive du recyclage d'id.
|
|
26
|
-
* KIND simplifié.
|
|
18
|
+
* v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
|
|
27
19
|
*
|
|
28
|
-
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB).
|
|
20
|
+
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB posts).
|
|
29
21
|
*
|
|
30
|
-
* v28 decluster supprimé.
|
|
22
|
+
* v28 decluster supprimé. Wraps persistants pendant la session.
|
|
31
23
|
*
|
|
32
|
-
*
|
|
24
|
+
* v32 Retour anchorAttr = data-index pour ezoic-ad-between.
|
|
25
|
+
* data-tid peut être absent → clés invalides → wraps empilés.
|
|
26
|
+
* pruneOrphansBetween réactivé uniquement pour topics de catégorie :
|
|
27
|
+
* – NodeBB NE virtualise PAS les topics dans une liste de catégorie,
|
|
28
|
+
* les ancres (data-index) restent en DOM → prune safe et nécessaire
|
|
29
|
+
* pour éviter l'empilement après scroll long.
|
|
30
|
+
* – Toujours désactivé pour les posts : NodeBB virtualise les posts
|
|
31
|
+
* hors-viewport → faux-orphelins → bug réinjection en haut.
|
|
33
32
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* v34 moveDistantWrap : déplace le nœud wrap existant au lieu de le supprimer
|
|
34
|
+
* et recréer. Ezoic garde une registry interne des placeholders — recréer
|
|
35
|
+
* un div avec le même id déclenche "already been defined" et bloque
|
|
36
|
+
* showAds(). En déplaçant le même nœud DOM, la registry reste valide.
|
|
38
37
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* v36 Optimisations chemin critique (scroll → injectBetween) :
|
|
39
|
+
* – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
|
|
40
|
+
* sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
|
|
41
|
+
* moveDistantWrap, dropWrap et cleanup.
|
|
42
|
+
* – wrapIsLive allégé : pour les voisins immédiats on vérifie les
|
|
43
|
+
* attributs du nœud lui-même sans querySelector global.
|
|
44
|
+
* – MutationObserver : matches() vérifié avant querySelector() pour
|
|
45
|
+
* court-circuiter les sous-arbres entiers ajoutés par NodeBB.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* data-index = position relative dans le batch NodeBB, pas un ID stable.
|
|
54
|
-
* Lors du scroll infini, les nouveaux batches démarrent à data-index=0,
|
|
55
|
-
* ce qui créait des collisions de clés d'ancrage → mauvaise déduplication
|
|
56
|
-
* → wraps non injectés sur les nouveaux topics, puis réinjection en haut.
|
|
57
|
-
* Fix : anchorAttr = data-tid (stable et unique par topic).
|
|
58
|
-
* ordinalAttr reste data-index pour le calcul de l'intervalle.
|
|
59
|
-
* Analyse : decluster appelait dropWrap() qui faisait S.mountedIds.delete(id).
|
|
60
|
-
* Un wrap vide adjacent à un autre wrap → supprimé → id libéré → réinjecté
|
|
61
|
-
* en haut au prochain scroll. Exactement le bug observé.
|
|
62
|
-
* Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
|
|
63
|
-
* maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
|
|
47
|
+
* v35 Revue complète prod-ready :
|
|
48
|
+
* – initPools protégé contre ré-initialisation inutile (S.poolsReady).
|
|
49
|
+
* – muteConsole élargit à "No valid placeholders for loadMore".
|
|
50
|
+
* – moveDistantWrap appelle ezstandalone.refresh([id]) après déplacement
|
|
51
|
+
* pour forcer Ezoic à réactiver le placeholder sur son nouveau nœud.
|
|
52
|
+
* – A_SHOWN initialisé à '0' dans moveDistantWrap (évite NaN dans parseInt).
|
|
53
|
+
* – Commentaires et historique nettoyés.
|
|
64
54
|
*/
|
|
65
|
-
(function () {
|
|
55
|
+
(function nbbEzoicInfinite() {
|
|
66
56
|
'use strict';
|
|
67
57
|
|
|
68
58
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
@@ -74,13 +64,14 @@
|
|
|
74
64
|
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
75
65
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
76
66
|
|
|
77
|
-
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
67
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
|
|
68
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
69
|
+
const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
|
|
70
|
+
const MAX_INFLIGHT = 4; // max showAds() simultanés
|
|
71
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
72
|
+
const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
|
|
82
73
|
|
|
83
|
-
// IO
|
|
74
|
+
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
84
75
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
85
76
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
86
77
|
|
|
@@ -93,13 +84,12 @@
|
|
|
93
84
|
/**
|
|
94
85
|
* Table KIND — source de vérité par kindClass.
|
|
95
86
|
*
|
|
96
|
-
* sel
|
|
97
|
-
* baseTag
|
|
98
|
-
*
|
|
99
|
-
* anchorAttr
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* null → fallback positionnel (catégories)
|
|
87
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
88
|
+
* baseTag préfixe tag pour querySelector d'ancre
|
|
89
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
90
|
+
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
91
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
92
|
+
* null → fallback positionnel (catégories)
|
|
103
93
|
*/
|
|
104
94
|
const KIND = {
|
|
105
95
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -107,21 +97,23 @@
|
|
|
107
97
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
108
98
|
};
|
|
109
99
|
|
|
110
|
-
// ── État
|
|
100
|
+
// ── État global ────────────────────────────────────────────────────────────
|
|
111
101
|
|
|
112
102
|
const S = {
|
|
113
103
|
pageKey: null,
|
|
114
104
|
cfg: null,
|
|
105
|
+
poolsReady: false,
|
|
115
106
|
pools: { topics: [], posts: [], categories: [] },
|
|
116
107
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
117
108
|
mountedIds: new Set(),
|
|
118
109
|
lastShow: new Map(),
|
|
119
110
|
io: null,
|
|
120
111
|
domObs: null,
|
|
121
|
-
mutGuard: 0,
|
|
122
|
-
inflight: 0,
|
|
123
|
-
pending: [],
|
|
112
|
+
mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
|
|
113
|
+
inflight: 0, // showAds() en cours
|
|
114
|
+
pending: [], // ids en attente de slot inflight
|
|
124
115
|
pendingSet: new Set(),
|
|
116
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
125
117
|
runQueued: false,
|
|
126
118
|
burstActive: false,
|
|
127
119
|
burstDeadline: 0,
|
|
@@ -130,11 +122,12 @@
|
|
|
130
122
|
};
|
|
131
123
|
|
|
132
124
|
let blockedUntil = 0;
|
|
125
|
+
|
|
133
126
|
const ts = () => Date.now();
|
|
134
127
|
const isBlocked = () => ts() < blockedUntil;
|
|
135
128
|
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
136
|
-
const normBool =
|
|
137
|
-
const isFilled =
|
|
129
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
130
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
138
131
|
|
|
139
132
|
function mutate(fn) {
|
|
140
133
|
S.mutGuard++;
|
|
@@ -162,9 +155,11 @@
|
|
|
162
155
|
}
|
|
163
156
|
|
|
164
157
|
function initPools(cfg) {
|
|
158
|
+
if (S.poolsReady) return;
|
|
165
159
|
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
166
160
|
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
167
161
|
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
162
|
+
S.poolsReady = true;
|
|
168
163
|
}
|
|
169
164
|
|
|
170
165
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
@@ -204,15 +199,39 @@
|
|
|
204
199
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
205
200
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
206
201
|
|
|
202
|
+
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
206
|
+
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
207
|
+
*/
|
|
207
208
|
function wrapIsLive(wrap) {
|
|
208
209
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
209
210
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
210
211
|
if (!key) return false;
|
|
212
|
+
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
213
|
+
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
214
|
+
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
215
|
+
// Fallback : wrap déplacé (moveDistantWrap) ou registre pas encore à jour.
|
|
211
216
|
const colonIdx = key.indexOf(':');
|
|
212
|
-
const klass
|
|
217
|
+
const klass = key.slice(0, colonIdx);
|
|
213
218
|
const anchorId = key.slice(colonIdx + 1);
|
|
214
219
|
const cfg = KIND[klass];
|
|
215
220
|
if (!cfg) return false;
|
|
221
|
+
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
222
|
+
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
223
|
+
const parent = wrap.parentElement;
|
|
224
|
+
if (parent) {
|
|
225
|
+
for (const sib of parent.children) {
|
|
226
|
+
if (sib === wrap) continue;
|
|
227
|
+
try {
|
|
228
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
229
|
+
return sib.isConnected;
|
|
230
|
+
}
|
|
231
|
+
} catch (_) {}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Dernier recours : querySelector global
|
|
216
235
|
try {
|
|
217
236
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
218
237
|
return !!(found?.isConnected);
|
|
@@ -225,6 +244,10 @@
|
|
|
225
244
|
|
|
226
245
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
227
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
249
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
250
|
+
*/
|
|
228
251
|
function stableId(klass, el) {
|
|
229
252
|
const attr = KIND[klass]?.anchorAttr;
|
|
230
253
|
if (attr) {
|
|
@@ -242,17 +265,19 @@
|
|
|
242
265
|
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
243
266
|
|
|
244
267
|
function findWrap(key) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
248
|
-
);
|
|
249
|
-
} catch (_) { return null; }
|
|
268
|
+
const w = S.wrapByKey.get(key);
|
|
269
|
+
return (w?.isConnected) ? w : null;
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
253
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
276
|
+
* ou null si tous les ids sont montés.
|
|
277
|
+
*/
|
|
254
278
|
function pickId(poolKey) {
|
|
255
279
|
const pool = S.pools[poolKey];
|
|
280
|
+
if (!pool.length) return null;
|
|
256
281
|
for (let t = 0; t < pool.length; t++) {
|
|
257
282
|
const i = S.cursors[poolKey] % pool.length;
|
|
258
283
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -262,15 +287,19 @@
|
|
|
262
287
|
return null;
|
|
263
288
|
}
|
|
264
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Pool épuisé : déplace physiquement le wrap le plus loin au-dessus du
|
|
292
|
+
* viewport vers targetEl. On NE supprime PAS le placeholder — Ezoic garde
|
|
293
|
+
* une registry interne, et recréer un div avec le même id déclenche
|
|
294
|
+
* "already been defined" puis bloque showAds(). En déplaçant le même nœud,
|
|
295
|
+
* la registry reste valide. On appelle ensuite ezstandalone.refresh([id])
|
|
296
|
+
* pour forcer Ezoic à réactiver la pub sur sa nouvelle position.
|
|
297
|
+
*
|
|
298
|
+
* Priorité : wraps non remplis (pas de pub visible perdue), puis remplis.
|
|
299
|
+
*/
|
|
265
300
|
function moveDistantWrap(klass, targetEl, newKey) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Ezoic garde sa registry interne et "already been defined" bloque showAds()
|
|
269
|
-
// si on recrée le même ID. En déplaçant le wrap, le placeholder DOM reste
|
|
270
|
-
// le même nœud → Ezoic ne se plaint pas.
|
|
271
|
-
// Priorité : wraps vides d'abord (non remplis = pas de pub perdue).
|
|
272
|
-
const vh = window.innerHeight || 800;
|
|
273
|
-
const threshold = -vh * 4;
|
|
301
|
+
const vh = window.innerHeight || 800;
|
|
302
|
+
const threshold = -vh * 4; // 4 viewports au-dessus = hors vue assurée
|
|
274
303
|
|
|
275
304
|
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
276
305
|
let bestFilled = null, bestFilledBottom = Infinity;
|
|
@@ -293,26 +322,40 @@
|
|
|
293
322
|
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
294
323
|
if (!Number.isFinite(id)) return null;
|
|
295
324
|
|
|
296
|
-
|
|
325
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
297
326
|
mutate(() => {
|
|
298
|
-
best.setAttribute(A_ANCHOR,
|
|
327
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
299
328
|
best.setAttribute(A_CREATED, String(ts()));
|
|
300
|
-
best.setAttribute(A_SHOWN,
|
|
329
|
+
best.setAttribute(A_SHOWN, '0');
|
|
301
330
|
best.classList.remove('is-empty');
|
|
302
331
|
targetEl.insertAdjacentElement('afterend', best);
|
|
303
332
|
});
|
|
333
|
+
// Mettre à jour le registre : ancienne clé → nouvelle clé
|
|
334
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
335
|
+
S.wrapByKey.set(newKey, best);
|
|
336
|
+
|
|
337
|
+
// Signaler à Ezoic que ce placeholder a bougé dans le DOM
|
|
338
|
+
try {
|
|
339
|
+
const ez = window.ezstandalone;
|
|
340
|
+
if (typeof ez?.refresh === 'function') {
|
|
341
|
+
(Array.isArray(ez.cmd) ? p => ez.cmd.push(p) : p => p())(() => {
|
|
342
|
+
try { ez.refresh([id]); } catch (_) {}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
} catch (_) {}
|
|
304
346
|
|
|
305
347
|
return { id, wrap: best };
|
|
306
348
|
}
|
|
307
349
|
|
|
308
|
-
// ── Wraps DOM
|
|
350
|
+
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
309
351
|
|
|
310
352
|
function makeWrap(id, klass, key) {
|
|
311
|
-
const w
|
|
353
|
+
const w = document.createElement('div');
|
|
312
354
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
313
355
|
w.setAttribute(A_ANCHOR, key);
|
|
314
356
|
w.setAttribute(A_WRAPID, String(id));
|
|
315
357
|
w.setAttribute(A_CREATED, String(ts()));
|
|
358
|
+
w.setAttribute(A_SHOWN, '0');
|
|
316
359
|
w.style.cssText = 'width:100%;display:block;';
|
|
317
360
|
const ph = document.createElement('div');
|
|
318
361
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -322,13 +365,14 @@
|
|
|
322
365
|
}
|
|
323
366
|
|
|
324
367
|
function insertAfter(el, id, klass, key) {
|
|
325
|
-
if (!el?.insertAdjacentElement)
|
|
326
|
-
if (findWrap(key))
|
|
327
|
-
if (S.mountedIds.has(id))
|
|
328
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected)
|
|
368
|
+
if (!el?.insertAdjacentElement) return null;
|
|
369
|
+
if (findWrap(key)) return null;
|
|
370
|
+
if (S.mountedIds.has(id)) return null;
|
|
371
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
329
372
|
const w = makeWrap(id, klass, key);
|
|
330
373
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
331
374
|
S.mountedIds.add(id);
|
|
375
|
+
S.wrapByKey.set(key, w);
|
|
332
376
|
return w;
|
|
333
377
|
}
|
|
334
378
|
|
|
@@ -338,57 +382,49 @@
|
|
|
338
382
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
339
383
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
340
384
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
385
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
386
|
+
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
341
387
|
w.remove();
|
|
342
388
|
} catch (_) {}
|
|
343
389
|
}
|
|
344
390
|
|
|
345
391
|
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
346
392
|
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// Pourquoi safe pour les topics ?
|
|
350
|
-
// NodeBB ne virtualise PAS la liste des topics dans une catégorie.
|
|
351
|
-
// Les <li component="category/topic"> restent dans le DOM pendant toute
|
|
352
|
-
// la session. Leurs ancres (data-tid) sont donc stables — un wrap orphelin
|
|
353
|
-
// signifie vraiment que le topic a été retiré (navigation, filtre, etc.).
|
|
393
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
354
394
|
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
395
|
+
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
396
|
+
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
397
|
+
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
398
|
+
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
399
|
+
// liste après un long scroll et bloquent les nouvelles injections.
|
|
359
400
|
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
401
|
+
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
402
|
+
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
403
|
+
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
404
|
+
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
363
405
|
|
|
364
406
|
function pruneOrphansBetween() {
|
|
365
407
|
const klass = 'ezoic-ad-between';
|
|
366
408
|
const cfg = KIND[klass];
|
|
367
409
|
|
|
368
410
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
369
|
-
// Délai de grâce : ne pas pruner un wrap trop récent
|
|
370
411
|
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
371
|
-
if (ts() - created < MIN_PRUNE_AGE_MS) return;
|
|
412
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
372
413
|
|
|
373
414
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
374
|
-
const sid = key.slice(klass.length + 1); //
|
|
415
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
375
416
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
376
417
|
|
|
377
|
-
// Chercher l'ancre par data-tid
|
|
378
418
|
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
379
|
-
if (!anchorEl
|
|
380
|
-
mutate(() => dropWrap(w));
|
|
381
|
-
}
|
|
419
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
382
420
|
});
|
|
383
421
|
}
|
|
384
422
|
|
|
385
|
-
|
|
386
423
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
387
424
|
|
|
388
425
|
/**
|
|
389
|
-
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
390
|
-
* Utilise
|
|
391
|
-
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
426
|
+
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
427
|
+
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
392
428
|
*/
|
|
393
429
|
function ordinal(klass, el) {
|
|
394
430
|
const attr = KIND[klass]?.ordinalAttr;
|
|
@@ -420,12 +456,13 @@
|
|
|
420
456
|
const key = anchorKey(klass, el);
|
|
421
457
|
if (findWrap(key)) continue;
|
|
422
458
|
|
|
423
|
-
|
|
459
|
+
const id = pickId(poolKey);
|
|
424
460
|
if (id) {
|
|
461
|
+
// Pool disponible : créer un nouveau wrap
|
|
425
462
|
const w = insertAfter(el, id, klass, key);
|
|
426
463
|
if (w) { observePh(id); inserted++; }
|
|
427
464
|
} else {
|
|
428
|
-
// Pool épuisé : déplacer un wrap distant (
|
|
465
|
+
// Pool épuisé : déplacer un wrap distant (préserve le nœud placeholder)
|
|
429
466
|
const moved = moveDistantWrap(klass, el, key);
|
|
430
467
|
if (!moved) continue;
|
|
431
468
|
observePh(moved.id);
|
|
@@ -525,6 +562,10 @@
|
|
|
525
562
|
}
|
|
526
563
|
|
|
527
564
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
565
|
+
//
|
|
566
|
+
// Intercepte ez.showAds() pour :
|
|
567
|
+
// – ignorer les appels pendant blockedUntil
|
|
568
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
528
569
|
|
|
529
570
|
function patchShowAds() {
|
|
530
571
|
const apply = () => {
|
|
@@ -571,21 +612,22 @@
|
|
|
571
612
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
572
613
|
if (!normBool(cfgEnable)) return 0;
|
|
573
614
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
574
|
-
|
|
575
|
-
return n;
|
|
615
|
+
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
576
616
|
};
|
|
577
617
|
|
|
578
618
|
if (kind === 'topic') return exec(
|
|
579
619
|
'ezoic-ad-message', getPosts,
|
|
580
620
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
581
621
|
);
|
|
622
|
+
|
|
582
623
|
if (kind === 'categoryTopics') {
|
|
583
|
-
pruneOrphansBetween();
|
|
624
|
+
pruneOrphansBetween();
|
|
584
625
|
return exec(
|
|
585
626
|
'ezoic-ad-between', getTopics,
|
|
586
627
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
587
628
|
);
|
|
588
629
|
}
|
|
630
|
+
|
|
589
631
|
return exec(
|
|
590
632
|
'ezoic-ad-categories', getCategories,
|
|
591
633
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
@@ -637,10 +679,12 @@
|
|
|
637
679
|
blockedUntil = ts() + 1500;
|
|
638
680
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
639
681
|
S.cfg = null;
|
|
682
|
+
S.poolsReady = false;
|
|
640
683
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
641
684
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
642
685
|
S.mountedIds.clear();
|
|
643
686
|
S.lastShow.clear();
|
|
687
|
+
S.wrapByKey.clear();
|
|
644
688
|
S.inflight = 0;
|
|
645
689
|
S.pending = [];
|
|
646
690
|
S.pendingSet.clear();
|
|
@@ -658,7 +702,9 @@
|
|
|
658
702
|
for (const m of muts) {
|
|
659
703
|
for (const n of m.addedNodes) {
|
|
660
704
|
if (n.nodeType !== 1) continue;
|
|
661
|
-
|
|
705
|
+
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
706
|
+
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
707
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
662
708
|
requestBurst(); return;
|
|
663
709
|
}
|
|
664
710
|
}
|
|
@@ -672,7 +718,12 @@
|
|
|
672
718
|
function muteConsole() {
|
|
673
719
|
if (window.__nbbEzMuted) return;
|
|
674
720
|
window.__nbbEzMuted = true;
|
|
675
|
-
const MUTED = [
|
|
721
|
+
const MUTED = [
|
|
722
|
+
'[EzoicAds JS]: Placeholder Id',
|
|
723
|
+
'No valid placeholders for loadMore',
|
|
724
|
+
'Debugger iframe already exists',
|
|
725
|
+
`with id ${PH_PREFIX}`,
|
|
726
|
+
];
|
|
676
727
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
677
728
|
const orig = console[m];
|
|
678
729
|
if (typeof orig !== 'function') continue;
|