nodebb-plugin-ezoic-infinite 1.7.4 → 1.7.5
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 +172 -274
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v21.1
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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).
|
|
4
|
+
* Historique des corrections majeures
|
|
5
|
+
* ────────────────────────────────────
|
|
6
|
+
* v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
|
|
7
|
+
* Suppression du recyclage de wraps (moveWrapAfter). Cleanup complet navigation.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
|
|
10
|
+
* la position dans le batch courant.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* v20 Table KIND : anchorAttr/ordinalAttr/baseTag explicites par kindClass.
|
|
13
|
+
* Fix fatal catégories : data-cid au lieu de data-index inexistant.
|
|
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.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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é)
|
|
19
|
+
* v21 Suppression de toute la logique wyvern.js (pause/destroy avant remove) :
|
|
20
|
+
* les erreurs wyvern viennent du SDK Ezoic lui-même lors de ses propres
|
|
21
|
+
* refreshes internes, pas de nos suppressions. Nos wraps filled ne sont
|
|
22
|
+
* de toute façon jamais supprimés (règle pruneOrphans/decluster).
|
|
23
|
+
* Refactorisation finale prod-ready : code unifié, zéro duplication,
|
|
24
|
+
* commentaires essentiels uniquement.
|
|
31
25
|
*/
|
|
32
26
|
(function () {
|
|
33
27
|
'use strict';
|
|
34
28
|
|
|
35
29
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
30
|
|
|
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 FILL_GRACE_MS
|
|
46
|
-
const EMPTY_CHECK_MS
|
|
47
|
-
const
|
|
48
|
-
const MAX_INFLIGHT
|
|
49
|
-
const SHOW_THROTTLE_MS
|
|
50
|
-
const BURST_COOLDOWN_MS
|
|
51
|
-
|
|
52
|
-
//
|
|
31
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
32
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
33
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
34
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
35
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
36
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
37
|
+
|
|
38
|
+
const MIN_PRUNE_AGE_MS = 8_000; // garde-fou post-batch NodeBB
|
|
39
|
+
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
40
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
41
|
+
const MAX_INSERTS_RUN = 6;
|
|
42
|
+
const MAX_INFLIGHT = 4;
|
|
43
|
+
const SHOW_THROTTLE_MS = 900;
|
|
44
|
+
const BURST_COOLDOWN_MS = 200;
|
|
45
|
+
|
|
46
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
47
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
48
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
49
|
|
|
@@ -60,20 +54,18 @@
|
|
|
60
54
|
};
|
|
61
55
|
|
|
62
56
|
/**
|
|
63
|
-
* Table
|
|
57
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
58
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* Pour posts ET topics : data-index (0-based, toujours présent).
|
|
76
|
-
* Pour catégories : pas d'infinite scroll → fallback positionnel.
|
|
59
|
+
* sel : sélecteur CSS complet
|
|
60
|
+
* baseTag : préfixe tag pour les querySelector de recherche d'ancre
|
|
61
|
+
* (vide pour posts car leur sélecteur commence par '[')
|
|
62
|
+
* anchorAttr : attribut DOM STABLE → clé unique du wrap, permanent
|
|
63
|
+
* data-pid posts (id message, immuable)
|
|
64
|
+
* data-index topics (index dans la liste)
|
|
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)
|
|
77
69
|
*/
|
|
78
70
|
const KIND = {
|
|
79
71
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -84,22 +76,18 @@
|
|
|
84
76
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
85
77
|
|
|
86
78
|
const S = {
|
|
87
|
-
pageKey:
|
|
88
|
-
cfg:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
inflight: 0,
|
|
100
|
-
pending: [],
|
|
101
|
-
pendingSet: new Set(),
|
|
102
|
-
|
|
79
|
+
pageKey: null,
|
|
80
|
+
cfg: null,
|
|
81
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
82
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
83
|
+
mountedIds: new Set(), // IDs Ezoic montés dans le DOM
|
|
84
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
85
|
+
io: null,
|
|
86
|
+
domObs: null,
|
|
87
|
+
mutGuard: 0,
|
|
88
|
+
inflight: 0,
|
|
89
|
+
pending: [],
|
|
90
|
+
pendingSet: new Set(),
|
|
103
91
|
runQueued: false,
|
|
104
92
|
burstActive: false,
|
|
105
93
|
burstDeadline: 0,
|
|
@@ -108,8 +96,12 @@
|
|
|
108
96
|
};
|
|
109
97
|
|
|
110
98
|
let blockedUntil = 0;
|
|
111
|
-
|
|
112
|
-
const ts
|
|
99
|
+
let poolsReady = false; // true dès que les pools sont initialisés pour la page courante
|
|
100
|
+
const ts = () => Date.now();
|
|
101
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
102
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
103
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
104
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
113
105
|
|
|
114
106
|
function mutate(fn) {
|
|
115
107
|
S.mutGuard++;
|
|
@@ -127,12 +119,6 @@
|
|
|
127
119
|
return S.cfg;
|
|
128
120
|
}
|
|
129
121
|
|
|
130
|
-
function initPools(cfg) {
|
|
131
|
-
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
132
|
-
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
133
|
-
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
122
|
function parseIds(raw) {
|
|
137
123
|
const out = [], seen = new Set();
|
|
138
124
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -142,12 +128,13 @@
|
|
|
142
128
|
return out;
|
|
143
129
|
}
|
|
144
130
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
131
|
+
function initPools(cfg) {
|
|
132
|
+
if (poolsReady) return;
|
|
133
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
134
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
135
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
136
|
+
poolsReady = true;
|
|
137
|
+
}
|
|
151
138
|
|
|
152
139
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
153
140
|
|
|
@@ -171,13 +158,13 @@
|
|
|
171
158
|
return 'other';
|
|
172
159
|
}
|
|
173
160
|
|
|
174
|
-
// ── DOM
|
|
161
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
175
162
|
|
|
176
163
|
function getPosts() {
|
|
177
164
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
178
165
|
if (!el.isConnected) return false;
|
|
179
166
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
180
|
-
const p = el.parentElement?.closest(
|
|
167
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
181
168
|
if (p && p !== el) return false;
|
|
182
169
|
return el.getAttribute('component') !== 'post/parent';
|
|
183
170
|
});
|
|
@@ -193,46 +180,41 @@
|
|
|
193
180
|
);
|
|
194
181
|
}
|
|
195
182
|
|
|
196
|
-
// ── Ancres stables
|
|
183
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
197
184
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
function stableId(kindClass, el) {
|
|
204
|
-
const attr = KIND[kindClass]?.anchorAttr;
|
|
185
|
+
// Map anchorKey → wrap Element — évite un querySelector full-DOM à chaque injection
|
|
186
|
+
const wrapByKey = new Map();
|
|
187
|
+
|
|
188
|
+
function stableId(klass, el) {
|
|
189
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
205
190
|
if (attr) {
|
|
206
191
|
const v = el.getAttribute(attr);
|
|
207
192
|
if (v !== null && v !== '') return v;
|
|
208
193
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
i++;
|
|
215
|
-
}
|
|
216
|
-
} catch (_) {}
|
|
194
|
+
let i = 0;
|
|
195
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
196
|
+
if (s === el) return `i${i}`;
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
217
199
|
return 'i0';
|
|
218
200
|
}
|
|
219
201
|
|
|
220
|
-
const
|
|
202
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
221
203
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
204
|
+
const findWrap = (key) => {
|
|
205
|
+
const w = wrapByKey.get(key);
|
|
206
|
+
// Vérifier que le wrap est toujours dans le DOM (il peut avoir été dropWrap'd)
|
|
207
|
+
if (w && w.isConnected) return w;
|
|
208
|
+
if (w) wrapByKey.delete(key); // nettoyage lazy
|
|
209
|
+
return null;
|
|
210
|
+
};
|
|
229
211
|
|
|
230
212
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
231
213
|
|
|
232
214
|
function pickId(poolKey) {
|
|
233
215
|
const pool = S.pools[poolKey];
|
|
234
216
|
for (let t = 0; t < pool.length; t++) {
|
|
235
|
-
const i
|
|
217
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
236
218
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
237
219
|
const id = pool[i];
|
|
238
220
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -243,7 +225,7 @@
|
|
|
243
225
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
244
226
|
|
|
245
227
|
function makeWrap(id, klass, key) {
|
|
246
|
-
const w
|
|
228
|
+
const w = document.createElement('div');
|
|
247
229
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
248
230
|
w.setAttribute(A_ANCHOR, key);
|
|
249
231
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -257,55 +239,25 @@
|
|
|
257
239
|
}
|
|
258
240
|
|
|
259
241
|
function insertAfter(el, id, klass, key) {
|
|
260
|
-
if (!el?.insertAdjacentElement)
|
|
261
|
-
if (findWrap(key))
|
|
262
|
-
if (S.mountedIds.has(id))
|
|
263
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected)
|
|
242
|
+
if (!el?.insertAdjacentElement) return null;
|
|
243
|
+
if (findWrap(key)) return null;
|
|
244
|
+
if (S.mountedIds.has(id)) return null;
|
|
245
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
264
246
|
const w = makeWrap(id, klass, key);
|
|
265
247
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
266
248
|
S.mountedIds.add(id);
|
|
249
|
+
wrapByKey.set(key, w);
|
|
267
250
|
return w;
|
|
268
251
|
}
|
|
269
252
|
|
|
270
|
-
/**
|
|
271
|
-
* Retire proprement un wrap du DOM.
|
|
272
|
-
*
|
|
273
|
-
* Sans précaution, retirer un wrap contenant un player vidéo Ezoic (wyvern.js)
|
|
274
|
-
* déclenche des erreurs async sur des nœuds détachés :
|
|
275
|
-
* "Cannot read properties of null (reading 'paused')"
|
|
276
|
-
* "Cannot read properties of null (reading 'offsetWidth')"
|
|
277
|
-
* "Invalid target for null#trigger / null#on"
|
|
278
|
-
*
|
|
279
|
-
* On pause les media et on tente de notifier l'API wyvern avant remove().
|
|
280
|
-
*/
|
|
281
253
|
function dropWrap(w) {
|
|
282
254
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
w.querySelectorAll('video, audio').forEach(m => {
|
|
286
|
-
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
287
|
-
});
|
|
288
|
-
} catch (_) {}
|
|
289
|
-
|
|
290
|
-
// 2. Notifier l'API wyvern si disponible
|
|
291
|
-
try {
|
|
292
|
-
if (window.wyvern && typeof window.wyvern === 'object') {
|
|
293
|
-
w.querySelectorAll('[id^="ezoic-"],[class*="wyvern"],[class*="ezoic-video"]')
|
|
294
|
-
.forEach(n => { try { window.wyvern.destroy?.(n); } catch (_) {} });
|
|
295
|
-
}
|
|
296
|
-
} catch (_) {}
|
|
297
|
-
|
|
298
|
-
// 3. Unobserve (guard instanceof Element — unobserve(null) corrompt l'IO)
|
|
299
|
-
try {
|
|
300
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
301
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
302
|
-
} catch (_) {}
|
|
303
|
-
|
|
304
|
-
// 4. Libérer l'id du pool
|
|
255
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
256
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
305
257
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
306
258
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
307
|
-
|
|
308
|
-
|
|
259
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
260
|
+
if (key) wrapByKey.delete(key);
|
|
309
261
|
w.remove();
|
|
310
262
|
} catch (_) {}
|
|
311
263
|
}
|
|
@@ -313,45 +265,39 @@
|
|
|
313
265
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
314
266
|
|
|
315
267
|
/**
|
|
316
|
-
* Supprime les wraps VIDES dont l'
|
|
268
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
317
269
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* ezoic-ad-categories → li[data-cid="7"]
|
|
270
|
+
* On ne supprime JAMAIS un wrap rempli (filled) :
|
|
271
|
+
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
272
|
+
* virtualisation NodeBB — l'ancre reviendra.
|
|
273
|
+
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
274
|
+
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
275
|
+
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
325
276
|
*/
|
|
326
277
|
function pruneOrphans(klass) {
|
|
327
278
|
const meta = KIND[klass];
|
|
328
279
|
if (!meta) return;
|
|
329
280
|
|
|
330
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)
|
|
331
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS)
|
|
332
|
-
if (isFilled(w))
|
|
281
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
282
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) continue;
|
|
283
|
+
if (isFilled(w)) continue;
|
|
333
284
|
|
|
334
285
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
335
286
|
const sid = key.slice(klass.length + 1);
|
|
336
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
287
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
337
288
|
|
|
338
|
-
|
|
339
|
-
// baseTag='' pour posts → sélecteur : [data-pid="123"] (correct, sans tag ambigu)
|
|
340
|
-
// baseTag='li' pour topics/categories → li[data-index="5"], li[data-cid="7"]
|
|
341
|
-
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
289
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
342
290
|
const anchorEl = document.querySelector(sel);
|
|
343
291
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
344
|
-
}
|
|
292
|
+
}
|
|
345
293
|
}
|
|
346
294
|
|
|
347
295
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
348
296
|
|
|
349
297
|
/**
|
|
350
|
-
* Deux wraps adjacents
|
|
351
|
-
* Priorité : filled > en grâce
|
|
352
|
-
*
|
|
353
|
-
* Règle de sécurité wyvern : on ne supprime JAMAIS un wrap rempli.
|
|
354
|
-
* Si les deux wraps adjacents sont remplis, on laisse en place (edge case rare).
|
|
298
|
+
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
299
|
+
* Priorité : filled > en grâce de fill > vide.
|
|
300
|
+
* Jamais de suppression d'un wrap rempli (même raison que pruneOrphans).
|
|
355
301
|
*/
|
|
356
302
|
function decluster(klass) {
|
|
357
303
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
@@ -365,14 +311,9 @@
|
|
|
365
311
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
366
312
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
367
313
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Ne jamais retirer un wrap rempli (player actif potentiel)
|
|
372
|
-
if (!wFilled && !pFilled) mutate(() => dropWrap(w)); // les deux vides → retirer le courant
|
|
373
|
-
else if (!wFilled && pFilled) mutate(() => dropWrap(w)); // courant vide, précédent rempli → retirer le courant
|
|
374
|
-
else if ( wFilled && !pFilled) mutate(() => dropWrap(prev)); // courant rempli, précédent vide → retirer le précédent
|
|
375
|
-
// les deux remplis → rien (on ne touche pas)
|
|
314
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
315
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
316
|
+
// les deux remplis → on ne touche pas
|
|
376
317
|
break;
|
|
377
318
|
}
|
|
378
319
|
}
|
|
@@ -381,35 +322,23 @@
|
|
|
381
322
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
382
323
|
|
|
383
324
|
/**
|
|
384
|
-
* Ordinal 0-based
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* null pour catégories). Si absent, fallback positionnel dans le parent.
|
|
388
|
-
*
|
|
389
|
-
* Pour les posts : KIND.baseTag='' donc le fallback itère sur les enfants directs
|
|
390
|
-
* du parent en filtrant par sélecteur complet (pas juste le tag).
|
|
325
|
+
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
326
|
+
* Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
|
|
327
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
391
328
|
*/
|
|
392
329
|
function ordinal(klass, el) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (meta?.ordinalAttr) {
|
|
397
|
-
const v = el.getAttribute(meta.ordinalAttr);
|
|
330
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
331
|
+
if (attr) {
|
|
332
|
+
const v = el.getAttribute(attr);
|
|
398
333
|
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
399
334
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (s === el) return i;
|
|
408
|
-
// Compter uniquement les éléments du même type (pas les wraps ou autres)
|
|
409
|
-
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
410
|
-
}
|
|
411
|
-
} catch (_) {}
|
|
412
|
-
|
|
335
|
+
// Fallback positionnel — compte uniquement les éléments du même type
|
|
336
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
337
|
+
let i = 0;
|
|
338
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
339
|
+
if (s === el) return i;
|
|
340
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
341
|
+
}
|
|
413
342
|
return 0;
|
|
414
343
|
}
|
|
415
344
|
|
|
@@ -418,20 +347,18 @@
|
|
|
418
347
|
let inserted = 0;
|
|
419
348
|
|
|
420
349
|
for (const el of items) {
|
|
421
|
-
if (inserted >=
|
|
350
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
422
351
|
if (!el?.isConnected) continue;
|
|
423
352
|
|
|
424
|
-
const ord
|
|
425
|
-
|
|
426
|
-
if (!isTarget) continue;
|
|
427
|
-
|
|
353
|
+
const ord = ordinal(klass, el);
|
|
354
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
428
355
|
if (adjacentWrap(el)) continue;
|
|
429
356
|
|
|
430
|
-
const key =
|
|
431
|
-
if (findWrap(key)) continue;
|
|
357
|
+
const key = anchorKey(klass, el);
|
|
358
|
+
if (findWrap(key)) continue;
|
|
432
359
|
|
|
433
360
|
const id = pickId(poolKey);
|
|
434
|
-
if (!id) continue;
|
|
361
|
+
if (!id) continue;
|
|
435
362
|
|
|
436
363
|
const w = insertAfter(el, id, klass, key);
|
|
437
364
|
if (w) { observePh(id); inserted++; }
|
|
@@ -443,7 +370,6 @@
|
|
|
443
370
|
|
|
444
371
|
function getIO() {
|
|
445
372
|
if (S.io) return S.io;
|
|
446
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
447
373
|
try {
|
|
448
374
|
S.io = new IntersectionObserver(entries => {
|
|
449
375
|
for (const e of entries) {
|
|
@@ -452,7 +378,7 @@
|
|
|
452
378
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
453
379
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
454
380
|
}
|
|
455
|
-
}, { root: null, rootMargin:
|
|
381
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
456
382
|
} catch (_) { S.io = null; }
|
|
457
383
|
return S.io;
|
|
458
384
|
}
|
|
@@ -503,7 +429,6 @@
|
|
|
503
429
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
504
430
|
S.lastShow.set(id, t);
|
|
505
431
|
|
|
506
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
507
432
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
508
433
|
|
|
509
434
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -524,7 +449,6 @@
|
|
|
524
449
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
525
450
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
526
451
|
if (!wrap || !ph?.isConnected) return;
|
|
527
|
-
// Un show plus récent → ne pas toucher
|
|
528
452
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
529
453
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
530
454
|
} catch (_) {}
|
|
@@ -543,7 +467,7 @@
|
|
|
543
467
|
const orig = ez.showAds.bind(ez);
|
|
544
468
|
ez.showAds = function (...args) {
|
|
545
469
|
if (isBlocked()) return;
|
|
546
|
-
const ids
|
|
470
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
547
471
|
const seen = new Set();
|
|
548
472
|
for (const v of ids) {
|
|
549
473
|
const id = parseInt(v, 10);
|
|
@@ -562,11 +486,10 @@
|
|
|
562
486
|
}
|
|
563
487
|
}
|
|
564
488
|
|
|
565
|
-
// ── Core
|
|
489
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
566
490
|
|
|
567
491
|
async function runCore() {
|
|
568
492
|
if (isBlocked()) return 0;
|
|
569
|
-
patchShowAds();
|
|
570
493
|
|
|
571
494
|
const cfg = await fetchConfig();
|
|
572
495
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -577,10 +500,9 @@
|
|
|
577
500
|
|
|
578
501
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
579
502
|
if (!normBool(cfgEnable)) return 0;
|
|
580
|
-
const items = getItems();
|
|
581
503
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
582
504
|
pruneOrphans(klass);
|
|
583
|
-
const n = injectBetween(klass,
|
|
505
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
584
506
|
if (n) decluster(klass);
|
|
585
507
|
return n;
|
|
586
508
|
};
|
|
@@ -593,14 +515,13 @@
|
|
|
593
515
|
'ezoic-ad-between', getTopics,
|
|
594
516
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
595
517
|
);
|
|
596
|
-
|
|
518
|
+
return exec(
|
|
597
519
|
'ezoic-ad-categories', getCategories,
|
|
598
520
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
599
521
|
);
|
|
600
|
-
return 0;
|
|
601
522
|
}
|
|
602
523
|
|
|
603
|
-
// ── Scheduler
|
|
524
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
604
525
|
|
|
605
526
|
function scheduleRun(cb) {
|
|
606
527
|
if (S.runQueued) return;
|
|
@@ -610,7 +531,7 @@
|
|
|
610
531
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
611
532
|
let n = 0;
|
|
612
533
|
try { n = await runCore(); } catch (_) {}
|
|
613
|
-
|
|
534
|
+
cb?.(n);
|
|
614
535
|
});
|
|
615
536
|
}
|
|
616
537
|
|
|
@@ -618,10 +539,8 @@
|
|
|
618
539
|
if (isBlocked()) return;
|
|
619
540
|
const t = ts();
|
|
620
541
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
621
|
-
S.lastBurstTs
|
|
622
|
-
|
|
623
|
-
const pk = pageKey();
|
|
624
|
-
S.pageKey = pk;
|
|
542
|
+
S.lastBurstTs = t;
|
|
543
|
+
S.pageKey = pageKey();
|
|
625
544
|
S.burstDeadline = t + 2000;
|
|
626
545
|
|
|
627
546
|
if (S.burstActive) return;
|
|
@@ -629,7 +548,7 @@
|
|
|
629
548
|
S.burstCount = 0;
|
|
630
549
|
|
|
631
550
|
const step = () => {
|
|
632
|
-
if (pageKey() !==
|
|
551
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
633
552
|
S.burstActive = false; return;
|
|
634
553
|
}
|
|
635
554
|
S.burstCount++;
|
|
@@ -641,21 +560,13 @@
|
|
|
641
560
|
step();
|
|
642
561
|
}
|
|
643
562
|
|
|
644
|
-
// ── Cleanup
|
|
563
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
645
564
|
|
|
646
565
|
function cleanup() {
|
|
647
566
|
blockedUntil = ts() + 1500;
|
|
648
|
-
|
|
649
|
-
// Pause tous les media dans nos wraps AVANT de les retirer du DOM.
|
|
650
|
-
// Empêche wyvern.js de continuer d'exécuter ses callbacks async sur des
|
|
651
|
-
// nœuds détachés (erreurs "Cannot read 'paused'", "offsetWidth"…).
|
|
652
|
-
try {
|
|
653
|
-
document.querySelectorAll(`.${WRAP_CLASS} video, .${WRAP_CLASS} audio`).forEach(m => {
|
|
654
|
-
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
655
|
-
});
|
|
656
|
-
} catch (_) {}
|
|
657
|
-
|
|
567
|
+
poolsReady = false;
|
|
658
568
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
569
|
+
wrapByKey.clear();
|
|
659
570
|
S.cfg = null;
|
|
660
571
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
661
572
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -668,19 +579,17 @@
|
|
|
668
579
|
S.runQueued = false;
|
|
669
580
|
}
|
|
670
581
|
|
|
671
|
-
// ──
|
|
582
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
672
583
|
|
|
673
584
|
function ensureDomObserver() {
|
|
674
585
|
if (S.domObs) return;
|
|
586
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
675
587
|
S.domObs = new MutationObserver(muts => {
|
|
676
588
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
677
589
|
for (const m of muts) {
|
|
678
|
-
if (!m.addedNodes?.length) continue;
|
|
679
590
|
for (const n of m.addedNodes) {
|
|
680
591
|
if (n.nodeType !== 1) continue;
|
|
681
|
-
if (n.matches?.(
|
|
682
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
683
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
592
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
684
593
|
requestBurst(); return;
|
|
685
594
|
}
|
|
686
595
|
}
|
|
@@ -706,29 +615,21 @@
|
|
|
706
615
|
}
|
|
707
616
|
|
|
708
617
|
function ensureTcfLocator() {
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
713
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
714
|
-
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
618
|
+
// L'iframe __tcfapiLocator route les appels postMessage du CMP.
|
|
619
|
+
// En navigation ajaxify, NodeBB peut la retirer du DOM → erreurs CMP.
|
|
620
|
+
// Un MutationObserver la recrée dès qu'elle disparaît.
|
|
715
621
|
try {
|
|
716
622
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
717
|
-
|
|
718
623
|
const inject = () => {
|
|
719
624
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
720
625
|
const f = document.createElement('iframe');
|
|
721
626
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
722
627
|
(document.body || document.documentElement).appendChild(f);
|
|
723
628
|
};
|
|
724
|
-
|
|
725
629
|
inject();
|
|
726
|
-
|
|
727
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
728
630
|
if (!window.__nbbTcfObs) {
|
|
729
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
730
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
731
|
-
{ childList: true, subtree: true });
|
|
631
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
632
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
732
633
|
}
|
|
733
634
|
} catch (_) {}
|
|
734
635
|
}
|
|
@@ -738,10 +639,10 @@
|
|
|
738
639
|
const head = document.head;
|
|
739
640
|
if (!head) return;
|
|
740
641
|
for (const [rel, href, cors] of [
|
|
741
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
742
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
743
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
744
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
642
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
643
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
644
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
645
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
745
646
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
746
647
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
747
648
|
]) {
|
|
@@ -755,7 +656,7 @@
|
|
|
755
656
|
}
|
|
756
657
|
}
|
|
757
658
|
|
|
758
|
-
// ── Bindings
|
|
659
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
759
660
|
|
|
760
661
|
function bindNodeBB() {
|
|
761
662
|
const $ = window.jQuery;
|
|
@@ -766,19 +667,16 @@
|
|
|
766
667
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
767
668
|
S.pageKey = pageKey();
|
|
768
669
|
blockedUntil = 0;
|
|
769
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
770
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
670
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
671
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
771
672
|
});
|
|
772
673
|
|
|
773
|
-
const
|
|
774
|
-
'action:ajaxify.contentLoaded',
|
|
775
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
674
|
+
const burstEvts = [
|
|
675
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
776
676
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
777
677
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
678
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
778
679
|
|
|
779
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
780
|
-
|
|
781
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
782
680
|
try {
|
|
783
681
|
require(['hooks'], hooks => {
|
|
784
682
|
if (typeof hooks?.on !== 'function') return;
|