nodebb-plugin-ezoic-infinite 1.7.21 → 1.7.22
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 +244 -149
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,58 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v20)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Correctifs critiques vs v19
|
|
5
|
+
* ───────────────────────────
|
|
6
|
+
* [BUG FATAL] Pubs catégories disparaissent au premier scroll sur mobile
|
|
7
|
+
* pruneOrphans cherchait l'ancre via `[data-index]`, mais les éléments
|
|
8
|
+
* `li[component="categories/category"]` ont `data-cid`, pas `data-index`.
|
|
9
|
+
* → anchorEl toujours null → suppression à chaque runCore() → disparition.
|
|
10
|
+
* Fix : table KIND_META qui mappe chaque kindClass vers son attribut d'ancre
|
|
11
|
+
* stable (data-pid pour posts, data-index pour topics, data-cid pour catégories).
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* [BUG] decluster : break trop large stoppait tout quand UN wrap était en grâce
|
|
14
|
+
* Fix : on skip uniquement le wrap courant, pas toute la boucle.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Fix posts : baseTag vide → ordinal fallback cassé → pubs jamais injectées.
|
|
15
|
-
* IO fixe (une instance, jamais recréée). Burst cooldown 200 ms.
|
|
16
|
-
* Fix unobserve(null) → corruption IO → pubads error au scroll retour.
|
|
17
|
-
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
16
|
+
* [BUG] injectBetween : `break` sur pool épuisé empêchait d'observer les wraps
|
|
17
|
+
* existants sur les items suivants. Fix : `continue` au lieu de `break`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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).
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
24
21
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
27
24
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
|
|
35
|
-
* maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
|
|
25
|
+
* Nettoyage
|
|
26
|
+
* ─────────
|
|
27
|
+
* - Suppression du scroll boost (complexité sans gain mesurable côté SPA statique)
|
|
28
|
+
* - MAX_INFLIGHT unique desktop/mobile (inutile de différencier)
|
|
29
|
+
* - getAnchorKey/getGlobalOrdinal fusionnés en helpers cohérents
|
|
30
|
+
* - Commentaires internes allégés (code auto-documenté)
|
|
36
31
|
*/
|
|
37
32
|
(function () {
|
|
38
33
|
'use strict';
|
|
39
34
|
|
|
40
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
41
36
|
|
|
42
|
-
const WRAP_CLASS
|
|
43
|
-
const PH_PREFIX
|
|
44
|
-
const A_ANCHOR
|
|
45
|
-
const A_WRAPID
|
|
46
|
-
const A_CREATED
|
|
47
|
-
const A_SHOWN
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
38
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
39
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
40
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic (number string)
|
|
41
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création (ms)
|
|
42
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds (ms)
|
|
43
|
+
|
|
44
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
45
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds où l'on ne decluster pas
|
|
46
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
47
|
+
const MAX_INSERTS_PER_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
56
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
57
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
58
55
|
|
|
@@ -63,37 +60,40 @@
|
|
|
63
60
|
};
|
|
64
61
|
|
|
65
62
|
/**
|
|
66
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
67
64
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* null → fallback positionnel (catégories)
|
|
65
|
+
* L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
|
|
66
|
+
* quelle que soit la page ou la virtualisation :
|
|
67
|
+
* posts → data-pid (id du message, unique et permanent)
|
|
68
|
+
* topics → data-index (position 0-based dans la liste, fourni par NodeBB)
|
|
69
|
+
* catégories → data-cid (id de la catégorie, unique et permanent)
|
|
70
|
+
* ← C'était le bug v19 : on cherchait data-index ici
|
|
75
71
|
*/
|
|
76
72
|
const KIND = {
|
|
77
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
78
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
79
|
-
'ezoic-ad-categories': { sel: SEL.category,
|
|
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' },
|
|
80
76
|
};
|
|
81
77
|
|
|
82
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
83
79
|
|
|
84
80
|
const S = {
|
|
85
|
-
pageKey:
|
|
86
|
-
cfg:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
pageKey: null,
|
|
82
|
+
cfg: null,
|
|
83
|
+
|
|
84
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
85
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
86
|
+
mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
|
|
87
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
88
|
+
|
|
89
|
+
io: null,
|
|
90
|
+
domObs: null,
|
|
91
|
+
mutGuard: 0, // compteur internalMutation
|
|
92
|
+
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
96
|
+
|
|
97
97
|
runQueued: false,
|
|
98
98
|
burstActive: false,
|
|
99
99
|
burstDeadline: 0,
|
|
@@ -102,11 +102,8 @@
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
108
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
109
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
110
107
|
|
|
111
108
|
function mutate(fn) {
|
|
112
109
|
S.mutGuard++;
|
|
@@ -124,6 +121,12 @@
|
|
|
124
121
|
return S.cfg;
|
|
125
122
|
}
|
|
126
123
|
|
|
124
|
+
function initPools(cfg) {
|
|
125
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
126
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
127
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
128
|
+
}
|
|
129
|
+
|
|
127
130
|
function parseIds(raw) {
|
|
128
131
|
const out = [], seen = new Set();
|
|
129
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -133,11 +136,12 @@
|
|
|
133
136
|
return out;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
140
|
+
|
|
141
|
+
const isFilled = (n) =>
|
|
142
|
+
!!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
143
|
+
|
|
144
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
141
145
|
|
|
142
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
143
147
|
|
|
@@ -161,13 +165,13 @@
|
|
|
161
165
|
return 'other';
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
165
169
|
|
|
166
170
|
function getPosts() {
|
|
167
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
168
172
|
if (!el.isConnected) return false;
|
|
169
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
170
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
171
175
|
if (p && p !== el) return false;
|
|
172
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
173
177
|
});
|
|
@@ -183,28 +187,36 @@
|
|
|
183
187
|
);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
// ── Ancres stables
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Retourne l'identifiant stable de l'élément selon son kindClass.
|
|
194
|
+
* Utilise l'attribut défini dans KIND (data-pid, data-index, data-cid).
|
|
195
|
+
* Fallback positionnel si l'attribut est absent.
|
|
196
|
+
*/
|
|
197
|
+
function stableId(kindClass, el) {
|
|
198
|
+
const attr = KIND[kindClass]?.anchorAttr;
|
|
190
199
|
if (attr) {
|
|
191
200
|
const v = el.getAttribute(attr);
|
|
192
201
|
if (v !== null && v !== '') return v;
|
|
193
202
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
// Fallback : position dans le parent
|
|
204
|
+
try {
|
|
205
|
+
let i = 0;
|
|
206
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
207
|
+
if (s === el) return `i${i}`;
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
} catch (_) {}
|
|
199
211
|
return 'i0';
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
const
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
203
215
|
|
|
204
|
-
function findWrap(
|
|
216
|
+
function findWrap(anchorKey) {
|
|
205
217
|
try {
|
|
206
218
|
return document.querySelector(
|
|
207
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
208
220
|
);
|
|
209
221
|
} catch (_) { return null; }
|
|
210
222
|
}
|
|
@@ -214,7 +226,7 @@
|
|
|
214
226
|
function pickId(poolKey) {
|
|
215
227
|
const pool = S.pools[poolKey];
|
|
216
228
|
for (let t = 0; t < pool.length; t++) {
|
|
217
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
218
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
219
231
|
const id = pool[i];
|
|
220
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -225,7 +237,7 @@
|
|
|
225
237
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
226
238
|
|
|
227
239
|
function makeWrap(id, klass, key) {
|
|
228
|
-
const w
|
|
240
|
+
const w = document.createElement('div');
|
|
229
241
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
230
242
|
w.setAttribute(A_ANCHOR, key);
|
|
231
243
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -239,10 +251,10 @@
|
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
function insertAfter(el, id, klass, key) {
|
|
242
|
-
if (!el?.insertAdjacentElement)
|
|
243
|
-
if (findWrap(key))
|
|
244
|
-
if (S.mountedIds.has(id))
|
|
245
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected)
|
|
254
|
+
if (!el?.insertAdjacentElement) return null;
|
|
255
|
+
if (findWrap(key)) return null; // ancre déjà présente
|
|
256
|
+
if (S.mountedIds.has(id)) return null; // id déjà monté
|
|
257
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
246
258
|
const w = makeWrap(id, klass, key);
|
|
247
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
248
260
|
S.mountedIds.add(id);
|
|
@@ -251,44 +263,100 @@
|
|
|
251
263
|
|
|
252
264
|
function dropWrap(w) {
|
|
253
265
|
try {
|
|
254
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
255
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
256
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
257
267
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
268
|
+
// IMPORTANT : ne passer unobserve que si c'est un vrai Element.
|
|
269
|
+
// unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
|
|
270
|
+
// "parameter 1 is not of type Element" sur le prochain observe).
|
|
271
|
+
try {
|
|
272
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
274
|
+
} catch (_) {}
|
|
258
275
|
w.remove();
|
|
259
276
|
} catch (_) {}
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
// ── Prune
|
|
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.
|
|
279
|
+
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
271
280
|
|
|
281
|
+
/**
|
|
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
|
|
289
|
+
*
|
|
290
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
291
|
+
*/
|
|
292
|
+
function pruneOrphans(klass) {
|
|
293
|
+
const meta = KIND[klass];
|
|
294
|
+
if (!meta) return;
|
|
295
|
+
|
|
296
|
+
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
297
|
+
|
|
298
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
299
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
300
|
+
|
|
301
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
+
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
303
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
304
|
+
|
|
305
|
+
const anchorEl = document.querySelector(
|
|
306
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
+
);
|
|
308
|
+
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
316
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
317
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
318
|
+
*/
|
|
319
|
+
function decluster(klass) {
|
|
320
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
322
|
+
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
|
+
|
|
325
|
+
let prev = w.previousElementSibling, steps = 0;
|
|
326
|
+
while (prev && steps++ < 3) {
|
|
327
|
+
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
|
+
|
|
329
|
+
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
331
|
+
|
|
332
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
333
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
272
338
|
|
|
273
339
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
274
340
|
|
|
275
341
|
/**
|
|
276
342
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
277
|
-
*
|
|
278
|
-
*
|
|
343
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
344
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
279
345
|
*/
|
|
280
346
|
function ordinal(klass, el) {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
347
|
+
const di = el.getAttribute('data-index');
|
|
348
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
349
|
+
// Fallback positionnel
|
|
350
|
+
try {
|
|
351
|
+
const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
|
|
352
|
+
if (tag) {
|
|
353
|
+
let i = 0;
|
|
354
|
+
for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
|
|
355
|
+
if (n === el) return i;
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (_) {}
|
|
292
360
|
return 0;
|
|
293
361
|
}
|
|
294
362
|
|
|
@@ -297,18 +365,20 @@
|
|
|
297
365
|
let inserted = 0;
|
|
298
366
|
|
|
299
367
|
for (const el of items) {
|
|
300
|
-
if (inserted >=
|
|
368
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
301
369
|
if (!el?.isConnected) continue;
|
|
302
370
|
|
|
303
|
-
const ord
|
|
304
|
-
|
|
371
|
+
const ord = ordinal(klass, el);
|
|
372
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
373
|
+
if (!isTarget) continue;
|
|
374
|
+
|
|
305
375
|
if (adjacentWrap(el)) continue;
|
|
306
376
|
|
|
307
|
-
const key =
|
|
308
|
-
if (findWrap(key)) continue;
|
|
377
|
+
const key = makeAnchorKey(klass, el);
|
|
378
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
309
379
|
|
|
310
380
|
const id = pickId(poolKey);
|
|
311
|
-
if (!id) continue; // pool
|
|
381
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
312
382
|
|
|
313
383
|
const w = insertAfter(el, id, klass, key);
|
|
314
384
|
if (w) { observePh(id); inserted++; }
|
|
@@ -320,6 +390,7 @@
|
|
|
320
390
|
|
|
321
391
|
function getIO() {
|
|
322
392
|
if (S.io) return S.io;
|
|
393
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
323
394
|
try {
|
|
324
395
|
S.io = new IntersectionObserver(entries => {
|
|
325
396
|
for (const e of entries) {
|
|
@@ -328,7 +399,7 @@
|
|
|
328
399
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
329
400
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
330
401
|
}
|
|
331
|
-
}, { root: null, rootMargin:
|
|
402
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
332
403
|
} catch (_) { S.io = null; }
|
|
333
404
|
return S.io;
|
|
334
405
|
}
|
|
@@ -379,6 +450,7 @@
|
|
|
379
450
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
380
451
|
S.lastShow.set(id, t);
|
|
381
452
|
|
|
453
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
382
454
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
383
455
|
|
|
384
456
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -399,6 +471,7 @@
|
|
|
399
471
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
400
472
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
401
473
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
+
// Un show plus récent → ne pas toucher
|
|
402
475
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
403
476
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
404
477
|
} catch (_) {}
|
|
@@ -417,7 +490,7 @@
|
|
|
417
490
|
const orig = ez.showAds.bind(ez);
|
|
418
491
|
ez.showAds = function (...args) {
|
|
419
492
|
if (isBlocked()) return;
|
|
420
|
-
const ids
|
|
493
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
421
494
|
const seen = new Set();
|
|
422
495
|
for (const v of ids) {
|
|
423
496
|
const id = parseInt(v, 10);
|
|
@@ -436,7 +509,7 @@
|
|
|
436
509
|
}
|
|
437
510
|
}
|
|
438
511
|
|
|
439
|
-
// ── Core
|
|
512
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
440
513
|
|
|
441
514
|
async function runCore() {
|
|
442
515
|
if (isBlocked()) return 0;
|
|
@@ -451,8 +524,11 @@
|
|
|
451
524
|
|
|
452
525
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
453
526
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
+
const items = getItems();
|
|
454
528
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
455
|
-
|
|
529
|
+
pruneOrphans(klass);
|
|
530
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
531
|
+
if (n) decluster(klass);
|
|
456
532
|
return n;
|
|
457
533
|
};
|
|
458
534
|
|
|
@@ -464,13 +540,14 @@
|
|
|
464
540
|
'ezoic-ad-between', getTopics,
|
|
465
541
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
466
542
|
);
|
|
467
|
-
return exec(
|
|
543
|
+
if (kind === 'categories') return exec(
|
|
468
544
|
'ezoic-ad-categories', getCategories,
|
|
469
545
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
470
546
|
);
|
|
547
|
+
return 0;
|
|
471
548
|
}
|
|
472
549
|
|
|
473
|
-
// ── Scheduler
|
|
550
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
474
551
|
|
|
475
552
|
function scheduleRun(cb) {
|
|
476
553
|
if (S.runQueued) return;
|
|
@@ -488,8 +565,10 @@
|
|
|
488
565
|
if (isBlocked()) return;
|
|
489
566
|
const t = ts();
|
|
490
567
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
491
|
-
S.lastBurstTs
|
|
492
|
-
|
|
568
|
+
S.lastBurstTs = t;
|
|
569
|
+
|
|
570
|
+
const pk = pageKey();
|
|
571
|
+
S.pageKey = pk;
|
|
493
572
|
S.burstDeadline = t + 2000;
|
|
494
573
|
|
|
495
574
|
if (S.burstActive) return;
|
|
@@ -497,7 +576,7 @@
|
|
|
497
576
|
S.burstCount = 0;
|
|
498
577
|
|
|
499
578
|
const step = () => {
|
|
500
|
-
if (pageKey() !==
|
|
579
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
501
580
|
S.burstActive = false; return;
|
|
502
581
|
}
|
|
503
582
|
S.burstCount++;
|
|
@@ -509,7 +588,7 @@
|
|
|
509
588
|
step();
|
|
510
589
|
}
|
|
511
590
|
|
|
512
|
-
// ── Cleanup navigation
|
|
591
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
513
592
|
|
|
514
593
|
function cleanup() {
|
|
515
594
|
blockedUntil = ts() + 1500;
|
|
@@ -526,17 +605,19 @@
|
|
|
526
605
|
S.runQueued = false;
|
|
527
606
|
}
|
|
528
607
|
|
|
529
|
-
// ──
|
|
608
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
530
609
|
|
|
531
610
|
function ensureDomObserver() {
|
|
532
611
|
if (S.domObs) return;
|
|
533
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
534
612
|
S.domObs = new MutationObserver(muts => {
|
|
535
613
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
536
614
|
for (const m of muts) {
|
|
615
|
+
if (!m.addedNodes?.length) continue;
|
|
537
616
|
for (const n of m.addedNodes) {
|
|
538
617
|
if (n.nodeType !== 1) continue;
|
|
539
|
-
if (
|
|
618
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
619
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
620
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
540
621
|
requestBurst(); return;
|
|
541
622
|
}
|
|
542
623
|
}
|
|
@@ -562,18 +643,29 @@
|
|
|
562
643
|
}
|
|
563
644
|
|
|
564
645
|
function ensureTcfLocator() {
|
|
646
|
+
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
647
|
+
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
648
|
+
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
649
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
650
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
651
|
+
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
565
652
|
try {
|
|
566
653
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
+
|
|
567
655
|
const inject = () => {
|
|
568
656
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
569
657
|
const f = document.createElement('iframe');
|
|
570
658
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
571
659
|
(document.body || document.documentElement).appendChild(f);
|
|
572
660
|
};
|
|
661
|
+
|
|
573
662
|
inject();
|
|
663
|
+
|
|
664
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
574
665
|
if (!window.__nbbTcfObs) {
|
|
575
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
576
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
666
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
667
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
+
{ childList: true, subtree: true });
|
|
577
669
|
}
|
|
578
670
|
} catch (_) {}
|
|
579
671
|
}
|
|
@@ -583,10 +675,10 @@
|
|
|
583
675
|
const head = document.head;
|
|
584
676
|
if (!head) return;
|
|
585
677
|
for (const [rel, href, cors] of [
|
|
586
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
587
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
588
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
589
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true
|
|
678
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
679
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
680
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
681
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
590
682
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
591
683
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
592
684
|
]) {
|
|
@@ -600,7 +692,7 @@
|
|
|
600
692
|
}
|
|
601
693
|
}
|
|
602
694
|
|
|
603
|
-
// ── Bindings
|
|
695
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
604
696
|
|
|
605
697
|
function bindNodeBB() {
|
|
606
698
|
const $ = window.jQuery;
|
|
@@ -611,16 +703,19 @@
|
|
|
611
703
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
612
704
|
S.pageKey = pageKey();
|
|
613
705
|
blockedUntil = 0;
|
|
614
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
615
|
-
|
|
706
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
707
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
616
708
|
});
|
|
617
709
|
|
|
618
|
-
const
|
|
619
|
-
'action:ajaxify.contentLoaded',
|
|
710
|
+
const BURST_EVENTS = [
|
|
711
|
+
'action:ajaxify.contentLoaded',
|
|
712
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
620
713
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
621
714
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
622
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
623
715
|
|
|
716
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
+
|
|
718
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
624
719
|
try {
|
|
625
720
|
require(['hooks'], hooks => {
|
|
626
721
|
if (typeof hooks?.on !== 'function') return;
|