nodebb-plugin-ezoic-infinite 1.7.22 → 1.7.23
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 +157 -244
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v29
|
|
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
|
+
* data-index = position relative dans le batch NodeBB, pas un ID stable.
|
|
34
|
+
* Lors du scroll infini, les nouveaux batches démarrent à data-index=0,
|
|
35
|
+
* ce qui créait des collisions de clés d'ancrage → mauvaise déduplication
|
|
36
|
+
* → wraps non injectés sur les nouveaux topics, puis réinjection en haut.
|
|
37
|
+
* Fix : anchorAttr = data-tid (stable et unique par topic).
|
|
38
|
+
* ordinalAttr reste data-index pour le calcul de l'intervalle.
|
|
39
|
+
* Analyse : decluster appelait dropWrap() qui faisait S.mountedIds.delete(id).
|
|
40
|
+
* Un wrap vide adjacent à un autre wrap → supprimé → id libéré → réinjecté
|
|
41
|
+
* en haut au prochain scroll. Exactement le bug observé.
|
|
42
|
+
* Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
|
|
43
|
+
* maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
|
|
31
44
|
*/
|
|
32
45
|
(function () {
|
|
33
46
|
'use strict';
|
|
34
47
|
|
|
35
48
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
49
|
|
|
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)
|
|
50
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
51
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
52
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
53
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
54
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
55
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
56
|
+
|
|
57
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
58
|
+
const MAX_INSERTS_RUN = 6;
|
|
59
|
+
const MAX_INFLIGHT = 4;
|
|
60
|
+
const SHOW_THROTTLE_MS = 900;
|
|
61
|
+
const BURST_COOLDOWN_MS = 200;
|
|
62
|
+
|
|
63
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
64
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
65
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
66
|
|
|
@@ -60,40 +71,37 @@
|
|
|
60
71
|
};
|
|
61
72
|
|
|
62
73
|
/**
|
|
63
|
-
* Table
|
|
74
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
75
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
76
|
+
* sel : sélecteur CSS complet
|
|
77
|
+
* baseTag : préfixe tag pour querySelector d'ancre
|
|
78
|
+
* (vide pour posts car sélecteur commence par '[')
|
|
79
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
80
|
+
* data-pid posts / data-index topics / data-cid catégories
|
|
81
|
+
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
82
|
+
* null → fallback positionnel (catégories)
|
|
71
83
|
*/
|
|
72
84
|
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' },
|
|
85
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
86
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-tid', ordinalAttr: 'data-index' },
|
|
87
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
76
88
|
};
|
|
77
89
|
|
|
78
90
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
91
|
|
|
80
92
|
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
|
-
|
|
93
|
+
pageKey: null,
|
|
94
|
+
cfg: null,
|
|
95
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
96
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
97
|
+
mountedIds: new Set(),
|
|
98
|
+
lastShow: new Map(),
|
|
99
|
+
io: null,
|
|
100
|
+
domObs: null,
|
|
101
|
+
mutGuard: 0,
|
|
102
|
+
inflight: 0,
|
|
103
|
+
pending: [],
|
|
104
|
+
pendingSet: new Set(),
|
|
97
105
|
runQueued: false,
|
|
98
106
|
burstActive: false,
|
|
99
107
|
burstDeadline: 0,
|
|
@@ -102,8 +110,11 @@
|
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
113
|
+
const ts = () => Date.now();
|
|
114
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
115
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
116
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
117
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
118
|
|
|
108
119
|
function mutate(fn) {
|
|
109
120
|
S.mutGuard++;
|
|
@@ -121,12 +132,6 @@
|
|
|
121
132
|
return S.cfg;
|
|
122
133
|
}
|
|
123
134
|
|
|
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
135
|
function parseIds(raw) {
|
|
131
136
|
const out = [], seen = new Set();
|
|
132
137
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +141,11 @@
|
|
|
136
141
|
return out;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
144
|
+
function initPools(cfg) {
|
|
145
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
146
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
147
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
148
|
+
}
|
|
145
149
|
|
|
146
150
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
151
|
|
|
@@ -165,13 +169,13 @@
|
|
|
165
169
|
return 'other';
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
// ── DOM
|
|
172
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
173
|
|
|
170
174
|
function getPosts() {
|
|
171
175
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
176
|
if (!el.isConnected) return false;
|
|
173
177
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
178
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
179
|
if (p && p !== el) return false;
|
|
176
180
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
181
|
});
|
|
@@ -187,36 +191,28 @@
|
|
|
187
191
|
);
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
// ── Ancres stables
|
|
194
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
195
|
|
|
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;
|
|
196
|
+
function stableId(klass, el) {
|
|
197
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
198
|
if (attr) {
|
|
200
199
|
const v = el.getAttribute(attr);
|
|
201
200
|
if (v !== null && v !== '') return v;
|
|
202
201
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
202
|
+
let i = 0;
|
|
203
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
204
|
+
if (s === el) return `i${i}`;
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
211
207
|
return 'i0';
|
|
212
208
|
}
|
|
213
209
|
|
|
214
|
-
const
|
|
210
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
211
|
|
|
216
|
-
function findWrap(
|
|
212
|
+
function findWrap(key) {
|
|
217
213
|
try {
|
|
218
214
|
return document.querySelector(
|
|
219
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
215
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
216
|
);
|
|
221
217
|
} catch (_) { return null; }
|
|
222
218
|
}
|
|
@@ -226,7 +222,7 @@
|
|
|
226
222
|
function pickId(poolKey) {
|
|
227
223
|
const pool = S.pools[poolKey];
|
|
228
224
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
225
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
226
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
227
|
const id = pool[i];
|
|
232
228
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -237,7 +233,7 @@
|
|
|
237
233
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
234
|
|
|
239
235
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
236
|
+
const w = document.createElement('div');
|
|
241
237
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
238
|
w.setAttribute(A_ANCHOR, key);
|
|
243
239
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,10 +247,10 @@
|
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
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)
|
|
250
|
+
if (!el?.insertAdjacentElement) return null;
|
|
251
|
+
if (findWrap(key)) return null;
|
|
252
|
+
if (S.mountedIds.has(id)) return null;
|
|
253
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
254
|
const w = makeWrap(id, klass, key);
|
|
259
255
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
256
|
S.mountedIds.add(id);
|
|
@@ -263,100 +259,44 @@
|
|
|
263
259
|
|
|
264
260
|
function dropWrap(w) {
|
|
265
261
|
try {
|
|
262
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
263
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
266
264
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
265
|
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
266
|
w.remove();
|
|
276
267
|
} catch (_) {}
|
|
277
268
|
}
|
|
278
269
|
|
|
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 ──────────────────────────────────────────────────────────────
|
|
270
|
+
// ── Prune : désactivé ─────────────────────────────────────────────────────
|
|
271
|
+
//
|
|
272
|
+
// pruneOrphans() a été supprimé car il causait le bug "pubs en haut".
|
|
273
|
+
// NodeBB virtualise les posts hors viewport → les ancres disparaissent du DOM
|
|
274
|
+
// temporairement → pruneOrphans supprimait les wraps → scroll retour → les
|
|
275
|
+
// ancres revenaient → injectBetween réinjectait tout en haut.
|
|
276
|
+
//
|
|
277
|
+
// Les wraps ne sont supprimés que par cleanup() à chaque navigation ajaxify.
|
|
278
|
+
// decluster() et pruneOrphans() sont désactivés — voir v28.
|
|
313
279
|
|
|
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
280
|
|
|
339
281
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
340
282
|
|
|
341
283
|
/**
|
|
342
284
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
285
|
+
* Utilise KIND[klass].ordinalAttr (data-index pour posts/topics).
|
|
286
|
+
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
345
287
|
*/
|
|
346
288
|
function ordinal(klass, el) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
} catch (_) {}
|
|
289
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
290
|
+
if (attr) {
|
|
291
|
+
const v = el.getAttribute(attr);
|
|
292
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
293
|
+
}
|
|
294
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
295
|
+
let i = 0;
|
|
296
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
297
|
+
if (s === el) return i;
|
|
298
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
299
|
+
}
|
|
360
300
|
return 0;
|
|
361
301
|
}
|
|
362
302
|
|
|
@@ -365,20 +305,18 @@
|
|
|
365
305
|
let inserted = 0;
|
|
366
306
|
|
|
367
307
|
for (const el of items) {
|
|
368
|
-
if (inserted >=
|
|
308
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
369
309
|
if (!el?.isConnected) continue;
|
|
370
310
|
|
|
371
|
-
const ord
|
|
372
|
-
|
|
373
|
-
if (!isTarget) continue;
|
|
374
|
-
|
|
311
|
+
const ord = ordinal(klass, el);
|
|
312
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
375
313
|
if (adjacentWrap(el)) continue;
|
|
376
314
|
|
|
377
|
-
const key =
|
|
378
|
-
if (findWrap(key)) continue;
|
|
315
|
+
const key = anchorKey(klass, el);
|
|
316
|
+
if (findWrap(key)) continue;
|
|
379
317
|
|
|
380
318
|
const id = pickId(poolKey);
|
|
381
|
-
if (!id) continue; // pool
|
|
319
|
+
if (!id) continue; // pool épuisé : tous les ids sont montés, on passe au suivant
|
|
382
320
|
|
|
383
321
|
const w = insertAfter(el, id, klass, key);
|
|
384
322
|
if (w) { observePh(id); inserted++; }
|
|
@@ -390,7 +328,6 @@
|
|
|
390
328
|
|
|
391
329
|
function getIO() {
|
|
392
330
|
if (S.io) return S.io;
|
|
393
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
394
331
|
try {
|
|
395
332
|
S.io = new IntersectionObserver(entries => {
|
|
396
333
|
for (const e of entries) {
|
|
@@ -399,7 +336,7 @@
|
|
|
399
336
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
337
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
338
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
339
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
340
|
} catch (_) { S.io = null; }
|
|
404
341
|
return S.io;
|
|
405
342
|
}
|
|
@@ -450,7 +387,6 @@
|
|
|
450
387
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
451
388
|
S.lastShow.set(id, t);
|
|
452
389
|
|
|
453
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
454
390
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
455
391
|
|
|
456
392
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -471,7 +407,6 @@
|
|
|
471
407
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
472
408
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
473
409
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
-
// Un show plus récent → ne pas toucher
|
|
475
410
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
476
411
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
477
412
|
} catch (_) {}
|
|
@@ -490,7 +425,7 @@
|
|
|
490
425
|
const orig = ez.showAds.bind(ez);
|
|
491
426
|
ez.showAds = function (...args) {
|
|
492
427
|
if (isBlocked()) return;
|
|
493
|
-
const ids
|
|
428
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
494
429
|
const seen = new Set();
|
|
495
430
|
for (const v of ids) {
|
|
496
431
|
const id = parseInt(v, 10);
|
|
@@ -509,7 +444,7 @@
|
|
|
509
444
|
}
|
|
510
445
|
}
|
|
511
446
|
|
|
512
|
-
// ── Core
|
|
447
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
513
448
|
|
|
514
449
|
async function runCore() {
|
|
515
450
|
if (isBlocked()) return 0;
|
|
@@ -524,11 +459,8 @@
|
|
|
524
459
|
|
|
525
460
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
526
461
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
-
const items = getItems();
|
|
528
462
|
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);
|
|
463
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
532
464
|
return n;
|
|
533
465
|
};
|
|
534
466
|
|
|
@@ -540,14 +472,13 @@
|
|
|
540
472
|
'ezoic-ad-between', getTopics,
|
|
541
473
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
542
474
|
);
|
|
543
|
-
|
|
475
|
+
return exec(
|
|
544
476
|
'ezoic-ad-categories', getCategories,
|
|
545
477
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
546
478
|
);
|
|
547
|
-
return 0;
|
|
548
479
|
}
|
|
549
480
|
|
|
550
|
-
// ── Scheduler
|
|
481
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
551
482
|
|
|
552
483
|
function scheduleRun(cb) {
|
|
553
484
|
if (S.runQueued) return;
|
|
@@ -565,10 +496,8 @@
|
|
|
565
496
|
if (isBlocked()) return;
|
|
566
497
|
const t = ts();
|
|
567
498
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
568
|
-
S.lastBurstTs
|
|
569
|
-
|
|
570
|
-
const pk = pageKey();
|
|
571
|
-
S.pageKey = pk;
|
|
499
|
+
S.lastBurstTs = t;
|
|
500
|
+
S.pageKey = pageKey();
|
|
572
501
|
S.burstDeadline = t + 2000;
|
|
573
502
|
|
|
574
503
|
if (S.burstActive) return;
|
|
@@ -576,7 +505,7 @@
|
|
|
576
505
|
S.burstCount = 0;
|
|
577
506
|
|
|
578
507
|
const step = () => {
|
|
579
|
-
if (pageKey() !==
|
|
508
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
580
509
|
S.burstActive = false; return;
|
|
581
510
|
}
|
|
582
511
|
S.burstCount++;
|
|
@@ -588,7 +517,7 @@
|
|
|
588
517
|
step();
|
|
589
518
|
}
|
|
590
519
|
|
|
591
|
-
// ── Cleanup
|
|
520
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
592
521
|
|
|
593
522
|
function cleanup() {
|
|
594
523
|
blockedUntil = ts() + 1500;
|
|
@@ -605,19 +534,17 @@
|
|
|
605
534
|
S.runQueued = false;
|
|
606
535
|
}
|
|
607
536
|
|
|
608
|
-
// ──
|
|
537
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
609
538
|
|
|
610
539
|
function ensureDomObserver() {
|
|
611
540
|
if (S.domObs) return;
|
|
541
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
612
542
|
S.domObs = new MutationObserver(muts => {
|
|
613
543
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
614
544
|
for (const m of muts) {
|
|
615
|
-
if (!m.addedNodes?.length) continue;
|
|
616
545
|
for (const n of m.addedNodes) {
|
|
617
546
|
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)) {
|
|
547
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
621
548
|
requestBurst(); return;
|
|
622
549
|
}
|
|
623
550
|
}
|
|
@@ -643,29 +570,18 @@
|
|
|
643
570
|
}
|
|
644
571
|
|
|
645
572
|
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
573
|
try {
|
|
653
574
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
-
|
|
655
575
|
const inject = () => {
|
|
656
576
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
657
577
|
const f = document.createElement('iframe');
|
|
658
578
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
659
579
|
(document.body || document.documentElement).appendChild(f);
|
|
660
580
|
};
|
|
661
|
-
|
|
662
581
|
inject();
|
|
663
|
-
|
|
664
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
665
582
|
if (!window.__nbbTcfObs) {
|
|
666
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
667
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
-
{ childList: true, subtree: true });
|
|
583
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
584
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
669
585
|
}
|
|
670
586
|
} catch (_) {}
|
|
671
587
|
}
|
|
@@ -675,10 +591,10 @@
|
|
|
675
591
|
const head = document.head;
|
|
676
592
|
if (!head) return;
|
|
677
593
|
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],
|
|
594
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
595
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
596
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
597
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
682
598
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
683
599
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
684
600
|
]) {
|
|
@@ -692,7 +608,7 @@
|
|
|
692
608
|
}
|
|
693
609
|
}
|
|
694
610
|
|
|
695
|
-
// ── Bindings
|
|
611
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
696
612
|
|
|
697
613
|
function bindNodeBB() {
|
|
698
614
|
const $ = window.jQuery;
|
|
@@ -703,19 +619,16 @@
|
|
|
703
619
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
704
620
|
S.pageKey = pageKey();
|
|
705
621
|
blockedUntil = 0;
|
|
706
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
707
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
622
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
623
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
708
624
|
});
|
|
709
625
|
|
|
710
|
-
const
|
|
711
|
-
'action:ajaxify.contentLoaded',
|
|
712
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
626
|
+
const burstEvts = [
|
|
627
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
713
628
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
714
629
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
630
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
715
631
|
|
|
716
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
-
|
|
718
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
719
632
|
try {
|
|
720
633
|
require(['hooks'], hooks => {
|
|
721
634
|
if (typeof hooks?.on !== 'function') return;
|