nodebb-plugin-ezoic-infinite 1.7.22 → 1.7.24
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 +214 -249
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v30
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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).
|
|
4
|
+
* Historique des corrections majeures
|
|
5
|
+
* ────────────────────────────────────
|
|
6
|
+
* v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
|
|
7
|
+
* Suppression du recyclage de wraps. Cleanup complet navigation.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
|
|
10
|
+
* la position dans le batch courant.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* v20 Table KIND : anchorAttr/ordinalAttr/baseTag explicites par kindClass.
|
|
13
|
+
* Fix fatal catégories : data-cid au lieu de data-index inexistant.
|
|
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.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* v25 Table KIND unifiée avec baseTag + ordinalAttr.
|
|
20
|
+
* Fix scroll-up / virtualisation NodeBB :
|
|
21
|
+
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
22
|
+
* Tentative recyclage d'id (v25) → cause exactement le même bug (wraps
|
|
23
|
+
* déplacés laissent les positions originales libres → réinjection en haut).
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* v26 Suppression définitive du recyclage d'id.
|
|
26
|
+
* KIND simplifié.
|
|
24
27
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB).
|
|
29
|
+
*
|
|
30
|
+
* v28 decluster supprimé. pruneOrphans supprimé (v27). Wraps persistants sur session.
|
|
31
|
+
*
|
|
32
|
+
* v29 Fix ancrage topics : data-index → data-tid.
|
|
33
|
+
*
|
|
34
|
+
* v30 Fix adjacentWrap : ne compte plus les wraps orphelins (ancre hors DOM).
|
|
35
|
+
* Quand NodeBB virtualise et retire des topics du DOM, les wraps restent
|
|
36
|
+
* en place (div dans le ul). adjacentWrap(el) retournait true sur ces
|
|
37
|
+
* wraps orphelins → injection bloquée sur les topics suivants.
|
|
38
|
+
* Fix : adjacentWrap vérifie que le wrap voisin a son ancre dans le DOM.
|
|
39
|
+
* recycleOrphanId() : quand le pool est épuisé, recycle les wraps orphelins
|
|
40
|
+
* non remplis qui sont loin au-dessus du viewport.
|
|
41
|
+
* data-index = position relative dans le batch NodeBB, pas un ID stable.
|
|
42
|
+
* Lors du scroll infini, les nouveaux batches démarrent à data-index=0,
|
|
43
|
+
* ce qui créait des collisions de clés d'ancrage → mauvaise déduplication
|
|
44
|
+
* → wraps non injectés sur les nouveaux topics, puis réinjection en haut.
|
|
45
|
+
* Fix : anchorAttr = data-tid (stable et unique par topic).
|
|
46
|
+
* ordinalAttr reste data-index pour le calcul de l'intervalle.
|
|
47
|
+
* Analyse : decluster appelait dropWrap() qui faisait S.mountedIds.delete(id).
|
|
48
|
+
* Un wrap vide adjacent à un autre wrap → supprimé → id libéré → réinjecté
|
|
49
|
+
* en haut au prochain scroll. Exactement le bug observé.
|
|
50
|
+
* Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
|
|
51
|
+
* maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
|
|
31
52
|
*/
|
|
32
53
|
(function () {
|
|
33
54
|
'use strict';
|
|
34
55
|
|
|
35
56
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
57
|
|
|
37
|
-
const WRAP_CLASS
|
|
38
|
-
const PH_PREFIX
|
|
39
|
-
const A_ANCHOR
|
|
40
|
-
const A_WRAPID
|
|
41
|
-
const A_CREATED
|
|
42
|
-
const A_SHOWN
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
58
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
59
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
60
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
61
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
62
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
63
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
64
|
+
|
|
65
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
66
|
+
const MAX_INSERTS_RUN = 6;
|
|
67
|
+
const MAX_INFLIGHT = 4;
|
|
68
|
+
const SHOW_THROTTLE_MS = 900;
|
|
69
|
+
const BURST_COOLDOWN_MS = 200;
|
|
70
|
+
|
|
71
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
72
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
73
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
74
|
|
|
@@ -60,40 +79,37 @@
|
|
|
60
79
|
};
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
|
-
* Table
|
|
82
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
83
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
84
|
+
* sel : sélecteur CSS complet
|
|
85
|
+
* baseTag : préfixe tag pour querySelector d'ancre
|
|
86
|
+
* (vide pour posts car sélecteur commence par '[')
|
|
87
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
88
|
+
* data-pid posts / data-index topics / data-cid catégories
|
|
89
|
+
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
90
|
+
* null → fallback positionnel (catégories)
|
|
71
91
|
*/
|
|
72
92
|
const KIND = {
|
|
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' },
|
|
93
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
94
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-tid', ordinalAttr: 'data-index' },
|
|
95
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
76
96
|
};
|
|
77
97
|
|
|
78
98
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
99
|
|
|
80
100
|
const S = {
|
|
81
|
-
pageKey:
|
|
82
|
-
cfg:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
inflight: 0,
|
|
94
|
-
pending: [],
|
|
95
|
-
pendingSet: new Set(),
|
|
96
|
-
|
|
101
|
+
pageKey: null,
|
|
102
|
+
cfg: null,
|
|
103
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
104
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
105
|
+
mountedIds: new Set(),
|
|
106
|
+
lastShow: new Map(),
|
|
107
|
+
io: null,
|
|
108
|
+
domObs: null,
|
|
109
|
+
mutGuard: 0,
|
|
110
|
+
inflight: 0,
|
|
111
|
+
pending: [],
|
|
112
|
+
pendingSet: new Set(),
|
|
97
113
|
runQueued: false,
|
|
98
114
|
burstActive: false,
|
|
99
115
|
burstDeadline: 0,
|
|
@@ -102,8 +118,11 @@
|
|
|
102
118
|
};
|
|
103
119
|
|
|
104
120
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
121
|
+
const ts = () => Date.now();
|
|
122
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
123
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
124
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
125
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
126
|
|
|
108
127
|
function mutate(fn) {
|
|
109
128
|
S.mutGuard++;
|
|
@@ -121,12 +140,6 @@
|
|
|
121
140
|
return S.cfg;
|
|
122
141
|
}
|
|
123
142
|
|
|
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
|
-
|
|
130
143
|
function parseIds(raw) {
|
|
131
144
|
const out = [], seen = new Set();
|
|
132
145
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +149,11 @@
|
|
|
136
149
|
return out;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
152
|
+
function initPools(cfg) {
|
|
153
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
154
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
155
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
156
|
+
}
|
|
145
157
|
|
|
146
158
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
159
|
|
|
@@ -165,13 +177,13 @@
|
|
|
165
177
|
return 'other';
|
|
166
178
|
}
|
|
167
179
|
|
|
168
|
-
// ── DOM
|
|
180
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
181
|
|
|
170
182
|
function getPosts() {
|
|
171
183
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
184
|
if (!el.isConnected) return false;
|
|
173
185
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
186
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
187
|
if (p && p !== el) return false;
|
|
176
188
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
189
|
});
|
|
@@ -180,43 +192,47 @@
|
|
|
180
192
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
181
193
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
182
194
|
|
|
195
|
+
function wrapIsLive(wrap) {
|
|
196
|
+
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
197
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
198
|
+
if (!key) return false;
|
|
199
|
+
const colonIdx = key.indexOf(':');
|
|
200
|
+
const klass = key.slice(0, colonIdx);
|
|
201
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
202
|
+
const cfg = KIND[klass];
|
|
203
|
+
if (!cfg) return false;
|
|
204
|
+
try {
|
|
205
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
206
|
+
return !!(found?.isConnected);
|
|
207
|
+
} catch (_) { return false; }
|
|
208
|
+
}
|
|
209
|
+
|
|
183
210
|
function adjacentWrap(el) {
|
|
184
|
-
return
|
|
185
|
-
el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
186
|
-
el.previousElementSibling?.classList?.contains(WRAP_CLASS)
|
|
187
|
-
);
|
|
211
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
// ── Ancres stables
|
|
214
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
215
|
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
216
|
+
function stableId(klass, el) {
|
|
217
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
218
|
if (attr) {
|
|
200
219
|
const v = el.getAttribute(attr);
|
|
201
220
|
if (v !== null && v !== '') return v;
|
|
202
221
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
222
|
+
let i = 0;
|
|
223
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
224
|
+
if (s === el) return `i${i}`;
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
211
227
|
return 'i0';
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
const
|
|
230
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
231
|
|
|
216
|
-
function findWrap(
|
|
232
|
+
function findWrap(key) {
|
|
217
233
|
try {
|
|
218
234
|
return document.querySelector(
|
|
219
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
235
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
236
|
);
|
|
221
237
|
} catch (_) { return null; }
|
|
222
238
|
}
|
|
@@ -226,7 +242,7 @@
|
|
|
226
242
|
function pickId(poolKey) {
|
|
227
243
|
const pool = S.pools[poolKey];
|
|
228
244
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
245
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
246
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
247
|
const id = pool[i];
|
|
232
248
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -234,10 +250,42 @@
|
|
|
234
250
|
return null;
|
|
235
251
|
}
|
|
236
252
|
|
|
253
|
+
function recycleOrphanId(klass) {
|
|
254
|
+
// Quand le pool est épuisé : cherche un wrap orphelin (ancre hors DOM, non rempli)
|
|
255
|
+
// loin au-dessus du viewport et libère son ID.
|
|
256
|
+
const vh = window.innerHeight || 800;
|
|
257
|
+
const threshold = -vh * 3;
|
|
258
|
+
let best = null, bestBottom = Infinity;
|
|
259
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
260
|
+
if (wrap.getAttribute(A_CREATED) === null) return;
|
|
261
|
+
if (isFilled(wrap)) return;
|
|
262
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
263
|
+
if (!key) return;
|
|
264
|
+
const colonIdx = key.indexOf(':');
|
|
265
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
266
|
+
const cfg = KIND[klass];
|
|
267
|
+
if (!cfg) return;
|
|
268
|
+
try {
|
|
269
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
270
|
+
if (found?.isConnected) return; // ancre encore dans le DOM, pas orphelin
|
|
271
|
+
} catch (_) { return; }
|
|
272
|
+
try {
|
|
273
|
+
const rect = wrap.getBoundingClientRect();
|
|
274
|
+
if (rect.bottom > threshold) return;
|
|
275
|
+
if (rect.bottom < bestBottom) { bestBottom = rect.bottom; best = wrap; }
|
|
276
|
+
} catch (_) {}
|
|
277
|
+
});
|
|
278
|
+
if (!best) return null;
|
|
279
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
280
|
+
if (!Number.isFinite(id)) return null;
|
|
281
|
+
mutate(() => dropWrap(best));
|
|
282
|
+
return id;
|
|
283
|
+
}
|
|
284
|
+
|
|
237
285
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
286
|
|
|
239
287
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
288
|
+
const w = document.createElement('div');
|
|
241
289
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
290
|
w.setAttribute(A_ANCHOR, key);
|
|
243
291
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,10 +299,10 @@
|
|
|
251
299
|
}
|
|
252
300
|
|
|
253
301
|
function insertAfter(el, id, klass, key) {
|
|
254
|
-
if (!el?.insertAdjacentElement)
|
|
255
|
-
if (findWrap(key))
|
|
256
|
-
if (S.mountedIds.has(id))
|
|
257
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected)
|
|
302
|
+
if (!el?.insertAdjacentElement) return null;
|
|
303
|
+
if (findWrap(key)) return null;
|
|
304
|
+
if (S.mountedIds.has(id)) return null;
|
|
305
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
306
|
const w = makeWrap(id, klass, key);
|
|
259
307
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
308
|
S.mountedIds.add(id);
|
|
@@ -263,100 +311,44 @@
|
|
|
263
311
|
|
|
264
312
|
function dropWrap(w) {
|
|
265
313
|
try {
|
|
314
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
315
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
266
316
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
317
|
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 (_) {}
|
|
275
318
|
w.remove();
|
|
276
319
|
} catch (_) {}
|
|
277
320
|
}
|
|
278
321
|
|
|
279
|
-
// ── Prune
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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é).
|
|
291
|
-
*/
|
|
292
|
-
function pruneOrphans(klass) {
|
|
293
|
-
const meta = KIND[klass];
|
|
294
|
-
if (!meta) return;
|
|
295
|
-
|
|
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;
|
|
300
|
-
|
|
301
|
-
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
-
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
303
|
-
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
304
|
-
|
|
305
|
-
const anchorEl = document.querySelector(
|
|
306
|
-
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
307
|
-
);
|
|
308
|
-
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
322
|
+
// ── Prune : désactivé ─────────────────────────────────────────────────────
|
|
323
|
+
//
|
|
324
|
+
// pruneOrphans() a été supprimé car il causait le bug "pubs en haut".
|
|
325
|
+
// NodeBB virtualise les posts hors viewport → les ancres disparaissent du DOM
|
|
326
|
+
// temporairement → pruneOrphans supprimait les wraps → scroll retour → les
|
|
327
|
+
// ancres revenaient → injectBetween réinjectait tout en haut.
|
|
328
|
+
//
|
|
329
|
+
// Les wraps ne sont supprimés que par cleanup() à chaque navigation ajaxify.
|
|
330
|
+
// decluster() et pruneOrphans() sont désactivés — voir v28.
|
|
313
331
|
|
|
314
|
-
/**
|
|
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.
|
|
318
|
-
*/
|
|
319
|
-
function decluster(klass) {
|
|
320
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
-
// Grace sur le wrap courant : on le saute entièrement
|
|
322
|
-
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
|
-
|
|
325
|
-
let prev = w.previousElementSibling, steps = 0;
|
|
326
|
-
while (prev && steps++ < 3) {
|
|
327
|
-
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
|
-
|
|
329
|
-
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
331
|
-
|
|
332
|
-
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
333
|
-
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
332
|
|
|
339
333
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
340
334
|
|
|
341
335
|
/**
|
|
342
336
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
337
|
+
* Utilise KIND[klass].ordinalAttr (data-index pour posts/topics).
|
|
338
|
+
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
345
339
|
*/
|
|
346
340
|
function ordinal(klass, el) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
} catch (_) {}
|
|
341
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
342
|
+
if (attr) {
|
|
343
|
+
const v = el.getAttribute(attr);
|
|
344
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
345
|
+
}
|
|
346
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
347
|
+
let i = 0;
|
|
348
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
349
|
+
if (s === el) return i;
|
|
350
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
351
|
+
}
|
|
360
352
|
return 0;
|
|
361
353
|
}
|
|
362
354
|
|
|
@@ -365,20 +357,18 @@
|
|
|
365
357
|
let inserted = 0;
|
|
366
358
|
|
|
367
359
|
for (const el of items) {
|
|
368
|
-
if (inserted >=
|
|
360
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
369
361
|
if (!el?.isConnected) continue;
|
|
370
362
|
|
|
371
|
-
const ord
|
|
372
|
-
|
|
373
|
-
if (!isTarget) continue;
|
|
374
|
-
|
|
363
|
+
const ord = ordinal(klass, el);
|
|
364
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
375
365
|
if (adjacentWrap(el)) continue;
|
|
376
366
|
|
|
377
|
-
const key =
|
|
378
|
-
if (findWrap(key)) continue;
|
|
367
|
+
const key = anchorKey(klass, el);
|
|
368
|
+
if (findWrap(key)) continue;
|
|
379
369
|
|
|
380
|
-
|
|
381
|
-
if (!id)
|
|
370
|
+
let id = pickId(poolKey);
|
|
371
|
+
if (!id) { id = recycleOrphanId(klass); if (!id) continue; }
|
|
382
372
|
|
|
383
373
|
const w = insertAfter(el, id, klass, key);
|
|
384
374
|
if (w) { observePh(id); inserted++; }
|
|
@@ -390,7 +380,6 @@
|
|
|
390
380
|
|
|
391
381
|
function getIO() {
|
|
392
382
|
if (S.io) return S.io;
|
|
393
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
394
383
|
try {
|
|
395
384
|
S.io = new IntersectionObserver(entries => {
|
|
396
385
|
for (const e of entries) {
|
|
@@ -399,7 +388,7 @@
|
|
|
399
388
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
389
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
390
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
391
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
392
|
} catch (_) { S.io = null; }
|
|
404
393
|
return S.io;
|
|
405
394
|
}
|
|
@@ -450,7 +439,6 @@
|
|
|
450
439
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
451
440
|
S.lastShow.set(id, t);
|
|
452
441
|
|
|
453
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
454
442
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
455
443
|
|
|
456
444
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -471,7 +459,6 @@
|
|
|
471
459
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
472
460
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
473
461
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
-
// Un show plus récent → ne pas toucher
|
|
475
462
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
476
463
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
477
464
|
} catch (_) {}
|
|
@@ -490,7 +477,7 @@
|
|
|
490
477
|
const orig = ez.showAds.bind(ez);
|
|
491
478
|
ez.showAds = function (...args) {
|
|
492
479
|
if (isBlocked()) return;
|
|
493
|
-
const ids
|
|
480
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
494
481
|
const seen = new Set();
|
|
495
482
|
for (const v of ids) {
|
|
496
483
|
const id = parseInt(v, 10);
|
|
@@ -509,7 +496,7 @@
|
|
|
509
496
|
}
|
|
510
497
|
}
|
|
511
498
|
|
|
512
|
-
// ── Core
|
|
499
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
513
500
|
|
|
514
501
|
async function runCore() {
|
|
515
502
|
if (isBlocked()) return 0;
|
|
@@ -524,11 +511,8 @@
|
|
|
524
511
|
|
|
525
512
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
526
513
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
-
const items = getItems();
|
|
528
514
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
529
|
-
|
|
530
|
-
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
531
|
-
if (n) decluster(klass);
|
|
515
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
532
516
|
return n;
|
|
533
517
|
};
|
|
534
518
|
|
|
@@ -540,14 +524,13 @@
|
|
|
540
524
|
'ezoic-ad-between', getTopics,
|
|
541
525
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
542
526
|
);
|
|
543
|
-
|
|
527
|
+
return exec(
|
|
544
528
|
'ezoic-ad-categories', getCategories,
|
|
545
529
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
546
530
|
);
|
|
547
|
-
return 0;
|
|
548
531
|
}
|
|
549
532
|
|
|
550
|
-
// ── Scheduler
|
|
533
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
551
534
|
|
|
552
535
|
function scheduleRun(cb) {
|
|
553
536
|
if (S.runQueued) return;
|
|
@@ -565,10 +548,8 @@
|
|
|
565
548
|
if (isBlocked()) return;
|
|
566
549
|
const t = ts();
|
|
567
550
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
568
|
-
S.lastBurstTs
|
|
569
|
-
|
|
570
|
-
const pk = pageKey();
|
|
571
|
-
S.pageKey = pk;
|
|
551
|
+
S.lastBurstTs = t;
|
|
552
|
+
S.pageKey = pageKey();
|
|
572
553
|
S.burstDeadline = t + 2000;
|
|
573
554
|
|
|
574
555
|
if (S.burstActive) return;
|
|
@@ -576,7 +557,7 @@
|
|
|
576
557
|
S.burstCount = 0;
|
|
577
558
|
|
|
578
559
|
const step = () => {
|
|
579
|
-
if (pageKey() !==
|
|
560
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
580
561
|
S.burstActive = false; return;
|
|
581
562
|
}
|
|
582
563
|
S.burstCount++;
|
|
@@ -588,7 +569,7 @@
|
|
|
588
569
|
step();
|
|
589
570
|
}
|
|
590
571
|
|
|
591
|
-
// ── Cleanup
|
|
572
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
592
573
|
|
|
593
574
|
function cleanup() {
|
|
594
575
|
blockedUntil = ts() + 1500;
|
|
@@ -605,19 +586,17 @@
|
|
|
605
586
|
S.runQueued = false;
|
|
606
587
|
}
|
|
607
588
|
|
|
608
|
-
// ──
|
|
589
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
609
590
|
|
|
610
591
|
function ensureDomObserver() {
|
|
611
592
|
if (S.domObs) return;
|
|
593
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
612
594
|
S.domObs = new MutationObserver(muts => {
|
|
613
595
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
614
596
|
for (const m of muts) {
|
|
615
|
-
if (!m.addedNodes?.length) continue;
|
|
616
597
|
for (const n of m.addedNodes) {
|
|
617
598
|
if (n.nodeType !== 1) continue;
|
|
618
|
-
if (n.matches?.(
|
|
619
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
620
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
599
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
621
600
|
requestBurst(); return;
|
|
622
601
|
}
|
|
623
602
|
}
|
|
@@ -643,29 +622,18 @@
|
|
|
643
622
|
}
|
|
644
623
|
|
|
645
624
|
function ensureTcfLocator() {
|
|
646
|
-
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
647
|
-
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
648
|
-
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
649
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
650
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
651
|
-
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
652
625
|
try {
|
|
653
626
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
-
|
|
655
627
|
const inject = () => {
|
|
656
628
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
657
629
|
const f = document.createElement('iframe');
|
|
658
630
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
659
631
|
(document.body || document.documentElement).appendChild(f);
|
|
660
632
|
};
|
|
661
|
-
|
|
662
633
|
inject();
|
|
663
|
-
|
|
664
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
665
634
|
if (!window.__nbbTcfObs) {
|
|
666
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
667
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
-
{ childList: true, subtree: true });
|
|
635
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
636
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
669
637
|
}
|
|
670
638
|
} catch (_) {}
|
|
671
639
|
}
|
|
@@ -675,10 +643,10 @@
|
|
|
675
643
|
const head = document.head;
|
|
676
644
|
if (!head) return;
|
|
677
645
|
for (const [rel, href, cors] of [
|
|
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],
|
|
646
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
647
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
648
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
649
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
682
650
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
683
651
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
684
652
|
]) {
|
|
@@ -692,7 +660,7 @@
|
|
|
692
660
|
}
|
|
693
661
|
}
|
|
694
662
|
|
|
695
|
-
// ── Bindings
|
|
663
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
696
664
|
|
|
697
665
|
function bindNodeBB() {
|
|
698
666
|
const $ = window.jQuery;
|
|
@@ -703,19 +671,16 @@
|
|
|
703
671
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
704
672
|
S.pageKey = pageKey();
|
|
705
673
|
blockedUntil = 0;
|
|
706
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
707
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
674
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
675
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
708
676
|
});
|
|
709
677
|
|
|
710
|
-
const
|
|
711
|
-
'action:ajaxify.contentLoaded',
|
|
712
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
678
|
+
const burstEvts = [
|
|
679
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
713
680
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
714
681
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
682
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
715
683
|
|
|
716
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
-
|
|
718
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
719
684
|
try {
|
|
720
685
|
require(['hooks'], hooks => {
|
|
721
686
|
if (typeof hooks?.on !== 'function') return;
|