nodebb-plugin-ezoic-infinite 1.7.9 → 1.7.10
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 +210 -163
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,49 +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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
21
|
+
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
24
|
+
*
|
|
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é)
|
|
25
31
|
*/
|
|
26
32
|
(function () {
|
|
27
33
|
'use strict';
|
|
28
34
|
|
|
29
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
30
36
|
|
|
31
|
-
const WRAP_CLASS
|
|
32
|
-
const PH_PREFIX
|
|
33
|
-
const A_ANCHOR
|
|
34
|
-
const A_WRAPID
|
|
35
|
-
const A_CREATED
|
|
36
|
-
const A_SHOWN
|
|
37
|
-
|
|
38
|
-
const MIN_PRUNE_AGE_MS
|
|
39
|
-
const FILL_GRACE_MS
|
|
40
|
-
const EMPTY_CHECK_MS
|
|
41
|
-
const
|
|
42
|
-
const MAX_INFLIGHT
|
|
43
|
-
const SHOW_THROTTLE_MS
|
|
44
|
-
const BURST_COOLDOWN_MS
|
|
45
|
-
|
|
46
|
-
// IO
|
|
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)
|
|
47
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
48
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
49
55
|
|
|
@@ -54,40 +60,40 @@
|
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
58
64
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* data-cid catégories (id catégorie, immuable)
|
|
66
|
-
* ordinalAttr: attribut 0-based pour le calcul de l'intervalle
|
|
67
|
-
* data-index posts + topics (fourni par NodeBB)
|
|
68
|
-
* null catégories (page statique → fallback positionnel)
|
|
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
|
|
69
71
|
*/
|
|
70
72
|
const KIND = {
|
|
71
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
72
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
73
|
-
'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' },
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
78
80
|
const S = {
|
|
79
|
-
pageKey:
|
|
80
|
-
cfg:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
|
|
91
97
|
runQueued: false,
|
|
92
98
|
burstActive: false,
|
|
93
99
|
burstDeadline: 0,
|
|
@@ -96,11 +102,8 @@
|
|
|
96
102
|
};
|
|
97
103
|
|
|
98
104
|
let blockedUntil = 0;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
102
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
103
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
104
107
|
|
|
105
108
|
function mutate(fn) {
|
|
106
109
|
S.mutGuard++;
|
|
@@ -118,6 +121,12 @@
|
|
|
118
121
|
return S.cfg;
|
|
119
122
|
}
|
|
120
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
|
+
|
|
121
130
|
function parseIds(raw) {
|
|
122
131
|
const out = [], seen = new Set();
|
|
123
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -127,11 +136,12 @@
|
|
|
127
136
|
return out;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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; } };
|
|
135
145
|
|
|
136
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
137
147
|
|
|
@@ -155,13 +165,13 @@
|
|
|
155
165
|
return 'other';
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
159
169
|
|
|
160
170
|
function getPosts() {
|
|
161
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
162
172
|
if (!el.isConnected) return false;
|
|
163
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
164
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
165
175
|
if (p && p !== el) return false;
|
|
166
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
167
177
|
});
|
|
@@ -177,28 +187,36 @@
|
|
|
177
187
|
);
|
|
178
188
|
}
|
|
179
189
|
|
|
180
|
-
// ── Ancres stables
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
199
|
if (attr) {
|
|
185
200
|
const v = el.getAttribute(attr);
|
|
186
201
|
if (v !== null && v !== '') return v;
|
|
187
202
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 (_) {}
|
|
193
211
|
return 'i0';
|
|
194
212
|
}
|
|
195
213
|
|
|
196
|
-
const
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
197
215
|
|
|
198
|
-
function findWrap(
|
|
216
|
+
function findWrap(anchorKey) {
|
|
199
217
|
try {
|
|
200
218
|
return document.querySelector(
|
|
201
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
202
220
|
);
|
|
203
221
|
} catch (_) { return null; }
|
|
204
222
|
}
|
|
@@ -208,7 +226,7 @@
|
|
|
208
226
|
function pickId(poolKey) {
|
|
209
227
|
const pool = S.pools[poolKey];
|
|
210
228
|
for (let t = 0; t < pool.length; t++) {
|
|
211
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
212
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
213
231
|
const id = pool[i];
|
|
214
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -219,7 +237,7 @@
|
|
|
219
237
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
220
238
|
|
|
221
239
|
function makeWrap(id, klass, key) {
|
|
222
|
-
const w
|
|
240
|
+
const w = document.createElement('div');
|
|
223
241
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
224
242
|
w.setAttribute(A_ANCHOR, key);
|
|
225
243
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -233,10 +251,10 @@
|
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
function insertAfter(el, id, klass, key) {
|
|
236
|
-
if (!el?.insertAdjacentElement)
|
|
237
|
-
if (findWrap(key))
|
|
238
|
-
if (S.mountedIds.has(id))
|
|
239
|
-
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;
|
|
240
258
|
const w = makeWrap(id, klass, key);
|
|
241
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
242
260
|
S.mountedIds.add(id);
|
|
@@ -245,12 +263,15 @@
|
|
|
245
263
|
|
|
246
264
|
function dropWrap(w) {
|
|
247
265
|
try {
|
|
248
|
-
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
249
|
-
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
250
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
251
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
252
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
253
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 (_) {}
|
|
254
275
|
w.remove();
|
|
255
276
|
} catch (_) {}
|
|
256
277
|
}
|
|
@@ -258,42 +279,46 @@
|
|
|
258
279
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
259
280
|
|
|
260
281
|
/**
|
|
261
|
-
* Supprime les wraps
|
|
282
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
262
283
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* -
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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é).
|
|
269
291
|
*/
|
|
270
292
|
function pruneOrphans(klass) {
|
|
271
293
|
const meta = KIND[klass];
|
|
272
294
|
if (!meta) return;
|
|
273
295
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
300
|
|
|
278
301
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
279
|
-
const sid = key.slice(klass.length + 1);
|
|
280
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
302
|
+
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
303
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
281
304
|
|
|
282
|
-
const
|
|
283
|
-
|
|
305
|
+
const anchorEl = document.querySelector(
|
|
306
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
+
);
|
|
284
308
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
285
|
-
}
|
|
309
|
+
});
|
|
286
310
|
}
|
|
287
311
|
|
|
288
312
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
289
313
|
|
|
290
314
|
/**
|
|
291
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
292
|
-
* Priorité : filled > en grâce
|
|
293
|
-
*
|
|
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.
|
|
294
318
|
*/
|
|
295
319
|
function decluster(klass) {
|
|
296
320
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
297
322
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
298
323
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
299
324
|
|
|
@@ -302,11 +327,10 @@
|
|
|
302
327
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
303
328
|
|
|
304
329
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
305
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
330
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
306
331
|
|
|
307
332
|
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
308
333
|
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
309
|
-
// les deux remplis → on ne touche pas
|
|
310
334
|
break;
|
|
311
335
|
}
|
|
312
336
|
}
|
|
@@ -316,22 +340,23 @@
|
|
|
316
340
|
|
|
317
341
|
/**
|
|
318
342
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
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).
|
|
321
345
|
*/
|
|
322
346
|
function ordinal(klass, el) {
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 (_) {}
|
|
335
360
|
return 0;
|
|
336
361
|
}
|
|
337
362
|
|
|
@@ -340,18 +365,20 @@
|
|
|
340
365
|
let inserted = 0;
|
|
341
366
|
|
|
342
367
|
for (const el of items) {
|
|
343
|
-
if (inserted >=
|
|
368
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
344
369
|
if (!el?.isConnected) continue;
|
|
345
370
|
|
|
346
|
-
const ord
|
|
347
|
-
|
|
371
|
+
const ord = ordinal(klass, el);
|
|
372
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
373
|
+
if (!isTarget) continue;
|
|
374
|
+
|
|
348
375
|
if (adjacentWrap(el)) continue;
|
|
349
376
|
|
|
350
|
-
const key =
|
|
351
|
-
if (findWrap(key)) continue;
|
|
377
|
+
const key = makeAnchorKey(klass, el);
|
|
378
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
352
379
|
|
|
353
380
|
const id = pickId(poolKey);
|
|
354
|
-
if (!id) continue;
|
|
381
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
355
382
|
|
|
356
383
|
const w = insertAfter(el, id, klass, key);
|
|
357
384
|
if (w) { observePh(id); inserted++; }
|
|
@@ -363,6 +390,7 @@
|
|
|
363
390
|
|
|
364
391
|
function getIO() {
|
|
365
392
|
if (S.io) return S.io;
|
|
393
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
366
394
|
try {
|
|
367
395
|
S.io = new IntersectionObserver(entries => {
|
|
368
396
|
for (const e of entries) {
|
|
@@ -371,7 +399,7 @@
|
|
|
371
399
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
372
400
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
373
401
|
}
|
|
374
|
-
}, { root: null, rootMargin:
|
|
402
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
375
403
|
} catch (_) { S.io = null; }
|
|
376
404
|
return S.io;
|
|
377
405
|
}
|
|
@@ -422,6 +450,7 @@
|
|
|
422
450
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
423
451
|
S.lastShow.set(id, t);
|
|
424
452
|
|
|
453
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
425
454
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
426
455
|
|
|
427
456
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -442,6 +471,7 @@
|
|
|
442
471
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
443
472
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
444
473
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
+
// Un show plus récent → ne pas toucher
|
|
445
475
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
446
476
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
447
477
|
} catch (_) {}
|
|
@@ -460,7 +490,7 @@
|
|
|
460
490
|
const orig = ez.showAds.bind(ez);
|
|
461
491
|
ez.showAds = function (...args) {
|
|
462
492
|
if (isBlocked()) return;
|
|
463
|
-
const ids
|
|
493
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
464
494
|
const seen = new Set();
|
|
465
495
|
for (const v of ids) {
|
|
466
496
|
const id = parseInt(v, 10);
|
|
@@ -479,7 +509,7 @@
|
|
|
479
509
|
}
|
|
480
510
|
}
|
|
481
511
|
|
|
482
|
-
// ── Core
|
|
512
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
483
513
|
|
|
484
514
|
async function runCore() {
|
|
485
515
|
if (isBlocked()) return 0;
|
|
@@ -494,9 +524,10 @@
|
|
|
494
524
|
|
|
495
525
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
496
526
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
+
const items = getItems();
|
|
497
528
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
498
529
|
pruneOrphans(klass);
|
|
499
|
-
const n = injectBetween(klass,
|
|
530
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
500
531
|
if (n) decluster(klass);
|
|
501
532
|
return n;
|
|
502
533
|
};
|
|
@@ -509,13 +540,14 @@
|
|
|
509
540
|
'ezoic-ad-between', getTopics,
|
|
510
541
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
511
542
|
);
|
|
512
|
-
return exec(
|
|
543
|
+
if (kind === 'categories') return exec(
|
|
513
544
|
'ezoic-ad-categories', getCategories,
|
|
514
545
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
515
546
|
);
|
|
547
|
+
return 0;
|
|
516
548
|
}
|
|
517
549
|
|
|
518
|
-
// ── Scheduler
|
|
550
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
519
551
|
|
|
520
552
|
function scheduleRun(cb) {
|
|
521
553
|
if (S.runQueued) return;
|
|
@@ -525,7 +557,7 @@
|
|
|
525
557
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
526
558
|
let n = 0;
|
|
527
559
|
try { n = await runCore(); } catch (_) {}
|
|
528
|
-
cb?.(n);
|
|
560
|
+
try { cb?.(n); } catch (_) {}
|
|
529
561
|
});
|
|
530
562
|
}
|
|
531
563
|
|
|
@@ -533,8 +565,10 @@
|
|
|
533
565
|
if (isBlocked()) return;
|
|
534
566
|
const t = ts();
|
|
535
567
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
536
|
-
S.lastBurstTs
|
|
537
|
-
|
|
568
|
+
S.lastBurstTs = t;
|
|
569
|
+
|
|
570
|
+
const pk = pageKey();
|
|
571
|
+
S.pageKey = pk;
|
|
538
572
|
S.burstDeadline = t + 2000;
|
|
539
573
|
|
|
540
574
|
if (S.burstActive) return;
|
|
@@ -542,7 +576,7 @@
|
|
|
542
576
|
S.burstCount = 0;
|
|
543
577
|
|
|
544
578
|
const step = () => {
|
|
545
|
-
if (pageKey() !==
|
|
579
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
546
580
|
S.burstActive = false; return;
|
|
547
581
|
}
|
|
548
582
|
S.burstCount++;
|
|
@@ -554,7 +588,7 @@
|
|
|
554
588
|
step();
|
|
555
589
|
}
|
|
556
590
|
|
|
557
|
-
// ── Cleanup navigation
|
|
591
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
558
592
|
|
|
559
593
|
function cleanup() {
|
|
560
594
|
blockedUntil = ts() + 1500;
|
|
@@ -571,17 +605,19 @@
|
|
|
571
605
|
S.runQueued = false;
|
|
572
606
|
}
|
|
573
607
|
|
|
574
|
-
// ──
|
|
608
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
575
609
|
|
|
576
610
|
function ensureDomObserver() {
|
|
577
611
|
if (S.domObs) return;
|
|
578
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
579
612
|
S.domObs = new MutationObserver(muts => {
|
|
580
613
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
581
614
|
for (const m of muts) {
|
|
615
|
+
if (!m.addedNodes?.length) continue;
|
|
582
616
|
for (const n of m.addedNodes) {
|
|
583
617
|
if (n.nodeType !== 1) continue;
|
|
584
|
-
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)) {
|
|
585
621
|
requestBurst(); return;
|
|
586
622
|
}
|
|
587
623
|
}
|
|
@@ -607,21 +643,29 @@
|
|
|
607
643
|
}
|
|
608
644
|
|
|
609
645
|
function ensureTcfLocator() {
|
|
610
|
-
//
|
|
611
|
-
// En navigation ajaxify, NodeBB peut
|
|
612
|
-
//
|
|
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.
|
|
613
652
|
try {
|
|
614
653
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
+
|
|
615
655
|
const inject = () => {
|
|
616
656
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
617
657
|
const f = document.createElement('iframe');
|
|
618
658
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
619
659
|
(document.body || document.documentElement).appendChild(f);
|
|
620
660
|
};
|
|
661
|
+
|
|
621
662
|
inject();
|
|
663
|
+
|
|
664
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
622
665
|
if (!window.__nbbTcfObs) {
|
|
623
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
666
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
667
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
+
{ childList: true, subtree: true });
|
|
625
669
|
}
|
|
626
670
|
} catch (_) {}
|
|
627
671
|
}
|
|
@@ -631,10 +675,10 @@
|
|
|
631
675
|
const head = document.head;
|
|
632
676
|
if (!head) return;
|
|
633
677
|
for (const [rel, href, cors] of [
|
|
634
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
635
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
636
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
637
|
-
['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],
|
|
638
682
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
639
683
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
640
684
|
]) {
|
|
@@ -648,7 +692,7 @@
|
|
|
648
692
|
}
|
|
649
693
|
}
|
|
650
694
|
|
|
651
|
-
// ── Bindings
|
|
695
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
652
696
|
|
|
653
697
|
function bindNodeBB() {
|
|
654
698
|
const $ = window.jQuery;
|
|
@@ -659,16 +703,19 @@
|
|
|
659
703
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
660
704
|
S.pageKey = pageKey();
|
|
661
705
|
blockedUntil = 0;
|
|
662
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
-
|
|
706
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
707
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
664
708
|
});
|
|
665
709
|
|
|
666
|
-
const
|
|
667
|
-
'action:ajaxify.contentLoaded',
|
|
710
|
+
const BURST_EVENTS = [
|
|
711
|
+
'action:ajaxify.contentLoaded',
|
|
712
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
668
713
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
669
714
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
671
715
|
|
|
716
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
+
|
|
718
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
672
719
|
try {
|
|
673
720
|
require(['hooks'], hooks => {
|
|
674
721
|
if (typeof hooks?.on !== 'function') return;
|