nodebb-plugin-ezoic-infinite 1.7.11 → 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 +143 -188
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,62 +250,63 @@
|
|
|
279
250
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
251
|
|
|
281
252
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
253
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
283
254
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
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é).
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
302
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
303
|
-
if (isFilled(w)) return; // jamais supprimer un wrap rempli
|
|
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;
|
|
304
269
|
|
|
305
270
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
306
271
|
const sid = key.slice(klass.length + 1);
|
|
307
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
272
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
308
273
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
);
|
|
274
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
275
|
+
const anchorEl = document.querySelector(sel);
|
|
312
276
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
313
|
-
}
|
|
277
|
+
}
|
|
314
278
|
}
|
|
315
279
|
|
|
316
280
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
317
281
|
|
|
318
282
|
/**
|
|
319
|
-
* Deux wraps adjacents
|
|
320
|
-
*
|
|
321
|
-
*
|
|
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
|
|
322
290
|
*/
|
|
323
291
|
function decluster(klass) {
|
|
324
292
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
325
|
-
|
|
293
|
+
if (isFilled(w)) continue;
|
|
294
|
+
const wAge = ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
295
|
+
if (wAge < INJECT_GRACE_MS) continue;
|
|
326
296
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
327
297
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
328
298
|
|
|
329
299
|
let prev = w.previousElementSibling, steps = 0;
|
|
330
300
|
while (prev && steps++ < 3) {
|
|
331
301
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
332
|
-
|
|
302
|
+
if (isFilled(prev)) break;
|
|
303
|
+
const pAge = ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10);
|
|
304
|
+
if (pAge < INJECT_GRACE_MS) break;
|
|
333
305
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
334
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
306
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
335
307
|
|
|
336
|
-
|
|
337
|
-
|
|
308
|
+
// Les deux vides et hors grâce → supprimer le courant
|
|
309
|
+
mutate(() => dropWrap(w));
|
|
338
310
|
break;
|
|
339
311
|
}
|
|
340
312
|
}
|
|
@@ -344,14 +316,13 @@
|
|
|
344
316
|
|
|
345
317
|
/**
|
|
346
318
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
347
|
-
*
|
|
348
|
-
*
|
|
319
|
+
* Posts/topics : data-index (NodeBB 4.x).
|
|
320
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
349
321
|
*/
|
|
350
322
|
function ordinal(klass, el) {
|
|
351
323
|
const di = el.getAttribute('data-index');
|
|
352
324
|
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
353
|
-
// Fallback
|
|
354
|
-
// baseTag='' (posts) où `:scope > ` sans tag ne fonctionne pas.
|
|
325
|
+
// Fallback — s.matches(fullSel) pour ne pas compter les wraps intercalés
|
|
355
326
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
356
327
|
let i = 0;
|
|
357
328
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -369,17 +340,16 @@
|
|
|
369
340
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
370
341
|
if (!el?.isConnected) continue;
|
|
371
342
|
|
|
372
|
-
const ord
|
|
373
|
-
|
|
374
|
-
if (!isTarget) continue;
|
|
343
|
+
const ord = ordinal(klass, el);
|
|
344
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
375
345
|
|
|
376
346
|
if (adjacentWrap(el)) continue;
|
|
377
347
|
|
|
378
348
|
const key = makeAnchorKey(klass, el);
|
|
379
|
-
if (findWrap(key)) continue;
|
|
349
|
+
if (findWrap(key)) continue;
|
|
380
350
|
|
|
381
351
|
const id = pickId(poolKey);
|
|
382
|
-
if (!id) continue;
|
|
352
|
+
if (!id) continue;
|
|
383
353
|
|
|
384
354
|
const w = insertAfter(el, id, klass, key);
|
|
385
355
|
if (w) { observePh(id); inserted++; }
|
|
@@ -391,7 +361,6 @@
|
|
|
391
361
|
|
|
392
362
|
function getIO() {
|
|
393
363
|
if (S.io) return S.io;
|
|
394
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
395
364
|
try {
|
|
396
365
|
S.io = new IntersectionObserver(entries => {
|
|
397
366
|
for (const e of entries) {
|
|
@@ -400,7 +369,7 @@
|
|
|
400
369
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
401
370
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
402
371
|
}
|
|
403
|
-
}, { root: null, rootMargin:
|
|
372
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
404
373
|
} catch (_) { S.io = null; }
|
|
405
374
|
return S.io;
|
|
406
375
|
}
|
|
@@ -451,7 +420,6 @@
|
|
|
451
420
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
452
421
|
S.lastShow.set(id, t);
|
|
453
422
|
|
|
454
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
455
423
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
456
424
|
|
|
457
425
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -472,7 +440,6 @@
|
|
|
472
440
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
473
441
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
474
442
|
if (!wrap || !ph?.isConnected) return;
|
|
475
|
-
// Un show plus récent → ne pas toucher
|
|
476
443
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
477
444
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
478
445
|
} catch (_) {}
|
|
@@ -491,7 +458,7 @@
|
|
|
491
458
|
const orig = ez.showAds.bind(ez);
|
|
492
459
|
ez.showAds = function (...args) {
|
|
493
460
|
if (isBlocked()) return;
|
|
494
|
-
const ids
|
|
461
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
495
462
|
const seen = new Set();
|
|
496
463
|
for (const v of ids) {
|
|
497
464
|
const id = parseInt(v, 10);
|
|
@@ -510,11 +477,10 @@
|
|
|
510
477
|
}
|
|
511
478
|
}
|
|
512
479
|
|
|
513
|
-
// ── Core
|
|
480
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
514
481
|
|
|
515
482
|
async function runCore() {
|
|
516
483
|
if (isBlocked()) return 0;
|
|
517
|
-
patchShowAds();
|
|
518
484
|
|
|
519
485
|
const cfg = await fetchConfig();
|
|
520
486
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -525,10 +491,9 @@
|
|
|
525
491
|
|
|
526
492
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
527
493
|
if (!normBool(cfgEnable)) return 0;
|
|
528
|
-
const items = getItems();
|
|
529
494
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
530
495
|
pruneOrphans(klass);
|
|
531
|
-
const n = injectBetween(klass,
|
|
496
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
532
497
|
if (n) decluster(klass);
|
|
533
498
|
return n;
|
|
534
499
|
};
|
|
@@ -569,7 +534,7 @@
|
|
|
569
534
|
S.lastBurstTs = t;
|
|
570
535
|
|
|
571
536
|
const pk = pageKey();
|
|
572
|
-
S.pageKey
|
|
537
|
+
S.pageKey = pk;
|
|
573
538
|
S.burstDeadline = t + 2000;
|
|
574
539
|
|
|
575
540
|
if (S.burstActive) return;
|
|
@@ -589,11 +554,13 @@
|
|
|
589
554
|
step();
|
|
590
555
|
}
|
|
591
556
|
|
|
592
|
-
// ── Cleanup
|
|
557
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
593
558
|
|
|
594
559
|
function cleanup() {
|
|
595
560
|
blockedUntil = ts() + 1500;
|
|
561
|
+
poolsReady = false;
|
|
596
562
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
563
|
+
wrapByKey.clear();
|
|
597
564
|
S.cfg = null;
|
|
598
565
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
599
566
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -610,15 +577,14 @@
|
|
|
610
577
|
|
|
611
578
|
function ensureDomObserver() {
|
|
612
579
|
if (S.domObs) return;
|
|
580
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
613
581
|
S.domObs = new MutationObserver(muts => {
|
|
614
582
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
615
583
|
for (const m of muts) {
|
|
616
584
|
if (!m.addedNodes?.length) continue;
|
|
617
585
|
for (const n of m.addedNodes) {
|
|
618
586
|
if (n.nodeType !== 1) continue;
|
|
619
|
-
if (n.matches?.(
|
|
620
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
621
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
587
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
622
588
|
requestBurst(); return;
|
|
623
589
|
}
|
|
624
590
|
}
|
|
@@ -644,29 +610,20 @@
|
|
|
644
610
|
}
|
|
645
611
|
|
|
646
612
|
function ensureTcfLocator() {
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
650
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
651
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
652
|
-
// 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.
|
|
653
615
|
try {
|
|
654
616
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
655
|
-
|
|
656
617
|
const inject = () => {
|
|
657
618
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
658
619
|
const f = document.createElement('iframe');
|
|
659
620
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
660
621
|
(document.body || document.documentElement).appendChild(f);
|
|
661
622
|
};
|
|
662
|
-
|
|
663
623
|
inject();
|
|
664
|
-
|
|
665
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
666
624
|
if (!window.__nbbTcfObs) {
|
|
667
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
668
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
669
|
-
{ childList: true, subtree: true });
|
|
625
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
626
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
670
627
|
}
|
|
671
628
|
} catch (_) {}
|
|
672
629
|
}
|
|
@@ -676,10 +633,10 @@
|
|
|
676
633
|
const head = document.head;
|
|
677
634
|
if (!head) return;
|
|
678
635
|
for (const [rel, href, cors] of [
|
|
679
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
680
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
681
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
682
|
-
['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 ],
|
|
683
640
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
684
641
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
685
642
|
]) {
|
|
@@ -713,10 +670,8 @@
|
|
|
713
670
|
'action:posts.loaded', 'action:topics.loaded',
|
|
714
671
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
715
672
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
716
|
-
|
|
717
673
|
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
718
674
|
|
|
719
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
720
675
|
try {
|
|
721
676
|
require(['hooks'], hooks => {
|
|
722
677
|
if (typeof hooks?.on !== 'function') return;
|