nodebb-plugin-ezoic-infinite 1.7.4 → 1.7.6
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 +183 -273
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.2
|
|
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,29 @@
|
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
// Unobserve seulement si le placeholder est encore dans le DOM.
|
|
256
|
+
// unobserve() sur un nœud détaché corrompt l'IO interne de pubads
|
|
257
|
+
// → "Failed to execute 'observe': parameter 1 is not of type 'Element'"
|
|
258
|
+
// sur tous les observe() suivants.
|
|
259
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
260
|
+
if (ph instanceof Element && ph.isConnected) S.io?.unobserve(ph);
|
|
305
261
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
306
262
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
307
|
-
|
|
308
|
-
|
|
263
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
264
|
+
if (key) wrapByKey.delete(key);
|
|
309
265
|
w.remove();
|
|
310
266
|
} catch (_) {}
|
|
311
267
|
}
|
|
@@ -313,45 +269,39 @@
|
|
|
313
269
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
314
270
|
|
|
315
271
|
/**
|
|
316
|
-
* Supprime les wraps VIDES dont l'
|
|
272
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
317
273
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* ezoic-ad-categories → li[data-cid="7"]
|
|
274
|
+
* On ne supprime JAMAIS un wrap rempli (filled) :
|
|
275
|
+
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
276
|
+
* virtualisation NodeBB — l'ancre reviendra.
|
|
277
|
+
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
278
|
+
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
279
|
+
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
325
280
|
*/
|
|
326
281
|
function pruneOrphans(klass) {
|
|
327
282
|
const meta = KIND[klass];
|
|
328
283
|
if (!meta) return;
|
|
329
284
|
|
|
330
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)
|
|
331
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS)
|
|
332
|
-
if (isFilled(w))
|
|
285
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
286
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) continue;
|
|
287
|
+
if (isFilled(w)) continue;
|
|
333
288
|
|
|
334
289
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
335
290
|
const sid = key.slice(klass.length + 1);
|
|
336
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
291
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
337
292
|
|
|
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, '\\"')}"]`;
|
|
293
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
342
294
|
const anchorEl = document.querySelector(sel);
|
|
343
295
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
344
|
-
}
|
|
296
|
+
}
|
|
345
297
|
}
|
|
346
298
|
|
|
347
299
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
348
300
|
|
|
349
301
|
/**
|
|
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).
|
|
302
|
+
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
303
|
+
* Priorité : filled > en grâce de fill > vide.
|
|
304
|
+
* Jamais de suppression d'un wrap rempli (même raison que pruneOrphans).
|
|
355
305
|
*/
|
|
356
306
|
function decluster(klass) {
|
|
357
307
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
@@ -365,14 +315,9 @@
|
|
|
365
315
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
366
316
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
367
317
|
|
|
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)
|
|
318
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
319
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
320
|
+
// les deux remplis → on ne touche pas
|
|
376
321
|
break;
|
|
377
322
|
}
|
|
378
323
|
}
|
|
@@ -381,35 +326,23 @@
|
|
|
381
326
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
382
327
|
|
|
383
328
|
/**
|
|
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).
|
|
329
|
+
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
330
|
+
* Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
|
|
331
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
391
332
|
*/
|
|
392
333
|
function ordinal(klass, el) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (meta?.ordinalAttr) {
|
|
397
|
-
const v = el.getAttribute(meta.ordinalAttr);
|
|
334
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
335
|
+
if (attr) {
|
|
336
|
+
const v = el.getAttribute(attr);
|
|
398
337
|
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
399
338
|
}
|
|
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
|
-
|
|
339
|
+
// Fallback positionnel — compte uniquement les éléments du même type
|
|
340
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
341
|
+
let i = 0;
|
|
342
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
343
|
+
if (s === el) return i;
|
|
344
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
345
|
+
}
|
|
413
346
|
return 0;
|
|
414
347
|
}
|
|
415
348
|
|
|
@@ -418,20 +351,18 @@
|
|
|
418
351
|
let inserted = 0;
|
|
419
352
|
|
|
420
353
|
for (const el of items) {
|
|
421
|
-
if (inserted >=
|
|
354
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
422
355
|
if (!el?.isConnected) continue;
|
|
423
356
|
|
|
424
|
-
const ord
|
|
425
|
-
|
|
426
|
-
if (!isTarget) continue;
|
|
427
|
-
|
|
357
|
+
const ord = ordinal(klass, el);
|
|
358
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
428
359
|
if (adjacentWrap(el)) continue;
|
|
429
360
|
|
|
430
|
-
const key =
|
|
431
|
-
if (findWrap(key)) continue;
|
|
361
|
+
const key = anchorKey(klass, el);
|
|
362
|
+
if (findWrap(key)) continue;
|
|
432
363
|
|
|
433
364
|
const id = pickId(poolKey);
|
|
434
|
-
if (!id) continue;
|
|
365
|
+
if (!id) continue;
|
|
435
366
|
|
|
436
367
|
const w = insertAfter(el, id, klass, key);
|
|
437
368
|
if (w) { observePh(id); inserted++; }
|
|
@@ -443,7 +374,6 @@
|
|
|
443
374
|
|
|
444
375
|
function getIO() {
|
|
445
376
|
if (S.io) return S.io;
|
|
446
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
447
377
|
try {
|
|
448
378
|
S.io = new IntersectionObserver(entries => {
|
|
449
379
|
for (const e of entries) {
|
|
@@ -452,14 +382,14 @@
|
|
|
452
382
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
453
383
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
454
384
|
}
|
|
455
|
-
}, { root: null, rootMargin:
|
|
385
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
456
386
|
} catch (_) { S.io = null; }
|
|
457
387
|
return S.io;
|
|
458
388
|
}
|
|
459
389
|
|
|
460
390
|
function observePh(id) {
|
|
461
391
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
462
|
-
if (ph
|
|
392
|
+
if (ph instanceof Element && ph.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
463
393
|
}
|
|
464
394
|
|
|
465
395
|
function enqueueShow(id) {
|
|
@@ -503,7 +433,6 @@
|
|
|
503
433
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
504
434
|
S.lastShow.set(id, t);
|
|
505
435
|
|
|
506
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
507
436
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
508
437
|
|
|
509
438
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -524,7 +453,6 @@
|
|
|
524
453
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
525
454
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
526
455
|
if (!wrap || !ph?.isConnected) return;
|
|
527
|
-
// Un show plus récent → ne pas toucher
|
|
528
456
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
529
457
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
530
458
|
} catch (_) {}
|
|
@@ -543,7 +471,7 @@
|
|
|
543
471
|
const orig = ez.showAds.bind(ez);
|
|
544
472
|
ez.showAds = function (...args) {
|
|
545
473
|
if (isBlocked()) return;
|
|
546
|
-
const ids
|
|
474
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
547
475
|
const seen = new Set();
|
|
548
476
|
for (const v of ids) {
|
|
549
477
|
const id = parseInt(v, 10);
|
|
@@ -562,11 +490,10 @@
|
|
|
562
490
|
}
|
|
563
491
|
}
|
|
564
492
|
|
|
565
|
-
// ── Core
|
|
493
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
566
494
|
|
|
567
495
|
async function runCore() {
|
|
568
496
|
if (isBlocked()) return 0;
|
|
569
|
-
patchShowAds();
|
|
570
497
|
|
|
571
498
|
const cfg = await fetchConfig();
|
|
572
499
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -577,10 +504,9 @@
|
|
|
577
504
|
|
|
578
505
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
579
506
|
if (!normBool(cfgEnable)) return 0;
|
|
580
|
-
const items = getItems();
|
|
581
507
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
582
508
|
pruneOrphans(klass);
|
|
583
|
-
const n = injectBetween(klass,
|
|
509
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
584
510
|
if (n) decluster(klass);
|
|
585
511
|
return n;
|
|
586
512
|
};
|
|
@@ -593,14 +519,13 @@
|
|
|
593
519
|
'ezoic-ad-between', getTopics,
|
|
594
520
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
595
521
|
);
|
|
596
|
-
|
|
522
|
+
return exec(
|
|
597
523
|
'ezoic-ad-categories', getCategories,
|
|
598
524
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
599
525
|
);
|
|
600
|
-
return 0;
|
|
601
526
|
}
|
|
602
527
|
|
|
603
|
-
// ── Scheduler
|
|
528
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
604
529
|
|
|
605
530
|
function scheduleRun(cb) {
|
|
606
531
|
if (S.runQueued) return;
|
|
@@ -610,7 +535,7 @@
|
|
|
610
535
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
611
536
|
let n = 0;
|
|
612
537
|
try { n = await runCore(); } catch (_) {}
|
|
613
|
-
|
|
538
|
+
cb?.(n);
|
|
614
539
|
});
|
|
615
540
|
}
|
|
616
541
|
|
|
@@ -618,10 +543,8 @@
|
|
|
618
543
|
if (isBlocked()) return;
|
|
619
544
|
const t = ts();
|
|
620
545
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
621
|
-
S.lastBurstTs
|
|
622
|
-
|
|
623
|
-
const pk = pageKey();
|
|
624
|
-
S.pageKey = pk;
|
|
546
|
+
S.lastBurstTs = t;
|
|
547
|
+
S.pageKey = pageKey();
|
|
625
548
|
S.burstDeadline = t + 2000;
|
|
626
549
|
|
|
627
550
|
if (S.burstActive) return;
|
|
@@ -629,7 +552,7 @@
|
|
|
629
552
|
S.burstCount = 0;
|
|
630
553
|
|
|
631
554
|
const step = () => {
|
|
632
|
-
if (pageKey() !==
|
|
555
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
633
556
|
S.burstActive = false; return;
|
|
634
557
|
}
|
|
635
558
|
S.burstCount++;
|
|
@@ -641,21 +564,21 @@
|
|
|
641
564
|
step();
|
|
642
565
|
}
|
|
643
566
|
|
|
644
|
-
// ── Cleanup
|
|
567
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
645
568
|
|
|
646
569
|
function cleanup() {
|
|
647
570
|
blockedUntil = ts() + 1500;
|
|
571
|
+
poolsReady = false;
|
|
648
572
|
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
});
|
|
656
|
-
} catch (_) {}
|
|
573
|
+
// Déconnecter l'IO AVANT les dropWrap pour éviter tout unobserve parasite.
|
|
574
|
+
// disconnect() vide la liste interne des cibles observées — les références
|
|
575
|
+
// aux placeholders de la page courante sont effacées proprement.
|
|
576
|
+
// L'IO sera recréé à ajaxify.end via getIO().
|
|
577
|
+
try { S.io?.disconnect(); } catch (_) {}
|
|
578
|
+
S.io = null;
|
|
657
579
|
|
|
658
580
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
581
|
+
wrapByKey.clear();
|
|
659
582
|
S.cfg = null;
|
|
660
583
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
661
584
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -668,19 +591,17 @@
|
|
|
668
591
|
S.runQueued = false;
|
|
669
592
|
}
|
|
670
593
|
|
|
671
|
-
// ──
|
|
594
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
672
595
|
|
|
673
596
|
function ensureDomObserver() {
|
|
674
597
|
if (S.domObs) return;
|
|
598
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
675
599
|
S.domObs = new MutationObserver(muts => {
|
|
676
600
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
677
601
|
for (const m of muts) {
|
|
678
|
-
if (!m.addedNodes?.length) continue;
|
|
679
602
|
for (const n of m.addedNodes) {
|
|
680
603
|
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)) {
|
|
604
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
684
605
|
requestBurst(); return;
|
|
685
606
|
}
|
|
686
607
|
}
|
|
@@ -706,29 +627,21 @@
|
|
|
706
627
|
}
|
|
707
628
|
|
|
708
629
|
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.
|
|
630
|
+
// L'iframe __tcfapiLocator route les appels postMessage du CMP.
|
|
631
|
+
// En navigation ajaxify, NodeBB peut la retirer du DOM → erreurs CMP.
|
|
632
|
+
// Un MutationObserver la recrée dès qu'elle disparaît.
|
|
715
633
|
try {
|
|
716
634
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
717
|
-
|
|
718
635
|
const inject = () => {
|
|
719
636
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
720
637
|
const f = document.createElement('iframe');
|
|
721
638
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
722
639
|
(document.body || document.documentElement).appendChild(f);
|
|
723
640
|
};
|
|
724
|
-
|
|
725
641
|
inject();
|
|
726
|
-
|
|
727
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
728
642
|
if (!window.__nbbTcfObs) {
|
|
729
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
730
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
731
|
-
{ childList: true, subtree: true });
|
|
643
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
644
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
732
645
|
}
|
|
733
646
|
} catch (_) {}
|
|
734
647
|
}
|
|
@@ -738,10 +651,10 @@
|
|
|
738
651
|
const head = document.head;
|
|
739
652
|
if (!head) return;
|
|
740
653
|
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],
|
|
654
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
655
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
656
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
657
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
745
658
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
746
659
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
747
660
|
]) {
|
|
@@ -755,7 +668,7 @@
|
|
|
755
668
|
}
|
|
756
669
|
}
|
|
757
670
|
|
|
758
|
-
// ── Bindings
|
|
671
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
759
672
|
|
|
760
673
|
function bindNodeBB() {
|
|
761
674
|
const $ = window.jQuery;
|
|
@@ -766,19 +679,16 @@
|
|
|
766
679
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
767
680
|
S.pageKey = pageKey();
|
|
768
681
|
blockedUntil = 0;
|
|
769
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
770
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
682
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
683
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
771
684
|
});
|
|
772
685
|
|
|
773
|
-
const
|
|
774
|
-
'action:ajaxify.contentLoaded',
|
|
775
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
686
|
+
const burstEvts = [
|
|
687
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
776
688
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
777
689
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
690
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
778
691
|
|
|
779
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
780
|
-
|
|
781
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
782
692
|
try {
|
|
783
693
|
require(['hooks'], hooks => {
|
|
784
694
|
if (typeof hooks?.on !== 'function') return;
|