nodebb-plugin-ezoic-infinite 1.7.12 → 1.7.13
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 +192 -139
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NodeBB Ezoic Infinite Ads — client.js v23
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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).
|
|
12
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
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
|
+
*
|
|
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é)
|
|
12
31
|
*/
|
|
13
32
|
(function () {
|
|
14
33
|
'use strict';
|
|
15
34
|
|
|
16
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
17
36
|
|
|
18
|
-
const WRAP_CLASS
|
|
19
|
-
const PH_PREFIX
|
|
20
|
-
const A_ANCHOR
|
|
21
|
-
const A_WRAPID
|
|
22
|
-
const A_CREATED
|
|
23
|
-
const A_SHOWN
|
|
24
|
-
|
|
25
|
-
const MIN_PRUNE_AGE_MS
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
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
|
|
29
47
|
const MAX_INSERTS_PER_RUN = 6;
|
|
30
48
|
const MAX_INFLIGHT = 4;
|
|
31
49
|
const SHOW_THROTTLE_MS = 900;
|
|
32
50
|
const BURST_COOLDOWN_MS = 200;
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
const
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
53
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
36
55
|
|
|
37
56
|
const SEL = {
|
|
38
57
|
post: '[component="post"][data-pid]',
|
|
@@ -41,53 +60,50 @@
|
|
|
41
60
|
};
|
|
42
61
|
|
|
43
62
|
/**
|
|
44
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
45
64
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* data-
|
|
49
|
-
* data-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
52
71
|
*/
|
|
53
72
|
const KIND = {
|
|
54
|
-
'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid'
|
|
55
|
-
'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index'
|
|
56
|
-
'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid'
|
|
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' },
|
|
57
76
|
};
|
|
58
77
|
|
|
59
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
60
79
|
|
|
61
80
|
const S = {
|
|
62
|
-
pageKey:
|
|
63
|
-
cfg:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
|
|
97
|
+
runQueued: false,
|
|
98
|
+
burstActive: false,
|
|
76
99
|
burstDeadline: 0,
|
|
77
|
-
burstCount:
|
|
78
|
-
lastBurstTs:
|
|
100
|
+
burstCount: 0,
|
|
101
|
+
lastBurstTs: 0,
|
|
79
102
|
};
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
let poolsReady = false; // initPools une seule fois par page
|
|
85
|
-
|
|
86
|
-
const ts = () => Date.now();
|
|
87
|
-
const isBlocked = () => ts() < blockedUntil;
|
|
88
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
89
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
90
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
104
|
+
let blockedUntil = 0;
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
91
107
|
|
|
92
108
|
function mutate(fn) {
|
|
93
109
|
S.mutGuard++;
|
|
@@ -105,6 +121,12 @@
|
|
|
105
121
|
return S.cfg;
|
|
106
122
|
}
|
|
107
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
|
+
|
|
108
130
|
function parseIds(raw) {
|
|
109
131
|
const out = [], seen = new Set();
|
|
110
132
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -114,13 +136,12 @@
|
|
|
114
136
|
return out;
|
|
115
137
|
}
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
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; } };
|
|
124
145
|
|
|
125
146
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
126
147
|
|
|
@@ -144,13 +165,13 @@
|
|
|
144
165
|
return 'other';
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
// ──
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
148
169
|
|
|
149
170
|
function getPosts() {
|
|
150
171
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
151
172
|
if (!el.isConnected) return false;
|
|
152
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
153
|
-
const p = el.parentElement?.closest(
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
154
175
|
if (p && p !== el) return false;
|
|
155
176
|
return el.getAttribute('component') !== 'post/parent';
|
|
156
177
|
});
|
|
@@ -166,29 +187,38 @@
|
|
|
166
187
|
);
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
// ── Ancres stables
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
170
191
|
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
199
|
if (attr) {
|
|
174
200
|
const v = el.getAttribute(attr);
|
|
175
201
|
if (v !== null && v !== '') return v;
|
|
176
202
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 (_) {}
|
|
182
211
|
return 'i0';
|
|
183
212
|
}
|
|
184
213
|
|
|
185
214
|
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
186
215
|
|
|
187
|
-
function findWrap(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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; }
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
@@ -196,7 +226,7 @@
|
|
|
196
226
|
function pickId(poolKey) {
|
|
197
227
|
const pool = S.pools[poolKey];
|
|
198
228
|
for (let t = 0; t < pool.length; t++) {
|
|
199
|
-
const i
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
200
230
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
201
231
|
const id = pool[i];
|
|
202
232
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -221,14 +251,13 @@
|
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
function insertAfter(el, id, klass, key) {
|
|
224
|
-
if (!el?.insertAdjacentElement)
|
|
225
|
-
if (findWrap(key))
|
|
226
|
-
if (S.mountedIds.has(id))
|
|
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é
|
|
227
257
|
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
228
258
|
const w = makeWrap(id, klass, key);
|
|
229
259
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
230
260
|
S.mountedIds.add(id);
|
|
231
|
-
wrapByKey.set(key, w);
|
|
232
261
|
return w;
|
|
233
262
|
}
|
|
234
263
|
|
|
@@ -236,9 +265,9 @@
|
|
|
236
265
|
try {
|
|
237
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
238
267
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
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).
|
|
242
271
|
try {
|
|
243
272
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
244
273
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
@@ -250,58 +279,65 @@
|
|
|
250
279
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
251
280
|
|
|
252
281
|
/**
|
|
253
|
-
* 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
|
|
254
289
|
*
|
|
255
|
-
*
|
|
256
|
-
* 1. Pas avant MIN_PRUNE_AGE_MS (DOM post-batch pas encore stabilisé)
|
|
257
|
-
* 2. Jamais si filled (pub affichée — SDK Ezoic a des callbacks async dessus)
|
|
258
|
-
* 3. Pas avant INJECT_GRACE_MS depuis création (fill Ezoic peut être très async)
|
|
290
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
259
291
|
*/
|
|
260
292
|
function pruneOrphans(klass) {
|
|
261
293
|
const meta = KIND[klass];
|
|
262
294
|
if (!meta) return;
|
|
263
295
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
304
|
|
|
270
305
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
271
306
|
const sid = key.slice(klass.length + 1);
|
|
272
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
307
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
273
308
|
|
|
274
|
-
const
|
|
275
|
-
|
|
309
|
+
const anchorEl = document.querySelector(
|
|
310
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
311
|
+
);
|
|
276
312
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
277
|
-
}
|
|
313
|
+
});
|
|
278
314
|
}
|
|
279
315
|
|
|
280
316
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
281
317
|
|
|
282
318
|
/**
|
|
283
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
284
|
-
*
|
|
319
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
285
320
|
* Règles absolues :
|
|
286
|
-
* - Jamais supprimer un wrap filled
|
|
287
|
-
* - Jamais supprimer un wrap <
|
|
288
|
-
*
|
|
289
|
-
*
|
|
321
|
+
* - Jamais supprimer un wrap filled (pub affichée)
|
|
322
|
+
* - Jamais supprimer un wrap < FILL_GRACE_MS depuis création OU depuis showAds
|
|
323
|
+
* (le fill Ezoic est async : l'enchère SSP peut prendre plusieurs secondes
|
|
324
|
+
* après showAds, et showAds lui-même peut arriver bien après l'injection)
|
|
325
|
+
* - Si les deux sont filled → on ne touche rien
|
|
290
326
|
*/
|
|
291
327
|
function decluster(klass) {
|
|
292
328
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
293
|
-
if (isFilled(w)) continue;
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
329
|
+
if (isFilled(w)) continue; // filled = intouchable
|
|
330
|
+
const wCreated = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
331
|
+
if (ts() - wCreated < FILL_GRACE_MS) continue; // trop récent
|
|
296
332
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
297
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
333
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue; // showAds récent
|
|
298
334
|
|
|
299
335
|
let prev = w.previousElementSibling, steps = 0;
|
|
300
336
|
while (prev && steps++ < 3) {
|
|
301
337
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
302
|
-
if (isFilled(prev)) break;
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
338
|
+
if (isFilled(prev)) break; // précédent filled = intouchable
|
|
339
|
+
const pCreated = parseInt(prev.getAttribute(A_CREATED) || '0', 10);
|
|
340
|
+
if (ts() - pCreated < FILL_GRACE_MS) break;
|
|
305
341
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
306
342
|
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
307
343
|
|
|
@@ -316,13 +352,14 @@
|
|
|
316
352
|
|
|
317
353
|
/**
|
|
318
354
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
355
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
356
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
321
357
|
*/
|
|
322
358
|
function ordinal(klass, el) {
|
|
323
359
|
const di = el.getAttribute('data-index');
|
|
324
360
|
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
325
|
-
// Fallback —
|
|
361
|
+
// Fallback positionnel — filtre par sélecteur complet pour éviter le bug
|
|
362
|
+
// baseTag='' (posts) où `:scope > ` sans tag ne fonctionne pas.
|
|
326
363
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
327
364
|
let i = 0;
|
|
328
365
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -340,16 +377,17 @@
|
|
|
340
377
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
341
378
|
if (!el?.isConnected) continue;
|
|
342
379
|
|
|
343
|
-
const ord
|
|
344
|
-
|
|
380
|
+
const ord = ordinal(klass, el);
|
|
381
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
382
|
+
if (!isTarget) continue;
|
|
345
383
|
|
|
346
384
|
if (adjacentWrap(el)) continue;
|
|
347
385
|
|
|
348
386
|
const key = makeAnchorKey(klass, el);
|
|
349
|
-
if (findWrap(key)) continue;
|
|
387
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
350
388
|
|
|
351
389
|
const id = pickId(poolKey);
|
|
352
|
-
if (!id) continue;
|
|
390
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
353
391
|
|
|
354
392
|
const w = insertAfter(el, id, klass, key);
|
|
355
393
|
if (w) { observePh(id); inserted++; }
|
|
@@ -361,6 +399,7 @@
|
|
|
361
399
|
|
|
362
400
|
function getIO() {
|
|
363
401
|
if (S.io) return S.io;
|
|
402
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
364
403
|
try {
|
|
365
404
|
S.io = new IntersectionObserver(entries => {
|
|
366
405
|
for (const e of entries) {
|
|
@@ -369,7 +408,7 @@
|
|
|
369
408
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
370
409
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
371
410
|
}
|
|
372
|
-
}, { root: null, rootMargin:
|
|
411
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
373
412
|
} catch (_) { S.io = null; }
|
|
374
413
|
return S.io;
|
|
375
414
|
}
|
|
@@ -420,6 +459,7 @@
|
|
|
420
459
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
421
460
|
S.lastShow.set(id, t);
|
|
422
461
|
|
|
462
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
423
463
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
424
464
|
|
|
425
465
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -440,6 +480,7 @@
|
|
|
440
480
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
441
481
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
442
482
|
if (!wrap || !ph?.isConnected) return;
|
|
483
|
+
// Un show plus récent → ne pas toucher
|
|
443
484
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
444
485
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
445
486
|
} catch (_) {}
|
|
@@ -458,7 +499,7 @@
|
|
|
458
499
|
const orig = ez.showAds.bind(ez);
|
|
459
500
|
ez.showAds = function (...args) {
|
|
460
501
|
if (isBlocked()) return;
|
|
461
|
-
const ids
|
|
502
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
462
503
|
const seen = new Set();
|
|
463
504
|
for (const v of ids) {
|
|
464
505
|
const id = parseInt(v, 10);
|
|
@@ -477,10 +518,11 @@
|
|
|
477
518
|
}
|
|
478
519
|
}
|
|
479
520
|
|
|
480
|
-
// ── Core
|
|
521
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
481
522
|
|
|
482
523
|
async function runCore() {
|
|
483
524
|
if (isBlocked()) return 0;
|
|
525
|
+
patchShowAds();
|
|
484
526
|
|
|
485
527
|
const cfg = await fetchConfig();
|
|
486
528
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -491,9 +533,10 @@
|
|
|
491
533
|
|
|
492
534
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
493
535
|
if (!normBool(cfgEnable)) return 0;
|
|
536
|
+
const items = getItems();
|
|
494
537
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
495
538
|
pruneOrphans(klass);
|
|
496
|
-
const n = injectBetween(klass,
|
|
539
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
497
540
|
if (n) decluster(klass);
|
|
498
541
|
return n;
|
|
499
542
|
};
|
|
@@ -534,7 +577,7 @@
|
|
|
534
577
|
S.lastBurstTs = t;
|
|
535
578
|
|
|
536
579
|
const pk = pageKey();
|
|
537
|
-
S.pageKey
|
|
580
|
+
S.pageKey = pk;
|
|
538
581
|
S.burstDeadline = t + 2000;
|
|
539
582
|
|
|
540
583
|
if (S.burstActive) return;
|
|
@@ -554,13 +597,11 @@
|
|
|
554
597
|
step();
|
|
555
598
|
}
|
|
556
599
|
|
|
557
|
-
// ── Cleanup navigation
|
|
600
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
558
601
|
|
|
559
602
|
function cleanup() {
|
|
560
603
|
blockedUntil = ts() + 1500;
|
|
561
|
-
poolsReady = false;
|
|
562
604
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
563
|
-
wrapByKey.clear();
|
|
564
605
|
S.cfg = null;
|
|
565
606
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
566
607
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -577,14 +618,15 @@
|
|
|
577
618
|
|
|
578
619
|
function ensureDomObserver() {
|
|
579
620
|
if (S.domObs) return;
|
|
580
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
581
621
|
S.domObs = new MutationObserver(muts => {
|
|
582
622
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
583
623
|
for (const m of muts) {
|
|
584
624
|
if (!m.addedNodes?.length) continue;
|
|
585
625
|
for (const n of m.addedNodes) {
|
|
586
626
|
if (n.nodeType !== 1) continue;
|
|
587
|
-
if (
|
|
627
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
628
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
629
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
588
630
|
requestBurst(); return;
|
|
589
631
|
}
|
|
590
632
|
}
|
|
@@ -610,20 +652,29 @@
|
|
|
610
652
|
}
|
|
611
653
|
|
|
612
654
|
function ensureTcfLocator() {
|
|
613
|
-
//
|
|
614
|
-
// En navigation ajaxify, NodeBB peut
|
|
655
|
+
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
656
|
+
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
657
|
+
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
658
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
659
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
660
|
+
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
615
661
|
try {
|
|
616
662
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
663
|
+
|
|
617
664
|
const inject = () => {
|
|
618
665
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
619
666
|
const f = document.createElement('iframe');
|
|
620
667
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
621
668
|
(document.body || document.documentElement).appendChild(f);
|
|
622
669
|
};
|
|
670
|
+
|
|
623
671
|
inject();
|
|
672
|
+
|
|
673
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
624
674
|
if (!window.__nbbTcfObs) {
|
|
625
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
626
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
675
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
676
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
677
|
+
{ childList: true, subtree: true });
|
|
627
678
|
}
|
|
628
679
|
} catch (_) {}
|
|
629
680
|
}
|
|
@@ -633,10 +684,10 @@
|
|
|
633
684
|
const head = document.head;
|
|
634
685
|
if (!head) return;
|
|
635
686
|
for (const [rel, href, cors] of [
|
|
636
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
637
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
638
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
639
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true
|
|
687
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
688
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
689
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
690
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
640
691
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
641
692
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
642
693
|
]) {
|
|
@@ -670,8 +721,10 @@
|
|
|
670
721
|
'action:posts.loaded', 'action:topics.loaded',
|
|
671
722
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
672
723
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
724
|
+
|
|
673
725
|
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
674
726
|
|
|
727
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
675
728
|
try {
|
|
676
729
|
require(['hooks'], hooks => {
|
|
677
730
|
if (typeof hooks?.on !== 'function') return;
|