nodebb-plugin-ezoic-infinite 1.7.18 → 1.7.20
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 +164 -211
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v26
|
|
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. 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
|
-
*
|
|
19
|
+
* v25 Table KIND unifiée avec baseTag + ordinalAttr.
|
|
20
|
+
* Fix scroll-up / virtualisation NodeBB :
|
|
21
|
+
* – pruneOrphans : PRUNE_STABLE_MS = 45 s, isFilled guard en premier.
|
|
22
|
+
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
23
|
+
* Tentative recyclage d'id (v25) → cause exactement le même bug (wraps
|
|
24
|
+
* déplacés laissent les positions originales libres → réinjection en haut).
|
|
21
25
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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é)
|
|
26
|
+
* v26 Suppression définitive du recyclage d'id.
|
|
27
|
+
* Pool épuisé = on attend que pruneOrphans libère des ids (> 45 s hors DOM).
|
|
28
|
+
* Suppression de scrollDir, pickRecyclableWrap, moveWrapAfter.
|
|
29
|
+
* KIND simplifié : retrait du flag recyclable inutile.
|
|
31
30
|
*/
|
|
32
31
|
(function () {
|
|
33
32
|
'use strict';
|
|
34
33
|
|
|
35
34
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
35
|
|
|
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
|
|
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
|
-
//
|
|
36
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
37
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
38
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
39
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
40
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
41
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
42
|
+
|
|
43
|
+
const PRUNE_STABLE_MS = 45_000; // délai avant pruning (évite faux-orphelins scroll-up)
|
|
44
|
+
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
45
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
46
|
+
const MAX_INSERTS_RUN = 6;
|
|
47
|
+
const MAX_INFLIGHT = 4;
|
|
48
|
+
const SHOW_THROTTLE_MS = 900;
|
|
49
|
+
const BURST_COOLDOWN_MS = 200;
|
|
50
|
+
|
|
51
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
52
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
53
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
54
|
|
|
@@ -60,40 +59,37 @@
|
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
/**
|
|
63
|
-
* Table
|
|
62
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
63
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
64
|
+
* sel : sélecteur CSS complet
|
|
65
|
+
* baseTag : préfixe tag pour querySelector d'ancre
|
|
66
|
+
* (vide pour posts car sélecteur commence par '[')
|
|
67
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
68
|
+
* data-pid posts / data-index topics / data-cid catégories
|
|
69
|
+
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
70
|
+
* null → fallback positionnel (catégories)
|
|
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(),
|
|
86
|
+
lastShow: new Map(),
|
|
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,10 @@
|
|
|
263
247
|
|
|
264
248
|
function dropWrap(w) {
|
|
265
249
|
try {
|
|
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,58 +258,59 @@
|
|
|
279
258
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
259
|
|
|
281
260
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
283
|
-
*
|
|
284
|
-
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
285
|
-
* Exemples :
|
|
286
|
-
* ezoic-ad-message → cherche [data-pid="123"]
|
|
287
|
-
* ezoic-ad-between → cherche [data-index="5"]
|
|
288
|
-
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
261
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
289
262
|
*
|
|
290
|
-
*
|
|
263
|
+
* isFilled en premier : un wrap rempli n'est JAMAIS supprimé.
|
|
264
|
+
* PRUNE_STABLE_MS (45 s) : pendant cette fenêtre une ancre absente est
|
|
265
|
+
* considérée comme virtualisée par NodeBB (scroll up), pas comme orphelin réel.
|
|
291
266
|
*/
|
|
292
267
|
function pruneOrphans(klass) {
|
|
293
268
|
const meta = KIND[klass];
|
|
294
269
|
if (!meta) return;
|
|
295
270
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
271
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
272
|
+
if (isFilled(w)) continue;
|
|
273
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
300
274
|
|
|
301
275
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
-
const sid = key.slice(klass.length + 1);
|
|
303
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
276
|
+
const sid = key.slice(klass.length + 1);
|
|
277
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
304
278
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
);
|
|
279
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
280
|
+
const anchorEl = document.querySelector(sel);
|
|
308
281
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
|
-
}
|
|
282
|
+
}
|
|
310
283
|
}
|
|
311
284
|
|
|
312
285
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
313
286
|
|
|
314
287
|
/**
|
|
315
|
-
* Deux wraps adjacents
|
|
316
|
-
*
|
|
317
|
-
*
|
|
288
|
+
* Deux wraps adjacents → supprimer le courant s'il est vide et hors grâce.
|
|
289
|
+
* Guards dans l'ordre :
|
|
290
|
+
* 1. isFilled(w) → jamais toucher un wrap rempli
|
|
291
|
+
* 2. A_CREATED < FILL_GRACE → wrap trop récent (pas encore showAds'd)
|
|
292
|
+
* 3. A_SHOWN grace → fill en cours
|
|
293
|
+
* 4. isFilled(prev) → voisin rempli, intouchable → break
|
|
294
|
+
* 5. A_CREATED prev grace → voisin trop récent → break
|
|
295
|
+
* 6. A_SHOWN prev grace → break
|
|
296
|
+
* → les deux vides et hors grâce : supprimer le courant
|
|
318
297
|
*/
|
|
319
298
|
function decluster(klass) {
|
|
320
299
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
-
|
|
300
|
+
if (isFilled(w)) continue;
|
|
301
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
322
302
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
303
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
304
|
|
|
325
305
|
let prev = w.previousElementSibling, steps = 0;
|
|
326
306
|
while (prev && steps++ < 3) {
|
|
327
307
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
|
-
|
|
308
|
+
if (isFilled(prev)) break;
|
|
309
|
+
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
329
310
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
311
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
331
312
|
|
|
332
|
-
|
|
333
|
-
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
313
|
+
mutate(() => dropWrap(w));
|
|
334
314
|
break;
|
|
335
315
|
}
|
|
336
316
|
}
|
|
@@ -340,23 +320,21 @@
|
|
|
340
320
|
|
|
341
321
|
/**
|
|
342
322
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
323
|
+
* Utilise KIND[klass].ordinalAttr (data-index pour posts/topics).
|
|
324
|
+
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
345
325
|
*/
|
|
346
326
|
function ordinal(klass, el) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
} catch (_) {}
|
|
327
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
328
|
+
if (attr) {
|
|
329
|
+
const v = el.getAttribute(attr);
|
|
330
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
331
|
+
}
|
|
332
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
333
|
+
let i = 0;
|
|
334
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
335
|
+
if (s === el) return i;
|
|
336
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
337
|
+
}
|
|
360
338
|
return 0;
|
|
361
339
|
}
|
|
362
340
|
|
|
@@ -365,20 +343,18 @@
|
|
|
365
343
|
let inserted = 0;
|
|
366
344
|
|
|
367
345
|
for (const el of items) {
|
|
368
|
-
if (inserted >=
|
|
346
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
369
347
|
if (!el?.isConnected) continue;
|
|
370
348
|
|
|
371
|
-
const ord
|
|
372
|
-
|
|
373
|
-
if (!isTarget) continue;
|
|
374
|
-
|
|
349
|
+
const ord = ordinal(klass, el);
|
|
350
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
375
351
|
if (adjacentWrap(el)) continue;
|
|
376
352
|
|
|
377
|
-
const key =
|
|
378
|
-
if (findWrap(key)) continue;
|
|
353
|
+
const key = anchorKey(klass, el);
|
|
354
|
+
if (findWrap(key)) continue;
|
|
379
355
|
|
|
380
356
|
const id = pickId(poolKey);
|
|
381
|
-
if (!id) continue; // pool
|
|
357
|
+
if (!id) continue; // pool épuisé : on attend que pruneOrphans libère des ids
|
|
382
358
|
|
|
383
359
|
const w = insertAfter(el, id, klass, key);
|
|
384
360
|
if (w) { observePh(id); inserted++; }
|
|
@@ -390,7 +366,6 @@
|
|
|
390
366
|
|
|
391
367
|
function getIO() {
|
|
392
368
|
if (S.io) return S.io;
|
|
393
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
394
369
|
try {
|
|
395
370
|
S.io = new IntersectionObserver(entries => {
|
|
396
371
|
for (const e of entries) {
|
|
@@ -399,7 +374,7 @@
|
|
|
399
374
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
375
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
376
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
377
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
378
|
} catch (_) { S.io = null; }
|
|
404
379
|
return S.io;
|
|
405
380
|
}
|
|
@@ -450,7 +425,6 @@
|
|
|
450
425
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
451
426
|
S.lastShow.set(id, t);
|
|
452
427
|
|
|
453
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
454
428
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
455
429
|
|
|
456
430
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -471,7 +445,6 @@
|
|
|
471
445
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
472
446
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
473
447
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
-
// Un show plus récent → ne pas toucher
|
|
475
448
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
476
449
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
477
450
|
} catch (_) {}
|
|
@@ -490,7 +463,7 @@
|
|
|
490
463
|
const orig = ez.showAds.bind(ez);
|
|
491
464
|
ez.showAds = function (...args) {
|
|
492
465
|
if (isBlocked()) return;
|
|
493
|
-
const ids
|
|
466
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
494
467
|
const seen = new Set();
|
|
495
468
|
for (const v of ids) {
|
|
496
469
|
const id = parseInt(v, 10);
|
|
@@ -509,7 +482,7 @@
|
|
|
509
482
|
}
|
|
510
483
|
}
|
|
511
484
|
|
|
512
|
-
// ── Core
|
|
485
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
513
486
|
|
|
514
487
|
async function runCore() {
|
|
515
488
|
if (isBlocked()) return 0;
|
|
@@ -524,10 +497,9 @@
|
|
|
524
497
|
|
|
525
498
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
526
499
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
-
const items = getItems();
|
|
528
500
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
529
501
|
pruneOrphans(klass);
|
|
530
|
-
const n = injectBetween(klass,
|
|
502
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
531
503
|
if (n) decluster(klass);
|
|
532
504
|
return n;
|
|
533
505
|
};
|
|
@@ -540,14 +512,13 @@
|
|
|
540
512
|
'ezoic-ad-between', getTopics,
|
|
541
513
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
542
514
|
);
|
|
543
|
-
|
|
515
|
+
return exec(
|
|
544
516
|
'ezoic-ad-categories', getCategories,
|
|
545
517
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
546
518
|
);
|
|
547
|
-
return 0;
|
|
548
519
|
}
|
|
549
520
|
|
|
550
|
-
// ── Scheduler
|
|
521
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
551
522
|
|
|
552
523
|
function scheduleRun(cb) {
|
|
553
524
|
if (S.runQueued) return;
|
|
@@ -565,10 +536,8 @@
|
|
|
565
536
|
if (isBlocked()) return;
|
|
566
537
|
const t = ts();
|
|
567
538
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
568
|
-
S.lastBurstTs
|
|
569
|
-
|
|
570
|
-
const pk = pageKey();
|
|
571
|
-
S.pageKey = pk;
|
|
539
|
+
S.lastBurstTs = t;
|
|
540
|
+
S.pageKey = pageKey();
|
|
572
541
|
S.burstDeadline = t + 2000;
|
|
573
542
|
|
|
574
543
|
if (S.burstActive) return;
|
|
@@ -576,7 +545,7 @@
|
|
|
576
545
|
S.burstCount = 0;
|
|
577
546
|
|
|
578
547
|
const step = () => {
|
|
579
|
-
if (pageKey() !==
|
|
548
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
580
549
|
S.burstActive = false; return;
|
|
581
550
|
}
|
|
582
551
|
S.burstCount++;
|
|
@@ -588,7 +557,7 @@
|
|
|
588
557
|
step();
|
|
589
558
|
}
|
|
590
559
|
|
|
591
|
-
// ── Cleanup
|
|
560
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
592
561
|
|
|
593
562
|
function cleanup() {
|
|
594
563
|
blockedUntil = ts() + 1500;
|
|
@@ -605,19 +574,17 @@
|
|
|
605
574
|
S.runQueued = false;
|
|
606
575
|
}
|
|
607
576
|
|
|
608
|
-
// ──
|
|
577
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
609
578
|
|
|
610
579
|
function ensureDomObserver() {
|
|
611
580
|
if (S.domObs) return;
|
|
581
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
612
582
|
S.domObs = new MutationObserver(muts => {
|
|
613
583
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
614
584
|
for (const m of muts) {
|
|
615
|
-
if (!m.addedNodes?.length) continue;
|
|
616
585
|
for (const n of m.addedNodes) {
|
|
617
586
|
if (n.nodeType !== 1) continue;
|
|
618
|
-
if (n.matches?.(
|
|
619
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
620
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
587
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
621
588
|
requestBurst(); return;
|
|
622
589
|
}
|
|
623
590
|
}
|
|
@@ -643,29 +610,18 @@
|
|
|
643
610
|
}
|
|
644
611
|
|
|
645
612
|
function ensureTcfLocator() {
|
|
646
|
-
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
647
|
-
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
648
|
-
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
649
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
650
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
651
|
-
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
652
613
|
try {
|
|
653
614
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
-
|
|
655
615
|
const inject = () => {
|
|
656
616
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
657
617
|
const f = document.createElement('iframe');
|
|
658
618
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
659
619
|
(document.body || document.documentElement).appendChild(f);
|
|
660
620
|
};
|
|
661
|
-
|
|
662
621
|
inject();
|
|
663
|
-
|
|
664
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
665
622
|
if (!window.__nbbTcfObs) {
|
|
666
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
667
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
-
{ childList: true, subtree: true });
|
|
623
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
669
625
|
}
|
|
670
626
|
} catch (_) {}
|
|
671
627
|
}
|
|
@@ -675,10 +631,10 @@
|
|
|
675
631
|
const head = document.head;
|
|
676
632
|
if (!head) return;
|
|
677
633
|
for (const [rel, href, cors] of [
|
|
678
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
679
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
680
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
681
|
-
['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 ],
|
|
682
638
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
683
639
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
684
640
|
]) {
|
|
@@ -692,7 +648,7 @@
|
|
|
692
648
|
}
|
|
693
649
|
}
|
|
694
650
|
|
|
695
|
-
// ── Bindings
|
|
651
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
696
652
|
|
|
697
653
|
function bindNodeBB() {
|
|
698
654
|
const $ = window.jQuery;
|
|
@@ -703,19 +659,16 @@
|
|
|
703
659
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
704
660
|
S.pageKey = pageKey();
|
|
705
661
|
blockedUntil = 0;
|
|
706
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
707
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
662
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
708
664
|
});
|
|
709
665
|
|
|
710
|
-
const
|
|
711
|
-
'action:ajaxify.contentLoaded',
|
|
712
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
666
|
+
const burstEvts = [
|
|
667
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
713
668
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
714
669
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
715
671
|
|
|
716
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
-
|
|
718
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
719
672
|
try {
|
|
720
673
|
require(['hooks'], hooks => {
|
|
721
674
|
if (typeof hooks?.on !== 'function') return;
|