nodebb-plugin-ezoic-infinite 1.7.28 → 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 +191 -122
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,13 +287,19 @@
|
|
|
262
287
|
return null;
|
|
263
288
|
}
|
|
264
289
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
*/
|
|
300
|
+
function moveDistantWrap(klass, targetEl, newKey) {
|
|
301
|
+
const vh = window.innerHeight || 800;
|
|
302
|
+
const threshold = -vh * 4; // 4 viewports au-dessus = hors vue assurée
|
|
272
303
|
|
|
273
304
|
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
274
305
|
let bestFilled = null, bestFilledBottom = Infinity;
|
|
@@ -287,20 +318,44 @@
|
|
|
287
318
|
|
|
288
319
|
const best = bestEmpty ?? bestFilled;
|
|
289
320
|
if (!best) return null;
|
|
321
|
+
|
|
290
322
|
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
291
323
|
if (!Number.isFinite(id)) return null;
|
|
292
|
-
|
|
293
|
-
|
|
324
|
+
|
|
325
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
326
|
+
mutate(() => {
|
|
327
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
328
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
329
|
+
best.setAttribute(A_SHOWN, '0');
|
|
330
|
+
best.classList.remove('is-empty');
|
|
331
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
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 (_) {}
|
|
346
|
+
|
|
347
|
+
return { id, wrap: best };
|
|
294
348
|
}
|
|
295
349
|
|
|
296
|
-
// ── Wraps DOM
|
|
350
|
+
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
297
351
|
|
|
298
352
|
function makeWrap(id, klass, key) {
|
|
299
|
-
const w
|
|
353
|
+
const w = document.createElement('div');
|
|
300
354
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
301
355
|
w.setAttribute(A_ANCHOR, key);
|
|
302
356
|
w.setAttribute(A_WRAPID, String(id));
|
|
303
357
|
w.setAttribute(A_CREATED, String(ts()));
|
|
358
|
+
w.setAttribute(A_SHOWN, '0');
|
|
304
359
|
w.style.cssText = 'width:100%;display:block;';
|
|
305
360
|
const ph = document.createElement('div');
|
|
306
361
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -310,13 +365,14 @@
|
|
|
310
365
|
}
|
|
311
366
|
|
|
312
367
|
function insertAfter(el, id, klass, key) {
|
|
313
|
-
if (!el?.insertAdjacentElement)
|
|
314
|
-
if (findWrap(key))
|
|
315
|
-
if (S.mountedIds.has(id))
|
|
316
|
-
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;
|
|
317
372
|
const w = makeWrap(id, klass, key);
|
|
318
373
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
319
374
|
S.mountedIds.add(id);
|
|
375
|
+
S.wrapByKey.set(key, w);
|
|
320
376
|
return w;
|
|
321
377
|
}
|
|
322
378
|
|
|
@@ -326,57 +382,49 @@
|
|
|
326
382
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
327
383
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
328
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);
|
|
329
387
|
w.remove();
|
|
330
388
|
} catch (_) {}
|
|
331
389
|
}
|
|
332
390
|
|
|
333
391
|
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
334
392
|
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
// Pourquoi safe pour les topics ?
|
|
338
|
-
// NodeBB ne virtualise PAS la liste des topics dans une catégorie.
|
|
339
|
-
// Les <li component="category/topic"> restent dans le DOM pendant toute
|
|
340
|
-
// la session. Leurs ancres (data-tid) sont donc stables — un wrap orphelin
|
|
341
|
-
// signifie vraiment que le topic a été retiré (navigation, filtre, etc.).
|
|
393
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
342
394
|
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
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.
|
|
347
400
|
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
351
405
|
|
|
352
406
|
function pruneOrphansBetween() {
|
|
353
407
|
const klass = 'ezoic-ad-between';
|
|
354
408
|
const cfg = KIND[klass];
|
|
355
409
|
|
|
356
410
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
357
|
-
// Délai de grâce : ne pas pruner un wrap trop récent
|
|
358
411
|
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
359
|
-
if (ts() - created < MIN_PRUNE_AGE_MS) return;
|
|
412
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
360
413
|
|
|
361
414
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
362
|
-
const sid = key.slice(klass.length + 1); //
|
|
415
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
363
416
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
364
417
|
|
|
365
|
-
// Chercher l'ancre par data-tid
|
|
366
418
|
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
367
|
-
if (!anchorEl
|
|
368
|
-
mutate(() => dropWrap(w));
|
|
369
|
-
}
|
|
419
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
370
420
|
});
|
|
371
421
|
}
|
|
372
422
|
|
|
373
|
-
|
|
374
423
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
375
424
|
|
|
376
425
|
/**
|
|
377
|
-
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
378
|
-
* Utilise
|
|
379
|
-
* 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.
|
|
380
428
|
*/
|
|
381
429
|
function ordinal(klass, el) {
|
|
382
430
|
const attr = KIND[klass]?.ordinalAttr;
|
|
@@ -408,11 +456,18 @@
|
|
|
408
456
|
const key = anchorKey(klass, el);
|
|
409
457
|
if (findWrap(key)) continue;
|
|
410
458
|
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
459
|
+
const id = pickId(poolKey);
|
|
460
|
+
if (id) {
|
|
461
|
+
// Pool disponible : créer un nouveau wrap
|
|
462
|
+
const w = insertAfter(el, id, klass, key);
|
|
463
|
+
if (w) { observePh(id); inserted++; }
|
|
464
|
+
} else {
|
|
465
|
+
// Pool épuisé : déplacer un wrap distant (préserve le nœud placeholder)
|
|
466
|
+
const moved = moveDistantWrap(klass, el, key);
|
|
467
|
+
if (!moved) continue;
|
|
468
|
+
observePh(moved.id);
|
|
469
|
+
inserted++;
|
|
470
|
+
}
|
|
416
471
|
}
|
|
417
472
|
return inserted;
|
|
418
473
|
}
|
|
@@ -507,6 +562,10 @@
|
|
|
507
562
|
}
|
|
508
563
|
|
|
509
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
|
|
510
569
|
|
|
511
570
|
function patchShowAds() {
|
|
512
571
|
const apply = () => {
|
|
@@ -553,21 +612,22 @@
|
|
|
553
612
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
554
613
|
if (!normBool(cfgEnable)) return 0;
|
|
555
614
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
556
|
-
|
|
557
|
-
return n;
|
|
615
|
+
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
558
616
|
};
|
|
559
617
|
|
|
560
618
|
if (kind === 'topic') return exec(
|
|
561
619
|
'ezoic-ad-message', getPosts,
|
|
562
620
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
563
621
|
);
|
|
622
|
+
|
|
564
623
|
if (kind === 'categoryTopics') {
|
|
565
|
-
pruneOrphansBetween();
|
|
624
|
+
pruneOrphansBetween();
|
|
566
625
|
return exec(
|
|
567
626
|
'ezoic-ad-between', getTopics,
|
|
568
627
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
569
628
|
);
|
|
570
629
|
}
|
|
630
|
+
|
|
571
631
|
return exec(
|
|
572
632
|
'ezoic-ad-categories', getCategories,
|
|
573
633
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
@@ -619,10 +679,12 @@
|
|
|
619
679
|
blockedUntil = ts() + 1500;
|
|
620
680
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
621
681
|
S.cfg = null;
|
|
682
|
+
S.poolsReady = false;
|
|
622
683
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
623
684
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
624
685
|
S.mountedIds.clear();
|
|
625
686
|
S.lastShow.clear();
|
|
687
|
+
S.wrapByKey.clear();
|
|
626
688
|
S.inflight = 0;
|
|
627
689
|
S.pending = [];
|
|
628
690
|
S.pendingSet.clear();
|
|
@@ -640,7 +702,9 @@
|
|
|
640
702
|
for (const m of muts) {
|
|
641
703
|
for (const n of m.addedNodes) {
|
|
642
704
|
if (n.nodeType !== 1) continue;
|
|
643
|
-
|
|
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;} })) {
|
|
644
708
|
requestBurst(); return;
|
|
645
709
|
}
|
|
646
710
|
}
|
|
@@ -654,7 +718,12 @@
|
|
|
654
718
|
function muteConsole() {
|
|
655
719
|
if (window.__nbbEzMuted) return;
|
|
656
720
|
window.__nbbEzMuted = true;
|
|
657
|
-
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
|
+
];
|
|
658
727
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
659
728
|
const orig = console[m];
|
|
660
729
|
if (typeof orig !== 'function') continue;
|