nodebb-plugin-ezoic-infinite 1.7.0 → 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 +437 -560
- package/public/style.css +15 -26
package/public/client.js
CHANGED
|
@@ -1,459 +1,413 @@
|
|
|
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(),
|
|
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
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
io: null,
|
|
90
|
+
domObs: null,
|
|
91
|
+
mutGuard: 0, // compteur internalMutation
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
scrollBoostUntil: 0,
|
|
100
|
-
lastScrollY: 0, lastScrollTs: 0,
|
|
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
|
-
|
|
132
|
-
|
|
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;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
try {
|
|
271
|
-
|
|
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();
|
|
272
270
|
} catch (_) {}
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
//
|
|
273
|
+
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
276
275
|
/**
|
|
277
|
-
* Supprime les wraps dont l'ancre
|
|
276
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
278
277
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* -
|
|
282
|
-
*
|
|
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
283
|
*
|
|
284
|
-
* On ne
|
|
285
|
-
* temps à NodeBB de finir d'insérer les posts du batch.
|
|
284
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
286
285
|
*/
|
|
287
|
-
function pruneOrphans(
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
wraps.forEach(wrap => {
|
|
291
|
-
const created = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
292
|
-
if (now() - created < 5_000) return; // trop récent, on laisse
|
|
286
|
+
function pruneOrphans(klass) {
|
|
287
|
+
const meta = KIND[klass];
|
|
288
|
+
if (!meta) return;
|
|
293
289
|
|
|
294
|
-
|
|
295
|
-
if (!anchorKey) { withInternalMutation(() => removeWrap(wrap)); return; }
|
|
290
|
+
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
296
291
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const ord = parseInt(ordStr, 10);
|
|
292
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
293
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
300
294
|
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
: document.querySelector(`${selector.split('[')[0]}[data-index="${ord}"]`);
|
|
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; }
|
|
305
298
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
299
|
+
const anchorEl = document.querySelector(
|
|
300
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
301
|
+
);
|
|
302
|
+
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
310
303
|
});
|
|
311
304
|
}
|
|
312
305
|
|
|
313
|
-
//
|
|
306
|
+
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
314
308
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
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.
|
|
317
312
|
*/
|
|
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++;
|
|
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;
|
|
346
329
|
}
|
|
347
330
|
}
|
|
348
331
|
}
|
|
349
332
|
|
|
350
|
-
//
|
|
333
|
+
// ── Injection ──────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
351
335
|
/**
|
|
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.
|
|
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).
|
|
363
339
|
*/
|
|
364
|
-
function
|
|
365
|
-
|
|
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;
|
|
355
|
+
}
|
|
366
356
|
|
|
367
|
-
|
|
357
|
+
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
358
|
+
if (!items.length) return 0;
|
|
368
359
|
let inserted = 0;
|
|
369
360
|
|
|
370
361
|
for (const el of items) {
|
|
371
|
-
if (inserted >=
|
|
362
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
372
363
|
if (!el?.isConnected) continue;
|
|
373
364
|
|
|
374
|
-
const ord
|
|
375
|
-
|
|
376
|
-
// Est-ce une position cible ?
|
|
365
|
+
const ord = ordinal(klass, el);
|
|
377
366
|
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
378
367
|
if (!isTarget) continue;
|
|
379
368
|
|
|
380
|
-
if (
|
|
369
|
+
if (adjacentWrap(el)) continue;
|
|
381
370
|
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
371
|
+
const key = makeAnchorKey(klass, el);
|
|
372
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
384
373
|
|
|
385
374
|
const id = pickId(poolKey);
|
|
386
|
-
if (!id)
|
|
387
|
-
|
|
388
|
-
const wrap = insertWrapAfter(el, id, kindClass, anchorKey);
|
|
389
|
-
if (!wrap) continue;
|
|
375
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
390
376
|
|
|
391
|
-
|
|
392
|
-
inserted++;
|
|
377
|
+
const w = insertAfter(el, id, klass, key);
|
|
378
|
+
if (w) { observePh(id); inserted++; }
|
|
393
379
|
}
|
|
394
|
-
|
|
395
380
|
return inserted;
|
|
396
381
|
}
|
|
397
382
|
|
|
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;
|
|
383
|
+
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
414
384
|
|
|
385
|
+
function getIO() {
|
|
386
|
+
if (S.io) return S.io;
|
|
387
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
415
388
|
try {
|
|
416
|
-
|
|
417
|
-
for (const
|
|
418
|
-
if (!
|
|
419
|
-
|
|
420
|
-
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);
|
|
421
394
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
422
395
|
}
|
|
423
396
|
}, { 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;
|
|
397
|
+
} catch (_) { S.io = null; }
|
|
398
|
+
return S.io;
|
|
434
399
|
}
|
|
435
400
|
|
|
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 (_) {}
|
|
401
|
+
function observePh(id) {
|
|
402
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
403
|
+
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
448
404
|
}
|
|
449
405
|
|
|
450
406
|
function enqueueShow(id) {
|
|
451
407
|
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); }
|
|
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); }
|
|
457
411
|
return;
|
|
458
412
|
}
|
|
459
413
|
startShow(id);
|
|
@@ -461,96 +415,73 @@
|
|
|
461
415
|
|
|
462
416
|
function drainQueue() {
|
|
463
417
|
if (isBlocked()) return;
|
|
464
|
-
while (
|
|
465
|
-
const id =
|
|
466
|
-
|
|
418
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
419
|
+
const id = S.pending.shift();
|
|
420
|
+
S.pendingSet.delete(id);
|
|
467
421
|
startShow(id);
|
|
468
422
|
}
|
|
469
423
|
}
|
|
470
424
|
|
|
471
425
|
function startShow(id) {
|
|
472
426
|
if (!id || isBlocked()) return;
|
|
473
|
-
|
|
427
|
+
S.inflight++;
|
|
474
428
|
let done = false;
|
|
475
|
-
|
|
476
429
|
const release = () => {
|
|
477
430
|
if (done) return;
|
|
478
431
|
done = true;
|
|
479
|
-
|
|
432
|
+
S.inflight = Math.max(0, S.inflight - 1);
|
|
480
433
|
drainQueue();
|
|
481
434
|
};
|
|
482
|
-
|
|
483
|
-
const timeout = setTimeout(release, 6500);
|
|
435
|
+
const timer = setTimeout(release, 7000);
|
|
484
436
|
|
|
485
437
|
requestAnimationFrame(() => {
|
|
486
438
|
try {
|
|
487
|
-
if (isBlocked()) { clearTimeout(
|
|
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(); }
|
|
488
442
|
|
|
489
|
-
const
|
|
490
|
-
if (
|
|
491
|
-
|
|
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);
|
|
492
446
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
clearTimeout(timeout); return release();
|
|
496
|
-
}
|
|
497
|
-
state.lastShowById.set(id, t);
|
|
498
|
-
|
|
499
|
-
// Marquer le timestamp du show sur le wrap (pour decluster grace period)
|
|
500
|
-
try {
|
|
501
|
-
const wrap = ph.closest?.(`.${WRAP_CLASS}`);
|
|
502
|
-
if (wrap) wrap.setAttribute(SHOWN_ATTR, String(t));
|
|
503
|
-
} catch (_) {}
|
|
447
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
448
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
504
449
|
|
|
505
450
|
window.ezstandalone = window.ezstandalone || {};
|
|
506
451
|
const ez = window.ezstandalone;
|
|
507
|
-
|
|
508
452
|
const doShow = () => {
|
|
509
453
|
try { ez.showAds(id); } catch (_) {}
|
|
510
454
|
scheduleEmptyCheck(id, t);
|
|
511
|
-
setTimeout(() => { clearTimeout(
|
|
455
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
512
456
|
};
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
else doShow();
|
|
516
|
-
} catch (_) { clearTimeout(timeout); release(); }
|
|
457
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
458
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
517
459
|
});
|
|
518
460
|
}
|
|
519
461
|
|
|
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
462
|
function scheduleEmptyCheck(id, showTs) {
|
|
526
463
|
setTimeout(() => {
|
|
527
464
|
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');
|
|
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));
|
|
539
471
|
} catch (_) {}
|
|
540
|
-
},
|
|
472
|
+
}, EMPTY_CHECK_MS);
|
|
541
473
|
}
|
|
542
474
|
|
|
543
|
-
//
|
|
475
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
476
|
+
|
|
544
477
|
function patchShowAds() {
|
|
545
478
|
const apply = () => {
|
|
546
479
|
try {
|
|
547
480
|
window.ezstandalone = window.ezstandalone || {};
|
|
548
481
|
const ez = window.ezstandalone;
|
|
549
|
-
if (window.
|
|
550
|
-
|
|
551
|
-
window.__nodebbEzoicPatched = true;
|
|
482
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
483
|
+
window.__nbbEzPatched = true;
|
|
552
484
|
const orig = ez.showAds.bind(ez);
|
|
553
|
-
|
|
554
485
|
ez.showAds = function (...args) {
|
|
555
486
|
if (isBlocked()) return;
|
|
556
487
|
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
@@ -558,24 +489,22 @@
|
|
|
558
489
|
for (const v of ids) {
|
|
559
490
|
const id = parseInt(v, 10);
|
|
560
491
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
561
|
-
|
|
562
|
-
if (!ph?.isConnected) continue;
|
|
492
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
|
|
563
493
|
seen.add(id);
|
|
564
494
|
try { orig(id); } catch (_) {}
|
|
565
495
|
}
|
|
566
496
|
};
|
|
567
497
|
} catch (_) {}
|
|
568
498
|
};
|
|
569
|
-
|
|
570
499
|
apply();
|
|
571
|
-
if (!window.
|
|
500
|
+
if (!window.__nbbEzPatched) {
|
|
572
501
|
window.ezstandalone = window.ezstandalone || {};
|
|
573
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
574
|
-
window.ezstandalone.cmd.push(apply);
|
|
502
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
575
503
|
}
|
|
576
504
|
}
|
|
577
505
|
|
|
578
|
-
//
|
|
506
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
579
508
|
async function runCore() {
|
|
580
509
|
if (isBlocked()) return 0;
|
|
581
510
|
patchShowAds();
|
|
@@ -585,155 +514,126 @@
|
|
|
585
514
|
initPools(cfg);
|
|
586
515
|
|
|
587
516
|
const kind = getKind();
|
|
588
|
-
|
|
517
|
+
if (kind === 'other') return 0;
|
|
589
518
|
|
|
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) => {
|
|
519
|
+
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
600
520
|
if (!normBool(cfgEnable)) return 0;
|
|
601
521
|
const items = getItems();
|
|
602
522
|
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);
|
|
523
|
+
pruneOrphans(klass);
|
|
524
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
525
|
+
if (n) decluster(klass);
|
|
608
526
|
return n;
|
|
609
527
|
};
|
|
610
528
|
|
|
611
|
-
if (kind === 'topic')
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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;
|
|
623
542
|
}
|
|
624
543
|
|
|
625
|
-
//
|
|
626
|
-
function scheduleRun(delayMs, cb) {
|
|
627
|
-
if (state.runQueued) return;
|
|
628
|
-
state.runQueued = true;
|
|
544
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
629
545
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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;
|
|
633
552
|
let n = 0;
|
|
634
553
|
try { n = await runCore(); } catch (_) {}
|
|
635
554
|
try { cb?.(n); } catch (_) {}
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const doRun = () => requestAnimationFrame(run);
|
|
639
|
-
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
640
|
-
else doRun();
|
|
555
|
+
});
|
|
641
556
|
}
|
|
642
557
|
|
|
643
558
|
function requestBurst() {
|
|
644
559
|
if (isBlocked()) return;
|
|
645
|
-
const t =
|
|
646
|
-
if (t -
|
|
647
|
-
|
|
560
|
+
const t = ts();
|
|
561
|
+
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
562
|
+
S.lastBurstTs = t;
|
|
648
563
|
|
|
649
|
-
const pk =
|
|
650
|
-
|
|
651
|
-
|
|
564
|
+
const pk = pageKey();
|
|
565
|
+
S.pageKey = pk;
|
|
566
|
+
S.burstDeadline = t + 2000;
|
|
652
567
|
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
568
|
+
if (S.burstActive) return;
|
|
569
|
+
S.burstActive = true;
|
|
570
|
+
S.burstCount = 0;
|
|
656
571
|
|
|
657
572
|
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);
|
|
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);
|
|
667
580
|
});
|
|
668
581
|
};
|
|
669
|
-
|
|
670
582
|
step();
|
|
671
583
|
}
|
|
672
584
|
|
|
673
|
-
//
|
|
674
|
-
function cleanup() {
|
|
675
|
-
blockedUntil = now() + 1500;
|
|
585
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
676
586
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
});
|
|
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 ───────────────────────────────────────────────────────────
|
|
680
603
|
|
|
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;
|
|
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
|
+
}
|
|
703
618
|
}
|
|
704
|
-
}
|
|
705
|
-
|
|
619
|
+
});
|
|
620
|
+
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
706
621
|
}
|
|
707
622
|
|
|
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 => {
|
|
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']) {
|
|
729
630
|
const orig = console[m];
|
|
730
|
-
if (typeof orig !== 'function')
|
|
731
|
-
console[m] = function (...
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
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);
|
|
735
635
|
};
|
|
736
|
-
}
|
|
636
|
+
}
|
|
737
637
|
}
|
|
738
638
|
|
|
739
639
|
function ensureTcfLocator() {
|
|
@@ -741,76 +641,64 @@
|
|
|
741
641
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
742
642
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
743
643
|
const f = document.createElement('iframe');
|
|
744
|
-
f.style.display = 'none'; f.id =
|
|
644
|
+
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
745
645
|
(document.body || document.documentElement).appendChild(f);
|
|
746
646
|
} catch (_) {}
|
|
747
647
|
}
|
|
748
648
|
|
|
749
|
-
const
|
|
649
|
+
const _warmed = new Set();
|
|
750
650
|
function warmNetwork() {
|
|
751
651
|
const head = document.head;
|
|
752
652
|
if (!head) return;
|
|
753
|
-
const
|
|
653
|
+
for (const [rel, href, cors] of [
|
|
754
654
|
['preconnect', 'https://g.ezoic.net', true],
|
|
755
655
|
['preconnect', 'https://go.ezoic.net', true],
|
|
756
656
|
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
757
657
|
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
758
658
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
759
659
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
760
|
-
]
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
head.appendChild(link);
|
|
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);
|
|
769
668
|
}
|
|
770
669
|
}
|
|
771
670
|
|
|
772
|
-
//
|
|
671
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
672
|
+
|
|
773
673
|
function bindNodeBB() {
|
|
774
674
|
const $ = window.jQuery;
|
|
775
675
|
if (!$) return;
|
|
776
676
|
|
|
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();
|
|
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();
|
|
791
684
|
});
|
|
792
685
|
|
|
793
|
-
const
|
|
686
|
+
const BURST_EVENTS = [
|
|
794
687
|
'action:ajaxify.contentLoaded',
|
|
795
|
-
'action:posts.loaded',
|
|
796
|
-
'action:
|
|
797
|
-
|
|
798
|
-
'action:category.loaded',
|
|
799
|
-
'action:topic.loaded',
|
|
800
|
-
].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(' ');
|
|
801
691
|
|
|
802
|
-
$(window).on(
|
|
692
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
803
693
|
|
|
694
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
804
695
|
try {
|
|
805
696
|
require(['hooks'], hooks => {
|
|
806
697
|
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 => {
|
|
698
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
699
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
812
700
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
813
|
-
}
|
|
701
|
+
}
|
|
814
702
|
});
|
|
815
703
|
} catch (_) {}
|
|
816
704
|
}
|
|
@@ -818,35 +706,24 @@
|
|
|
818
706
|
function bindScroll() {
|
|
819
707
|
let ticking = false;
|
|
820
708
|
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
709
|
if (ticking) return;
|
|
835
710
|
ticking = true;
|
|
836
711
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
837
712
|
}, { passive: true });
|
|
838
713
|
}
|
|
839
714
|
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
|
|
715
|
+
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
S.pageKey = pageKey();
|
|
718
|
+
muteConsole();
|
|
843
719
|
ensureTcfLocator();
|
|
844
720
|
warmNetwork();
|
|
845
721
|
patchShowAds();
|
|
846
|
-
|
|
722
|
+
getIO();
|
|
847
723
|
ensureDomObserver();
|
|
848
724
|
bindNodeBB();
|
|
849
725
|
bindScroll();
|
|
850
726
|
blockedUntil = 0;
|
|
851
727
|
requestBurst();
|
|
728
|
+
|
|
852
729
|
})();
|