nodebb-plugin-ezoic-infinite 1.7.14 → 1.7.16
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 +213 -176
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,51 +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
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
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)
|
|
49
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
50
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
51
55
|
|
|
@@ -56,40 +60,40 @@
|
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
60
64
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* data-cid catégories (id catégorie, immuable)
|
|
68
|
-
* ordinalAttr: attribut 0-based pour le calcul de l'intervalle
|
|
69
|
-
* data-index posts + topics (fourni par NodeBB)
|
|
70
|
-
* 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
|
|
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,12 +263,15 @@
|
|
|
247
263
|
|
|
248
264
|
function dropWrap(w) {
|
|
249
265
|
try {
|
|
250
|
-
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
251
|
-
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
252
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
253
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
254
266
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
255
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 (_) {}
|
|
256
275
|
w.remove();
|
|
257
276
|
} catch (_) {}
|
|
258
277
|
}
|
|
@@ -260,63 +279,58 @@
|
|
|
260
279
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
261
280
|
|
|
262
281
|
/**
|
|
263
|
-
* 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
|
|
264
289
|
*
|
|
265
|
-
* On ne
|
|
266
|
-
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
267
|
-
* virtualisation NodeBB — l'ancre reviendra.
|
|
268
|
-
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
269
|
-
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
270
|
-
* 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é).
|
|
271
291
|
*/
|
|
272
292
|
function pruneOrphans(klass) {
|
|
273
293
|
const meta = KIND[klass];
|
|
274
294
|
if (!meta) return;
|
|
275
295
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// peut avoir temporairement disparu du DOM par virtualisation NodeBB au
|
|
281
|
-
// scroll up — ce n'est pas un vrai orphelin.
|
|
282
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
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;
|
|
283
300
|
|
|
284
301
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
285
|
-
const sid = key.slice(klass.length + 1);
|
|
286
|
-
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; }
|
|
287
304
|
|
|
288
|
-
const
|
|
289
|
-
|
|
305
|
+
const anchorEl = document.querySelector(
|
|
306
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
+
);
|
|
290
308
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
291
|
-
}
|
|
309
|
+
});
|
|
292
310
|
}
|
|
293
311
|
|
|
294
312
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
295
313
|
|
|
296
314
|
/**
|
|
297
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
298
|
-
* Priorité : filled > en grâce
|
|
299
|
-
*
|
|
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.
|
|
300
318
|
*/
|
|
301
319
|
function decluster(klass) {
|
|
302
320
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
303
|
-
//
|
|
304
|
-
if (isFilled(w)) continue;
|
|
305
|
-
// Protéger par A_CREATED : un wrap récent attend encore son showAds async
|
|
306
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
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
|
|
|
310
325
|
let prev = w.previousElementSibling, steps = 0;
|
|
311
326
|
while (prev && steps++ < 3) {
|
|
312
327
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
313
|
-
|
|
314
|
-
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
328
|
+
|
|
315
329
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
316
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
330
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
317
331
|
|
|
318
|
-
|
|
319
|
-
mutate(() => dropWrap(
|
|
332
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
333
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
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,7 +509,7 @@
|
|
|
489
509
|
}
|
|
490
510
|
}
|
|
491
511
|
|
|
492
|
-
// ── Core
|
|
512
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
493
513
|
|
|
494
514
|
async function runCore() {
|
|
495
515
|
if (isBlocked()) return 0;
|
|
@@ -504,9 +524,10 @@
|
|
|
504
524
|
|
|
505
525
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
506
526
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
+
const items = getItems();
|
|
507
528
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
508
529
|
pruneOrphans(klass);
|
|
509
|
-
const n = injectBetween(klass,
|
|
530
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
510
531
|
if (n) decluster(klass);
|
|
511
532
|
return n;
|
|
512
533
|
};
|
|
@@ -519,13 +540,14 @@
|
|
|
519
540
|
'ezoic-ad-between', getTopics,
|
|
520
541
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
521
542
|
);
|
|
522
|
-
return exec(
|
|
543
|
+
if (kind === 'categories') return exec(
|
|
523
544
|
'ezoic-ad-categories', getCategories,
|
|
524
545
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
525
546
|
);
|
|
547
|
+
return 0;
|
|
526
548
|
}
|
|
527
549
|
|
|
528
|
-
// ── Scheduler
|
|
550
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
529
551
|
|
|
530
552
|
function scheduleRun(cb) {
|
|
531
553
|
if (S.runQueued) return;
|
|
@@ -535,7 +557,7 @@
|
|
|
535
557
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
536
558
|
let n = 0;
|
|
537
559
|
try { n = await runCore(); } catch (_) {}
|
|
538
|
-
cb?.(n);
|
|
560
|
+
try { cb?.(n); } catch (_) {}
|
|
539
561
|
});
|
|
540
562
|
}
|
|
541
563
|
|
|
@@ -543,8 +565,10 @@
|
|
|
543
565
|
if (isBlocked()) return;
|
|
544
566
|
const t = ts();
|
|
545
567
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
546
|
-
S.lastBurstTs
|
|
547
|
-
|
|
568
|
+
S.lastBurstTs = t;
|
|
569
|
+
|
|
570
|
+
const pk = pageKey();
|
|
571
|
+
S.pageKey = pk;
|
|
548
572
|
S.burstDeadline = t + 2000;
|
|
549
573
|
|
|
550
574
|
if (S.burstActive) return;
|
|
@@ -552,7 +576,7 @@
|
|
|
552
576
|
S.burstCount = 0;
|
|
553
577
|
|
|
554
578
|
const step = () => {
|
|
555
|
-
if (pageKey() !==
|
|
579
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
556
580
|
S.burstActive = false; return;
|
|
557
581
|
}
|
|
558
582
|
S.burstCount++;
|
|
@@ -564,7 +588,7 @@
|
|
|
564
588
|
step();
|
|
565
589
|
}
|
|
566
590
|
|
|
567
|
-
// ── Cleanup navigation
|
|
591
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
568
592
|
|
|
569
593
|
function cleanup() {
|
|
570
594
|
blockedUntil = ts() + 1500;
|
|
@@ -581,17 +605,19 @@
|
|
|
581
605
|
S.runQueued = false;
|
|
582
606
|
}
|
|
583
607
|
|
|
584
|
-
// ──
|
|
608
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
585
609
|
|
|
586
610
|
function ensureDomObserver() {
|
|
587
611
|
if (S.domObs) return;
|
|
588
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
589
612
|
S.domObs = new MutationObserver(muts => {
|
|
590
613
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
591
614
|
for (const m of muts) {
|
|
615
|
+
if (!m.addedNodes?.length) continue;
|
|
592
616
|
for (const n of m.addedNodes) {
|
|
593
617
|
if (n.nodeType !== 1) continue;
|
|
594
|
-
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)) {
|
|
595
621
|
requestBurst(); return;
|
|
596
622
|
}
|
|
597
623
|
}
|
|
@@ -617,21 +643,29 @@
|
|
|
617
643
|
}
|
|
618
644
|
|
|
619
645
|
function ensureTcfLocator() {
|
|
620
|
-
//
|
|
621
|
-
// En navigation ajaxify, NodeBB peut
|
|
622
|
-
//
|
|
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.
|
|
623
652
|
try {
|
|
624
653
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
+
|
|
625
655
|
const inject = () => {
|
|
626
656
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
627
657
|
const f = document.createElement('iframe');
|
|
628
658
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
629
659
|
(document.body || document.documentElement).appendChild(f);
|
|
630
660
|
};
|
|
661
|
+
|
|
631
662
|
inject();
|
|
663
|
+
|
|
664
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
632
665
|
if (!window.__nbbTcfObs) {
|
|
633
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
634
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
666
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
667
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
+
{ childList: true, subtree: true });
|
|
635
669
|
}
|
|
636
670
|
} catch (_) {}
|
|
637
671
|
}
|
|
@@ -641,10 +675,10 @@
|
|
|
641
675
|
const head = document.head;
|
|
642
676
|
if (!head) return;
|
|
643
677
|
for (const [rel, href, cors] of [
|
|
644
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
645
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
646
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
647
|
-
['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],
|
|
648
682
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
649
683
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
650
684
|
]) {
|
|
@@ -658,7 +692,7 @@
|
|
|
658
692
|
}
|
|
659
693
|
}
|
|
660
694
|
|
|
661
|
-
// ── Bindings
|
|
695
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
662
696
|
|
|
663
697
|
function bindNodeBB() {
|
|
664
698
|
const $ = window.jQuery;
|
|
@@ -669,16 +703,19 @@
|
|
|
669
703
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
670
704
|
S.pageKey = pageKey();
|
|
671
705
|
blockedUntil = 0;
|
|
672
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
673
|
-
|
|
706
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
707
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
674
708
|
});
|
|
675
709
|
|
|
676
|
-
const
|
|
677
|
-
'action:ajaxify.contentLoaded',
|
|
710
|
+
const BURST_EVENTS = [
|
|
711
|
+
'action:ajaxify.contentLoaded',
|
|
712
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
678
713
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
679
714
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
680
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
681
715
|
|
|
716
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
+
|
|
718
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
682
719
|
try {
|
|
683
720
|
require(['hooks'], hooks => {
|
|
684
721
|
if (typeof hooks?.on !== 'function') return;
|