nodebb-plugin-ezoic-infinite 1.7.18 → 1.7.19
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 +235 -214
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v25
|
|
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 (moveWrapAfter). 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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - Commentaires internes allégés (code auto-documenté)
|
|
19
|
+
* v25 Base v20.1 avec :
|
|
20
|
+
* • Fix scroll-up / virtualisation NodeBB :
|
|
21
|
+
* – pruneOrphans : PRUNE_STABLE_MS = 45 s, isFilled guard en premier.
|
|
22
|
+
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
23
|
+
* • Recyclage d'id (pool épuisé en infinite scroll) :
|
|
24
|
+
* – pickRecyclableWrap() : sélectionne le wrap vide le plus loin au-dessus
|
|
25
|
+
* du viewport (seuil -6 × vh), jamais pour ezoic-ad-message.
|
|
26
|
+
* – moveWrapAfter() : déplace le wrap vers sa nouvelle ancre.
|
|
27
|
+
* – scrollDir tracking pour n'autoriser le recyclage qu'en scroll down.
|
|
28
|
+
* • Table KIND unifiée avec baseTag + ordinalAttr + recyclable flag.
|
|
29
|
+
* • ordinal() : utilise KIND[klass].ordinalAttr, fallback positionnel propre.
|
|
31
30
|
*/
|
|
32
31
|
(function () {
|
|
33
32
|
'use strict';
|
|
34
33
|
|
|
35
34
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
35
|
|
|
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 FILL_GRACE_MS
|
|
46
|
-
const EMPTY_CHECK_MS
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
37
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
38
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
39
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
40
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
41
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
42
|
+
|
|
43
|
+
const PRUNE_STABLE_MS = 45_000; // délai avant pruning (évite faux-orphelins scroll-up)
|
|
44
|
+
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
45
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
46
|
+
const RECYCLE_THRESHOLD = 6; // nb de viewports au-dessus du seuil de recyclage
|
|
47
|
+
const MAX_INSERTS_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
55
|
|
|
@@ -60,50 +60,54 @@
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Table
|
|
63
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
65
|
+
* sel : sélecteur CSS complet
|
|
66
|
+
* baseTag : préfixe tag pour querySelector d'ancre
|
|
67
|
+
* (vide pour posts car sélecteur commence par '[')
|
|
68
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
69
|
+
* data-pid posts / data-index topics / data-cid catégories
|
|
70
|
+
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
71
|
+
* null → fallback positionnel (catégories)
|
|
72
|
+
* recyclable : autoriser le recyclage d'id quand le pool est épuisé
|
|
73
|
+
* false pour ezoic-ad-message (sauts visuels indésirables)
|
|
71
74
|
*/
|
|
72
75
|
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' },
|
|
76
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index', recyclable: false },
|
|
77
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index', recyclable: true },
|
|
78
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null, recyclable: true },
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
82
|
|
|
80
83
|
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
|
-
|
|
84
|
+
pageKey: null,
|
|
85
|
+
cfg: null,
|
|
86
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
87
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
88
|
+
mountedIds: new Set(),
|
|
89
|
+
lastShow: new Map(),
|
|
90
|
+
io: null,
|
|
91
|
+
domObs: null,
|
|
92
|
+
mutGuard: 0,
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
97
96
|
runQueued: false,
|
|
98
97
|
burstActive: false,
|
|
99
98
|
burstDeadline: 0,
|
|
100
99
|
burstCount: 0,
|
|
101
100
|
lastBurstTs: 0,
|
|
101
|
+
scrollDir: 1, // 1 = down, -1 = up
|
|
102
|
+
lastScrollY: 0,
|
|
102
103
|
};
|
|
103
104
|
|
|
104
105
|
let blockedUntil = 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
106
|
+
const ts = () => Date.now();
|
|
107
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
108
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
109
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
110
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
111
|
|
|
108
112
|
function mutate(fn) {
|
|
109
113
|
S.mutGuard++;
|
|
@@ -121,12 +125,6 @@
|
|
|
121
125
|
return S.cfg;
|
|
122
126
|
}
|
|
123
127
|
|
|
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
128
|
function parseIds(raw) {
|
|
131
129
|
const out = [], seen = new Set();
|
|
132
130
|
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
@@ -136,12 +134,11 @@
|
|
|
136
134
|
return out;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
137
|
+
function initPools(cfg) {
|
|
138
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
139
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
140
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
141
|
+
}
|
|
145
142
|
|
|
146
143
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
144
|
|
|
@@ -165,13 +162,13 @@
|
|
|
165
162
|
return 'other';
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
// ── DOM
|
|
165
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
169
166
|
|
|
170
167
|
function getPosts() {
|
|
171
168
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
172
169
|
if (!el.isConnected) return false;
|
|
173
170
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
174
|
-
const p = el.parentElement?.closest(
|
|
171
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
175
172
|
if (p && p !== el) return false;
|
|
176
173
|
return el.getAttribute('component') !== 'post/parent';
|
|
177
174
|
});
|
|
@@ -187,36 +184,28 @@
|
|
|
187
184
|
);
|
|
188
185
|
}
|
|
189
186
|
|
|
190
|
-
// ── Ancres stables
|
|
187
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
191
188
|
|
|
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;
|
|
189
|
+
function stableId(klass, el) {
|
|
190
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
199
191
|
if (attr) {
|
|
200
192
|
const v = el.getAttribute(attr);
|
|
201
193
|
if (v !== null && v !== '') return v;
|
|
202
194
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i++;
|
|
209
|
-
}
|
|
210
|
-
} catch (_) {}
|
|
195
|
+
let i = 0;
|
|
196
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
197
|
+
if (s === el) return `i${i}`;
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
211
200
|
return 'i0';
|
|
212
201
|
}
|
|
213
202
|
|
|
214
|
-
const
|
|
203
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
215
204
|
|
|
216
|
-
function findWrap(
|
|
205
|
+
function findWrap(key) {
|
|
217
206
|
try {
|
|
218
207
|
return document.querySelector(
|
|
219
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
208
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
209
|
);
|
|
221
210
|
} catch (_) { return null; }
|
|
222
211
|
}
|
|
@@ -226,7 +215,7 @@
|
|
|
226
215
|
function pickId(poolKey) {
|
|
227
216
|
const pool = S.pools[poolKey];
|
|
228
217
|
for (let t = 0; t < pool.length; t++) {
|
|
229
|
-
const i
|
|
218
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
219
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
220
|
const id = pool[i];
|
|
232
221
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -234,10 +223,53 @@
|
|
|
234
223
|
return null;
|
|
235
224
|
}
|
|
236
225
|
|
|
226
|
+
// ── Recyclage d'id ─────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Sélectionne le wrap vide le plus éloigné au-dessus du viewport.
|
|
230
|
+
* Conditions : kindClass.recyclable = true, scroll vers le bas,
|
|
231
|
+
* wrap vide (non filled), rect.bottom < -(RECYCLE_THRESHOLD × vh).
|
|
232
|
+
*/
|
|
233
|
+
function pickRecyclableWrap(klass) {
|
|
234
|
+
if (!KIND[klass]?.recyclable) return null;
|
|
235
|
+
if (S.scrollDir < 0) return null;
|
|
236
|
+
|
|
237
|
+
const vh = Math.max(300, window.innerHeight || 800);
|
|
238
|
+
const threshold = -(vh * RECYCLE_THRESHOLD);
|
|
239
|
+
let best = null, bestBottom = Infinity;
|
|
240
|
+
|
|
241
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
242
|
+
if (!w.isConnected || isFilled(w)) continue;
|
|
243
|
+
try {
|
|
244
|
+
const rect = w.getBoundingClientRect();
|
|
245
|
+
if (rect.bottom < threshold && rect.bottom < bestBottom) {
|
|
246
|
+
bestBottom = rect.bottom;
|
|
247
|
+
best = w;
|
|
248
|
+
}
|
|
249
|
+
} catch (_) {}
|
|
250
|
+
}
|
|
251
|
+
return best;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Déplace un wrap recyclé vers sa nouvelle ancre el.
|
|
256
|
+
* Réinitialise A_ANCHOR, A_CREATED, supprime A_SHOWN.
|
|
257
|
+
*/
|
|
258
|
+
function moveWrapAfter(el, wrap, newKey) {
|
|
259
|
+
try {
|
|
260
|
+
if (!el || !wrap?.isConnected) return null;
|
|
261
|
+
wrap.setAttribute(A_ANCHOR, newKey);
|
|
262
|
+
wrap.setAttribute(A_CREATED, String(ts()));
|
|
263
|
+
wrap.removeAttribute(A_SHOWN);
|
|
264
|
+
mutate(() => el.insertAdjacentElement('afterend', wrap));
|
|
265
|
+
return wrap;
|
|
266
|
+
} catch (_) { return null; }
|
|
267
|
+
}
|
|
268
|
+
|
|
237
269
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
238
270
|
|
|
239
271
|
function makeWrap(id, klass, key) {
|
|
240
|
-
const w
|
|
272
|
+
const w = document.createElement('div');
|
|
241
273
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
274
|
w.setAttribute(A_ANCHOR, key);
|
|
243
275
|
w.setAttribute(A_WRAPID, String(id));
|
|
@@ -251,10 +283,10 @@
|
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
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)
|
|
286
|
+
if (!el?.insertAdjacentElement) return null;
|
|
287
|
+
if (findWrap(key)) return null;
|
|
288
|
+
if (S.mountedIds.has(id)) return null;
|
|
289
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
290
|
const w = makeWrap(id, klass, key);
|
|
259
291
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
292
|
S.mountedIds.add(id);
|
|
@@ -263,15 +295,10 @@
|
|
|
263
295
|
|
|
264
296
|
function dropWrap(w) {
|
|
265
297
|
try {
|
|
298
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
299
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
266
300
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
301
|
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
302
|
w.remove();
|
|
276
303
|
} catch (_) {}
|
|
277
304
|
}
|
|
@@ -279,58 +306,59 @@
|
|
|
279
306
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
280
307
|
|
|
281
308
|
/**
|
|
282
|
-
* Supprime les wraps dont l'
|
|
309
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
283
310
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* ezoic-ad-between → cherche [data-index="5"]
|
|
288
|
-
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
289
|
-
*
|
|
290
|
-
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
311
|
+
* isFilled en premier : un wrap rempli n'est JAMAIS supprimé.
|
|
312
|
+
* PRUNE_STABLE_MS (45 s) : pendant cette fenêtre une ancre absente est
|
|
313
|
+
* considérée comme virtualisée par NodeBB (scroll up), pas comme orphelin réel.
|
|
291
314
|
*/
|
|
292
315
|
function pruneOrphans(klass) {
|
|
293
316
|
const meta = KIND[klass];
|
|
294
317
|
if (!meta) return;
|
|
295
318
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
319
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
320
|
+
if (isFilled(w)) continue;
|
|
321
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
300
322
|
|
|
301
323
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
302
|
-
const sid = key.slice(klass.length + 1);
|
|
303
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
324
|
+
const sid = key.slice(klass.length + 1);
|
|
325
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
304
326
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
);
|
|
327
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
328
|
+
const anchorEl = document.querySelector(sel);
|
|
308
329
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
309
|
-
}
|
|
330
|
+
}
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
313
334
|
|
|
314
335
|
/**
|
|
315
|
-
* Deux wraps adjacents
|
|
316
|
-
*
|
|
317
|
-
*
|
|
336
|
+
* Deux wraps adjacents → supprimer le courant s'il est vide et hors grâce.
|
|
337
|
+
* Guards dans l'ordre :
|
|
338
|
+
* 1. isFilled(w) → jamais toucher un wrap rempli
|
|
339
|
+
* 2. A_CREATED < FILL_GRACE → wrap trop récent (pas encore showAds'd)
|
|
340
|
+
* 3. A_SHOWN grace → fill en cours
|
|
341
|
+
* 4. isFilled(prev) → voisin rempli, intouchable → break
|
|
342
|
+
* 5. A_CREATED prev grace → voisin trop récent → break
|
|
343
|
+
* 6. A_SHOWN prev grace → break
|
|
344
|
+
* → les deux vides et hors grâce : supprimer le courant
|
|
318
345
|
*/
|
|
319
346
|
function decluster(klass) {
|
|
320
347
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
321
|
-
|
|
348
|
+
if (isFilled(w)) continue;
|
|
349
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
322
350
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
323
351
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
324
352
|
|
|
325
353
|
let prev = w.previousElementSibling, steps = 0;
|
|
326
354
|
while (prev && steps++ < 3) {
|
|
327
355
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
328
|
-
|
|
356
|
+
if (isFilled(prev)) break;
|
|
357
|
+
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
329
358
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
330
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
359
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
331
360
|
|
|
332
|
-
|
|
333
|
-
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
361
|
+
mutate(() => dropWrap(w));
|
|
334
362
|
break;
|
|
335
363
|
}
|
|
336
364
|
}
|
|
@@ -340,23 +368,21 @@
|
|
|
340
368
|
|
|
341
369
|
/**
|
|
342
370
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
343
|
-
*
|
|
344
|
-
*
|
|
371
|
+
* Utilise KIND[klass].ordinalAttr (data-index pour posts/topics).
|
|
372
|
+
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
345
373
|
*/
|
|
346
374
|
function ordinal(klass, el) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
} catch (_) {}
|
|
375
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
376
|
+
if (attr) {
|
|
377
|
+
const v = el.getAttribute(attr);
|
|
378
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
379
|
+
}
|
|
380
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
381
|
+
let i = 0;
|
|
382
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
383
|
+
if (s === el) return i;
|
|
384
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
385
|
+
}
|
|
360
386
|
return 0;
|
|
361
387
|
}
|
|
362
388
|
|
|
@@ -365,23 +391,33 @@
|
|
|
365
391
|
let inserted = 0;
|
|
366
392
|
|
|
367
393
|
for (const el of items) {
|
|
368
|
-
if (inserted >=
|
|
394
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
369
395
|
if (!el?.isConnected) continue;
|
|
370
396
|
|
|
371
|
-
const ord
|
|
372
|
-
|
|
373
|
-
if (!isTarget) continue;
|
|
374
|
-
|
|
397
|
+
const ord = ordinal(klass, el);
|
|
398
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
375
399
|
if (adjacentWrap(el)) continue;
|
|
376
400
|
|
|
377
|
-
const key =
|
|
378
|
-
if (findWrap(key)) continue;
|
|
401
|
+
const key = anchorKey(klass, el);
|
|
402
|
+
if (findWrap(key)) continue;
|
|
379
403
|
|
|
404
|
+
// 1. Tentative pool normal
|
|
380
405
|
const id = pickId(poolKey);
|
|
381
|
-
if (
|
|
406
|
+
if (id) {
|
|
407
|
+
const w = insertAfter(el, id, klass, key);
|
|
408
|
+
if (w) { observePh(id); inserted++; }
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
382
411
|
|
|
383
|
-
|
|
384
|
-
|
|
412
|
+
// 2. Pool épuisé → tentative de recyclage
|
|
413
|
+
const recyclable = pickRecyclableWrap(klass);
|
|
414
|
+
if (recyclable) {
|
|
415
|
+
const rid = parseInt(recyclable.getAttribute(A_WRAPID), 10);
|
|
416
|
+
const w = moveWrapAfter(el, recyclable, key);
|
|
417
|
+
if (w && Number.isFinite(rid)) { observePh(rid); inserted++; }
|
|
418
|
+
}
|
|
419
|
+
// Pool épuisé et pas de recyclage : on continue (items suivants peuvent
|
|
420
|
+
// avoir un wrap existant via findWrap, on ne break pas)
|
|
385
421
|
}
|
|
386
422
|
return inserted;
|
|
387
423
|
}
|
|
@@ -390,7 +426,6 @@
|
|
|
390
426
|
|
|
391
427
|
function getIO() {
|
|
392
428
|
if (S.io) return S.io;
|
|
393
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
394
429
|
try {
|
|
395
430
|
S.io = new IntersectionObserver(entries => {
|
|
396
431
|
for (const e of entries) {
|
|
@@ -399,7 +434,7 @@
|
|
|
399
434
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
435
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
436
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
437
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
438
|
} catch (_) { S.io = null; }
|
|
404
439
|
return S.io;
|
|
405
440
|
}
|
|
@@ -450,7 +485,6 @@
|
|
|
450
485
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
451
486
|
S.lastShow.set(id, t);
|
|
452
487
|
|
|
453
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
454
488
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
455
489
|
|
|
456
490
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -471,7 +505,6 @@
|
|
|
471
505
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
472
506
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
473
507
|
if (!wrap || !ph?.isConnected) return;
|
|
474
|
-
// Un show plus récent → ne pas toucher
|
|
475
508
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
476
509
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
477
510
|
} catch (_) {}
|
|
@@ -490,7 +523,7 @@
|
|
|
490
523
|
const orig = ez.showAds.bind(ez);
|
|
491
524
|
ez.showAds = function (...args) {
|
|
492
525
|
if (isBlocked()) return;
|
|
493
|
-
const ids
|
|
526
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
494
527
|
const seen = new Set();
|
|
495
528
|
for (const v of ids) {
|
|
496
529
|
const id = parseInt(v, 10);
|
|
@@ -509,7 +542,7 @@
|
|
|
509
542
|
}
|
|
510
543
|
}
|
|
511
544
|
|
|
512
|
-
// ── Core
|
|
545
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
513
546
|
|
|
514
547
|
async function runCore() {
|
|
515
548
|
if (isBlocked()) return 0;
|
|
@@ -524,10 +557,9 @@
|
|
|
524
557
|
|
|
525
558
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
526
559
|
if (!normBool(cfgEnable)) return 0;
|
|
527
|
-
const items = getItems();
|
|
528
560
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
529
561
|
pruneOrphans(klass);
|
|
530
|
-
const n = injectBetween(klass,
|
|
562
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
531
563
|
if (n) decluster(klass);
|
|
532
564
|
return n;
|
|
533
565
|
};
|
|
@@ -540,14 +572,13 @@
|
|
|
540
572
|
'ezoic-ad-between', getTopics,
|
|
541
573
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
542
574
|
);
|
|
543
|
-
|
|
575
|
+
return exec(
|
|
544
576
|
'ezoic-ad-categories', getCategories,
|
|
545
577
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
546
578
|
);
|
|
547
|
-
return 0;
|
|
548
579
|
}
|
|
549
580
|
|
|
550
|
-
// ── Scheduler
|
|
581
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
551
582
|
|
|
552
583
|
function scheduleRun(cb) {
|
|
553
584
|
if (S.runQueued) return;
|
|
@@ -565,10 +596,8 @@
|
|
|
565
596
|
if (isBlocked()) return;
|
|
566
597
|
const t = ts();
|
|
567
598
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
568
|
-
S.lastBurstTs
|
|
569
|
-
|
|
570
|
-
const pk = pageKey();
|
|
571
|
-
S.pageKey = pk;
|
|
599
|
+
S.lastBurstTs = t;
|
|
600
|
+
S.pageKey = pageKey();
|
|
572
601
|
S.burstDeadline = t + 2000;
|
|
573
602
|
|
|
574
603
|
if (S.burstActive) return;
|
|
@@ -576,7 +605,7 @@
|
|
|
576
605
|
S.burstCount = 0;
|
|
577
606
|
|
|
578
607
|
const step = () => {
|
|
579
|
-
if (pageKey() !==
|
|
608
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
580
609
|
S.burstActive = false; return;
|
|
581
610
|
}
|
|
582
611
|
S.burstCount++;
|
|
@@ -588,7 +617,7 @@
|
|
|
588
617
|
step();
|
|
589
618
|
}
|
|
590
619
|
|
|
591
|
-
// ── Cleanup
|
|
620
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
592
621
|
|
|
593
622
|
function cleanup() {
|
|
594
623
|
blockedUntil = ts() + 1500;
|
|
@@ -605,19 +634,17 @@
|
|
|
605
634
|
S.runQueued = false;
|
|
606
635
|
}
|
|
607
636
|
|
|
608
|
-
// ──
|
|
637
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
609
638
|
|
|
610
639
|
function ensureDomObserver() {
|
|
611
640
|
if (S.domObs) return;
|
|
641
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
612
642
|
S.domObs = new MutationObserver(muts => {
|
|
613
643
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
614
644
|
for (const m of muts) {
|
|
615
|
-
if (!m.addedNodes?.length) continue;
|
|
616
645
|
for (const n of m.addedNodes) {
|
|
617
646
|
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)) {
|
|
647
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
621
648
|
requestBurst(); return;
|
|
622
649
|
}
|
|
623
650
|
}
|
|
@@ -643,29 +670,18 @@
|
|
|
643
670
|
}
|
|
644
671
|
|
|
645
672
|
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
673
|
try {
|
|
653
674
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
654
|
-
|
|
655
675
|
const inject = () => {
|
|
656
676
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
657
677
|
const f = document.createElement('iframe');
|
|
658
678
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
659
679
|
(document.body || document.documentElement).appendChild(f);
|
|
660
680
|
};
|
|
661
|
-
|
|
662
681
|
inject();
|
|
663
|
-
|
|
664
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
665
682
|
if (!window.__nbbTcfObs) {
|
|
666
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
667
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
668
|
-
{ childList: true, subtree: true });
|
|
683
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
684
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
669
685
|
}
|
|
670
686
|
} catch (_) {}
|
|
671
687
|
}
|
|
@@ -675,10 +691,10 @@
|
|
|
675
691
|
const head = document.head;
|
|
676
692
|
if (!head) return;
|
|
677
693
|
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],
|
|
694
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
695
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
696
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
697
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
682
698
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
683
699
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
684
700
|
]) {
|
|
@@ -692,7 +708,7 @@
|
|
|
692
708
|
}
|
|
693
709
|
}
|
|
694
710
|
|
|
695
|
-
// ── Bindings
|
|
711
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
696
712
|
|
|
697
713
|
function bindNodeBB() {
|
|
698
714
|
const $ = window.jQuery;
|
|
@@ -703,19 +719,16 @@
|
|
|
703
719
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
704
720
|
S.pageKey = pageKey();
|
|
705
721
|
blockedUntil = 0;
|
|
706
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
707
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
722
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
723
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
708
724
|
});
|
|
709
725
|
|
|
710
|
-
const
|
|
711
|
-
'action:ajaxify.contentLoaded',
|
|
712
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
726
|
+
const burstEvts = [
|
|
727
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
713
728
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
714
729
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
730
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
715
731
|
|
|
716
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
717
|
-
|
|
718
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
719
732
|
try {
|
|
720
733
|
require(['hooks'], hooks => {
|
|
721
734
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -730,6 +743,13 @@
|
|
|
730
743
|
function bindScroll() {
|
|
731
744
|
let ticking = false;
|
|
732
745
|
window.addEventListener('scroll', () => {
|
|
746
|
+
// Suivi direction du scroll (nécessaire pour le recyclage conditionnel)
|
|
747
|
+
try {
|
|
748
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
749
|
+
const d = y - S.lastScrollY;
|
|
750
|
+
if (Math.abs(d) > 4) { S.scrollDir = d > 0 ? 1 : -1; S.lastScrollY = y; }
|
|
751
|
+
} catch (_) {}
|
|
752
|
+
|
|
733
753
|
if (ticking) return;
|
|
734
754
|
ticking = true;
|
|
735
755
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
@@ -739,6 +759,7 @@
|
|
|
739
759
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
740
760
|
|
|
741
761
|
S.pageKey = pageKey();
|
|
762
|
+
try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
|
|
742
763
|
muteConsole();
|
|
743
764
|
ensureTcfLocator();
|
|
744
765
|
warmNetwork();
|