nodebb-plugin-ezoic-infinite 1.6.99 → 1.7.1
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 +468 -562
- package/public/style.css +15 -26
package/public/client.js
CHANGED
|
@@ -1,445 +1,413 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js (
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v20)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* Correctifs critiques vs v19
|
|
5
|
+
* ───────────────────────────
|
|
6
|
+
* [BUG FATAL] Pubs catégories disparaissent au premier scroll sur mobile
|
|
7
|
+
* pruneOrphans cherchait l'ancre via `[data-index]`, mais les éléments
|
|
8
|
+
* `li[component="categories/category"]` ont `data-cid`, pas `data-index`.
|
|
9
|
+
* → anchorEl toujours null → suppression à chaque runCore() → disparition.
|
|
10
|
+
* Fix : table KIND_META qui mappe chaque kindClass vers son attribut d'ancre
|
|
11
|
+
* stable (data-pid pour posts, data-index pour topics, data-cid pour catégories).
|
|
12
|
+
*
|
|
13
|
+
* [BUG] decluster : break trop large stoppait tout quand UN wrap était en grâce
|
|
14
|
+
* Fix : on skip uniquement le wrap courant, pas toute la boucle.
|
|
15
|
+
*
|
|
16
|
+
* [BUG] injectBetween : `break` sur pool épuisé empêchait d'observer les wraps
|
|
17
|
+
* existants sur les items suivants. Fix : `continue` au lieu de `break`.
|
|
18
|
+
*
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
21
|
+
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
24
|
+
*
|
|
25
|
+
* Nettoyage
|
|
26
|
+
* ─────────
|
|
27
|
+
* - Suppression du scroll boost (complexité sans gain mesurable côté SPA statique)
|
|
28
|
+
* - MAX_INFLIGHT unique desktop/mobile (inutile de différencier)
|
|
29
|
+
* - getAnchorKey/getGlobalOrdinal fusionnés en helpers cohérents
|
|
30
|
+
* - Commentaires internes allégés (code auto-documenté)
|
|
18
31
|
*/
|
|
19
32
|
(function () {
|
|
20
33
|
'use strict';
|
|
21
34
|
|
|
22
|
-
//
|
|
23
|
-
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
24
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
25
|
-
const ANCHOR_ATTR = 'data-ezoic-anchor'; // unique key = kind:anchorId
|
|
26
|
-
const WRAPID_ATTR = 'data-ezoic-wrapid';
|
|
27
|
-
const CREATED_ATTR = 'data-ezoic-created';
|
|
35
|
+
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
28
36
|
|
|
37
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
38
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
39
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
40
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic (number string)
|
|
41
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création (ms)
|
|
42
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds (ms)
|
|
43
|
+
|
|
44
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
45
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds où l'on ne decluster pas
|
|
46
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
29
47
|
const MAX_INSERTS_PER_RUN = 6;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const MAX_INFLIGHT_MOBILE = 3;
|
|
43
|
-
const SHOW_THROTTLE_MS = 900;
|
|
44
|
-
|
|
45
|
-
const SELECTORS = {
|
|
46
|
-
topicItem: 'li[component="category/topic"]',
|
|
47
|
-
postItem: '[component="post"][data-pid]',
|
|
48
|
-
categoryItem: 'li[component="categories/category"]',
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
53
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
|
+
|
|
56
|
+
const SEL = {
|
|
57
|
+
post: '[component="post"][data-pid]',
|
|
58
|
+
topic: 'li[component="category/topic"]',
|
|
59
|
+
category: 'li[component="categories/category"]',
|
|
49
60
|
};
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
64
|
+
*
|
|
65
|
+
* L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
|
|
66
|
+
* quelle que soit la page ou la virtualisation :
|
|
67
|
+
* posts → data-pid (id du message, unique et permanent)
|
|
68
|
+
* topics → data-index (position 0-based dans la liste, fourni par NodeBB)
|
|
69
|
+
* catégories → data-cid (id de la catégorie, unique et permanent)
|
|
70
|
+
* ← C'était le bug v19 : on cherchait data-index ici
|
|
71
|
+
*/
|
|
72
|
+
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
|
+
};
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
return !!(node?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
67
|
-
}
|
|
78
|
+
// ── État ───────────────────────────────────────────────────────────────────
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
const state = {
|
|
80
|
+
const S = {
|
|
71
81
|
pageKey: null,
|
|
72
82
|
cfg: null,
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Suivi des IDs Ezoic actuellement montés dans le DOM (évite les doublons)
|
|
79
|
-
mountedIds: new Set(),
|
|
80
|
-
|
|
81
|
-
// Throttle par id
|
|
82
|
-
lastShowById: new Map(),
|
|
83
|
-
|
|
84
|
-
// Observers
|
|
85
|
-
domObs: null,
|
|
86
|
-
io: null,
|
|
87
|
-
ioMargin: null,
|
|
88
|
-
|
|
89
|
-
// Guard contre nos propres mutations
|
|
90
|
-
internalMutation: 0,
|
|
84
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
85
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
86
|
+
mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
|
|
87
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
91
88
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
pendingSet: new Set(),
|
|
89
|
+
io: null,
|
|
90
|
+
domObs: null,
|
|
91
|
+
mutGuard: 0, // compteur internalMutation
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
lastScrollTs: 0,
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
burstActive: false,
|
|
97
|
+
runQueued: false,
|
|
98
|
+
burstActive: false,
|
|
105
99
|
burstDeadline: 0,
|
|
106
|
-
burstCount:
|
|
107
|
-
|
|
100
|
+
burstCount: 0,
|
|
101
|
+
lastBurstTs: 0,
|
|
108
102
|
};
|
|
109
103
|
|
|
110
104
|
let blockedUntil = 0;
|
|
111
|
-
const isBlocked
|
|
112
|
-
const
|
|
105
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
106
|
+
const ts = () => Date.now();
|
|
113
107
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
try { fn(); } finally {
|
|
108
|
+
function mutate(fn) {
|
|
109
|
+
S.mutGuard++;
|
|
110
|
+
try { fn(); } finally { S.mutGuard--; }
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
//
|
|
113
|
+
// ── Config ─────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
120
115
|
async function fetchConfig() {
|
|
121
|
-
if (
|
|
116
|
+
if (S.cfg) return S.cfg;
|
|
122
117
|
try {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return state.cfg;
|
|
118
|
+
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
119
|
+
if (r.ok) S.cfg = await r.json();
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
return S.cfg;
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
function initPools(cfg) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
126
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
127
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseIds(raw) {
|
|
131
|
+
const out = [], seen = new Set();
|
|
132
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
133
|
+
const n = parseInt(v, 10);
|
|
134
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
140
|
+
|
|
141
|
+
const isFilled = (n) =>
|
|
142
|
+
!!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
143
|
+
|
|
144
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
145
|
+
|
|
146
|
+
// ── Page identity ──────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function pageKey() {
|
|
140
149
|
try {
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
if (
|
|
150
|
+
const d = window.ajaxify?.data;
|
|
151
|
+
if (d?.tid) return `t:${d.tid}`;
|
|
152
|
+
if (d?.cid) return `c:${d.cid}`;
|
|
144
153
|
} catch (_) {}
|
|
145
154
|
return location.pathname;
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
function getKind() {
|
|
149
158
|
const p = location.pathname;
|
|
150
|
-
if (/^\/topic\//.test(p))
|
|
151
|
-
if (/^\/category\//.test(p))
|
|
159
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
160
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
152
161
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
153
|
-
if (document.querySelector(
|
|
154
|
-
if (document.querySelector(
|
|
155
|
-
if (document.querySelector(
|
|
162
|
+
if (document.querySelector(SEL.category)) return 'categories';
|
|
163
|
+
if (document.querySelector(SEL.post)) return 'topic';
|
|
164
|
+
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
156
165
|
return 'other';
|
|
157
166
|
}
|
|
158
167
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function getPosts() {
|
|
171
|
+
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
162
172
|
if (!el.isConnected) return false;
|
|
163
173
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
return true;
|
|
174
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
175
|
+
if (p && p !== el) return false;
|
|
176
|
+
return el.getAttribute('component') !== 'post/parent';
|
|
168
177
|
});
|
|
169
178
|
}
|
|
170
179
|
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
181
|
+
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
182
|
+
|
|
183
|
+
function adjacentWrap(el) {
|
|
184
|
+
return !!(
|
|
185
|
+
el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
186
|
+
el.previousElementSibling?.classList?.contains(WRAP_CLASS)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
173
191
|
|
|
174
192
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
193
|
+
* Retourne l'identifiant stable de l'élément selon son kindClass.
|
|
194
|
+
* Utilise l'attribut défini dans KIND (data-pid, data-index, data-cid).
|
|
195
|
+
* Fallback positionnel si l'attribut est absent.
|
|
178
196
|
*/
|
|
179
|
-
function
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
function stableId(kindClass, el) {
|
|
198
|
+
const attr = KIND[kindClass]?.anchorAttr;
|
|
199
|
+
if (attr) {
|
|
200
|
+
const v = el.getAttribute(attr);
|
|
201
|
+
if (v !== null && v !== '') return v;
|
|
202
|
+
}
|
|
203
|
+
// Fallback : position dans le parent
|
|
204
|
+
try {
|
|
205
|
+
let i = 0;
|
|
206
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
207
|
+
if (s === el) return `i${i}`;
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
} catch (_) {}
|
|
211
|
+
return 'i0';
|
|
184
212
|
}
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
return document.querySelector(`.${WRAP_CLASS}[${ANCHOR_ATTR}="${CSS.escape(anchorKey)}"]`);
|
|
188
|
-
}
|
|
214
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
189
215
|
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return
|
|
216
|
+
function findWrap(anchorKey) {
|
|
217
|
+
try {
|
|
218
|
+
return document.querySelector(
|
|
219
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
220
|
+
);
|
|
221
|
+
} catch (_) { return null; }
|
|
196
222
|
}
|
|
197
223
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
* Retourne le prochain id disponible du pool (non déjà monté dans le DOM),
|
|
201
|
-
* en avançant le curseur rotatif.
|
|
202
|
-
*/
|
|
224
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
203
226
|
function pickId(poolKey) {
|
|
204
|
-
const pool =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % n;
|
|
211
|
-
const id = pool[idx];
|
|
212
|
-
if (!state.mountedIds.has(id)) return id;
|
|
227
|
+
const pool = S.pools[poolKey];
|
|
228
|
+
for (let t = 0; t < pool.length; t++) {
|
|
229
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
230
|
+
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
231
|
+
const id = pool[i];
|
|
232
|
+
if (!S.mountedIds.has(id)) return id;
|
|
213
233
|
}
|
|
214
|
-
return null;
|
|
234
|
+
return null;
|
|
215
235
|
}
|
|
216
236
|
|
|
217
|
-
//
|
|
218
|
-
function buildWrap(id, kindClass, anchorKey) {
|
|
219
|
-
const wrap = document.createElement('div');
|
|
220
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
221
|
-
wrap.setAttribute(ANCHOR_ATTR, anchorKey);
|
|
222
|
-
wrap.setAttribute(WRAPID_ATTR, String(id));
|
|
223
|
-
wrap.setAttribute(CREATED_ATTR, String(now()));
|
|
224
|
-
wrap.style.cssText = 'width:100%;display:block;';
|
|
237
|
+
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
225
238
|
|
|
239
|
+
function makeWrap(id, klass, key) {
|
|
240
|
+
const w = document.createElement('div');
|
|
241
|
+
w.className = `${WRAP_CLASS} ${klass}`;
|
|
242
|
+
w.setAttribute(A_ANCHOR, key);
|
|
243
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
244
|
+
w.setAttribute(A_CREATED, String(ts()));
|
|
245
|
+
w.style.cssText = 'width:100%;display:block;';
|
|
226
246
|
const ph = document.createElement('div');
|
|
227
|
-
ph.id = `${
|
|
247
|
+
ph.id = `${PH_PREFIX}${id}`;
|
|
228
248
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return wrap;
|
|
249
|
+
w.appendChild(ph);
|
|
250
|
+
return w;
|
|
232
251
|
}
|
|
233
252
|
|
|
234
|
-
function
|
|
253
|
+
function insertAfter(el, id, klass, key) {
|
|
235
254
|
if (!el?.insertAdjacentElement) return null;
|
|
236
|
-
if (
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const wrap = buildWrap(id, kindClass, anchorKey);
|
|
246
|
-
withInternalMutation(() => el.insertAdjacentElement('afterend', wrap));
|
|
247
|
-
state.mountedIds.add(id);
|
|
248
|
-
return wrap;
|
|
255
|
+
if (findWrap(key)) return null; // ancre déjà présente
|
|
256
|
+
if (S.mountedIds.has(id)) return null; // id déjà monté
|
|
257
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
258
|
+
const w = makeWrap(id, klass, key);
|
|
259
|
+
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
260
|
+
S.mountedIds.add(id);
|
|
261
|
+
return w;
|
|
249
262
|
}
|
|
250
263
|
|
|
251
|
-
function
|
|
264
|
+
function dropWrap(w) {
|
|
252
265
|
try {
|
|
253
|
-
const id = parseInt(
|
|
254
|
-
if (Number.isFinite(id))
|
|
255
|
-
|
|
266
|
+
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
267
|
+
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
268
|
+
try { S.io?.unobserve(w.querySelector(`[id^="${PH_PREFIX}"]`)); } catch (_) {}
|
|
269
|
+
w.remove();
|
|
256
270
|
} catch (_) {}
|
|
257
271
|
}
|
|
258
272
|
|
|
259
|
-
//
|
|
273
|
+
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
260
275
|
/**
|
|
261
276
|
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
262
|
-
*
|
|
277
|
+
*
|
|
278
|
+
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
279
|
+
* Exemples :
|
|
280
|
+
* ezoic-ad-message → cherche [data-pid="123"]
|
|
281
|
+
* ezoic-ad-between → cherche [data-index="5"]
|
|
282
|
+
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
283
|
+
*
|
|
284
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
263
285
|
*/
|
|
264
|
-
function pruneOrphans(
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
wraps.forEach(wrap => {
|
|
269
|
-
// Ne jamais pruner un wrap tout juste créé (fill lent côté Ezoic)
|
|
270
|
-
const created = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
271
|
-
if (created && (now() - created) < EMPTY_WRAP_TTL_MS) return;
|
|
272
|
-
|
|
273
|
-
const anchorKey = wrap.getAttribute(ANCHOR_ATTR);
|
|
274
|
-
if (!anchorKey) {
|
|
275
|
-
withInternalMutation(() => removeWrap(wrap));
|
|
276
|
-
removed++;
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
286
|
+
function pruneOrphans(klass) {
|
|
287
|
+
const meta = KIND[klass];
|
|
288
|
+
if (!meta) return;
|
|
279
289
|
|
|
280
|
-
|
|
281
|
-
const [, anchorId] = anchorKey.split(':');
|
|
282
|
-
const isPost = kindClass === 'ezoic-ad-message';
|
|
283
|
-
let anchorEl = null;
|
|
290
|
+
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
284
291
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} else {
|
|
288
|
-
anchorEl = document.querySelector(`${SELECTORS.topicItem}[data-index="${CSS.escape(anchorId)}"]`)
|
|
289
|
-
?? document.querySelector(`${SELECTORS.categoryItem}[data-index="${CSS.escape(anchorId)}"]`);
|
|
290
|
-
}
|
|
292
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
293
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
291
294
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
withInternalMutation(() => removeWrap(wrap));
|
|
296
|
-
removed++;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
});
|
|
295
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
296
|
+
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
297
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
300
298
|
|
|
301
|
-
|
|
299
|
+
const anchorEl = document.querySelector(
|
|
300
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
301
|
+
);
|
|
302
|
+
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
303
|
+
});
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
304
308
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
309
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
310
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
311
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
307
312
|
*/
|
|
308
|
-
function decluster(
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let steps = 0;
|
|
315
|
-
while (prev && steps < 3) {
|
|
316
|
-
if (prev.classList?.contains(WRAP_CLASS)) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
withInternalMutation(() => removeWrap(wrap));
|
|
325
|
-
removed++;
|
|
326
|
-
} else if (!pFilled) {
|
|
327
|
-
withInternalMutation(() => removeWrap(prev));
|
|
328
|
-
removed++;
|
|
329
|
-
}
|
|
330
|
-
// Si les deux sont remplis, laisser en place
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
prev = prev.previousElementSibling;
|
|
334
|
-
steps++;
|
|
313
|
+
function decluster(klass) {
|
|
314
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
315
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
316
|
+
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
317
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
318
|
+
|
|
319
|
+
let prev = w.previousElementSibling, steps = 0;
|
|
320
|
+
while (prev && steps++ < 3) {
|
|
321
|
+
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
322
|
+
|
|
323
|
+
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
324
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
325
|
+
|
|
326
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
327
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
328
|
+
break;
|
|
335
329
|
}
|
|
336
330
|
}
|
|
337
|
-
|
|
338
|
-
return removed;
|
|
339
331
|
}
|
|
340
332
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
333
|
+
// ── Injection ──────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
337
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
338
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
339
|
+
*/
|
|
340
|
+
function ordinal(klass, el) {
|
|
341
|
+
const di = el.getAttribute('data-index');
|
|
342
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
343
|
+
// Fallback positionnel
|
|
344
|
+
try {
|
|
345
|
+
const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
|
|
346
|
+
if (tag) {
|
|
347
|
+
let i = 0;
|
|
348
|
+
for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
|
|
349
|
+
if (n === el) return i;
|
|
350
|
+
i++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (_) {}
|
|
354
|
+
return 0;
|
|
347
355
|
}
|
|
348
356
|
|
|
349
|
-
function injectBetween(
|
|
357
|
+
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
350
358
|
if (!items.length) return 0;
|
|
359
|
+
let inserted = 0;
|
|
351
360
|
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
for (const el of items) {
|
|
362
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
363
|
+
if (!el?.isConnected) continue;
|
|
355
364
|
|
|
356
|
-
|
|
357
|
-
|
|
365
|
+
const ord = ordinal(klass, el);
|
|
366
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
367
|
+
if (!isTarget) continue;
|
|
358
368
|
|
|
359
|
-
|
|
360
|
-
if (!el?.isConnected) continue;
|
|
361
|
-
if (hasAdjacentWrap(el)) continue;
|
|
369
|
+
if (adjacentWrap(el)) continue;
|
|
362
370
|
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
371
|
+
const key = makeAnchorKey(klass, el);
|
|
372
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
365
373
|
|
|
366
374
|
const id = pickId(poolKey);
|
|
367
|
-
if (!id)
|
|
375
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
368
376
|
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
observePlaceholder(id);
|
|
373
|
-
inserted++;
|
|
377
|
+
const w = insertAfter(el, id, klass, key);
|
|
378
|
+
if (w) { observePh(id); inserted++; }
|
|
374
379
|
}
|
|
375
|
-
|
|
376
380
|
return inserted;
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
//
|
|
380
|
-
function getPreloadMargin() {
|
|
381
|
-
const m = isMobile() ? 'mobile' : 'desktop';
|
|
382
|
-
return PRELOAD_MARGIN[m + (isBoosted() ? 'Boosted' : '')];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function getMaxInflight() {
|
|
386
|
-
return (isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP) + (isBoosted() ? 1 : 0);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function ensurePreloadObserver() {
|
|
390
|
-
const margin = getPreloadMargin();
|
|
391
|
-
if (state.io && state.ioMargin === margin) return state.io;
|
|
392
|
-
|
|
393
|
-
state.io?.disconnect();
|
|
394
|
-
state.io = null;
|
|
383
|
+
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
395
384
|
|
|
385
|
+
function getIO() {
|
|
386
|
+
if (S.io) return S.io;
|
|
387
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
396
388
|
try {
|
|
397
|
-
|
|
398
|
-
for (const
|
|
399
|
-
if (!
|
|
400
|
-
|
|
401
|
-
const id = parseInt(
|
|
389
|
+
S.io = new IntersectionObserver(entries => {
|
|
390
|
+
for (const e of entries) {
|
|
391
|
+
if (!e.isIntersecting) continue;
|
|
392
|
+
S.io?.unobserve(e.target);
|
|
393
|
+
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
402
394
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
403
395
|
}
|
|
404
396
|
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
// Ré-observer les placeholders déjà dans le DOM
|
|
409
|
-
try {
|
|
410
|
-
document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`).forEach(n => {
|
|
411
|
-
try { state.io?.observe(n); } catch (_) {}
|
|
412
|
-
});
|
|
413
|
-
} catch (_) {}
|
|
414
|
-
|
|
415
|
-
return state.io;
|
|
397
|
+
} catch (_) { S.io = null; }
|
|
398
|
+
return S.io;
|
|
416
399
|
}
|
|
417
400
|
|
|
418
|
-
function
|
|
419
|
-
const ph = document.getElementById(`${
|
|
420
|
-
if (
|
|
421
|
-
try { state.io?.observe(ph); } catch (_) {}
|
|
422
|
-
|
|
423
|
-
// Si déjà proche du viewport → show immédiat
|
|
424
|
-
try {
|
|
425
|
-
const r = ph.getBoundingClientRect();
|
|
426
|
-
const screens = isBoosted() ? 6 : (isMobile() ? 4 : 2.5);
|
|
427
|
-
if (r.top < window.innerHeight * screens && r.bottom > -window.innerHeight * 2) {
|
|
428
|
-
enqueueShow(id);
|
|
429
|
-
}
|
|
430
|
-
} catch (_) {}
|
|
401
|
+
function observePh(id) {
|
|
402
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
403
|
+
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
431
404
|
}
|
|
432
405
|
|
|
433
406
|
function enqueueShow(id) {
|
|
434
407
|
if (!id || isBlocked()) return;
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
if (state.inflight >= getMaxInflight()) {
|
|
439
|
-
if (!state.pendingSet.has(id)) {
|
|
440
|
-
state.pending.push(id);
|
|
441
|
-
state.pendingSet.add(id);
|
|
442
|
-
}
|
|
408
|
+
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
409
|
+
if (S.inflight >= MAX_INFLIGHT) {
|
|
410
|
+
if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
|
|
443
411
|
return;
|
|
444
412
|
}
|
|
445
413
|
startShow(id);
|
|
@@ -447,81 +415,73 @@
|
|
|
447
415
|
|
|
448
416
|
function drainQueue() {
|
|
449
417
|
if (isBlocked()) return;
|
|
450
|
-
while (
|
|
451
|
-
const id =
|
|
452
|
-
|
|
418
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
419
|
+
const id = S.pending.shift();
|
|
420
|
+
S.pendingSet.delete(id);
|
|
453
421
|
startShow(id);
|
|
454
422
|
}
|
|
455
423
|
}
|
|
456
424
|
|
|
457
425
|
function startShow(id) {
|
|
458
426
|
if (!id || isBlocked()) return;
|
|
459
|
-
|
|
427
|
+
S.inflight++;
|
|
460
428
|
let done = false;
|
|
461
|
-
|
|
462
429
|
const release = () => {
|
|
463
430
|
if (done) return;
|
|
464
431
|
done = true;
|
|
465
|
-
|
|
432
|
+
S.inflight = Math.max(0, S.inflight - 1);
|
|
466
433
|
drainQueue();
|
|
467
434
|
};
|
|
468
|
-
|
|
469
|
-
const timeout = setTimeout(release, 6500);
|
|
435
|
+
const timer = setTimeout(release, 7000);
|
|
470
436
|
|
|
471
437
|
requestAnimationFrame(() => {
|
|
472
438
|
try {
|
|
473
|
-
if (isBlocked()) return release();
|
|
474
|
-
const ph = document.getElementById(`${
|
|
475
|
-
if (!ph?.isConnected) return release();
|
|
476
|
-
if (isFilledNode(ph)) { clearTimeout(timeout); return release(); }
|
|
439
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
440
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
441
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
477
442
|
|
|
478
|
-
const t =
|
|
479
|
-
if (t - (
|
|
480
|
-
|
|
443
|
+
const t = ts();
|
|
444
|
+
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
445
|
+
S.lastShow.set(id, t);
|
|
446
|
+
|
|
447
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
448
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
481
449
|
|
|
482
450
|
window.ezstandalone = window.ezstandalone || {};
|
|
483
451
|
const ez = window.ezstandalone;
|
|
484
|
-
|
|
485
452
|
const doShow = () => {
|
|
486
453
|
try { ez.showAds(id); } catch (_) {}
|
|
487
|
-
scheduleEmptyCheck(id);
|
|
488
|
-
setTimeout(release,
|
|
454
|
+
scheduleEmptyCheck(id, t);
|
|
455
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
489
456
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
else doShow();
|
|
493
|
-
} finally { /* timeout covers us */ }
|
|
457
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
458
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
494
459
|
});
|
|
495
460
|
}
|
|
496
461
|
|
|
497
|
-
function scheduleEmptyCheck(id) {
|
|
462
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
498
463
|
setTimeout(() => {
|
|
499
464
|
try {
|
|
500
|
-
const ph
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (created && (now() - created) < EMPTY_WRAP_TTL_MS) return;
|
|
507
|
-
|
|
508
|
-
if (!isFilledNode(ph)) wrap.classList.add('is-empty');
|
|
509
|
-
else wrap.classList.remove('is-empty');
|
|
465
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
466
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
467
|
+
if (!wrap || !ph?.isConnected) return;
|
|
468
|
+
// Un show plus récent → ne pas toucher
|
|
469
|
+
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
470
|
+
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
510
471
|
} catch (_) {}
|
|
511
|
-
},
|
|
472
|
+
}, EMPTY_CHECK_MS);
|
|
512
473
|
}
|
|
513
474
|
|
|
514
|
-
//
|
|
475
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
476
|
+
|
|
515
477
|
function patchShowAds() {
|
|
516
478
|
const apply = () => {
|
|
517
479
|
try {
|
|
518
480
|
window.ezstandalone = window.ezstandalone || {};
|
|
519
481
|
const ez = window.ezstandalone;
|
|
520
|
-
if (window.
|
|
521
|
-
|
|
522
|
-
window.__nodebbEzoicPatched = true;
|
|
482
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
483
|
+
window.__nbbEzPatched = true;
|
|
523
484
|
const orig = ez.showAds.bind(ez);
|
|
524
|
-
|
|
525
485
|
ez.showAds = function (...args) {
|
|
526
486
|
if (isBlocked()) return;
|
|
527
487
|
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
@@ -529,24 +489,22 @@
|
|
|
529
489
|
for (const v of ids) {
|
|
530
490
|
const id = parseInt(v, 10);
|
|
531
491
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
532
|
-
|
|
533
|
-
if (!ph?.isConnected) continue;
|
|
492
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
|
|
534
493
|
seen.add(id);
|
|
535
494
|
try { orig(id); } catch (_) {}
|
|
536
495
|
}
|
|
537
496
|
};
|
|
538
497
|
} catch (_) {}
|
|
539
498
|
};
|
|
540
|
-
|
|
541
499
|
apply();
|
|
542
|
-
if (!window.
|
|
500
|
+
if (!window.__nbbEzPatched) {
|
|
543
501
|
window.ezstandalone = window.ezstandalone || {};
|
|
544
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
545
|
-
window.ezstandalone.cmd.push(apply);
|
|
502
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
546
503
|
}
|
|
547
504
|
}
|
|
548
505
|
|
|
549
|
-
//
|
|
506
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
550
508
|
async function runCore() {
|
|
551
509
|
if (isBlocked()) return 0;
|
|
552
510
|
patchShowAds();
|
|
@@ -556,229 +514,191 @@
|
|
|
556
514
|
initPools(cfg);
|
|
557
515
|
|
|
558
516
|
const kind = getKind();
|
|
559
|
-
|
|
517
|
+
if (kind === 'other') return 0;
|
|
560
518
|
|
|
561
|
-
const
|
|
519
|
+
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
562
520
|
if (!normBool(cfgEnable)) return 0;
|
|
563
|
-
const items
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
521
|
+
const items = getItems();
|
|
522
|
+
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
523
|
+
pruneOrphans(klass);
|
|
524
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
525
|
+
if (n) decluster(klass);
|
|
567
526
|
return n;
|
|
568
527
|
};
|
|
569
528
|
|
|
570
|
-
if (kind === 'topic')
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
529
|
+
if (kind === 'topic') return exec(
|
|
530
|
+
'ezoic-ad-message', getPosts,
|
|
531
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
532
|
+
);
|
|
533
|
+
if (kind === 'categoryTopics') return exec(
|
|
534
|
+
'ezoic-ad-between', getTopics,
|
|
535
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
536
|
+
);
|
|
537
|
+
if (kind === 'categories') return exec(
|
|
538
|
+
'ezoic-ad-categories', getCategories,
|
|
539
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
540
|
+
);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
function scheduleRun(cb) {
|
|
547
|
+
if (S.runQueued) return;
|
|
548
|
+
S.runQueued = true;
|
|
549
|
+
requestAnimationFrame(async () => {
|
|
550
|
+
S.runQueued = false;
|
|
551
|
+
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
592
552
|
let n = 0;
|
|
593
553
|
try { n = await runCore(); } catch (_) {}
|
|
594
554
|
try { cb?.(n); } catch (_) {}
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
const doRun = () => requestAnimationFrame(run);
|
|
598
|
-
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
599
|
-
else doRun();
|
|
555
|
+
});
|
|
600
556
|
}
|
|
601
557
|
|
|
602
558
|
function requestBurst() {
|
|
603
559
|
if (isBlocked()) return;
|
|
604
|
-
const t =
|
|
605
|
-
if (t -
|
|
606
|
-
|
|
560
|
+
const t = ts();
|
|
561
|
+
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
562
|
+
S.lastBurstTs = t;
|
|
607
563
|
|
|
608
|
-
const pk =
|
|
609
|
-
|
|
610
|
-
|
|
564
|
+
const pk = pageKey();
|
|
565
|
+
S.pageKey = pk;
|
|
566
|
+
S.burstDeadline = t + 2000;
|
|
611
567
|
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
568
|
+
if (S.burstActive) return;
|
|
569
|
+
S.burstActive = true;
|
|
570
|
+
S.burstCount = 0;
|
|
615
571
|
|
|
616
572
|
const step = () => {
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
625
|
-
setTimeout(step, n > 0 ? 120 : 250);
|
|
573
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
574
|
+
S.burstActive = false; return;
|
|
575
|
+
}
|
|
576
|
+
S.burstCount++;
|
|
577
|
+
scheduleRun(n => {
|
|
578
|
+
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
579
|
+
setTimeout(step, n > 0 ? 150 : 300);
|
|
626
580
|
});
|
|
627
581
|
};
|
|
628
|
-
|
|
629
582
|
step();
|
|
630
583
|
}
|
|
631
584
|
|
|
632
|
-
//
|
|
633
|
-
function cleanup() {
|
|
634
|
-
// Bloquer toute injection pendant la transition
|
|
635
|
-
blockedUntil = now() + 1500;
|
|
585
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
636
586
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
587
|
+
function cleanup() {
|
|
588
|
+
blockedUntil = ts() + 1500;
|
|
589
|
+
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
590
|
+
S.cfg = null;
|
|
591
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
592
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
593
|
+
S.mountedIds.clear();
|
|
594
|
+
S.lastShow.clear();
|
|
595
|
+
S.inflight = 0;
|
|
596
|
+
S.pending = [];
|
|
597
|
+
S.pendingSet.clear();
|
|
598
|
+
S.burstActive = false;
|
|
599
|
+
S.runQueued = false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
641
603
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
// ─── DOM Observer ─────────────────────────────────────────────────────────
|
|
657
|
-
function shouldReact(mutations) {
|
|
658
|
-
for (const m of mutations) {
|
|
659
|
-
if (!m.addedNodes?.length) continue;
|
|
660
|
-
for (const n of m.addedNodes) {
|
|
661
|
-
if (n.nodeType !== 1) continue;
|
|
662
|
-
if (
|
|
663
|
-
n.matches?.(SELECTORS.postItem) || n.querySelector?.(SELECTORS.postItem) ||
|
|
664
|
-
n.matches?.(SELECTORS.topicItem) || n.querySelector?.(SELECTORS.topicItem) ||
|
|
665
|
-
n.matches?.(SELECTORS.categoryItem) || n.querySelector?.(SELECTORS.categoryItem)
|
|
666
|
-
) return true;
|
|
604
|
+
function ensureDomObserver() {
|
|
605
|
+
if (S.domObs) return;
|
|
606
|
+
S.domObs = new MutationObserver(muts => {
|
|
607
|
+
if (S.mutGuard > 0 || isBlocked()) return;
|
|
608
|
+
for (const m of muts) {
|
|
609
|
+
if (!m.addedNodes?.length) continue;
|
|
610
|
+
for (const n of m.addedNodes) {
|
|
611
|
+
if (n.nodeType !== 1) continue;
|
|
612
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
613
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
614
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
615
|
+
requestBurst(); return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
667
618
|
}
|
|
668
|
-
}
|
|
669
|
-
|
|
619
|
+
});
|
|
620
|
+
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
670
621
|
}
|
|
671
622
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
});
|
|
680
|
-
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// ─── Utilities: console mute + TCF + network warm ─────────────────────────
|
|
684
|
-
function muteNoisyConsole() {
|
|
685
|
-
if (window.__nodebbEzoicConsoleMuted) return;
|
|
686
|
-
window.__nodebbEzoicConsoleMuted = true;
|
|
687
|
-
const MUTED = [
|
|
688
|
-
'[EzoicAds JS]: Placeholder Id',
|
|
689
|
-
'Debugger iframe already exists',
|
|
690
|
-
'HTML element with id ezoic-pub-ad-placeholder-',
|
|
691
|
-
];
|
|
692
|
-
['log', 'info', 'warn', 'error'].forEach(m => {
|
|
623
|
+
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
function muteConsole() {
|
|
626
|
+
if (window.__nbbEzMuted) return;
|
|
627
|
+
window.__nbbEzMuted = true;
|
|
628
|
+
const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
|
|
629
|
+
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
693
630
|
const orig = console[m];
|
|
694
|
-
if (typeof orig !== 'function')
|
|
695
|
-
console[m] = function (...
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
orig.apply(console, args);
|
|
631
|
+
if (typeof orig !== 'function') continue;
|
|
632
|
+
console[m] = function (...a) {
|
|
633
|
+
if (typeof a[0] === 'string' && MUTED.some(p => a[0].includes(p))) return;
|
|
634
|
+
orig.apply(console, a);
|
|
699
635
|
};
|
|
700
|
-
}
|
|
636
|
+
}
|
|
701
637
|
}
|
|
702
638
|
|
|
703
639
|
function ensureTcfLocator() {
|
|
704
640
|
try {
|
|
705
641
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
706
642
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
707
|
-
const f =
|
|
708
|
-
|
|
709
|
-
});
|
|
643
|
+
const f = document.createElement('iframe');
|
|
644
|
+
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
710
645
|
(document.body || document.documentElement).appendChild(f);
|
|
711
646
|
} catch (_) {}
|
|
712
647
|
}
|
|
713
648
|
|
|
714
|
-
const
|
|
649
|
+
const _warmed = new Set();
|
|
715
650
|
function warmNetwork() {
|
|
716
651
|
const head = document.head;
|
|
717
652
|
if (!head) return;
|
|
718
|
-
const
|
|
719
|
-
['preconnect', 'https://g.ezoic.net',
|
|
720
|
-
['preconnect', 'https://go.ezoic.net',
|
|
721
|
-
['preconnect', 'https://securepubads.g.doubleclick.net',
|
|
722
|
-
['preconnect', 'https://pagead2.googlesyndication.com',
|
|
723
|
-
['dns-prefetch', 'https://g.ezoic.net',
|
|
724
|
-
['dns-prefetch', 'https://securepubads.g.doubleclick.net',
|
|
725
|
-
]
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
head.appendChild(link);
|
|
653
|
+
for (const [rel, href, cors] of [
|
|
654
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
655
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
656
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
657
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
658
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
659
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
660
|
+
]) {
|
|
661
|
+
const k = `${rel}|${href}`;
|
|
662
|
+
if (_warmed.has(k)) continue;
|
|
663
|
+
_warmed.add(k);
|
|
664
|
+
const l = document.createElement('link');
|
|
665
|
+
l.rel = rel; l.href = href;
|
|
666
|
+
if (cors) l.crossOrigin = 'anonymous';
|
|
667
|
+
head.appendChild(l);
|
|
734
668
|
}
|
|
735
669
|
}
|
|
736
670
|
|
|
737
|
-
//
|
|
671
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
672
|
+
|
|
738
673
|
function bindNodeBB() {
|
|
739
674
|
const $ = window.jQuery;
|
|
740
675
|
if (!$) return;
|
|
741
676
|
|
|
742
|
-
$(window).off('.
|
|
743
|
-
|
|
744
|
-
$(window).on('action:ajaxify.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
state.pageKey = getPageKey();
|
|
750
|
-
blockedUntil = 0;
|
|
751
|
-
muteNoisyConsole();
|
|
752
|
-
ensureTcfLocator();
|
|
753
|
-
warmNetwork();
|
|
754
|
-
patchShowAds();
|
|
755
|
-
ensurePreloadObserver();
|
|
756
|
-
ensureDomObserver();
|
|
757
|
-
requestBurst();
|
|
677
|
+
$(window).off('.nbbEzoic');
|
|
678
|
+
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
679
|
+
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
680
|
+
S.pageKey = pageKey();
|
|
681
|
+
blockedUntil = 0;
|
|
682
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
683
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
758
684
|
});
|
|
759
685
|
|
|
760
|
-
const
|
|
686
|
+
const BURST_EVENTS = [
|
|
761
687
|
'action:ajaxify.contentLoaded',
|
|
762
|
-
'action:posts.loaded',
|
|
763
|
-
'action:
|
|
764
|
-
|
|
765
|
-
'action:category.loaded',
|
|
766
|
-
'action:topic.loaded',
|
|
767
|
-
].map(e => `${e}.ezoicInfinite`).join(' ');
|
|
688
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
689
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
690
|
+
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
768
691
|
|
|
769
|
-
$(window).on(
|
|
692
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
770
693
|
|
|
771
|
-
// Hooks AMD (NodeBB 4.x)
|
|
694
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
772
695
|
try {
|
|
773
696
|
require(['hooks'], hooks => {
|
|
774
697
|
if (typeof hooks?.on !== 'function') return;
|
|
775
|
-
[
|
|
776
|
-
|
|
777
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
778
|
-
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
779
|
-
].forEach(ev => {
|
|
698
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
699
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
780
700
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
781
|
-
}
|
|
701
|
+
}
|
|
782
702
|
});
|
|
783
703
|
} catch (_) {}
|
|
784
704
|
}
|
|
@@ -786,38 +706,24 @@
|
|
|
786
706
|
function bindScroll() {
|
|
787
707
|
let ticking = false;
|
|
788
708
|
window.addEventListener('scroll', () => {
|
|
789
|
-
// Scroll boost
|
|
790
|
-
try {
|
|
791
|
-
const t = now();
|
|
792
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
793
|
-
if (state.lastScrollTs) {
|
|
794
|
-
const speed = Math.abs(y - state.lastScrollY) / Math.max(1, t - state.lastScrollTs);
|
|
795
|
-
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
796
|
-
const wasBoosted = isBoosted();
|
|
797
|
-
state.scrollBoostUntil = Math.max(state.scrollBoostUntil, t + BOOST_DURATION_MS);
|
|
798
|
-
if (!wasBoosted) ensurePreloadObserver();
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
state.lastScrollY = y;
|
|
802
|
-
state.lastScrollTs = t;
|
|
803
|
-
} catch (_) {}
|
|
804
|
-
|
|
805
709
|
if (ticking) return;
|
|
806
710
|
ticking = true;
|
|
807
711
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
808
712
|
}, { passive: true });
|
|
809
713
|
}
|
|
810
714
|
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
715
|
+
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
S.pageKey = pageKey();
|
|
718
|
+
muteConsole();
|
|
814
719
|
ensureTcfLocator();
|
|
815
720
|
warmNetwork();
|
|
816
721
|
patchShowAds();
|
|
817
|
-
|
|
722
|
+
getIO();
|
|
818
723
|
ensureDomObserver();
|
|
819
724
|
bindNodeBB();
|
|
820
725
|
bindScroll();
|
|
821
726
|
blockedUntil = 0;
|
|
822
727
|
requestBurst();
|
|
728
|
+
|
|
823
729
|
})();
|