nodebb-plugin-ezoic-infinite 1.7.10 → 1.7.12
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 +150 -194
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,57 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v23
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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é)
|
|
4
|
+
* v20 Table KIND : anchorAttr par kindClass. Fix catégories (data-cid).
|
|
5
|
+
* IO fixe une seule instance. Burst cooldown 200ms.
|
|
6
|
+
* v22 Fix ordinal fallback posts (baseTag vide cassait :scope>). isFilled guard pruneOrphans.
|
|
7
|
+
* v23 Fix "pubs écrasées" : INJECT_GRACE_MS protège les wraps non encore fills.
|
|
8
|
+
* decluster : ne supprime jamais un wrap filled, grace basée sur A_CREATED aussi.
|
|
9
|
+
* KIND table : baseTag explicite (évite le split fragile).
|
|
10
|
+
* wrapByKey Map O(1) pour findWrap. poolsReady (initPools une fois/page).
|
|
11
|
+
* patchShowAds sorti du hot path runCore. wrapByKey sync sur dropWrap/cleanup.
|
|
31
12
|
*/
|
|
32
13
|
(function () {
|
|
33
14
|
'use strict';
|
|
34
15
|
|
|
35
16
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
17
|
|
|
37
|
-
const WRAP_CLASS
|
|
38
|
-
const PH_PREFIX
|
|
39
|
-
const A_ANCHOR
|
|
40
|
-
const A_WRAPID
|
|
41
|
-
const A_CREATED
|
|
42
|
-
const A_SHOWN
|
|
43
|
-
|
|
44
|
-
const MIN_PRUNE_AGE_MS
|
|
45
|
-
const
|
|
46
|
-
const
|
|
18
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
19
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
20
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
21
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
22
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
23
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
24
|
+
|
|
25
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
26
|
+
const INJECT_GRACE_MS = 30_000; // fenêtre post-injection : wrap non supprimable (fill async en cours)
|
|
27
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds pour decluster
|
|
28
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
47
29
|
const MAX_INSERTS_PER_RUN = 6;
|
|
48
30
|
const MAX_INFLIGHT = 4;
|
|
49
31
|
const SHOW_THROTTLE_MS = 900;
|
|
50
32
|
const BURST_COOLDOWN_MS = 200;
|
|
51
33
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
34
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
35
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
36
|
|
|
56
37
|
const SEL = {
|
|
57
38
|
post: '[component="post"][data-pid]',
|
|
@@ -60,50 +41,53 @@
|
|
|
60
41
|
};
|
|
61
42
|
|
|
62
43
|
/**
|
|
63
|
-
* Table
|
|
44
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
45
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
46
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
47
|
+
* data-pid posts (id message, immuable)
|
|
48
|
+
* data-index topics (index dans la liste)
|
|
49
|
+
* data-cid catégories (id catégorie, immuable)
|
|
50
|
+
* baseTag : préfixe tag pour le querySelector d'ancre dans pruneOrphans.
|
|
51
|
+
* Vide pour posts (sélecteur sans tag). Explicite pour éviter le split fragile.
|
|
71
52
|
*/
|
|
72
53
|
const KIND = {
|
|
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' },
|
|
54
|
+
'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid', baseTag: '' },
|
|
55
|
+
'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index', baseTag: 'li' },
|
|
56
|
+
'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid', baseTag: 'li' },
|
|
76
57
|
};
|
|
77
58
|
|
|
78
59
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
60
|
|
|
80
61
|
const S = {
|
|
81
|
-
pageKey:
|
|
82
|
-
cfg:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
pendingSet: new Set(),
|
|
96
|
-
|
|
97
|
-
runQueued: false,
|
|
98
|
-
burstActive: false,
|
|
62
|
+
pageKey: null,
|
|
63
|
+
cfg: null,
|
|
64
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
65
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
66
|
+
mountedIds: new Set(), // IDs Ezoic dans le DOM
|
|
67
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
68
|
+
io: null,
|
|
69
|
+
domObs: null,
|
|
70
|
+
mutGuard: 0,
|
|
71
|
+
inflight: 0,
|
|
72
|
+
pending: [],
|
|
73
|
+
pendingSet: new Set(),
|
|
74
|
+
runQueued: false,
|
|
75
|
+
burstActive: false,
|
|
99
76
|
burstDeadline: 0,
|
|
100
|
-
burstCount:
|
|
101
|
-
lastBurstTs:
|
|
77
|
+
burstCount: 0,
|
|
78
|
+
lastBurstTs: 0,
|
|
102
79
|
};
|
|
103
80
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
81
|
+
// Map anchorKey → wrap Element — lookup O(1) au lieu de querySelector full-DOM
|
|
82
|
+
const wrapByKey = new Map();
|
|
83
|
+
let blockedUntil = 0;
|
|
84
|
+
let poolsReady = false; // initPools une seule fois par page
|
|
85
|
+
|
|
86
|
+
const ts = () => Date.now();
|
|
87
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
88
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
89
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
90
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
91
|
|
|
108
92
|
function mutate(fn) {
|
|
109
93
|
S.mutGuard++;
|
|
@@ -121,12 +105,6 @@
|
|
|
121
105
|
return S.cfg;
|
|
122
106
|
}
|
|
123
107
|
|
|
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
|
-
|
|
130
108
|
function parseIds(raw) {
|
|
131
109
|
const out = [], seen = new Set();
|
|
132
110
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +114,13 @@
|
|
|
136
114
|
return out;
|
|
137
115
|
}
|
|
138
116
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
117
|
+
function initPools(cfg) {
|
|
118
|
+
if (poolsReady) return;
|
|
119
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
120
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
121
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
122
|
+
poolsReady = true;
|
|
123
|
+
}
|
|
145
124
|
|
|
146
125
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
126
|
|
|
@@ -165,13 +144,13 @@
|
|
|
165
144
|
return 'other';
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
// ── DOM
|
|
147
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
148
|
|
|
170
149
|
function getPosts() {
|
|
171
150
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
151
|
if (!el.isConnected) return false;
|
|
173
152
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
153
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
154
|
if (p && p !== el) return false;
|
|
176
155
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
156
|
});
|
|
@@ -187,38 +166,29 @@
|
|
|
187
166
|
);
|
|
188
167
|
}
|
|
189
168
|
|
|
190
|
-
// ── Ancres stables
|
|
169
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
170
|
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
171
|
+
function stableId(klass, el) {
|
|
172
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
173
|
if (attr) {
|
|
200
174
|
const v = el.getAttribute(attr);
|
|
201
175
|
if (v !== null && v !== '') return v;
|
|
202
176
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
177
|
+
let i = 0;
|
|
178
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
179
|
+
if (s === el) return `i${i}`;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
211
182
|
return 'i0';
|
|
212
183
|
}
|
|
213
184
|
|
|
214
185
|
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
186
|
|
|
216
|
-
function findWrap(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} catch (_) { return null; }
|
|
187
|
+
function findWrap(key) {
|
|
188
|
+
const w = wrapByKey.get(key);
|
|
189
|
+
if (w && w.isConnected) return w;
|
|
190
|
+
if (w) wrapByKey.delete(key); // nettoyage lazy si wrap retiré hors de notre contrôle
|
|
191
|
+
return null;
|
|
222
192
|
}
|
|
223
193
|
|
|
224
194
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
@@ -226,7 +196,7 @@
|
|
|
226
196
|
function pickId(poolKey) {
|
|
227
197
|
const pool = S.pools[poolKey];
|
|
228
198
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
199
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
200
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
201
|
const id = pool[i];
|
|
232
202
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -251,13 +221,14 @@
|
|
|
251
221
|
}
|
|
252
222
|
|
|
253
223
|
function insertAfter(el, id, klass, key) {
|
|
254
|
-
if (!el?.insertAdjacentElement)
|
|
255
|
-
if (findWrap(key))
|
|
256
|
-
if (S.mountedIds.has(id))
|
|
224
|
+
if (!el?.insertAdjacentElement) return null;
|
|
225
|
+
if (findWrap(key)) return null;
|
|
226
|
+
if (S.mountedIds.has(id)) return null;
|
|
257
227
|
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
228
|
const w = makeWrap(id, klass, key);
|
|
259
229
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
230
|
S.mountedIds.add(id);
|
|
231
|
+
wrapByKey.set(key, w);
|
|
261
232
|
return w;
|
|
262
233
|
}
|
|
263
234
|
|
|
@@ -265,9 +236,9 @@
|
|
|
265
236
|
try {
|
|
266
237
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
238
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
//
|
|
239
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
240
|
+
if (key) wrapByKey.delete(key);
|
|
241
|
+
// unobserve avant remove — guard instanceof (unobserve(null) corrompt l'IO pubads)
|
|
271
242
|
try {
|
|
272
243
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
244
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
@@ -279,58 +250,63 @@
|
|
|
279
250
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
251
|
|
|
281
252
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
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
|
|
253
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
289
254
|
*
|
|
290
|
-
*
|
|
255
|
+
* Protections :
|
|
256
|
+
* 1. Pas avant MIN_PRUNE_AGE_MS (DOM post-batch pas encore stabilisé)
|
|
257
|
+
* 2. Jamais si filled (pub affichée — SDK Ezoic a des callbacks async dessus)
|
|
258
|
+
* 3. Pas avant INJECT_GRACE_MS depuis création (fill Ezoic peut être très async)
|
|
291
259
|
*/
|
|
292
260
|
function pruneOrphans(klass) {
|
|
293
261
|
const meta = KIND[klass];
|
|
294
262
|
if (!meta) return;
|
|
295
263
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
264
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
265
|
+
const age = ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
266
|
+
if (age < MIN_PRUNE_AGE_MS) continue;
|
|
267
|
+
if (isFilled(w)) continue;
|
|
268
|
+
if (age < INJECT_GRACE_MS) continue;
|
|
300
269
|
|
|
301
270
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
-
const sid = key.slice(klass.length + 1);
|
|
303
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
271
|
+
const sid = key.slice(klass.length + 1);
|
|
272
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
304
273
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
);
|
|
274
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
275
|
+
const anchorEl = document.querySelector(sel);
|
|
308
276
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
|
-
}
|
|
277
|
+
}
|
|
310
278
|
}
|
|
311
279
|
|
|
312
280
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
313
281
|
|
|
314
282
|
/**
|
|
315
|
-
* Deux wraps adjacents
|
|
316
|
-
*
|
|
317
|
-
*
|
|
283
|
+
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
284
|
+
*
|
|
285
|
+
* Règles absolues :
|
|
286
|
+
* - Jamais supprimer un wrap filled
|
|
287
|
+
* - Jamais supprimer un wrap < INJECT_GRACE_MS (fill Ezoic potentiellement en cours)
|
|
288
|
+
* - Jamais supprimer un wrap < FILL_GRACE_MS depuis le dernier showAds
|
|
289
|
+
* - Si les deux wraps adjacents sont vides et hors grâce → supprimer le courant
|
|
318
290
|
*/
|
|
319
291
|
function decluster(klass) {
|
|
320
292
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
-
|
|
293
|
+
if (isFilled(w)) continue;
|
|
294
|
+
const wAge = ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
295
|
+
if (wAge < INJECT_GRACE_MS) continue;
|
|
322
296
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
297
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
298
|
|
|
325
299
|
let prev = w.previousElementSibling, steps = 0;
|
|
326
300
|
while (prev && steps++ < 3) {
|
|
327
301
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
|
-
|
|
302
|
+
if (isFilled(prev)) break;
|
|
303
|
+
const pAge = ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10);
|
|
304
|
+
if (pAge < INJECT_GRACE_MS) break;
|
|
329
305
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
306
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
331
307
|
|
|
332
|
-
|
|
333
|
-
|
|
308
|
+
// Les deux vides et hors grâce → supprimer le courant
|
|
309
|
+
mutate(() => dropWrap(w));
|
|
334
310
|
break;
|
|
335
311
|
}
|
|
336
312
|
}
|
|
@@ -340,23 +316,19 @@
|
|
|
340
316
|
|
|
341
317
|
/**
|
|
342
318
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
319
|
+
* Posts/topics : data-index (NodeBB 4.x).
|
|
320
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
345
321
|
*/
|
|
346
322
|
function ordinal(klass, el) {
|
|
347
323
|
const di = el.getAttribute('data-index');
|
|
348
324
|
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
349
|
-
// Fallback
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
i++;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
} catch (_) {}
|
|
325
|
+
// Fallback — s.matches(fullSel) pour ne pas compter les wraps intercalés
|
|
326
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
327
|
+
let i = 0;
|
|
328
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
329
|
+
if (s === el) return i;
|
|
330
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
331
|
+
}
|
|
360
332
|
return 0;
|
|
361
333
|
}
|
|
362
334
|
|
|
@@ -368,17 +340,16 @@
|
|
|
368
340
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
369
341
|
if (!el?.isConnected) continue;
|
|
370
342
|
|
|
371
|
-
const ord
|
|
372
|
-
|
|
373
|
-
if (!isTarget) continue;
|
|
343
|
+
const ord = ordinal(klass, el);
|
|
344
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
374
345
|
|
|
375
346
|
if (adjacentWrap(el)) continue;
|
|
376
347
|
|
|
377
348
|
const key = makeAnchorKey(klass, el);
|
|
378
|
-
if (findWrap(key)) continue;
|
|
349
|
+
if (findWrap(key)) continue;
|
|
379
350
|
|
|
380
351
|
const id = pickId(poolKey);
|
|
381
|
-
if (!id) continue;
|
|
352
|
+
if (!id) continue;
|
|
382
353
|
|
|
383
354
|
const w = insertAfter(el, id, klass, key);
|
|
384
355
|
if (w) { observePh(id); inserted++; }
|
|
@@ -390,7 +361,6 @@
|
|
|
390
361
|
|
|
391
362
|
function getIO() {
|
|
392
363
|
if (S.io) return S.io;
|
|
393
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
394
364
|
try {
|
|
395
365
|
S.io = new IntersectionObserver(entries => {
|
|
396
366
|
for (const e of entries) {
|
|
@@ -399,7 +369,7 @@
|
|
|
399
369
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
370
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
371
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
372
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
373
|
} catch (_) { S.io = null; }
|
|
404
374
|
return S.io;
|
|
405
375
|
}
|
|
@@ -450,7 +420,6 @@
|
|
|
450
420
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
451
421
|
S.lastShow.set(id, t);
|
|
452
422
|
|
|
453
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
454
423
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
455
424
|
|
|
456
425
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -471,7 +440,6 @@
|
|
|
471
440
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
472
441
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
473
442
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
-
// Un show plus récent → ne pas toucher
|
|
475
443
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
476
444
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
477
445
|
} catch (_) {}
|
|
@@ -490,7 +458,7 @@
|
|
|
490
458
|
const orig = ez.showAds.bind(ez);
|
|
491
459
|
ez.showAds = function (...args) {
|
|
492
460
|
if (isBlocked()) return;
|
|
493
|
-
const ids
|
|
461
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
494
462
|
const seen = new Set();
|
|
495
463
|
for (const v of ids) {
|
|
496
464
|
const id = parseInt(v, 10);
|
|
@@ -509,11 +477,10 @@
|
|
|
509
477
|
}
|
|
510
478
|
}
|
|
511
479
|
|
|
512
|
-
// ── Core
|
|
480
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
513
481
|
|
|
514
482
|
async function runCore() {
|
|
515
483
|
if (isBlocked()) return 0;
|
|
516
|
-
patchShowAds();
|
|
517
484
|
|
|
518
485
|
const cfg = await fetchConfig();
|
|
519
486
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -524,10 +491,9 @@
|
|
|
524
491
|
|
|
525
492
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
526
493
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
-
const items = getItems();
|
|
528
494
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
529
495
|
pruneOrphans(klass);
|
|
530
|
-
const n = injectBetween(klass,
|
|
496
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
531
497
|
if (n) decluster(klass);
|
|
532
498
|
return n;
|
|
533
499
|
};
|
|
@@ -568,7 +534,7 @@
|
|
|
568
534
|
S.lastBurstTs = t;
|
|
569
535
|
|
|
570
536
|
const pk = pageKey();
|
|
571
|
-
S.pageKey
|
|
537
|
+
S.pageKey = pk;
|
|
572
538
|
S.burstDeadline = t + 2000;
|
|
573
539
|
|
|
574
540
|
if (S.burstActive) return;
|
|
@@ -588,11 +554,13 @@
|
|
|
588
554
|
step();
|
|
589
555
|
}
|
|
590
556
|
|
|
591
|
-
// ── Cleanup
|
|
557
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
592
558
|
|
|
593
559
|
function cleanup() {
|
|
594
560
|
blockedUntil = ts() + 1500;
|
|
561
|
+
poolsReady = false;
|
|
595
562
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
563
|
+
wrapByKey.clear();
|
|
596
564
|
S.cfg = null;
|
|
597
565
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
598
566
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -609,15 +577,14 @@
|
|
|
609
577
|
|
|
610
578
|
function ensureDomObserver() {
|
|
611
579
|
if (S.domObs) return;
|
|
580
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
612
581
|
S.domObs = new MutationObserver(muts => {
|
|
613
582
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
614
583
|
for (const m of muts) {
|
|
615
584
|
if (!m.addedNodes?.length) continue;
|
|
616
585
|
for (const n of m.addedNodes) {
|
|
617
586
|
if (n.nodeType !== 1) continue;
|
|
618
|
-
if (n.matches?.(
|
|
619
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
620
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
587
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
621
588
|
requestBurst(); return;
|
|
622
589
|
}
|
|
623
590
|
}
|
|
@@ -643,29 +610,20 @@
|
|
|
643
610
|
}
|
|
644
611
|
|
|
645
612
|
function ensureTcfLocator() {
|
|
646
|
-
//
|
|
647
|
-
//
|
|
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
|
+
// L'iframe __tcfapiLocator route les appels postMessage du CMP.
|
|
614
|
+
// En navigation ajaxify, NodeBB peut la retirer → erreurs CMP.
|
|
652
615
|
try {
|
|
653
616
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
-
|
|
655
617
|
const inject = () => {
|
|
656
618
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
657
619
|
const f = document.createElement('iframe');
|
|
658
620
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
659
621
|
(document.body || document.documentElement).appendChild(f);
|
|
660
622
|
};
|
|
661
|
-
|
|
662
623
|
inject();
|
|
663
|
-
|
|
664
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
665
624
|
if (!window.__nbbTcfObs) {
|
|
666
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
667
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
-
{ childList: true, subtree: true });
|
|
625
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
626
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
669
627
|
}
|
|
670
628
|
} catch (_) {}
|
|
671
629
|
}
|
|
@@ -675,10 +633,10 @@
|
|
|
675
633
|
const head = document.head;
|
|
676
634
|
if (!head) return;
|
|
677
635
|
for (const [rel, href, cors] of [
|
|
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],
|
|
636
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
637
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
638
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
639
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
682
640
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
683
641
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
684
642
|
]) {
|
|
@@ -712,10 +670,8 @@
|
|
|
712
670
|
'action:posts.loaded', 'action:topics.loaded',
|
|
713
671
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
714
672
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
715
|
-
|
|
716
673
|
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
674
|
|
|
718
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
719
675
|
try {
|
|
720
676
|
require(['hooks'], hooks => {
|
|
721
677
|
if (typeof hooks?.on !== 'function') return;
|