nodebb-plugin-ezoic-infinite 1.7.13 → 1.7.15
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 +159 -215
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
|
|
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,11 @@
|
|
|
102
96
|
};
|
|
103
97
|
|
|
104
98
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
99
|
+
const ts = () => Date.now();
|
|
100
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
101
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
102
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
103
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
104
|
|
|
108
105
|
function mutate(fn) {
|
|
109
106
|
S.mutGuard++;
|
|
@@ -121,12 +118,6 @@
|
|
|
121
118
|
return S.cfg;
|
|
122
119
|
}
|
|
123
120
|
|
|
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
121
|
function parseIds(raw) {
|
|
131
122
|
const out = [], seen = new Set();
|
|
132
123
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +127,11 @@
|
|
|
136
127
|
return out;
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
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
|
+
}
|
|
145
135
|
|
|
146
136
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
137
|
|
|
@@ -165,13 +155,13 @@
|
|
|
165
155
|
return 'other';
|
|
166
156
|
}
|
|
167
157
|
|
|
168
|
-
// ── DOM
|
|
158
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
159
|
|
|
170
160
|
function getPosts() {
|
|
171
161
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
162
|
if (!el.isConnected) return false;
|
|
173
163
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
164
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
165
|
if (p && p !== el) return false;
|
|
176
166
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
167
|
});
|
|
@@ -187,36 +177,28 @@
|
|
|
187
177
|
);
|
|
188
178
|
}
|
|
189
179
|
|
|
190
|
-
// ── Ancres stables
|
|
180
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
181
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
* Utilise l'attribut défini dans KIND (data-pid, data-index, data-cid).
|
|
195
|
-
* Fallback positionnel si l'attribut est absent.
|
|
196
|
-
*/
|
|
197
|
-
function stableId(kindClass, el) {
|
|
198
|
-
const attr = KIND[kindClass]?.anchorAttr;
|
|
182
|
+
function stableId(klass, el) {
|
|
183
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
184
|
if (attr) {
|
|
200
185
|
const v = el.getAttribute(attr);
|
|
201
186
|
if (v !== null && v !== '') return v;
|
|
202
187
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
188
|
+
let i = 0;
|
|
189
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
190
|
+
if (s === el) return `i${i}`;
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
211
193
|
return 'i0';
|
|
212
194
|
}
|
|
213
195
|
|
|
214
|
-
const
|
|
196
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
197
|
|
|
216
|
-
function findWrap(
|
|
198
|
+
function findWrap(key) {
|
|
217
199
|
try {
|
|
218
200
|
return document.querySelector(
|
|
219
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
201
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
202
|
);
|
|
221
203
|
} catch (_) { return null; }
|
|
222
204
|
}
|
|
@@ -226,7 +208,7 @@
|
|
|
226
208
|
function pickId(poolKey) {
|
|
227
209
|
const pool = S.pools[poolKey];
|
|
228
210
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
211
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
212
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
213
|
const id = pool[i];
|
|
232
214
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -237,7 +219,7 @@
|
|
|
237
219
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
220
|
|
|
239
221
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
222
|
+
const w = document.createElement('div');
|
|
241
223
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
224
|
w.setAttribute(A_ANCHOR, key);
|
|
243
225
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,10 +233,10 @@
|
|
|
251
233
|
}
|
|
252
234
|
|
|
253
235
|
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)
|
|
236
|
+
if (!el?.insertAdjacentElement) return null;
|
|
237
|
+
if (findWrap(key)) return null;
|
|
238
|
+
if (S.mountedIds.has(id)) return null;
|
|
239
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
240
|
const w = makeWrap(id, klass, key);
|
|
259
241
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
242
|
S.mountedIds.add(id);
|
|
@@ -263,15 +245,12 @@
|
|
|
263
245
|
|
|
264
246
|
function dropWrap(w) {
|
|
265
247
|
try {
|
|
248
|
+
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
249
|
+
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
250
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
251
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
266
252
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
253
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
268
|
-
// IMPORTANT : ne passer unobserve que si c'est un vrai Element.
|
|
269
|
-
// unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
|
|
270
|
-
// "parameter 1 is not of type Element" sur le prochain observe).
|
|
271
|
-
try {
|
|
272
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
274
|
-
} catch (_) {}
|
|
275
254
|
w.remove();
|
|
276
255
|
} catch (_) {}
|
|
277
256
|
}
|
|
@@ -279,70 +258,55 @@
|
|
|
279
258
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
259
|
|
|
281
260
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
261
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
283
262
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
263
|
+
* On ne supprime JAMAIS un wrap rempli (filled) :
|
|
264
|
+
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
265
|
+
* virtualisation NodeBB — l'ancre reviendra.
|
|
266
|
+
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
267
|
+
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
268
|
+
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
291
269
|
*/
|
|
292
270
|
function pruneOrphans(klass) {
|
|
293
271
|
const meta = KIND[klass];
|
|
294
272
|
if (!meta) return;
|
|
295
273
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const baseTag = meta.sel.split('[')[0];
|
|
300
|
-
|
|
301
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
302
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
303
|
-
if (isFilled(w)) return; // jamais supprimer un wrap rempli
|
|
274
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
275
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) continue;
|
|
276
|
+
if (isFilled(w)) continue;
|
|
304
277
|
|
|
305
278
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
306
279
|
const sid = key.slice(klass.length + 1);
|
|
307
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
280
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
308
281
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
);
|
|
282
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
283
|
+
const anchorEl = document.querySelector(sel);
|
|
312
284
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
313
|
-
}
|
|
285
|
+
}
|
|
314
286
|
}
|
|
315
287
|
|
|
316
288
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
317
289
|
|
|
318
290
|
/**
|
|
319
|
-
* Deux wraps adjacents
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* - Jamais supprimer un wrap < FILL_GRACE_MS depuis création OU depuis showAds
|
|
323
|
-
* (le fill Ezoic est async : l'enchère SSP peut prendre plusieurs secondes
|
|
324
|
-
* après showAds, et showAds lui-même peut arriver bien après l'injection)
|
|
325
|
-
* - Si les deux sont filled → on ne touche rien
|
|
291
|
+
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
292
|
+
* Priorité : filled > en grâce de fill > vide.
|
|
293
|
+
* Jamais de suppression d'un wrap rempli (même raison que pruneOrphans).
|
|
326
294
|
*/
|
|
327
295
|
function decluster(klass) {
|
|
328
296
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
329
|
-
if (isFilled(w)) continue; // filled = intouchable
|
|
330
|
-
const wCreated = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
331
|
-
if (ts() - wCreated < FILL_GRACE_MS) continue; // trop récent
|
|
332
297
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
333
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
298
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
334
299
|
|
|
335
300
|
let prev = w.previousElementSibling, steps = 0;
|
|
336
301
|
while (prev && steps++ < 3) {
|
|
337
302
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
338
|
-
|
|
339
|
-
const pCreated = parseInt(prev.getAttribute(A_CREATED) || '0', 10);
|
|
340
|
-
if (ts() - pCreated < FILL_GRACE_MS) break;
|
|
303
|
+
|
|
341
304
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
342
305
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
343
306
|
|
|
344
|
-
|
|
345
|
-
mutate(() => dropWrap(
|
|
307
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
308
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
309
|
+
// les deux remplis → on ne touche pas
|
|
346
310
|
break;
|
|
347
311
|
}
|
|
348
312
|
}
|
|
@@ -352,14 +316,16 @@
|
|
|
352
316
|
|
|
353
317
|
/**
|
|
354
318
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
319
|
+
* Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
|
|
320
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
357
321
|
*/
|
|
358
322
|
function ordinal(klass, el) {
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
323
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
324
|
+
if (attr) {
|
|
325
|
+
const v = el.getAttribute(attr);
|
|
326
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
327
|
+
}
|
|
328
|
+
// Fallback positionnel — compte uniquement les éléments du même type
|
|
363
329
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
364
330
|
let i = 0;
|
|
365
331
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -374,20 +340,18 @@
|
|
|
374
340
|
let inserted = 0;
|
|
375
341
|
|
|
376
342
|
for (const el of items) {
|
|
377
|
-
if (inserted >=
|
|
343
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
378
344
|
if (!el?.isConnected) continue;
|
|
379
345
|
|
|
380
|
-
const ord
|
|
381
|
-
|
|
382
|
-
if (!isTarget) continue;
|
|
383
|
-
|
|
346
|
+
const ord = ordinal(klass, el);
|
|
347
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
384
348
|
if (adjacentWrap(el)) continue;
|
|
385
349
|
|
|
386
|
-
const key =
|
|
387
|
-
if (findWrap(key)) continue;
|
|
350
|
+
const key = anchorKey(klass, el);
|
|
351
|
+
if (findWrap(key)) continue;
|
|
388
352
|
|
|
389
353
|
const id = pickId(poolKey);
|
|
390
|
-
if (!id) continue;
|
|
354
|
+
if (!id) continue;
|
|
391
355
|
|
|
392
356
|
const w = insertAfter(el, id, klass, key);
|
|
393
357
|
if (w) { observePh(id); inserted++; }
|
|
@@ -399,7 +363,6 @@
|
|
|
399
363
|
|
|
400
364
|
function getIO() {
|
|
401
365
|
if (S.io) return S.io;
|
|
402
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
403
366
|
try {
|
|
404
367
|
S.io = new IntersectionObserver(entries => {
|
|
405
368
|
for (const e of entries) {
|
|
@@ -408,7 +371,7 @@
|
|
|
408
371
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
409
372
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
410
373
|
}
|
|
411
|
-
}, { root: null, rootMargin:
|
|
374
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
412
375
|
} catch (_) { S.io = null; }
|
|
413
376
|
return S.io;
|
|
414
377
|
}
|
|
@@ -459,7 +422,6 @@
|
|
|
459
422
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
460
423
|
S.lastShow.set(id, t);
|
|
461
424
|
|
|
462
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
463
425
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
464
426
|
|
|
465
427
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -480,7 +442,6 @@
|
|
|
480
442
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
481
443
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
482
444
|
if (!wrap || !ph?.isConnected) return;
|
|
483
|
-
// Un show plus récent → ne pas toucher
|
|
484
445
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
485
446
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
486
447
|
} catch (_) {}
|
|
@@ -499,7 +460,7 @@
|
|
|
499
460
|
const orig = ez.showAds.bind(ez);
|
|
500
461
|
ez.showAds = function (...args) {
|
|
501
462
|
if (isBlocked()) return;
|
|
502
|
-
const ids
|
|
463
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
503
464
|
const seen = new Set();
|
|
504
465
|
for (const v of ids) {
|
|
505
466
|
const id = parseInt(v, 10);
|
|
@@ -518,7 +479,7 @@
|
|
|
518
479
|
}
|
|
519
480
|
}
|
|
520
481
|
|
|
521
|
-
// ── Core
|
|
482
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
522
483
|
|
|
523
484
|
async function runCore() {
|
|
524
485
|
if (isBlocked()) return 0;
|
|
@@ -533,10 +494,9 @@
|
|
|
533
494
|
|
|
534
495
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
535
496
|
if (!normBool(cfgEnable)) return 0;
|
|
536
|
-
const items = getItems();
|
|
537
497
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
538
498
|
pruneOrphans(klass);
|
|
539
|
-
const n = injectBetween(klass,
|
|
499
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
540
500
|
if (n) decluster(klass);
|
|
541
501
|
return n;
|
|
542
502
|
};
|
|
@@ -549,14 +509,13 @@
|
|
|
549
509
|
'ezoic-ad-between', getTopics,
|
|
550
510
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
551
511
|
);
|
|
552
|
-
|
|
512
|
+
return exec(
|
|
553
513
|
'ezoic-ad-categories', getCategories,
|
|
554
514
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
555
515
|
);
|
|
556
|
-
return 0;
|
|
557
516
|
}
|
|
558
517
|
|
|
559
|
-
// ── Scheduler
|
|
518
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
560
519
|
|
|
561
520
|
function scheduleRun(cb) {
|
|
562
521
|
if (S.runQueued) return;
|
|
@@ -566,7 +525,7 @@
|
|
|
566
525
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
567
526
|
let n = 0;
|
|
568
527
|
try { n = await runCore(); } catch (_) {}
|
|
569
|
-
|
|
528
|
+
cb?.(n);
|
|
570
529
|
});
|
|
571
530
|
}
|
|
572
531
|
|
|
@@ -574,10 +533,8 @@
|
|
|
574
533
|
if (isBlocked()) return;
|
|
575
534
|
const t = ts();
|
|
576
535
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
577
|
-
S.lastBurstTs
|
|
578
|
-
|
|
579
|
-
const pk = pageKey();
|
|
580
|
-
S.pageKey = pk;
|
|
536
|
+
S.lastBurstTs = t;
|
|
537
|
+
S.pageKey = pageKey();
|
|
581
538
|
S.burstDeadline = t + 2000;
|
|
582
539
|
|
|
583
540
|
if (S.burstActive) return;
|
|
@@ -585,7 +542,7 @@
|
|
|
585
542
|
S.burstCount = 0;
|
|
586
543
|
|
|
587
544
|
const step = () => {
|
|
588
|
-
if (pageKey() !==
|
|
545
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
589
546
|
S.burstActive = false; return;
|
|
590
547
|
}
|
|
591
548
|
S.burstCount++;
|
|
@@ -597,7 +554,7 @@
|
|
|
597
554
|
step();
|
|
598
555
|
}
|
|
599
556
|
|
|
600
|
-
// ── Cleanup
|
|
557
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
601
558
|
|
|
602
559
|
function cleanup() {
|
|
603
560
|
blockedUntil = ts() + 1500;
|
|
@@ -614,19 +571,17 @@
|
|
|
614
571
|
S.runQueued = false;
|
|
615
572
|
}
|
|
616
573
|
|
|
617
|
-
// ──
|
|
574
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
618
575
|
|
|
619
576
|
function ensureDomObserver() {
|
|
620
577
|
if (S.domObs) return;
|
|
578
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
621
579
|
S.domObs = new MutationObserver(muts => {
|
|
622
580
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
623
581
|
for (const m of muts) {
|
|
624
|
-
if (!m.addedNodes?.length) continue;
|
|
625
582
|
for (const n of m.addedNodes) {
|
|
626
583
|
if (n.nodeType !== 1) continue;
|
|
627
|
-
if (n.matches?.(
|
|
628
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
629
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
584
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
630
585
|
requestBurst(); return;
|
|
631
586
|
}
|
|
632
587
|
}
|
|
@@ -652,29 +607,21 @@
|
|
|
652
607
|
}
|
|
653
608
|
|
|
654
609
|
function ensureTcfLocator() {
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
659
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
660
|
-
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
610
|
+
// L'iframe __tcfapiLocator route les appels postMessage du CMP.
|
|
611
|
+
// En navigation ajaxify, NodeBB peut la retirer du DOM → erreurs CMP.
|
|
612
|
+
// Un MutationObserver la recrée dès qu'elle disparaît.
|
|
661
613
|
try {
|
|
662
614
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
663
|
-
|
|
664
615
|
const inject = () => {
|
|
665
616
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
666
617
|
const f = document.createElement('iframe');
|
|
667
618
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
668
619
|
(document.body || document.documentElement).appendChild(f);
|
|
669
620
|
};
|
|
670
|
-
|
|
671
621
|
inject();
|
|
672
|
-
|
|
673
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
674
622
|
if (!window.__nbbTcfObs) {
|
|
675
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
676
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
677
|
-
{ childList: true, subtree: true });
|
|
623
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
678
625
|
}
|
|
679
626
|
} catch (_) {}
|
|
680
627
|
}
|
|
@@ -684,10 +631,10 @@
|
|
|
684
631
|
const head = document.head;
|
|
685
632
|
if (!head) return;
|
|
686
633
|
for (const [rel, href, cors] of [
|
|
687
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
688
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
689
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
690
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
634
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
635
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
636
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
637
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
691
638
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
692
639
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
693
640
|
]) {
|
|
@@ -701,7 +648,7 @@
|
|
|
701
648
|
}
|
|
702
649
|
}
|
|
703
650
|
|
|
704
|
-
// ── Bindings
|
|
651
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
705
652
|
|
|
706
653
|
function bindNodeBB() {
|
|
707
654
|
const $ = window.jQuery;
|
|
@@ -712,19 +659,16 @@
|
|
|
712
659
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
713
660
|
S.pageKey = pageKey();
|
|
714
661
|
blockedUntil = 0;
|
|
715
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
716
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
662
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
717
664
|
});
|
|
718
665
|
|
|
719
|
-
const
|
|
720
|
-
'action:ajaxify.contentLoaded',
|
|
721
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
666
|
+
const burstEvts = [
|
|
667
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
722
668
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
723
669
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
724
671
|
|
|
725
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
726
|
-
|
|
727
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
728
672
|
try {
|
|
729
673
|
require(['hooks'], hooks => {
|
|
730
674
|
if (typeof hooks?.on !== 'function') return;
|