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