nodebb-plugin-ezoic-infinite 1.7.20 → 1.7.22
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 +211 -164
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v20)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Correctifs critiques vs v19
|
|
5
|
+
* ───────────────────────────
|
|
6
|
+
* [BUG FATAL] Pubs catégories disparaissent au premier scroll sur mobile
|
|
7
|
+
* pruneOrphans cherchait l'ancre via `[data-index]`, mais les éléments
|
|
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).
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* [BUG] decluster : break trop large stoppait tout quand UN wrap était en grâce
|
|
14
|
+
* Fix : on skip uniquement le wrap courant, pas toute la boucle.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
16
|
+
* [BUG] injectBetween : `break` sur pool épuisé empêchait d'observer les wraps
|
|
17
|
+
* existants sur les items suivants. Fix : `continue` au lieu de `break`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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).
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
25
21
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
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é)
|
|
30
31
|
*/
|
|
31
32
|
(function () {
|
|
32
33
|
'use strict';
|
|
33
34
|
|
|
34
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
35
36
|
|
|
36
|
-
const WRAP_CLASS
|
|
37
|
-
const PH_PREFIX
|
|
38
|
-
const A_ANCHOR
|
|
39
|
-
const A_WRAPID
|
|
40
|
-
const A_CREATED
|
|
41
|
-
const A_SHOWN
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const FILL_GRACE_MS
|
|
45
|
-
const EMPTY_CHECK_MS
|
|
46
|
-
const
|
|
47
|
-
const MAX_INFLIGHT
|
|
48
|
-
const SHOW_THROTTLE_MS
|
|
49
|
-
const BURST_COOLDOWN_MS
|
|
50
|
-
|
|
51
|
-
// IO
|
|
37
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
38
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
39
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
40
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic (number string)
|
|
41
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création (ms)
|
|
42
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds (ms)
|
|
43
|
+
|
|
44
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
45
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds où l'on ne decluster pas
|
|
46
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
47
|
+
const MAX_INSERTS_PER_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
52
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
53
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
54
55
|
|
|
@@ -59,37 +60,40 @@
|
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
63
64
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* null → fallback positionnel (catégories)
|
|
65
|
+
* L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
|
|
66
|
+
* quelle que soit la page ou la virtualisation :
|
|
67
|
+
* posts → data-pid (id du message, unique et permanent)
|
|
68
|
+
* topics → data-index (position 0-based dans la liste, fourni par NodeBB)
|
|
69
|
+
* catégories → data-cid (id de la catégorie, unique et permanent)
|
|
70
|
+
* ← C'était le bug v19 : on cherchait data-index ici
|
|
71
71
|
*/
|
|
72
72
|
const KIND = {
|
|
73
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
74
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
75
|
-
'ezoic-ad-categories': { sel: SEL.category,
|
|
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' },
|
|
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
|
-
|
|
81
|
+
pageKey: null,
|
|
82
|
+
cfg: null,
|
|
83
|
+
|
|
84
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
85
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
86
|
+
mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
|
|
87
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
88
|
+
|
|
89
|
+
io: null,
|
|
90
|
+
domObs: null,
|
|
91
|
+
mutGuard: 0, // compteur internalMutation
|
|
92
|
+
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
96
|
+
|
|
93
97
|
runQueued: false,
|
|
94
98
|
burstActive: false,
|
|
95
99
|
burstDeadline: 0,
|
|
@@ -98,11 +102,8 @@
|
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
let blockedUntil = 0;
|
|
101
|
-
const
|
|
102
|
-
const
|
|
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]'));
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
106
107
|
|
|
107
108
|
function mutate(fn) {
|
|
108
109
|
S.mutGuard++;
|
|
@@ -120,6 +121,12 @@
|
|
|
120
121
|
return S.cfg;
|
|
121
122
|
}
|
|
122
123
|
|
|
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
|
+
|
|
123
130
|
function parseIds(raw) {
|
|
124
131
|
const out = [], seen = new Set();
|
|
125
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -129,11 +136,12 @@
|
|
|
129
136
|
return out;
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
140
|
+
|
|
141
|
+
const isFilled = (n) =>
|
|
142
|
+
!!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
143
|
+
|
|
144
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
137
145
|
|
|
138
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
139
147
|
|
|
@@ -157,13 +165,13 @@
|
|
|
157
165
|
return 'other';
|
|
158
166
|
}
|
|
159
167
|
|
|
160
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
161
169
|
|
|
162
170
|
function getPosts() {
|
|
163
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
164
172
|
if (!el.isConnected) return false;
|
|
165
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
166
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
167
175
|
if (p && p !== el) return false;
|
|
168
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
169
177
|
});
|
|
@@ -179,28 +187,36 @@
|
|
|
179
187
|
);
|
|
180
188
|
}
|
|
181
189
|
|
|
182
|
-
// ── Ancres stables
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Retourne l'identifiant stable de l'élément selon son kindClass.
|
|
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;
|
|
186
199
|
if (attr) {
|
|
187
200
|
const v = el.getAttribute(attr);
|
|
188
201
|
if (v !== null && v !== '') return v;
|
|
189
202
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
// Fallback : position dans le parent
|
|
204
|
+
try {
|
|
205
|
+
let i = 0;
|
|
206
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
207
|
+
if (s === el) return `i${i}`;
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
} catch (_) {}
|
|
195
211
|
return 'i0';
|
|
196
212
|
}
|
|
197
213
|
|
|
198
|
-
const
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
199
215
|
|
|
200
|
-
function findWrap(
|
|
216
|
+
function findWrap(anchorKey) {
|
|
201
217
|
try {
|
|
202
218
|
return document.querySelector(
|
|
203
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
204
220
|
);
|
|
205
221
|
} catch (_) { return null; }
|
|
206
222
|
}
|
|
@@ -210,7 +226,7 @@
|
|
|
210
226
|
function pickId(poolKey) {
|
|
211
227
|
const pool = S.pools[poolKey];
|
|
212
228
|
for (let t = 0; t < pool.length; t++) {
|
|
213
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
214
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
215
231
|
const id = pool[i];
|
|
216
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -221,7 +237,7 @@
|
|
|
221
237
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
222
238
|
|
|
223
239
|
function makeWrap(id, klass, key) {
|
|
224
|
-
const w
|
|
240
|
+
const w = document.createElement('div');
|
|
225
241
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
226
242
|
w.setAttribute(A_ANCHOR, key);
|
|
227
243
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -235,10 +251,10 @@
|
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
function insertAfter(el, id, klass, key) {
|
|
238
|
-
if (!el?.insertAdjacentElement)
|
|
239
|
-
if (findWrap(key))
|
|
240
|
-
if (S.mountedIds.has(id))
|
|
241
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected)
|
|
254
|
+
if (!el?.insertAdjacentElement) return null;
|
|
255
|
+
if (findWrap(key)) return null; // ancre déjà présente
|
|
256
|
+
if (S.mountedIds.has(id)) return null; // id déjà monté
|
|
257
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
242
258
|
const w = makeWrap(id, klass, key);
|
|
243
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
244
260
|
S.mountedIds.add(id);
|
|
@@ -247,10 +263,15 @@
|
|
|
247
263
|
|
|
248
264
|
function dropWrap(w) {
|
|
249
265
|
try {
|
|
250
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
251
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
252
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
253
267
|
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 (_) {}
|
|
254
275
|
w.remove();
|
|
255
276
|
} catch (_) {}
|
|
256
277
|
}
|
|
@@ -258,59 +279,58 @@
|
|
|
258
279
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
259
280
|
|
|
260
281
|
/**
|
|
261
|
-
* Supprime les wraps
|
|
282
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
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
|
|
262
289
|
*
|
|
263
|
-
*
|
|
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.
|
|
290
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
266
291
|
*/
|
|
267
292
|
function pruneOrphans(klass) {
|
|
268
293
|
const meta = KIND[klass];
|
|
269
294
|
if (!meta) return;
|
|
270
295
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
297
|
+
|
|
298
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
299
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
274
300
|
|
|
275
301
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
276
|
-
const sid = key.slice(klass.length + 1);
|
|
277
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
302
|
+
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
303
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
278
304
|
|
|
279
|
-
const
|
|
280
|
-
|
|
305
|
+
const anchorEl = document.querySelector(
|
|
306
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
+
);
|
|
281
308
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
282
|
-
}
|
|
309
|
+
});
|
|
283
310
|
}
|
|
284
311
|
|
|
285
312
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
286
313
|
|
|
287
314
|
/**
|
|
288
|
-
* Deux wraps adjacents → supprimer le
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
|
315
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
316
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
317
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
297
318
|
*/
|
|
298
319
|
function decluster(klass) {
|
|
299
320
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
300
|
-
|
|
301
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
321
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
302
322
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
303
323
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
304
324
|
|
|
305
325
|
let prev = w.previousElementSibling, steps = 0;
|
|
306
326
|
while (prev && steps++ < 3) {
|
|
307
327
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
308
|
-
|
|
309
|
-
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
328
|
+
|
|
310
329
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
311
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
330
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
312
331
|
|
|
313
|
-
mutate(() => dropWrap(w));
|
|
332
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
333
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
314
334
|
break;
|
|
315
335
|
}
|
|
316
336
|
}
|
|
@@ -320,21 +340,23 @@
|
|
|
320
340
|
|
|
321
341
|
/**
|
|
322
342
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
323
|
-
*
|
|
324
|
-
*
|
|
343
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
344
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
325
345
|
*/
|
|
326
346
|
function ordinal(klass, el) {
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
const di = el.getAttribute('data-index');
|
|
348
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
349
|
+
// Fallback positionnel
|
|
350
|
+
try {
|
|
351
|
+
const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
|
|
352
|
+
if (tag) {
|
|
353
|
+
let i = 0;
|
|
354
|
+
for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
|
|
355
|
+
if (n === el) return i;
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (_) {}
|
|
338
360
|
return 0;
|
|
339
361
|
}
|
|
340
362
|
|
|
@@ -343,18 +365,20 @@
|
|
|
343
365
|
let inserted = 0;
|
|
344
366
|
|
|
345
367
|
for (const el of items) {
|
|
346
|
-
if (inserted >=
|
|
368
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
347
369
|
if (!el?.isConnected) continue;
|
|
348
370
|
|
|
349
|
-
const ord
|
|
350
|
-
|
|
371
|
+
const ord = ordinal(klass, el);
|
|
372
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
373
|
+
if (!isTarget) continue;
|
|
374
|
+
|
|
351
375
|
if (adjacentWrap(el)) continue;
|
|
352
376
|
|
|
353
|
-
const key =
|
|
354
|
-
if (findWrap(key)) continue;
|
|
377
|
+
const key = makeAnchorKey(klass, el);
|
|
378
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
355
379
|
|
|
356
380
|
const id = pickId(poolKey);
|
|
357
|
-
if (!id) continue; // pool
|
|
381
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
358
382
|
|
|
359
383
|
const w = insertAfter(el, id, klass, key);
|
|
360
384
|
if (w) { observePh(id); inserted++; }
|
|
@@ -366,6 +390,7 @@
|
|
|
366
390
|
|
|
367
391
|
function getIO() {
|
|
368
392
|
if (S.io) return S.io;
|
|
393
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
369
394
|
try {
|
|
370
395
|
S.io = new IntersectionObserver(entries => {
|
|
371
396
|
for (const e of entries) {
|
|
@@ -374,7 +399,7 @@
|
|
|
374
399
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
375
400
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
376
401
|
}
|
|
377
|
-
}, { root: null, rootMargin:
|
|
402
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
378
403
|
} catch (_) { S.io = null; }
|
|
379
404
|
return S.io;
|
|
380
405
|
}
|
|
@@ -425,6 +450,7 @@
|
|
|
425
450
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
426
451
|
S.lastShow.set(id, t);
|
|
427
452
|
|
|
453
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
428
454
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
429
455
|
|
|
430
456
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -445,6 +471,7 @@
|
|
|
445
471
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
446
472
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
447
473
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
+
// Un show plus récent → ne pas toucher
|
|
448
475
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
449
476
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
450
477
|
} catch (_) {}
|
|
@@ -463,7 +490,7 @@
|
|
|
463
490
|
const orig = ez.showAds.bind(ez);
|
|
464
491
|
ez.showAds = function (...args) {
|
|
465
492
|
if (isBlocked()) return;
|
|
466
|
-
const ids
|
|
493
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
467
494
|
const seen = new Set();
|
|
468
495
|
for (const v of ids) {
|
|
469
496
|
const id = parseInt(v, 10);
|
|
@@ -482,7 +509,7 @@
|
|
|
482
509
|
}
|
|
483
510
|
}
|
|
484
511
|
|
|
485
|
-
// ── Core
|
|
512
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
486
513
|
|
|
487
514
|
async function runCore() {
|
|
488
515
|
if (isBlocked()) return 0;
|
|
@@ -497,9 +524,10 @@
|
|
|
497
524
|
|
|
498
525
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
499
526
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
+
const items = getItems();
|
|
500
528
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
501
529
|
pruneOrphans(klass);
|
|
502
|
-
const n = injectBetween(klass,
|
|
530
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
503
531
|
if (n) decluster(klass);
|
|
504
532
|
return n;
|
|
505
533
|
};
|
|
@@ -512,13 +540,14 @@
|
|
|
512
540
|
'ezoic-ad-between', getTopics,
|
|
513
541
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
514
542
|
);
|
|
515
|
-
return exec(
|
|
543
|
+
if (kind === 'categories') return exec(
|
|
516
544
|
'ezoic-ad-categories', getCategories,
|
|
517
545
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
518
546
|
);
|
|
547
|
+
return 0;
|
|
519
548
|
}
|
|
520
549
|
|
|
521
|
-
// ── Scheduler
|
|
550
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
522
551
|
|
|
523
552
|
function scheduleRun(cb) {
|
|
524
553
|
if (S.runQueued) return;
|
|
@@ -536,8 +565,10 @@
|
|
|
536
565
|
if (isBlocked()) return;
|
|
537
566
|
const t = ts();
|
|
538
567
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
539
|
-
S.lastBurstTs
|
|
540
|
-
|
|
568
|
+
S.lastBurstTs = t;
|
|
569
|
+
|
|
570
|
+
const pk = pageKey();
|
|
571
|
+
S.pageKey = pk;
|
|
541
572
|
S.burstDeadline = t + 2000;
|
|
542
573
|
|
|
543
574
|
if (S.burstActive) return;
|
|
@@ -545,7 +576,7 @@
|
|
|
545
576
|
S.burstCount = 0;
|
|
546
577
|
|
|
547
578
|
const step = () => {
|
|
548
|
-
if (pageKey() !==
|
|
579
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
549
580
|
S.burstActive = false; return;
|
|
550
581
|
}
|
|
551
582
|
S.burstCount++;
|
|
@@ -557,7 +588,7 @@
|
|
|
557
588
|
step();
|
|
558
589
|
}
|
|
559
590
|
|
|
560
|
-
// ── Cleanup navigation
|
|
591
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
561
592
|
|
|
562
593
|
function cleanup() {
|
|
563
594
|
blockedUntil = ts() + 1500;
|
|
@@ -574,17 +605,19 @@
|
|
|
574
605
|
S.runQueued = false;
|
|
575
606
|
}
|
|
576
607
|
|
|
577
|
-
// ──
|
|
608
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
578
609
|
|
|
579
610
|
function ensureDomObserver() {
|
|
580
611
|
if (S.domObs) return;
|
|
581
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
582
612
|
S.domObs = new MutationObserver(muts => {
|
|
583
613
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
584
614
|
for (const m of muts) {
|
|
615
|
+
if (!m.addedNodes?.length) continue;
|
|
585
616
|
for (const n of m.addedNodes) {
|
|
586
617
|
if (n.nodeType !== 1) continue;
|
|
587
|
-
if (
|
|
618
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
619
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
620
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
588
621
|
requestBurst(); return;
|
|
589
622
|
}
|
|
590
623
|
}
|
|
@@ -610,18 +643,29 @@
|
|
|
610
643
|
}
|
|
611
644
|
|
|
612
645
|
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.
|
|
613
652
|
try {
|
|
614
653
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
+
|
|
615
655
|
const inject = () => {
|
|
616
656
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
617
657
|
const f = document.createElement('iframe');
|
|
618
658
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
619
659
|
(document.body || document.documentElement).appendChild(f);
|
|
620
660
|
};
|
|
661
|
+
|
|
621
662
|
inject();
|
|
663
|
+
|
|
664
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
622
665
|
if (!window.__nbbTcfObs) {
|
|
623
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
666
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
667
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
+
{ childList: true, subtree: true });
|
|
625
669
|
}
|
|
626
670
|
} catch (_) {}
|
|
627
671
|
}
|
|
@@ -631,10 +675,10 @@
|
|
|
631
675
|
const head = document.head;
|
|
632
676
|
if (!head) return;
|
|
633
677
|
for (const [rel, href, cors] of [
|
|
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
|
|
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],
|
|
638
682
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
639
683
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
640
684
|
]) {
|
|
@@ -648,7 +692,7 @@
|
|
|
648
692
|
}
|
|
649
693
|
}
|
|
650
694
|
|
|
651
|
-
// ── Bindings
|
|
695
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
652
696
|
|
|
653
697
|
function bindNodeBB() {
|
|
654
698
|
const $ = window.jQuery;
|
|
@@ -659,16 +703,19 @@
|
|
|
659
703
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
660
704
|
S.pageKey = pageKey();
|
|
661
705
|
blockedUntil = 0;
|
|
662
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
-
|
|
706
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
707
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
664
708
|
});
|
|
665
709
|
|
|
666
|
-
const
|
|
667
|
-
'action:ajaxify.contentLoaded',
|
|
710
|
+
const BURST_EVENTS = [
|
|
711
|
+
'action:ajaxify.contentLoaded',
|
|
712
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
668
713
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
669
714
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
671
715
|
|
|
716
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
+
|
|
718
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
672
719
|
try {
|
|
673
720
|
require(['hooks'], hooks => {
|
|
674
721
|
if (typeof hooks?.on !== 'function') return;
|