nodebb-plugin-ezoic-infinite 1.7.9 → 1.7.11
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 +204 -156
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 v22
|
|
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,11 +102,8 @@
|
|
|
96
102
|
};
|
|
97
103
|
|
|
98
104
|
let blockedUntil = 0;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
102
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
103
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
104
107
|
|
|
105
108
|
function mutate(fn) {
|
|
106
109
|
S.mutGuard++;
|
|
@@ -118,6 +121,12 @@
|
|
|
118
121
|
return S.cfg;
|
|
119
122
|
}
|
|
120
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
|
+
|
|
121
130
|
function parseIds(raw) {
|
|
122
131
|
const out = [], seen = new Set();
|
|
123
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -127,11 +136,12 @@
|
|
|
127
136
|
return out;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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; } };
|
|
135
145
|
|
|
136
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
137
147
|
|
|
@@ -155,13 +165,13 @@
|
|
|
155
165
|
return 'other';
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
159
169
|
|
|
160
170
|
function getPosts() {
|
|
161
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
162
172
|
if (!el.isConnected) return false;
|
|
163
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
164
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
165
175
|
if (p && p !== el) return false;
|
|
166
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
167
177
|
});
|
|
@@ -177,28 +187,36 @@
|
|
|
177
187
|
);
|
|
178
188
|
}
|
|
179
189
|
|
|
180
|
-
// ── Ancres stables
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
199
|
if (attr) {
|
|
185
200
|
const v = el.getAttribute(attr);
|
|
186
201
|
if (v !== null && v !== '') return v;
|
|
187
202
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 (_) {}
|
|
193
211
|
return 'i0';
|
|
194
212
|
}
|
|
195
213
|
|
|
196
|
-
const
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
197
215
|
|
|
198
|
-
function findWrap(
|
|
216
|
+
function findWrap(anchorKey) {
|
|
199
217
|
try {
|
|
200
218
|
return document.querySelector(
|
|
201
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
202
220
|
);
|
|
203
221
|
} catch (_) { return null; }
|
|
204
222
|
}
|
|
@@ -208,7 +226,7 @@
|
|
|
208
226
|
function pickId(poolKey) {
|
|
209
227
|
const pool = S.pools[poolKey];
|
|
210
228
|
for (let t = 0; t < pool.length; t++) {
|
|
211
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
212
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
213
231
|
const id = pool[i];
|
|
214
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -219,7 +237,7 @@
|
|
|
219
237
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
220
238
|
|
|
221
239
|
function makeWrap(id, klass, key) {
|
|
222
|
-
const w
|
|
240
|
+
const w = document.createElement('div');
|
|
223
241
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
224
242
|
w.setAttribute(A_ANCHOR, key);
|
|
225
243
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -233,10 +251,10 @@
|
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
function insertAfter(el, id, klass, key) {
|
|
236
|
-
if (!el?.insertAdjacentElement)
|
|
237
|
-
if (findWrap(key))
|
|
238
|
-
if (S.mountedIds.has(id))
|
|
239
|
-
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;
|
|
240
258
|
const w = makeWrap(id, klass, key);
|
|
241
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
242
260
|
S.mountedIds.add(id);
|
|
@@ -245,12 +263,15 @@
|
|
|
245
263
|
|
|
246
264
|
function dropWrap(w) {
|
|
247
265
|
try {
|
|
248
|
-
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
249
|
-
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
250
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
251
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
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,42 +279,50 @@
|
|
|
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.
|
|
262
283
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* -
|
|
267
|
-
*
|
|
268
|
-
*
|
|
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
|
|
289
|
+
*
|
|
290
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
269
291
|
*/
|
|
270
292
|
function pruneOrphans(klass) {
|
|
271
293
|
const meta = KIND[klass];
|
|
272
294
|
if (!meta) return;
|
|
273
295
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
296
|
+
// baseTag déduit du sélecteur : 'li' pour topics/catégories, '' pour posts.
|
|
297
|
+
// Pour les posts (baseTag=''), on cherche juste [data-pid="X"] sans préfixe
|
|
298
|
+
// — c'est correct car data-pid est unique dans le DOM.
|
|
299
|
+
const baseTag = meta.sel.split('[')[0];
|
|
300
|
+
|
|
301
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
302
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
303
|
+
if (isFilled(w)) return; // jamais supprimer un wrap rempli
|
|
277
304
|
|
|
278
305
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
279
306
|
const sid = key.slice(klass.length + 1);
|
|
280
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
307
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
281
308
|
|
|
282
|
-
const
|
|
283
|
-
|
|
309
|
+
const anchorEl = document.querySelector(
|
|
310
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
311
|
+
);
|
|
284
312
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
285
|
-
}
|
|
313
|
+
});
|
|
286
314
|
}
|
|
287
315
|
|
|
288
316
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
289
317
|
|
|
290
318
|
/**
|
|
291
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
292
|
-
* Priorité : filled > en grâce
|
|
293
|
-
*
|
|
319
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
320
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
321
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
294
322
|
*/
|
|
295
323
|
function decluster(klass) {
|
|
296
324
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
325
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
297
326
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
298
327
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
299
328
|
|
|
@@ -302,11 +331,10 @@
|
|
|
302
331
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
303
332
|
|
|
304
333
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
305
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
334
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
306
335
|
|
|
307
336
|
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
308
337
|
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
309
|
-
// les deux remplis → on ne touche pas
|
|
310
338
|
break;
|
|
311
339
|
}
|
|
312
340
|
}
|
|
@@ -316,16 +344,14 @@
|
|
|
316
344
|
|
|
317
345
|
/**
|
|
318
346
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
347
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
348
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
321
349
|
*/
|
|
322
350
|
function ordinal(klass, el) {
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
// Fallback positionnel — compte uniquement les éléments du même type
|
|
351
|
+
const di = el.getAttribute('data-index');
|
|
352
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
353
|
+
// Fallback positionnel — filtre par sélecteur complet pour éviter le bug
|
|
354
|
+
// baseTag='' (posts) où `:scope > ` sans tag ne fonctionne pas.
|
|
329
355
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
330
356
|
let i = 0;
|
|
331
357
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -340,18 +366,20 @@
|
|
|
340
366
|
let inserted = 0;
|
|
341
367
|
|
|
342
368
|
for (const el of items) {
|
|
343
|
-
if (inserted >=
|
|
369
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
344
370
|
if (!el?.isConnected) continue;
|
|
345
371
|
|
|
346
|
-
const ord
|
|
347
|
-
|
|
372
|
+
const ord = ordinal(klass, el);
|
|
373
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
374
|
+
if (!isTarget) continue;
|
|
375
|
+
|
|
348
376
|
if (adjacentWrap(el)) continue;
|
|
349
377
|
|
|
350
|
-
const key =
|
|
351
|
-
if (findWrap(key)) continue;
|
|
378
|
+
const key = makeAnchorKey(klass, el);
|
|
379
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
352
380
|
|
|
353
381
|
const id = pickId(poolKey);
|
|
354
|
-
if (!id) continue;
|
|
382
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
355
383
|
|
|
356
384
|
const w = insertAfter(el, id, klass, key);
|
|
357
385
|
if (w) { observePh(id); inserted++; }
|
|
@@ -363,6 +391,7 @@
|
|
|
363
391
|
|
|
364
392
|
function getIO() {
|
|
365
393
|
if (S.io) return S.io;
|
|
394
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
366
395
|
try {
|
|
367
396
|
S.io = new IntersectionObserver(entries => {
|
|
368
397
|
for (const e of entries) {
|
|
@@ -371,7 +400,7 @@
|
|
|
371
400
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
372
401
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
373
402
|
}
|
|
374
|
-
}, { root: null, rootMargin:
|
|
403
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
375
404
|
} catch (_) { S.io = null; }
|
|
376
405
|
return S.io;
|
|
377
406
|
}
|
|
@@ -422,6 +451,7 @@
|
|
|
422
451
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
423
452
|
S.lastShow.set(id, t);
|
|
424
453
|
|
|
454
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
425
455
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
426
456
|
|
|
427
457
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -442,6 +472,7 @@
|
|
|
442
472
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
443
473
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
444
474
|
if (!wrap || !ph?.isConnected) return;
|
|
475
|
+
// Un show plus récent → ne pas toucher
|
|
445
476
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
446
477
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
447
478
|
} catch (_) {}
|
|
@@ -460,7 +491,7 @@
|
|
|
460
491
|
const orig = ez.showAds.bind(ez);
|
|
461
492
|
ez.showAds = function (...args) {
|
|
462
493
|
if (isBlocked()) return;
|
|
463
|
-
const ids
|
|
494
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
464
495
|
const seen = new Set();
|
|
465
496
|
for (const v of ids) {
|
|
466
497
|
const id = parseInt(v, 10);
|
|
@@ -479,7 +510,7 @@
|
|
|
479
510
|
}
|
|
480
511
|
}
|
|
481
512
|
|
|
482
|
-
// ── Core
|
|
513
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
483
514
|
|
|
484
515
|
async function runCore() {
|
|
485
516
|
if (isBlocked()) return 0;
|
|
@@ -494,9 +525,10 @@
|
|
|
494
525
|
|
|
495
526
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
496
527
|
if (!normBool(cfgEnable)) return 0;
|
|
528
|
+
const items = getItems();
|
|
497
529
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
498
530
|
pruneOrphans(klass);
|
|
499
|
-
const n = injectBetween(klass,
|
|
531
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
500
532
|
if (n) decluster(klass);
|
|
501
533
|
return n;
|
|
502
534
|
};
|
|
@@ -509,13 +541,14 @@
|
|
|
509
541
|
'ezoic-ad-between', getTopics,
|
|
510
542
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
511
543
|
);
|
|
512
|
-
return exec(
|
|
544
|
+
if (kind === 'categories') return exec(
|
|
513
545
|
'ezoic-ad-categories', getCategories,
|
|
514
546
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
515
547
|
);
|
|
548
|
+
return 0;
|
|
516
549
|
}
|
|
517
550
|
|
|
518
|
-
// ── Scheduler
|
|
551
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
519
552
|
|
|
520
553
|
function scheduleRun(cb) {
|
|
521
554
|
if (S.runQueued) return;
|
|
@@ -525,7 +558,7 @@
|
|
|
525
558
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
526
559
|
let n = 0;
|
|
527
560
|
try { n = await runCore(); } catch (_) {}
|
|
528
|
-
cb?.(n);
|
|
561
|
+
try { cb?.(n); } catch (_) {}
|
|
529
562
|
});
|
|
530
563
|
}
|
|
531
564
|
|
|
@@ -533,8 +566,10 @@
|
|
|
533
566
|
if (isBlocked()) return;
|
|
534
567
|
const t = ts();
|
|
535
568
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
536
|
-
S.lastBurstTs
|
|
537
|
-
|
|
569
|
+
S.lastBurstTs = t;
|
|
570
|
+
|
|
571
|
+
const pk = pageKey();
|
|
572
|
+
S.pageKey = pk;
|
|
538
573
|
S.burstDeadline = t + 2000;
|
|
539
574
|
|
|
540
575
|
if (S.burstActive) return;
|
|
@@ -542,7 +577,7 @@
|
|
|
542
577
|
S.burstCount = 0;
|
|
543
578
|
|
|
544
579
|
const step = () => {
|
|
545
|
-
if (pageKey() !==
|
|
580
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
546
581
|
S.burstActive = false; return;
|
|
547
582
|
}
|
|
548
583
|
S.burstCount++;
|
|
@@ -554,7 +589,7 @@
|
|
|
554
589
|
step();
|
|
555
590
|
}
|
|
556
591
|
|
|
557
|
-
// ── Cleanup navigation
|
|
592
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
558
593
|
|
|
559
594
|
function cleanup() {
|
|
560
595
|
blockedUntil = ts() + 1500;
|
|
@@ -571,17 +606,19 @@
|
|
|
571
606
|
S.runQueued = false;
|
|
572
607
|
}
|
|
573
608
|
|
|
574
|
-
// ──
|
|
609
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
575
610
|
|
|
576
611
|
function ensureDomObserver() {
|
|
577
612
|
if (S.domObs) return;
|
|
578
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
579
613
|
S.domObs = new MutationObserver(muts => {
|
|
580
614
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
581
615
|
for (const m of muts) {
|
|
616
|
+
if (!m.addedNodes?.length) continue;
|
|
582
617
|
for (const n of m.addedNodes) {
|
|
583
618
|
if (n.nodeType !== 1) continue;
|
|
584
|
-
if (
|
|
619
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
620
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
621
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
585
622
|
requestBurst(); return;
|
|
586
623
|
}
|
|
587
624
|
}
|
|
@@ -607,21 +644,29 @@
|
|
|
607
644
|
}
|
|
608
645
|
|
|
609
646
|
function ensureTcfLocator() {
|
|
610
|
-
//
|
|
611
|
-
// En navigation ajaxify, NodeBB peut
|
|
612
|
-
//
|
|
647
|
+
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
648
|
+
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
649
|
+
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
650
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
651
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
652
|
+
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
613
653
|
try {
|
|
614
654
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
655
|
+
|
|
615
656
|
const inject = () => {
|
|
616
657
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
617
658
|
const f = document.createElement('iframe');
|
|
618
659
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
619
660
|
(document.body || document.documentElement).appendChild(f);
|
|
620
661
|
};
|
|
662
|
+
|
|
621
663
|
inject();
|
|
664
|
+
|
|
665
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
622
666
|
if (!window.__nbbTcfObs) {
|
|
623
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
667
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
668
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
669
|
+
{ childList: true, subtree: true });
|
|
625
670
|
}
|
|
626
671
|
} catch (_) {}
|
|
627
672
|
}
|
|
@@ -631,10 +676,10 @@
|
|
|
631
676
|
const head = document.head;
|
|
632
677
|
if (!head) return;
|
|
633
678
|
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
|
|
679
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
680
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
681
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
682
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
638
683
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
639
684
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
640
685
|
]) {
|
|
@@ -648,7 +693,7 @@
|
|
|
648
693
|
}
|
|
649
694
|
}
|
|
650
695
|
|
|
651
|
-
// ── Bindings
|
|
696
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
652
697
|
|
|
653
698
|
function bindNodeBB() {
|
|
654
699
|
const $ = window.jQuery;
|
|
@@ -659,16 +704,19 @@
|
|
|
659
704
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
660
705
|
S.pageKey = pageKey();
|
|
661
706
|
blockedUntil = 0;
|
|
662
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
-
|
|
707
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
708
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
664
709
|
});
|
|
665
710
|
|
|
666
|
-
const
|
|
667
|
-
'action:ajaxify.contentLoaded',
|
|
711
|
+
const BURST_EVENTS = [
|
|
712
|
+
'action:ajaxify.contentLoaded',
|
|
713
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
668
714
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
669
715
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
671
716
|
|
|
717
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
718
|
+
|
|
719
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
672
720
|
try {
|
|
673
721
|
require(['hooks'], hooks => {
|
|
674
722
|
if (typeof hooks?.on !== 'function') return;
|