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