nodebb-plugin-ezoic-infinite 1.7.8 → 1.7.10
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 +216 -182
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,49 +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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
21
|
+
*
|
|
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é)
|
|
25
31
|
*/
|
|
26
32
|
(function () {
|
|
27
33
|
'use strict';
|
|
28
34
|
|
|
29
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
30
36
|
|
|
31
|
-
const WRAP_CLASS
|
|
32
|
-
const PH_PREFIX
|
|
33
|
-
const A_ANCHOR
|
|
34
|
-
const A_WRAPID
|
|
35
|
-
const A_CREATED
|
|
36
|
-
const A_SHOWN
|
|
37
|
-
|
|
38
|
-
const MIN_PRUNE_AGE_MS
|
|
39
|
-
const FILL_GRACE_MS
|
|
40
|
-
const EMPTY_CHECK_MS
|
|
41
|
-
const
|
|
42
|
-
const MAX_INFLIGHT
|
|
43
|
-
const SHOW_THROTTLE_MS
|
|
44
|
-
const BURST_COOLDOWN_MS
|
|
45
|
-
|
|
46
|
-
// 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)
|
|
47
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
48
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
49
55
|
|
|
@@ -54,40 +60,40 @@
|
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
58
64
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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)
|
|
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
|
|
69
71
|
*/
|
|
70
72
|
const KIND = {
|
|
71
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
72
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
73
|
-
'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' },
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
78
80
|
const S = {
|
|
79
|
-
pageKey:
|
|
80
|
-
cfg:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
|
|
91
97
|
runQueued: false,
|
|
92
98
|
burstActive: false,
|
|
93
99
|
burstDeadline: 0,
|
|
@@ -96,12 +102,8 @@
|
|
|
96
102
|
};
|
|
97
103
|
|
|
98
104
|
let blockedUntil = 0;
|
|
99
|
-
|
|
100
|
-
const ts
|
|
101
|
-
const isBlocked = () => ts() < blockedUntil;
|
|
102
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
103
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
104
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
105
107
|
|
|
106
108
|
function mutate(fn) {
|
|
107
109
|
S.mutGuard++;
|
|
@@ -119,6 +121,12 @@
|
|
|
119
121
|
return S.cfg;
|
|
120
122
|
}
|
|
121
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
|
+
|
|
122
130
|
function parseIds(raw) {
|
|
123
131
|
const out = [], seen = new Set();
|
|
124
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -128,13 +136,12 @@
|
|
|
128
136
|
return out;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
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; } };
|
|
138
145
|
|
|
139
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
140
147
|
|
|
@@ -158,13 +165,13 @@
|
|
|
158
165
|
return 'other';
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
162
169
|
|
|
163
170
|
function getPosts() {
|
|
164
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
165
172
|
if (!el.isConnected) return false;
|
|
166
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
167
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
168
175
|
if (p && p !== el) return false;
|
|
169
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
170
177
|
});
|
|
@@ -180,41 +187,46 @@
|
|
|
180
187
|
);
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
// ── Ancres stables
|
|
184
|
-
|
|
185
|
-
// Map anchorKey → wrap Element — évite un querySelector full-DOM à chaque injection
|
|
186
|
-
const wrapByKey = new Map();
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
190
199
|
if (attr) {
|
|
191
200
|
const v = el.getAttribute(attr);
|
|
192
201
|
if (v !== null && v !== '') return v;
|
|
193
202
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 (_) {}
|
|
199
211
|
return 'i0';
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
const
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
216
|
+
function findWrap(anchorKey) {
|
|
217
|
+
try {
|
|
218
|
+
return document.querySelector(
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
|
+
);
|
|
221
|
+
} catch (_) { return null; }
|
|
222
|
+
}
|
|
211
223
|
|
|
212
224
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
213
225
|
|
|
214
226
|
function pickId(poolKey) {
|
|
215
227
|
const pool = S.pools[poolKey];
|
|
216
228
|
for (let t = 0; t < pool.length; t++) {
|
|
217
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
218
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
219
231
|
const id = pool[i];
|
|
220
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -225,7 +237,7 @@
|
|
|
225
237
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
226
238
|
|
|
227
239
|
function makeWrap(id, klass, key) {
|
|
228
|
-
const w
|
|
240
|
+
const w = document.createElement('div');
|
|
229
241
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
230
242
|
w.setAttribute(A_ANCHOR, key);
|
|
231
243
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -239,28 +251,27 @@
|
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
function insertAfter(el, id, klass, key) {
|
|
242
|
-
if (!el?.insertAdjacentElement)
|
|
243
|
-
if (findWrap(key))
|
|
244
|
-
if (S.mountedIds.has(id))
|
|
245
|
-
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;
|
|
246
258
|
const w = makeWrap(id, klass, key);
|
|
247
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
248
260
|
S.mountedIds.add(id);
|
|
249
|
-
wrapByKey.set(key, w);
|
|
250
261
|
return w;
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
function dropWrap(w) {
|
|
254
265
|
try {
|
|
255
|
-
// Unobserve AVANT w.remove() — le placeholder est encore dans le DOM
|
|
256
|
-
// à ce stade, ce qui est requis par l'IO. Guard instanceof uniquement
|
|
257
|
-
// (même logique que v20.3 qui fonctionnait).
|
|
258
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
259
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
260
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
261
267
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
262
|
-
|
|
263
|
-
|
|
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 (_) {}
|
|
264
275
|
w.remove();
|
|
265
276
|
} catch (_) {}
|
|
266
277
|
}
|
|
@@ -268,42 +279,46 @@
|
|
|
268
279
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
269
280
|
|
|
270
281
|
/**
|
|
271
|
-
* 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
|
|
272
289
|
*
|
|
273
|
-
* On ne
|
|
274
|
-
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
275
|
-
* virtualisation NodeBB — l'ancre reviendra.
|
|
276
|
-
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
277
|
-
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
278
|
-
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
290
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
279
291
|
*/
|
|
280
292
|
function pruneOrphans(klass) {
|
|
281
293
|
const meta = KIND[klass];
|
|
282
294
|
if (!meta) return;
|
|
283
295
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
287
300
|
|
|
288
301
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
289
|
-
const sid = key.slice(klass.length + 1);
|
|
290
|
-
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; }
|
|
291
304
|
|
|
292
|
-
const
|
|
293
|
-
|
|
305
|
+
const anchorEl = document.querySelector(
|
|
306
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
+
);
|
|
294
308
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
295
|
-
}
|
|
309
|
+
});
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
299
313
|
|
|
300
314
|
/**
|
|
301
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
302
|
-
* Priorité : filled > en grâce
|
|
303
|
-
*
|
|
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.
|
|
304
318
|
*/
|
|
305
319
|
function decluster(klass) {
|
|
306
320
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
307
322
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
308
323
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
309
324
|
|
|
@@ -312,11 +327,10 @@
|
|
|
312
327
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
313
328
|
|
|
314
329
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
315
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
330
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
316
331
|
|
|
317
332
|
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
318
333
|
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
319
|
-
// les deux remplis → on ne touche pas
|
|
320
334
|
break;
|
|
321
335
|
}
|
|
322
336
|
}
|
|
@@ -326,22 +340,23 @@
|
|
|
326
340
|
|
|
327
341
|
/**
|
|
328
342
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
329
|
-
*
|
|
330
|
-
*
|
|
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).
|
|
331
345
|
*/
|
|
332
346
|
function ordinal(klass, el) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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 (_) {}
|
|
345
360
|
return 0;
|
|
346
361
|
}
|
|
347
362
|
|
|
@@ -350,18 +365,20 @@
|
|
|
350
365
|
let inserted = 0;
|
|
351
366
|
|
|
352
367
|
for (const el of items) {
|
|
353
|
-
if (inserted >=
|
|
368
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
354
369
|
if (!el?.isConnected) continue;
|
|
355
370
|
|
|
356
|
-
const ord
|
|
357
|
-
|
|
371
|
+
const ord = ordinal(klass, el);
|
|
372
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
373
|
+
if (!isTarget) continue;
|
|
374
|
+
|
|
358
375
|
if (adjacentWrap(el)) continue;
|
|
359
376
|
|
|
360
|
-
const key =
|
|
361
|
-
if (findWrap(key)) continue;
|
|
377
|
+
const key = makeAnchorKey(klass, el);
|
|
378
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
362
379
|
|
|
363
380
|
const id = pickId(poolKey);
|
|
364
|
-
if (!id) continue;
|
|
381
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
365
382
|
|
|
366
383
|
const w = insertAfter(el, id, klass, key);
|
|
367
384
|
if (w) { observePh(id); inserted++; }
|
|
@@ -373,6 +390,7 @@
|
|
|
373
390
|
|
|
374
391
|
function getIO() {
|
|
375
392
|
if (S.io) return S.io;
|
|
393
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
376
394
|
try {
|
|
377
395
|
S.io = new IntersectionObserver(entries => {
|
|
378
396
|
for (const e of entries) {
|
|
@@ -381,7 +399,7 @@
|
|
|
381
399
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
382
400
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
383
401
|
}
|
|
384
|
-
}, { root: null, rootMargin:
|
|
402
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
385
403
|
} catch (_) { S.io = null; }
|
|
386
404
|
return S.io;
|
|
387
405
|
}
|
|
@@ -432,6 +450,7 @@
|
|
|
432
450
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
433
451
|
S.lastShow.set(id, t);
|
|
434
452
|
|
|
453
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
435
454
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
436
455
|
|
|
437
456
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -452,6 +471,7 @@
|
|
|
452
471
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
453
472
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
454
473
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
+
// Un show plus récent → ne pas toucher
|
|
455
475
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
456
476
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
457
477
|
} catch (_) {}
|
|
@@ -470,7 +490,7 @@
|
|
|
470
490
|
const orig = ez.showAds.bind(ez);
|
|
471
491
|
ez.showAds = function (...args) {
|
|
472
492
|
if (isBlocked()) return;
|
|
473
|
-
const ids
|
|
493
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
474
494
|
const seen = new Set();
|
|
475
495
|
for (const v of ids) {
|
|
476
496
|
const id = parseInt(v, 10);
|
|
@@ -489,10 +509,11 @@
|
|
|
489
509
|
}
|
|
490
510
|
}
|
|
491
511
|
|
|
492
|
-
// ── Core
|
|
512
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
493
513
|
|
|
494
514
|
async function runCore() {
|
|
495
515
|
if (isBlocked()) return 0;
|
|
516
|
+
patchShowAds();
|
|
496
517
|
|
|
497
518
|
const cfg = await fetchConfig();
|
|
498
519
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -503,9 +524,10 @@
|
|
|
503
524
|
|
|
504
525
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
505
526
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
+
const items = getItems();
|
|
506
528
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
507
529
|
pruneOrphans(klass);
|
|
508
|
-
const n = injectBetween(klass,
|
|
530
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
509
531
|
if (n) decluster(klass);
|
|
510
532
|
return n;
|
|
511
533
|
};
|
|
@@ -518,13 +540,14 @@
|
|
|
518
540
|
'ezoic-ad-between', getTopics,
|
|
519
541
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
520
542
|
);
|
|
521
|
-
return exec(
|
|
543
|
+
if (kind === 'categories') return exec(
|
|
522
544
|
'ezoic-ad-categories', getCategories,
|
|
523
545
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
524
546
|
);
|
|
547
|
+
return 0;
|
|
525
548
|
}
|
|
526
549
|
|
|
527
|
-
// ── Scheduler
|
|
550
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
528
551
|
|
|
529
552
|
function scheduleRun(cb) {
|
|
530
553
|
if (S.runQueued) return;
|
|
@@ -534,7 +557,7 @@
|
|
|
534
557
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
535
558
|
let n = 0;
|
|
536
559
|
try { n = await runCore(); } catch (_) {}
|
|
537
|
-
cb?.(n);
|
|
560
|
+
try { cb?.(n); } catch (_) {}
|
|
538
561
|
});
|
|
539
562
|
}
|
|
540
563
|
|
|
@@ -542,8 +565,10 @@
|
|
|
542
565
|
if (isBlocked()) return;
|
|
543
566
|
const t = ts();
|
|
544
567
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
545
|
-
S.lastBurstTs
|
|
546
|
-
|
|
568
|
+
S.lastBurstTs = t;
|
|
569
|
+
|
|
570
|
+
const pk = pageKey();
|
|
571
|
+
S.pageKey = pk;
|
|
547
572
|
S.burstDeadline = t + 2000;
|
|
548
573
|
|
|
549
574
|
if (S.burstActive) return;
|
|
@@ -551,7 +576,7 @@
|
|
|
551
576
|
S.burstCount = 0;
|
|
552
577
|
|
|
553
578
|
const step = () => {
|
|
554
|
-
if (pageKey() !==
|
|
579
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
555
580
|
S.burstActive = false; return;
|
|
556
581
|
}
|
|
557
582
|
S.burstCount++;
|
|
@@ -563,13 +588,11 @@
|
|
|
563
588
|
step();
|
|
564
589
|
}
|
|
565
590
|
|
|
566
|
-
// ── Cleanup navigation
|
|
591
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
567
592
|
|
|
568
593
|
function cleanup() {
|
|
569
594
|
blockedUntil = ts() + 1500;
|
|
570
|
-
poolsReady = false;
|
|
571
595
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
572
|
-
wrapByKey.clear();
|
|
573
596
|
S.cfg = null;
|
|
574
597
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
575
598
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -582,19 +605,19 @@
|
|
|
582
605
|
S.runQueued = false;
|
|
583
606
|
}
|
|
584
607
|
|
|
585
|
-
// ──
|
|
608
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
586
609
|
|
|
587
610
|
function ensureDomObserver() {
|
|
588
611
|
if (S.domObs) return;
|
|
589
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
590
612
|
S.domObs = new MutationObserver(muts => {
|
|
591
|
-
// Ne rien faire pendant la navigation (cleanup posé blockedUntil)
|
|
592
|
-
// ou si c'est nous qui mutons le DOM.
|
|
593
613
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
594
614
|
for (const m of muts) {
|
|
615
|
+
if (!m.addedNodes?.length) continue;
|
|
595
616
|
for (const n of m.addedNodes) {
|
|
596
617
|
if (n.nodeType !== 1) continue;
|
|
597
|
-
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)) {
|
|
598
621
|
requestBurst(); return;
|
|
599
622
|
}
|
|
600
623
|
}
|
|
@@ -620,21 +643,29 @@
|
|
|
620
643
|
}
|
|
621
644
|
|
|
622
645
|
function ensureTcfLocator() {
|
|
623
|
-
//
|
|
624
|
-
// En navigation ajaxify, NodeBB peut
|
|
625
|
-
//
|
|
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.
|
|
626
652
|
try {
|
|
627
653
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
+
|
|
628
655
|
const inject = () => {
|
|
629
656
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
630
657
|
const f = document.createElement('iframe');
|
|
631
658
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
632
659
|
(document.body || document.documentElement).appendChild(f);
|
|
633
660
|
};
|
|
661
|
+
|
|
634
662
|
inject();
|
|
663
|
+
|
|
664
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
635
665
|
if (!window.__nbbTcfObs) {
|
|
636
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
637
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
666
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
667
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
+
{ childList: true, subtree: true });
|
|
638
669
|
}
|
|
639
670
|
} catch (_) {}
|
|
640
671
|
}
|
|
@@ -644,10 +675,10 @@
|
|
|
644
675
|
const head = document.head;
|
|
645
676
|
if (!head) return;
|
|
646
677
|
for (const [rel, href, cors] of [
|
|
647
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
648
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
649
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
650
|
-
['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],
|
|
651
682
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
652
683
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
653
684
|
]) {
|
|
@@ -661,7 +692,7 @@
|
|
|
661
692
|
}
|
|
662
693
|
}
|
|
663
694
|
|
|
664
|
-
// ── Bindings
|
|
695
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
665
696
|
|
|
666
697
|
function bindNodeBB() {
|
|
667
698
|
const $ = window.jQuery;
|
|
@@ -672,16 +703,19 @@
|
|
|
672
703
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
673
704
|
S.pageKey = pageKey();
|
|
674
705
|
blockedUntil = 0;
|
|
675
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
676
|
-
|
|
706
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
707
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
677
708
|
});
|
|
678
709
|
|
|
679
|
-
const
|
|
680
|
-
'action:ajaxify.contentLoaded',
|
|
710
|
+
const BURST_EVENTS = [
|
|
711
|
+
'action:ajaxify.contentLoaded',
|
|
712
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
681
713
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
682
714
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
683
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
684
715
|
|
|
716
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
+
|
|
718
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
685
719
|
try {
|
|
686
720
|
require(['hooks'], hooks => {
|
|
687
721
|
if (typeof hooks?.on !== 'function') return;
|