nodebb-plugin-ezoic-infinite 1.7.13 → 1.7.14
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 +167 -213
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v24
|
|
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
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
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 PRUNE_STABLE_MS = 45_000; // délai avant qu'un wrap vide puisse être purgé
|
|
40
|
+
// (évite la suppression lors du scroll up / virtualisation NodeBB)
|
|
41
|
+
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
42
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
43
|
+
const MAX_INSERTS_RUN = 6;
|
|
44
|
+
const MAX_INFLIGHT = 4;
|
|
45
|
+
const SHOW_THROTTLE_MS = 900;
|
|
46
|
+
const BURST_COOLDOWN_MS = 200;
|
|
47
|
+
|
|
48
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
49
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
50
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
51
|
|
|
@@ -60,40 +56,40 @@
|
|
|
60
56
|
};
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
|
-
* Table
|
|
59
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
60
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
61
|
+
* sel : sélecteur CSS complet
|
|
62
|
+
* baseTag : préfixe tag pour les querySelector de recherche d'ancre
|
|
63
|
+
* (vide pour posts car leur sélecteur commence par '[')
|
|
64
|
+
* anchorAttr : attribut DOM STABLE → clé unique du wrap, permanent
|
|
65
|
+
* data-pid posts (id message, immuable)
|
|
66
|
+
* data-index topics (index dans la liste)
|
|
67
|
+
* data-cid catégories (id catégorie, immuable)
|
|
68
|
+
* ordinalAttr: attribut 0-based pour le calcul de l'intervalle
|
|
69
|
+
* data-index posts + topics (fourni par NodeBB)
|
|
70
|
+
* null catégories (page statique → fallback positionnel)
|
|
71
71
|
*/
|
|
72
72
|
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' },
|
|
73
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
74
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
75
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
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
|
-
|
|
81
|
+
pageKey: null,
|
|
82
|
+
cfg: null,
|
|
83
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
84
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
85
|
+
mountedIds: new Set(), // IDs Ezoic montés dans le DOM
|
|
86
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
87
|
+
io: null,
|
|
88
|
+
domObs: null,
|
|
89
|
+
mutGuard: 0,
|
|
90
|
+
inflight: 0,
|
|
91
|
+
pending: [],
|
|
92
|
+
pendingSet: new Set(),
|
|
97
93
|
runQueued: false,
|
|
98
94
|
burstActive: false,
|
|
99
95
|
burstDeadline: 0,
|
|
@@ -102,8 +98,11 @@
|
|
|
102
98
|
};
|
|
103
99
|
|
|
104
100
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
101
|
+
const ts = () => Date.now();
|
|
102
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
103
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
104
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
105
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
106
|
|
|
108
107
|
function mutate(fn) {
|
|
109
108
|
S.mutGuard++;
|
|
@@ -121,12 +120,6 @@
|
|
|
121
120
|
return S.cfg;
|
|
122
121
|
}
|
|
123
122
|
|
|
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
123
|
function parseIds(raw) {
|
|
131
124
|
const out = [], seen = new Set();
|
|
132
125
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +129,11 @@
|
|
|
136
129
|
return out;
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
132
|
+
function initPools(cfg) {
|
|
133
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
134
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
135
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
136
|
+
}
|
|
145
137
|
|
|
146
138
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
139
|
|
|
@@ -165,13 +157,13 @@
|
|
|
165
157
|
return 'other';
|
|
166
158
|
}
|
|
167
159
|
|
|
168
|
-
// ── DOM
|
|
160
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
161
|
|
|
170
162
|
function getPosts() {
|
|
171
163
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
164
|
if (!el.isConnected) return false;
|
|
173
165
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
166
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
167
|
if (p && p !== el) return false;
|
|
176
168
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
169
|
});
|
|
@@ -187,36 +179,28 @@
|
|
|
187
179
|
);
|
|
188
180
|
}
|
|
189
181
|
|
|
190
|
-
// ── Ancres stables
|
|
182
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
183
|
|
|
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;
|
|
184
|
+
function stableId(klass, el) {
|
|
185
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
186
|
if (attr) {
|
|
200
187
|
const v = el.getAttribute(attr);
|
|
201
188
|
if (v !== null && v !== '') return v;
|
|
202
189
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
190
|
+
let i = 0;
|
|
191
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
192
|
+
if (s === el) return `i${i}`;
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
211
195
|
return 'i0';
|
|
212
196
|
}
|
|
213
197
|
|
|
214
|
-
const
|
|
198
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
199
|
|
|
216
|
-
function findWrap(
|
|
200
|
+
function findWrap(key) {
|
|
217
201
|
try {
|
|
218
202
|
return document.querySelector(
|
|
219
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
203
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
204
|
);
|
|
221
205
|
} catch (_) { return null; }
|
|
222
206
|
}
|
|
@@ -226,7 +210,7 @@
|
|
|
226
210
|
function pickId(poolKey) {
|
|
227
211
|
const pool = S.pools[poolKey];
|
|
228
212
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
213
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
214
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
215
|
const id = pool[i];
|
|
232
216
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -237,7 +221,7 @@
|
|
|
237
221
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
222
|
|
|
239
223
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
224
|
+
const w = document.createElement('div');
|
|
241
225
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
226
|
w.setAttribute(A_ANCHOR, key);
|
|
243
227
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,10 +235,10 @@
|
|
|
251
235
|
}
|
|
252
236
|
|
|
253
237
|
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)
|
|
238
|
+
if (!el?.insertAdjacentElement) return null;
|
|
239
|
+
if (findWrap(key)) return null;
|
|
240
|
+
if (S.mountedIds.has(id)) return null;
|
|
241
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
242
|
const w = makeWrap(id, klass, key);
|
|
259
243
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
244
|
S.mountedIds.add(id);
|
|
@@ -263,15 +247,12 @@
|
|
|
263
247
|
|
|
264
248
|
function dropWrap(w) {
|
|
265
249
|
try {
|
|
250
|
+
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
251
|
+
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
252
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
253
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
266
254
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
255
|
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
256
|
w.remove();
|
|
276
257
|
} catch (_) {}
|
|
277
258
|
}
|
|
@@ -279,65 +260,58 @@
|
|
|
279
260
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
261
|
|
|
281
262
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
263
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
283
264
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
265
|
+
* On ne supprime JAMAIS un wrap rempli (filled) :
|
|
266
|
+
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
267
|
+
* virtualisation NodeBB — l'ancre reviendra.
|
|
268
|
+
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
269
|
+
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
270
|
+
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
291
271
|
*/
|
|
292
272
|
function pruneOrphans(klass) {
|
|
293
273
|
const meta = KIND[klass];
|
|
294
274
|
if (!meta) return;
|
|
295
275
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) <
|
|
303
|
-
if (isFilled(w)) return; // jamais supprimer un wrap rempli
|
|
276
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
277
|
+
// Ne jamais supprimer un wrap filled
|
|
278
|
+
if (isFilled(w)) continue;
|
|
279
|
+
// Attendre PRUNE_STABLE_MS depuis la création : pendant ce délai, l'ancre
|
|
280
|
+
// peut avoir temporairement disparu du DOM par virtualisation NodeBB au
|
|
281
|
+
// scroll up — ce n'est pas un vrai orphelin.
|
|
282
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
304
283
|
|
|
305
284
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
306
285
|
const sid = key.slice(klass.length + 1);
|
|
307
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
286
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
308
287
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
);
|
|
288
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
289
|
+
const anchorEl = document.querySelector(sel);
|
|
312
290
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
313
|
-
}
|
|
291
|
+
}
|
|
314
292
|
}
|
|
315
293
|
|
|
316
294
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
317
295
|
|
|
318
296
|
/**
|
|
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
|
|
297
|
+
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
298
|
+
* Priorité : filled > en grâce de fill > vide.
|
|
299
|
+
* Jamais de suppression d'un wrap rempli (même raison que pruneOrphans).
|
|
326
300
|
*/
|
|
327
301
|
function decluster(klass) {
|
|
328
302
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
303
|
+
// Ne jamais toucher un wrap filled
|
|
304
|
+
if (isFilled(w)) continue;
|
|
305
|
+
// Protéger par A_CREATED : un wrap récent attend encore son showAds async
|
|
306
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
332
307
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
333
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
308
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
334
309
|
|
|
335
310
|
let prev = w.previousElementSibling, steps = 0;
|
|
336
311
|
while (prev && steps++ < 3) {
|
|
337
312
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
338
|
-
if (isFilled(prev)) break;
|
|
339
|
-
|
|
340
|
-
if (ts() - pCreated < FILL_GRACE_MS) break;
|
|
313
|
+
if (isFilled(prev)) break;
|
|
314
|
+
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
341
315
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
342
316
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
343
317
|
|
|
@@ -352,14 +326,16 @@
|
|
|
352
326
|
|
|
353
327
|
/**
|
|
354
328
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
329
|
+
* Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
|
|
330
|
+
* Catégories : fallback positionnel (page statique, pas d'infinite scroll).
|
|
357
331
|
*/
|
|
358
332
|
function ordinal(klass, el) {
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
333
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
334
|
+
if (attr) {
|
|
335
|
+
const v = el.getAttribute(attr);
|
|
336
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
337
|
+
}
|
|
338
|
+
// Fallback positionnel — compte uniquement les éléments du même type
|
|
363
339
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
364
340
|
let i = 0;
|
|
365
341
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -374,20 +350,18 @@
|
|
|
374
350
|
let inserted = 0;
|
|
375
351
|
|
|
376
352
|
for (const el of items) {
|
|
377
|
-
if (inserted >=
|
|
353
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
378
354
|
if (!el?.isConnected) continue;
|
|
379
355
|
|
|
380
|
-
const ord
|
|
381
|
-
|
|
382
|
-
if (!isTarget) continue;
|
|
383
|
-
|
|
356
|
+
const ord = ordinal(klass, el);
|
|
357
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
384
358
|
if (adjacentWrap(el)) continue;
|
|
385
359
|
|
|
386
|
-
const key =
|
|
387
|
-
if (findWrap(key)) continue;
|
|
360
|
+
const key = anchorKey(klass, el);
|
|
361
|
+
if (findWrap(key)) continue;
|
|
388
362
|
|
|
389
363
|
const id = pickId(poolKey);
|
|
390
|
-
if (!id) continue;
|
|
364
|
+
if (!id) continue;
|
|
391
365
|
|
|
392
366
|
const w = insertAfter(el, id, klass, key);
|
|
393
367
|
if (w) { observePh(id); inserted++; }
|
|
@@ -399,7 +373,6 @@
|
|
|
399
373
|
|
|
400
374
|
function getIO() {
|
|
401
375
|
if (S.io) return S.io;
|
|
402
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
403
376
|
try {
|
|
404
377
|
S.io = new IntersectionObserver(entries => {
|
|
405
378
|
for (const e of entries) {
|
|
@@ -408,7 +381,7 @@
|
|
|
408
381
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
409
382
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
410
383
|
}
|
|
411
|
-
}, { root: null, rootMargin:
|
|
384
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
412
385
|
} catch (_) { S.io = null; }
|
|
413
386
|
return S.io;
|
|
414
387
|
}
|
|
@@ -459,7 +432,6 @@
|
|
|
459
432
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
460
433
|
S.lastShow.set(id, t);
|
|
461
434
|
|
|
462
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
463
435
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
464
436
|
|
|
465
437
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -480,7 +452,6 @@
|
|
|
480
452
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
481
453
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
482
454
|
if (!wrap || !ph?.isConnected) return;
|
|
483
|
-
// Un show plus récent → ne pas toucher
|
|
484
455
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
485
456
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
486
457
|
} catch (_) {}
|
|
@@ -499,7 +470,7 @@
|
|
|
499
470
|
const orig = ez.showAds.bind(ez);
|
|
500
471
|
ez.showAds = function (...args) {
|
|
501
472
|
if (isBlocked()) return;
|
|
502
|
-
const ids
|
|
473
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
503
474
|
const seen = new Set();
|
|
504
475
|
for (const v of ids) {
|
|
505
476
|
const id = parseInt(v, 10);
|
|
@@ -518,7 +489,7 @@
|
|
|
518
489
|
}
|
|
519
490
|
}
|
|
520
491
|
|
|
521
|
-
// ── Core
|
|
492
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
522
493
|
|
|
523
494
|
async function runCore() {
|
|
524
495
|
if (isBlocked()) return 0;
|
|
@@ -533,10 +504,9 @@
|
|
|
533
504
|
|
|
534
505
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
535
506
|
if (!normBool(cfgEnable)) return 0;
|
|
536
|
-
const items = getItems();
|
|
537
507
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
538
508
|
pruneOrphans(klass);
|
|
539
|
-
const n = injectBetween(klass,
|
|
509
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
540
510
|
if (n) decluster(klass);
|
|
541
511
|
return n;
|
|
542
512
|
};
|
|
@@ -549,14 +519,13 @@
|
|
|
549
519
|
'ezoic-ad-between', getTopics,
|
|
550
520
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
551
521
|
);
|
|
552
|
-
|
|
522
|
+
return exec(
|
|
553
523
|
'ezoic-ad-categories', getCategories,
|
|
554
524
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
555
525
|
);
|
|
556
|
-
return 0;
|
|
557
526
|
}
|
|
558
527
|
|
|
559
|
-
// ── Scheduler
|
|
528
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
560
529
|
|
|
561
530
|
function scheduleRun(cb) {
|
|
562
531
|
if (S.runQueued) return;
|
|
@@ -566,7 +535,7 @@
|
|
|
566
535
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
567
536
|
let n = 0;
|
|
568
537
|
try { n = await runCore(); } catch (_) {}
|
|
569
|
-
|
|
538
|
+
cb?.(n);
|
|
570
539
|
});
|
|
571
540
|
}
|
|
572
541
|
|
|
@@ -574,10 +543,8 @@
|
|
|
574
543
|
if (isBlocked()) return;
|
|
575
544
|
const t = ts();
|
|
576
545
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
577
|
-
S.lastBurstTs
|
|
578
|
-
|
|
579
|
-
const pk = pageKey();
|
|
580
|
-
S.pageKey = pk;
|
|
546
|
+
S.lastBurstTs = t;
|
|
547
|
+
S.pageKey = pageKey();
|
|
581
548
|
S.burstDeadline = t + 2000;
|
|
582
549
|
|
|
583
550
|
if (S.burstActive) return;
|
|
@@ -585,7 +552,7 @@
|
|
|
585
552
|
S.burstCount = 0;
|
|
586
553
|
|
|
587
554
|
const step = () => {
|
|
588
|
-
if (pageKey() !==
|
|
555
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
589
556
|
S.burstActive = false; return;
|
|
590
557
|
}
|
|
591
558
|
S.burstCount++;
|
|
@@ -597,7 +564,7 @@
|
|
|
597
564
|
step();
|
|
598
565
|
}
|
|
599
566
|
|
|
600
|
-
// ── Cleanup
|
|
567
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
601
568
|
|
|
602
569
|
function cleanup() {
|
|
603
570
|
blockedUntil = ts() + 1500;
|
|
@@ -614,19 +581,17 @@
|
|
|
614
581
|
S.runQueued = false;
|
|
615
582
|
}
|
|
616
583
|
|
|
617
|
-
// ──
|
|
584
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
618
585
|
|
|
619
586
|
function ensureDomObserver() {
|
|
620
587
|
if (S.domObs) return;
|
|
588
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
621
589
|
S.domObs = new MutationObserver(muts => {
|
|
622
590
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
623
591
|
for (const m of muts) {
|
|
624
|
-
if (!m.addedNodes?.length) continue;
|
|
625
592
|
for (const n of m.addedNodes) {
|
|
626
593
|
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)) {
|
|
594
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
630
595
|
requestBurst(); return;
|
|
631
596
|
}
|
|
632
597
|
}
|
|
@@ -652,29 +617,21 @@
|
|
|
652
617
|
}
|
|
653
618
|
|
|
654
619
|
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.
|
|
620
|
+
// L'iframe __tcfapiLocator route les appels postMessage du CMP.
|
|
621
|
+
// En navigation ajaxify, NodeBB peut la retirer du DOM → erreurs CMP.
|
|
622
|
+
// Un MutationObserver la recrée dès qu'elle disparaît.
|
|
661
623
|
try {
|
|
662
624
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
663
|
-
|
|
664
625
|
const inject = () => {
|
|
665
626
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
666
627
|
const f = document.createElement('iframe');
|
|
667
628
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
668
629
|
(document.body || document.documentElement).appendChild(f);
|
|
669
630
|
};
|
|
670
|
-
|
|
671
631
|
inject();
|
|
672
|
-
|
|
673
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
674
632
|
if (!window.__nbbTcfObs) {
|
|
675
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
676
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
677
|
-
{ childList: true, subtree: true });
|
|
633
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
634
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
678
635
|
}
|
|
679
636
|
} catch (_) {}
|
|
680
637
|
}
|
|
@@ -684,10 +641,10 @@
|
|
|
684
641
|
const head = document.head;
|
|
685
642
|
if (!head) return;
|
|
686
643
|
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],
|
|
644
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
645
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
646
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
647
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
691
648
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
692
649
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
693
650
|
]) {
|
|
@@ -701,7 +658,7 @@
|
|
|
701
658
|
}
|
|
702
659
|
}
|
|
703
660
|
|
|
704
|
-
// ── Bindings
|
|
661
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
705
662
|
|
|
706
663
|
function bindNodeBB() {
|
|
707
664
|
const $ = window.jQuery;
|
|
@@ -712,19 +669,16 @@
|
|
|
712
669
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
713
670
|
S.pageKey = pageKey();
|
|
714
671
|
blockedUntil = 0;
|
|
715
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
716
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
672
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
673
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
717
674
|
});
|
|
718
675
|
|
|
719
|
-
const
|
|
720
|
-
'action:ajaxify.contentLoaded',
|
|
721
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
676
|
+
const burstEvts = [
|
|
677
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
722
678
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
723
679
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
680
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
724
681
|
|
|
725
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
726
|
-
|
|
727
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
728
682
|
try {
|
|
729
683
|
require(['hooks'], hooks => {
|
|
730
684
|
if (typeof hooks?.on !== 'function') return;
|