nodebb-plugin-ezoic-infinite 1.7.3 → 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 +177 -275
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,40 +54,40 @@
|
|
|
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
|
-
*
|
|
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)
|
|
71
69
|
*/
|
|
72
70
|
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' },
|
|
71
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
72
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
73
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
77
|
|
|
80
78
|
const S = {
|
|
81
|
-
pageKey:
|
|
82
|
-
cfg:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
inflight: 0,
|
|
94
|
-
pending: [],
|
|
95
|
-
pendingSet: new Set(),
|
|
96
|
-
|
|
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(),
|
|
97
91
|
runQueued: false,
|
|
98
92
|
burstActive: false,
|
|
99
93
|
burstDeadline: 0,
|
|
@@ -102,8 +96,12 @@
|
|
|
102
96
|
};
|
|
103
97
|
|
|
104
98
|
let blockedUntil = 0;
|
|
105
|
-
|
|
106
|
-
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]'));
|
|
107
105
|
|
|
108
106
|
function mutate(fn) {
|
|
109
107
|
S.mutGuard++;
|
|
@@ -121,12 +119,6 @@
|
|
|
121
119
|
return S.cfg;
|
|
122
120
|
}
|
|
123
121
|
|
|
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
122
|
function parseIds(raw) {
|
|
131
123
|
const out = [], seen = new Set();
|
|
132
124
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +128,13 @@
|
|
|
136
128
|
return out;
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
138
|
|
|
146
139
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
140
|
|
|
@@ -165,13 +158,13 @@
|
|
|
165
158
|
return 'other';
|
|
166
159
|
}
|
|
167
160
|
|
|
168
|
-
// ── DOM
|
|
161
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
162
|
|
|
170
163
|
function getPosts() {
|
|
171
164
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
165
|
if (!el.isConnected) return false;
|
|
173
166
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
167
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
168
|
if (p && p !== el) return false;
|
|
176
169
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
170
|
});
|
|
@@ -187,46 +180,41 @@
|
|
|
187
180
|
);
|
|
188
181
|
}
|
|
189
182
|
|
|
190
|
-
// ── Ancres stables
|
|
183
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
184
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
function stableId(kindClass, el) {
|
|
198
|
-
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;
|
|
199
190
|
if (attr) {
|
|
200
191
|
const v = el.getAttribute(attr);
|
|
201
192
|
if (v !== null && v !== '') return v;
|
|
202
193
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
194
|
+
let i = 0;
|
|
195
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
196
|
+
if (s === el) return `i${i}`;
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
211
199
|
return 'i0';
|
|
212
200
|
}
|
|
213
201
|
|
|
214
|
-
const
|
|
202
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
203
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
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
|
+
};
|
|
223
211
|
|
|
224
212
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
225
213
|
|
|
226
214
|
function pickId(poolKey) {
|
|
227
215
|
const pool = S.pools[poolKey];
|
|
228
216
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
217
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
218
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
219
|
const id = pool[i];
|
|
232
220
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -237,7 +225,7 @@
|
|
|
237
225
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
226
|
|
|
239
227
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
228
|
+
const w = document.createElement('div');
|
|
241
229
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
230
|
w.setAttribute(A_ANCHOR, key);
|
|
243
231
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,55 +239,25 @@
|
|
|
251
239
|
}
|
|
252
240
|
|
|
253
241
|
function insertAfter(el, id, klass, key) {
|
|
254
|
-
if (!el?.insertAdjacentElement)
|
|
255
|
-
if (findWrap(key))
|
|
256
|
-
if (S.mountedIds.has(id))
|
|
257
|
-
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;
|
|
258
246
|
const w = makeWrap(id, klass, key);
|
|
259
247
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
248
|
S.mountedIds.add(id);
|
|
249
|
+
wrapByKey.set(key, w);
|
|
261
250
|
return w;
|
|
262
251
|
}
|
|
263
252
|
|
|
264
|
-
/**
|
|
265
|
-
* Retire proprement un wrap du DOM.
|
|
266
|
-
*
|
|
267
|
-
* Sans précaution, retirer un wrap contenant un player vidéo Ezoic (wyvern.js)
|
|
268
|
-
* déclenche des erreurs async sur des nœuds détachés :
|
|
269
|
-
* "Cannot read properties of null (reading 'paused')"
|
|
270
|
-
* "Cannot read properties of null (reading 'offsetWidth')"
|
|
271
|
-
* "Invalid target for null#trigger / null#on"
|
|
272
|
-
*
|
|
273
|
-
* On pause les media et on tente de notifier l'API wyvern avant remove().
|
|
274
|
-
*/
|
|
275
253
|
function dropWrap(w) {
|
|
276
254
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
w.querySelectorAll('video, audio').forEach(m => {
|
|
280
|
-
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
281
|
-
});
|
|
282
|
-
} catch (_) {}
|
|
283
|
-
|
|
284
|
-
// 2. Notifier l'API wyvern si disponible
|
|
285
|
-
try {
|
|
286
|
-
if (window.wyvern && typeof window.wyvern === 'object') {
|
|
287
|
-
w.querySelectorAll('[id^="ezoic-"],[class*="wyvern"],[class*="ezoic-video"]')
|
|
288
|
-
.forEach(n => { try { window.wyvern.destroy?.(n); } catch (_) {} });
|
|
289
|
-
}
|
|
290
|
-
} catch (_) {}
|
|
291
|
-
|
|
292
|
-
// 3. Unobserve (guard instanceof Element — unobserve(null) corrompt l'IO)
|
|
293
|
-
try {
|
|
294
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
295
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
296
|
-
} catch (_) {}
|
|
297
|
-
|
|
298
|
-
// 4. Libérer l'id du pool
|
|
255
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
256
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
299
257
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
300
258
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
301
|
-
|
|
302
|
-
|
|
259
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
260
|
+
if (key) wrapByKey.delete(key);
|
|
303
261
|
w.remove();
|
|
304
262
|
} catch (_) {}
|
|
305
263
|
}
|
|
@@ -307,58 +265,39 @@
|
|
|
307
265
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
308
266
|
|
|
309
267
|
/**
|
|
310
|
-
* Supprime les wraps dont l'
|
|
311
|
-
*
|
|
312
|
-
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
313
|
-
* Exemples :
|
|
314
|
-
* ezoic-ad-message → cherche [data-pid="123"]
|
|
315
|
-
* ezoic-ad-between → cherche [data-index="5"]
|
|
316
|
-
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
268
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
317
269
|
*
|
|
318
|
-
* On ne
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* - Il peut contenir un player wyvern actif → .remove() déclenche des
|
|
325
|
-
* erreurs async ("Cannot read 'paused'", "offsetWidth", "getChild"…).
|
|
326
|
-
* - Le post-ancre peut être temporairement virtualisé par NodeBB puis
|
|
327
|
-
* revenir — dans ce cas le wrap filled doit rester en place.
|
|
328
|
-
*
|
|
329
|
-
* Seuls les wraps VIDES dont l'ancre a disparu sont supprimés.
|
|
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.
|
|
330
276
|
*/
|
|
331
277
|
function pruneOrphans(klass) {
|
|
332
278
|
const meta = KIND[klass];
|
|
333
279
|
if (!meta) return;
|
|
334
280
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
339
|
-
|
|
340
|
-
// Ne jamais retirer un wrap qui contient du contenu (player potentiellement actif)
|
|
341
|
-
if (isFilled(w)) return;
|
|
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;
|
|
342
284
|
|
|
343
285
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
344
286
|
const sid = key.slice(klass.length + 1);
|
|
345
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
287
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
346
288
|
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
);
|
|
289
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
290
|
+
const anchorEl = document.querySelector(sel);
|
|
350
291
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
351
|
-
}
|
|
292
|
+
}
|
|
352
293
|
}
|
|
353
294
|
|
|
354
295
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
355
296
|
|
|
356
297
|
/**
|
|
357
|
-
* Deux wraps adjacents
|
|
358
|
-
* Priorité : filled > en grâce
|
|
359
|
-
*
|
|
360
|
-
* Règle de sécurité wyvern : on ne supprime JAMAIS un wrap rempli.
|
|
361
|
-
* 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).
|
|
362
301
|
*/
|
|
363
302
|
function decluster(klass) {
|
|
364
303
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
@@ -372,14 +311,9 @@
|
|
|
372
311
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
373
312
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
374
313
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// Ne jamais retirer un wrap rempli (player actif potentiel)
|
|
379
|
-
if (!wFilled && !pFilled) mutate(() => dropWrap(w)); // les deux vides → retirer le courant
|
|
380
|
-
else if (!wFilled && pFilled) mutate(() => dropWrap(w)); // courant vide, précédent rempli → retirer le courant
|
|
381
|
-
else if ( wFilled && !pFilled) mutate(() => dropWrap(prev)); // courant rempli, précédent vide → retirer le précédent
|
|
382
|
-
// 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
|
|
383
317
|
break;
|
|
384
318
|
}
|
|
385
319
|
}
|
|
@@ -389,23 +323,22 @@
|
|
|
389
323
|
|
|
390
324
|
/**
|
|
391
325
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
392
|
-
*
|
|
393
|
-
*
|
|
326
|
+
* Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
|
|
327
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
394
328
|
*/
|
|
395
329
|
function ordinal(klass, el) {
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
} catch (_) {}
|
|
330
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
331
|
+
if (attr) {
|
|
332
|
+
const v = el.getAttribute(attr);
|
|
333
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
334
|
+
}
|
|
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
|
+
}
|
|
409
342
|
return 0;
|
|
410
343
|
}
|
|
411
344
|
|
|
@@ -414,20 +347,18 @@
|
|
|
414
347
|
let inserted = 0;
|
|
415
348
|
|
|
416
349
|
for (const el of items) {
|
|
417
|
-
if (inserted >=
|
|
350
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
418
351
|
if (!el?.isConnected) continue;
|
|
419
352
|
|
|
420
|
-
const ord
|
|
421
|
-
|
|
422
|
-
if (!isTarget) continue;
|
|
423
|
-
|
|
353
|
+
const ord = ordinal(klass, el);
|
|
354
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
424
355
|
if (adjacentWrap(el)) continue;
|
|
425
356
|
|
|
426
|
-
const key =
|
|
427
|
-
if (findWrap(key)) continue;
|
|
357
|
+
const key = anchorKey(klass, el);
|
|
358
|
+
if (findWrap(key)) continue;
|
|
428
359
|
|
|
429
360
|
const id = pickId(poolKey);
|
|
430
|
-
if (!id) continue;
|
|
361
|
+
if (!id) continue;
|
|
431
362
|
|
|
432
363
|
const w = insertAfter(el, id, klass, key);
|
|
433
364
|
if (w) { observePh(id); inserted++; }
|
|
@@ -439,7 +370,6 @@
|
|
|
439
370
|
|
|
440
371
|
function getIO() {
|
|
441
372
|
if (S.io) return S.io;
|
|
442
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
443
373
|
try {
|
|
444
374
|
S.io = new IntersectionObserver(entries => {
|
|
445
375
|
for (const e of entries) {
|
|
@@ -448,7 +378,7 @@
|
|
|
448
378
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
449
379
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
450
380
|
}
|
|
451
|
-
}, { root: null, rootMargin:
|
|
381
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
452
382
|
} catch (_) { S.io = null; }
|
|
453
383
|
return S.io;
|
|
454
384
|
}
|
|
@@ -499,7 +429,6 @@
|
|
|
499
429
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
500
430
|
S.lastShow.set(id, t);
|
|
501
431
|
|
|
502
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
503
432
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
504
433
|
|
|
505
434
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -520,7 +449,6 @@
|
|
|
520
449
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
521
450
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
522
451
|
if (!wrap || !ph?.isConnected) return;
|
|
523
|
-
// Un show plus récent → ne pas toucher
|
|
524
452
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
525
453
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
526
454
|
} catch (_) {}
|
|
@@ -539,7 +467,7 @@
|
|
|
539
467
|
const orig = ez.showAds.bind(ez);
|
|
540
468
|
ez.showAds = function (...args) {
|
|
541
469
|
if (isBlocked()) return;
|
|
542
|
-
const ids
|
|
470
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
543
471
|
const seen = new Set();
|
|
544
472
|
for (const v of ids) {
|
|
545
473
|
const id = parseInt(v, 10);
|
|
@@ -558,11 +486,10 @@
|
|
|
558
486
|
}
|
|
559
487
|
}
|
|
560
488
|
|
|
561
|
-
// ── Core
|
|
489
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
562
490
|
|
|
563
491
|
async function runCore() {
|
|
564
492
|
if (isBlocked()) return 0;
|
|
565
|
-
patchShowAds();
|
|
566
493
|
|
|
567
494
|
const cfg = await fetchConfig();
|
|
568
495
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -573,10 +500,9 @@
|
|
|
573
500
|
|
|
574
501
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
575
502
|
if (!normBool(cfgEnable)) return 0;
|
|
576
|
-
const items = getItems();
|
|
577
503
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
578
504
|
pruneOrphans(klass);
|
|
579
|
-
const n = injectBetween(klass,
|
|
505
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
580
506
|
if (n) decluster(klass);
|
|
581
507
|
return n;
|
|
582
508
|
};
|
|
@@ -589,14 +515,13 @@
|
|
|
589
515
|
'ezoic-ad-between', getTopics,
|
|
590
516
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
591
517
|
);
|
|
592
|
-
|
|
518
|
+
return exec(
|
|
593
519
|
'ezoic-ad-categories', getCategories,
|
|
594
520
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
595
521
|
);
|
|
596
|
-
return 0;
|
|
597
522
|
}
|
|
598
523
|
|
|
599
|
-
// ── Scheduler
|
|
524
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
600
525
|
|
|
601
526
|
function scheduleRun(cb) {
|
|
602
527
|
if (S.runQueued) return;
|
|
@@ -606,7 +531,7 @@
|
|
|
606
531
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
607
532
|
let n = 0;
|
|
608
533
|
try { n = await runCore(); } catch (_) {}
|
|
609
|
-
|
|
534
|
+
cb?.(n);
|
|
610
535
|
});
|
|
611
536
|
}
|
|
612
537
|
|
|
@@ -614,10 +539,8 @@
|
|
|
614
539
|
if (isBlocked()) return;
|
|
615
540
|
const t = ts();
|
|
616
541
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
617
|
-
S.lastBurstTs
|
|
618
|
-
|
|
619
|
-
const pk = pageKey();
|
|
620
|
-
S.pageKey = pk;
|
|
542
|
+
S.lastBurstTs = t;
|
|
543
|
+
S.pageKey = pageKey();
|
|
621
544
|
S.burstDeadline = t + 2000;
|
|
622
545
|
|
|
623
546
|
if (S.burstActive) return;
|
|
@@ -625,7 +548,7 @@
|
|
|
625
548
|
S.burstCount = 0;
|
|
626
549
|
|
|
627
550
|
const step = () => {
|
|
628
|
-
if (pageKey() !==
|
|
551
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
629
552
|
S.burstActive = false; return;
|
|
630
553
|
}
|
|
631
554
|
S.burstCount++;
|
|
@@ -637,21 +560,13 @@
|
|
|
637
560
|
step();
|
|
638
561
|
}
|
|
639
562
|
|
|
640
|
-
// ── Cleanup
|
|
563
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
641
564
|
|
|
642
565
|
function cleanup() {
|
|
643
566
|
blockedUntil = ts() + 1500;
|
|
644
|
-
|
|
645
|
-
// Pause tous les media dans nos wraps AVANT de les retirer du DOM.
|
|
646
|
-
// Empêche wyvern.js de continuer d'exécuter ses callbacks async sur des
|
|
647
|
-
// nœuds détachés (erreurs "Cannot read 'paused'", "offsetWidth"…).
|
|
648
|
-
try {
|
|
649
|
-
document.querySelectorAll(`.${WRAP_CLASS} video, .${WRAP_CLASS} audio`).forEach(m => {
|
|
650
|
-
try { if (!m.paused) m.pause(); } catch (_) {}
|
|
651
|
-
});
|
|
652
|
-
} catch (_) {}
|
|
653
|
-
|
|
567
|
+
poolsReady = false;
|
|
654
568
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
569
|
+
wrapByKey.clear();
|
|
655
570
|
S.cfg = null;
|
|
656
571
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
657
572
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -664,19 +579,17 @@
|
|
|
664
579
|
S.runQueued = false;
|
|
665
580
|
}
|
|
666
581
|
|
|
667
|
-
// ──
|
|
582
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
668
583
|
|
|
669
584
|
function ensureDomObserver() {
|
|
670
585
|
if (S.domObs) return;
|
|
586
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
671
587
|
S.domObs = new MutationObserver(muts => {
|
|
672
588
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
673
589
|
for (const m of muts) {
|
|
674
|
-
if (!m.addedNodes?.length) continue;
|
|
675
590
|
for (const n of m.addedNodes) {
|
|
676
591
|
if (n.nodeType !== 1) continue;
|
|
677
|
-
if (n.matches?.(
|
|
678
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
679
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
592
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
680
593
|
requestBurst(); return;
|
|
681
594
|
}
|
|
682
595
|
}
|
|
@@ -702,29 +615,21 @@
|
|
|
702
615
|
}
|
|
703
616
|
|
|
704
617
|
function ensureTcfLocator() {
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
709
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
710
|
-
// 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.
|
|
711
621
|
try {
|
|
712
622
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
713
|
-
|
|
714
623
|
const inject = () => {
|
|
715
624
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
716
625
|
const f = document.createElement('iframe');
|
|
717
626
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
718
627
|
(document.body || document.documentElement).appendChild(f);
|
|
719
628
|
};
|
|
720
|
-
|
|
721
629
|
inject();
|
|
722
|
-
|
|
723
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
724
630
|
if (!window.__nbbTcfObs) {
|
|
725
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
726
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
727
|
-
{ childList: true, subtree: true });
|
|
631
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
632
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
728
633
|
}
|
|
729
634
|
} catch (_) {}
|
|
730
635
|
}
|
|
@@ -734,10 +639,10 @@
|
|
|
734
639
|
const head = document.head;
|
|
735
640
|
if (!head) return;
|
|
736
641
|
for (const [rel, href, cors] of [
|
|
737
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
738
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
739
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
740
|
-
['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 ],
|
|
741
646
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
742
647
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
743
648
|
]) {
|
|
@@ -751,7 +656,7 @@
|
|
|
751
656
|
}
|
|
752
657
|
}
|
|
753
658
|
|
|
754
|
-
// ── Bindings
|
|
659
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
755
660
|
|
|
756
661
|
function bindNodeBB() {
|
|
757
662
|
const $ = window.jQuery;
|
|
@@ -762,19 +667,16 @@
|
|
|
762
667
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
763
668
|
S.pageKey = pageKey();
|
|
764
669
|
blockedUntil = 0;
|
|
765
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
766
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
670
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
671
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
767
672
|
});
|
|
768
673
|
|
|
769
|
-
const
|
|
770
|
-
'action:ajaxify.contentLoaded',
|
|
771
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
674
|
+
const burstEvts = [
|
|
675
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
772
676
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
773
677
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
678
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
774
679
|
|
|
775
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
776
|
-
|
|
777
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
778
680
|
try {
|
|
779
681
|
require(['hooks'], hooks => {
|
|
780
682
|
if (typeof hooks?.on !== 'function') return;
|