nodebb-plugin-ezoic-infinite 1.6.98 → 1.6.99
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 +507 -1378
- package/public/style.css +47 -46
- package/public/test.txt +0 -1
package/public/client.js
CHANGED
|
@@ -1,368 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v18)
|
|
3
|
+
*
|
|
4
|
+
* Corrections majeures vs v17 :
|
|
5
|
+
* 1. ANCRAGE PAR pid/tid (data-pid, data-index) au lieu d'ordinalMap fragile.
|
|
6
|
+
* → Le même post garde toujours le même ID de wrap, quelle que soit la virtualisation.
|
|
7
|
+
* 2. CLEANUP COMPLET à chaque navigation ajaxify (wraps + curseurs + état pools).
|
|
8
|
+
* → Supprime les fantômes qui se réaffichaient après scroll up/down.
|
|
9
|
+
* 3. DEDUPLICATION par ancrage : on vérifie `data-ezoic-anchor` avant d'insérer.
|
|
10
|
+
* → Empêche la création de doublons lors de multiples passes DOM.
|
|
11
|
+
* 4. PAS de recyclage de wraps (moveWrapAfter supprimé).
|
|
12
|
+
* → La cause n°1 des pubs "qui sautent n'importe où".
|
|
13
|
+
* 5. pruneOrphanWraps simplifié : suppression réelle (remove) au lieu de hide.
|
|
14
|
+
* → Un wrap orphelin = mort, pas caché. Libère l'id dans le curseur.
|
|
15
|
+
* 6. Déduplication de l'id Ezoic par wrap : un id ne peut être monté qu'une fois.
|
|
16
|
+
* 7. V17 pile-fix SUPPRIMÉ (was conflicting with main logic).
|
|
17
|
+
* 8. Factorisation : helpers partagés, pas de duplication de logique entre kinds.
|
|
18
|
+
*/
|
|
1
19
|
(function () {
|
|
2
20
|
'use strict';
|
|
3
21
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
// IMPORTANT: must NOT collide with Ezoic's own markup (they use `.ezoic-ad`).
|
|
28
|
-
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
29
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
30
|
-
const POOL_ID = 'nodebb-ezoic-placeholder-pool';
|
|
31
|
-
|
|
32
|
-
// Smoothness caps
|
|
33
|
-
// Limit how many placements we inject per scan pass.
|
|
34
|
-
// Too low = you end up with only a handful of placeholders after ajaxify.
|
|
35
|
-
// Too high = jank on very long pages.
|
|
36
|
-
const MAX_INSERTS_PER_RUN = 8;
|
|
37
|
-
|
|
38
|
-
// Keep empty (unfilled) wraps alive for a while. Topics/messages can fill late (auction/CMP).
|
|
39
|
-
// Pruning too early makes ads look like they "disappear" while scrolling.
|
|
40
|
-
// Keep empty wraps alive; mobile fills can be slow.
|
|
41
|
-
function keepEmptyWrapMs() { return isMobile() ? 120000 : 60000; }
|
|
42
|
-
|
|
43
|
-
// Preload margins
|
|
44
|
-
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
45
|
-
// Mobile: larger preload window so ad fill requests start earlier and
|
|
46
|
-
// users don't scroll past empty placeholders.
|
|
47
|
-
const PRELOAD_MARGIN_MOBILE = '3200px 0px 3200px 0px';
|
|
48
|
-
const PRELOAD_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
|
|
49
|
-
const PRELOAD_MARGIN_MOBILE_BOOST = '5200px 0px 5200px 0px';
|
|
50
|
-
|
|
51
|
-
const BOOST_DURATION_MS = 2500;
|
|
52
|
-
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
53
|
-
|
|
54
|
-
const MAX_INFLIGHT_DESKTOP = 4;
|
|
55
|
-
const MAX_INFLIGHT_MOBILE = 3;
|
|
22
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
23
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
24
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
25
|
+
const ANCHOR_ATTR = 'data-ezoic-anchor'; // unique key = kind:anchorId
|
|
26
|
+
const WRAPID_ATTR = 'data-ezoic-wrapid';
|
|
27
|
+
const CREATED_ATTR = 'data-ezoic-created';
|
|
28
|
+
|
|
29
|
+
const MAX_INSERTS_PER_RUN = 6;
|
|
30
|
+
const EMPTY_WRAP_TTL_MS = 90_000; // 90 s avant de colapser un wrap vide
|
|
31
|
+
const FILL_WATCH_MS = 7_000;
|
|
32
|
+
|
|
33
|
+
const PRELOAD_MARGIN = {
|
|
34
|
+
desktop: '2000px 0px 2000px 0px',
|
|
35
|
+
mobile: '3000px 0px 3000px 0px',
|
|
36
|
+
desktopBoosted:'4500px 0px 4500px 0px',
|
|
37
|
+
mobileBoosted: '4500px 0px 4500px 0px',
|
|
38
|
+
};
|
|
39
|
+
const BOOST_DURATION_MS = 2500;
|
|
40
|
+
const BOOST_SPEED_PX_PER_MS = 2.2;
|
|
41
|
+
const MAX_INFLIGHT_DESKTOP = 4;
|
|
42
|
+
const MAX_INFLIGHT_MOBILE = 3;
|
|
43
|
+
const SHOW_THROTTLE_MS = 900;
|
|
56
44
|
|
|
57
45
|
const SELECTORS = {
|
|
58
|
-
topicItem:
|
|
59
|
-
postItem:
|
|
46
|
+
topicItem: 'li[component="category/topic"]',
|
|
47
|
+
postItem: '[component="post"][data-pid]',
|
|
60
48
|
categoryItem: 'li[component="categories/category"]',
|
|
61
49
|
};
|
|
62
50
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// These warnings are not actionable for end-users and can flood the console.
|
|
68
|
-
// We selectively silence the known spam patterns while keeping other warnings intact.
|
|
69
|
-
function muteNoisyConsole() {
|
|
70
|
-
try {
|
|
71
|
-
if (window.__nodebbEzoicConsoleMuted) return;
|
|
72
|
-
window.__nodebbEzoicConsoleMuted = true;
|
|
73
|
-
|
|
74
|
-
const shouldMute = (args) => {
|
|
75
|
-
try {
|
|
76
|
-
if (!args || !args.length) return false;
|
|
77
|
-
const s0 = typeof args[0] === 'string' ? args[0] : '';
|
|
78
|
-
// Duplicate placeholder definition spam (common when reusing ids in SPA/Ajaxify).
|
|
79
|
-
if (s0.includes('[EzoicAds JS]: Placeholder Id') && s0.includes('has already been defined')) return true;
|
|
80
|
-
// Ezoic debugger iframe spam.
|
|
81
|
-
if (s0.includes('Debugger iframe already exists')) return true;
|
|
82
|
-
// Missing placeholder spam (we already guard showAds; Ezoic still logs sometimes).
|
|
83
|
-
if (s0.includes('HTML element with id ezoic-pub-ad-placeholder-') && s0.includes('does not exist')) return true;
|
|
84
|
-
return false;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
51
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
52
|
+
const now = () => Date.now();
|
|
53
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
54
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
89
55
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
wrap('log');
|
|
100
|
-
wrap('info');
|
|
101
|
-
wrap('warn');
|
|
102
|
-
wrap('error');
|
|
103
|
-
} catch (e) {}
|
|
56
|
+
function uniqInts(raw) {
|
|
57
|
+
const out = [], seen = new Set();
|
|
58
|
+
for (const v of String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
59
|
+
const n = parseInt(v, 10);
|
|
60
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
104
63
|
}
|
|
105
64
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// "postMessage" / "addtlConsent" null errors. Ensuring it's present makes the environment stable.
|
|
109
|
-
function ensureTcfApiLocator() {
|
|
110
|
-
try {
|
|
111
|
-
// If a CMP is not present, do nothing.
|
|
112
|
-
if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
|
|
113
|
-
if (document.getElementById('__tcfapiLocator')) return;
|
|
114
|
-
const f = document.createElement('iframe');
|
|
115
|
-
f.style.display = 'none';
|
|
116
|
-
f.id = '__tcfapiLocator';
|
|
117
|
-
f.name = '__tcfapiLocator';
|
|
118
|
-
(document.body || document.documentElement).appendChild(f);
|
|
119
|
-
} catch (e) {}
|
|
65
|
+
function isFilledNode(node) {
|
|
66
|
+
return !!(node?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
120
67
|
}
|
|
121
68
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
function isFilledNode(node) {
|
|
125
|
-
return !!(node && node.querySelector && node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Ezoic injects inline `min-height:400px !important` on one or more nested wrappers.
|
|
129
|
-
// If the creative is 250px, this leaves ~150px empty space. Because it's inline+important,
|
|
130
|
-
// CSS alone cannot fix it reliably — we must rewrite the inline style.
|
|
131
|
-
function tightenEzoicMinHeight(wrap) {
|
|
132
|
-
try {
|
|
133
|
-
if (!wrap || !wrap.querySelector) return;
|
|
134
|
-
|
|
135
|
-
const iframes = wrap.querySelectorAll('iframe');
|
|
136
|
-
if (!iframes || !iframes.length) return;
|
|
137
|
-
|
|
138
|
-
// Find the closest "big" ezoic container that carries the 400px min-height.
|
|
139
|
-
const firstIframe = iframes[0];
|
|
140
|
-
let refNode = null;
|
|
141
|
-
let p = firstIframe && firstIframe.parentElement;
|
|
142
|
-
while (p && p !== wrap) {
|
|
143
|
-
if (p.classList && p.classList.contains('ezoic-ad')) {
|
|
144
|
-
const st = (p.getAttribute('style') || '').toLowerCase();
|
|
145
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
146
|
-
refNode = p;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
p = p.parentElement;
|
|
151
|
-
}
|
|
152
|
-
if (!refNode) {
|
|
153
|
-
refNode = wrap.querySelector('.ezoic-ad-adaptive') || wrap.querySelector('.ezoic-ad') || wrap;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let refTop = 0;
|
|
157
|
-
try { refTop = refNode.getBoundingClientRect().top; } catch (e) { refTop = 0; }
|
|
158
|
-
|
|
159
|
-
// Compute the rendered height needed inside refNode (visible iframes only).
|
|
160
|
-
let maxBottom = 0;
|
|
161
|
-
iframes.forEach((f) => {
|
|
162
|
-
if (!f || !f.getBoundingClientRect) return;
|
|
163
|
-
const rect = f.getBoundingClientRect();
|
|
164
|
-
if (rect.width <= 1 || rect.height <= 1) return;
|
|
165
|
-
const bottom = rect.bottom - refTop;
|
|
166
|
-
maxBottom = Math.max(maxBottom, bottom);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Fallback to attr/offset if layout metrics are not available.
|
|
170
|
-
if (!maxBottom) {
|
|
171
|
-
iframes.forEach((f) => {
|
|
172
|
-
const ah = parseInt(f.getAttribute('height') || '0', 10);
|
|
173
|
-
const oh = f.offsetHeight || 0;
|
|
174
|
-
maxBottom = Math.max(maxBottom, ah, oh);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (!maxBottom) return;
|
|
178
|
-
|
|
179
|
-
// NOTE: Do NOT add the Ezoic badge (reportline) height here.
|
|
180
|
-
// It is absolutely positioned and should not reserve layout space.
|
|
181
|
-
|
|
182
|
-
const h = Math.max(1, Math.ceil(maxBottom));
|
|
183
|
-
|
|
184
|
-
const tightenNode = (node) => {
|
|
185
|
-
if (!node || !node.style) return;
|
|
186
|
-
try { node.style.setProperty('min-height', h + 'px', 'important'); } catch (e) { node.style.minHeight = h + 'px'; }
|
|
187
|
-
try { node.style.setProperty('height', 'auto', 'important'); } catch (e) {}
|
|
188
|
-
try { node.style.setProperty('line-height', '0', 'important'); } catch (e) {}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Tighten refNode and any ancestor ezoic-ad nodes with the problematic min-height.
|
|
192
|
-
let cur = refNode;
|
|
193
|
-
while (cur && cur !== wrap) {
|
|
194
|
-
if (cur.classList && cur.classList.contains('ezoic-ad')) {
|
|
195
|
-
const st = (cur.getAttribute('style') || '').toLowerCase();
|
|
196
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
197
|
-
tightenNode(cur);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
cur = cur.parentElement;
|
|
201
|
-
}
|
|
202
|
-
tightenNode(refNode);
|
|
203
|
-
|
|
204
|
-
// Tighten any nested wrappers that also have the 400px min-height inline.
|
|
205
|
-
refNode.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive').forEach((n) => {
|
|
206
|
-
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
207
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
208
|
-
tightenNode(n);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Mobile friendliness: avoid giant fixed widths causing overflow/reflow.
|
|
213
|
-
if (isMobile()) {
|
|
214
|
-
[refNode].forEach((n) => {
|
|
215
|
-
try { n.style.setProperty('width', '100%', 'important'); } catch (e) {}
|
|
216
|
-
try { n.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
|
|
217
|
-
try { n.style.setProperty('min-width', '0', 'important'); } catch (e) {}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function watchWrapForFill(wrap) {
|
|
224
|
-
try {
|
|
225
|
-
if (!wrap || wrap.__ezFillObs) return;
|
|
226
|
-
|
|
227
|
-
// Ezoic can (re)apply inline styles after fill; keep tightening for a short window.
|
|
228
|
-
const start = now();
|
|
229
|
-
const tightenBurst = () => {
|
|
230
|
-
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
231
|
-
if (now() - start < 6000) {
|
|
232
|
-
setTimeout(tightenBurst, 350);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const obs = new MutationObserver((muts) => {
|
|
237
|
-
// If anything that looks like ad content appears, treat as filled.
|
|
238
|
-
if (isFilledNode(wrap)) {
|
|
239
|
-
wrap.classList.remove('is-empty');
|
|
240
|
-
tightenBurst();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// If Ezoic changes inline style on descendants (min-height:400!important), tighten again.
|
|
244
|
-
for (const m of muts) {
|
|
245
|
-
if (m.type === 'attributes' && m.attributeName === 'style') {
|
|
246
|
-
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Disconnect only after the burst window to avoid missing late style rewrites.
|
|
252
|
-
if (now() - start > 7000) {
|
|
253
|
-
try { obs.disconnect(); } catch (e) {}
|
|
254
|
-
wrap.__ezFillObs = null;
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
259
|
-
wrap.__ezFillObs = obs;
|
|
260
|
-
} catch (e) {}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Global safety net: sometimes Ezoic swaps nodes in ways that bypass our per-wrap observers.
|
|
264
|
-
// When we see an Ezoic container with min-height:400!important inside posts/topics, shrink it.
|
|
265
|
-
function globalGapFixInit() {
|
|
266
|
-
try {
|
|
267
|
-
if (window.__nodebbEzoicGapFix) return;
|
|
268
|
-
window.__nodebbEzoicGapFix = true;
|
|
269
|
-
|
|
270
|
-
// Observe only the main content area to minimize overhead.
|
|
271
|
-
const root = document.getElementById('content') || document.querySelector('[component="content"], #panel') || document.body;
|
|
272
|
-
|
|
273
|
-
const inPostArea = (el) => {
|
|
274
|
-
try {
|
|
275
|
-
return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
|
|
276
|
-
} catch (e) { return false; }
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const maybeFix = (root) => {
|
|
280
|
-
if (!root || !root.querySelectorAll) return;
|
|
281
|
-
const nodes = root.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]');
|
|
282
|
-
nodes.forEach((n) => {
|
|
283
|
-
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
284
|
-
if (!st.includes('min-height:400')) return;
|
|
285
|
-
if (!inPostArea(n)) return;
|
|
286
|
-
try {
|
|
287
|
-
const tmpWrap = n.closest('.' + WRAP_CLASS) || n.parentElement;
|
|
288
|
-
tightenEzoicMinHeight(tmpWrap || n);
|
|
289
|
-
} catch (e) {}
|
|
290
|
-
});
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
requestAnimationFrame(() => maybeFix(root));
|
|
294
|
-
|
|
295
|
-
// Batch DOM mutation processing into a single rAF to avoid doing work per mutation.
|
|
296
|
-
const pending = new Set();
|
|
297
|
-
let scheduled = false;
|
|
298
|
-
const scheduleFlush = () => {
|
|
299
|
-
if (scheduled) return;
|
|
300
|
-
scheduled = true;
|
|
301
|
-
requestAnimationFrame(() => {
|
|
302
|
-
scheduled = false;
|
|
303
|
-
pending.forEach((n) => {
|
|
304
|
-
try { maybeFix(n); } catch (e) {}
|
|
305
|
-
});
|
|
306
|
-
pending.clear();
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const obs = new MutationObserver((muts) => {
|
|
311
|
-
for (const m of muts) {
|
|
312
|
-
if (m.type === 'attributes') {
|
|
313
|
-
const t = m.target && m.target.nodeType === 1 ? m.target : m.target && m.target.parentElement;
|
|
314
|
-
if (t) pending.add(t);
|
|
315
|
-
} else if (m.addedNodes && m.addedNodes.length) {
|
|
316
|
-
m.addedNodes.forEach((n) => {
|
|
317
|
-
if (n && n.nodeType === 1) pending.add(n);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (pending.size) scheduleFlush();
|
|
322
|
-
});
|
|
323
|
-
obs.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
|
|
324
|
-
} catch (e) {}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ---------------- state ----------------
|
|
328
|
-
|
|
69
|
+
// ─── State ────────────────────────────────────────────────────────────────
|
|
329
70
|
const state = {
|
|
330
71
|
pageKey: null,
|
|
331
72
|
cfg: null,
|
|
332
73
|
|
|
333
|
-
// pools
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
curCategories: 0,
|
|
74
|
+
// ID pools + curseurs rotatifs
|
|
75
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
76
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
77
|
+
|
|
78
|
+
// Suivi des IDs Ezoic actuellement montés dans le DOM (évite les doublons)
|
|
79
|
+
mountedIds: new Set(),
|
|
340
80
|
|
|
341
|
-
//
|
|
81
|
+
// Throttle par id
|
|
342
82
|
lastShowById: new Map(),
|
|
343
83
|
|
|
344
|
-
//
|
|
84
|
+
// Observers
|
|
345
85
|
domObs: null,
|
|
346
86
|
io: null,
|
|
347
87
|
ioMargin: null,
|
|
348
88
|
|
|
349
|
-
//
|
|
350
|
-
|
|
89
|
+
// Guard contre nos propres mutations
|
|
90
|
+
internalMutation: 0,
|
|
351
91
|
|
|
352
|
-
//
|
|
92
|
+
// File de show
|
|
353
93
|
inflight: 0,
|
|
354
94
|
pending: [],
|
|
355
95
|
pendingSet: new Set(),
|
|
356
96
|
|
|
357
|
-
//
|
|
97
|
+
// Scroll boost
|
|
358
98
|
scrollBoostUntil: 0,
|
|
359
99
|
lastScrollY: 0,
|
|
360
100
|
lastScrollTs: 0,
|
|
361
101
|
|
|
362
|
-
//
|
|
363
|
-
heroDoneForPage: false,
|
|
364
|
-
|
|
365
|
-
// run scheduler
|
|
102
|
+
// Scheduler
|
|
366
103
|
runQueued: false,
|
|
367
104
|
burstActive: false,
|
|
368
105
|
burstDeadline: 0,
|
|
@@ -370,939 +107,491 @@ function globalGapFixInit() {
|
|
|
370
107
|
lastBurstReqTs: 0,
|
|
371
108
|
};
|
|
372
109
|
|
|
373
|
-
// Soft block during navigation / heavy DOM churn
|
|
374
110
|
let blockedUntil = 0;
|
|
375
|
-
const
|
|
111
|
+
const isBlocked = () => now() < blockedUntil;
|
|
112
|
+
const isBoosted = () => now() < state.scrollBoostUntil;
|
|
376
113
|
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// ---------------- utils ----------------
|
|
381
|
-
|
|
382
|
-
function normalizeBool(v) {
|
|
383
|
-
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
114
|
+
function withInternalMutation(fn) {
|
|
115
|
+
state.internalMutation++;
|
|
116
|
+
try { fn(); } finally { state.internalMutation--; }
|
|
384
117
|
}
|
|
385
118
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
return out;
|
|
119
|
+
// ─── Config ───────────────────────────────────────────────────────────────
|
|
120
|
+
async function fetchConfig() {
|
|
121
|
+
if (state.cfg) return state.cfg;
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
124
|
+
if (!res.ok) return null;
|
|
125
|
+
state.cfg = await res.json();
|
|
126
|
+
} catch (_) { state.cfg = null; }
|
|
127
|
+
return state.cfg;
|
|
397
128
|
}
|
|
398
129
|
|
|
399
|
-
function
|
|
400
|
-
if (!
|
|
401
|
-
|
|
402
|
-
.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return uniqInts(lines);
|
|
130
|
+
function initPools(cfg) {
|
|
131
|
+
if (!cfg) return;
|
|
132
|
+
state.pools.topics = uniqInts(cfg.placeholderIds || '');
|
|
133
|
+
state.pools.posts = uniqInts(cfg.messagePlaceholderIds || '');
|
|
134
|
+
state.pools.categories = uniqInts(cfg.categoryPlaceholderIds || '');
|
|
135
|
+
// Ne pas réinitialiser les curseurs ici (ils sont remis à 0 dans cleanup).
|
|
406
136
|
}
|
|
407
137
|
|
|
138
|
+
// ─── Page / Kind ──────────────────────────────────────────────────────────
|
|
408
139
|
function getPageKey() {
|
|
409
140
|
try {
|
|
410
|
-
const ax = window.ajaxify;
|
|
411
|
-
if (ax
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
} catch (e) {}
|
|
416
|
-
return window.location.pathname;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function isMobile() {
|
|
420
|
-
try { return window.innerWidth < 768; } catch (e) { return false; }
|
|
141
|
+
const ax = window.ajaxify?.data;
|
|
142
|
+
if (ax?.tid) return `topic:${ax.tid}`;
|
|
143
|
+
if (ax?.cid) return `cid:${ax.cid}:${location.pathname}`;
|
|
144
|
+
} catch (_) {}
|
|
145
|
+
return location.pathname;
|
|
421
146
|
}
|
|
422
147
|
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
function getMaxInflight() {
|
|
433
|
-
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
434
|
-
return base + (isBoosted() ? 1 : 0);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function withInternalDomChange(fn) {
|
|
438
|
-
state.internalDomChange++;
|
|
439
|
-
try { fn(); } finally { state.internalDomChange--; }
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ---------------- DOM helpers ----------------
|
|
443
|
-
|
|
444
|
-
function getTopicItems() {
|
|
445
|
-
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function getCategoryItems() {
|
|
449
|
-
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
148
|
+
function getKind() {
|
|
149
|
+
const p = location.pathname;
|
|
150
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
151
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
152
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
153
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
154
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
155
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
156
|
+
return 'other';
|
|
450
157
|
}
|
|
451
158
|
|
|
159
|
+
// ─── DOM helpers ──────────────────────────────────────────────────────────
|
|
452
160
|
function getPostContainers() {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (!el || !el.isConnected) return false;
|
|
161
|
+
return Array.from(document.querySelectorAll(SELECTORS.postItem)).filter(el => {
|
|
162
|
+
if (!el.isConnected) return false;
|
|
456
163
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
457
|
-
const parentPost = el.parentElement
|
|
164
|
+
const parentPost = el.parentElement?.closest('[component="post"][data-pid]');
|
|
458
165
|
if (parentPost && parentPost !== el) return false;
|
|
459
166
|
if (el.getAttribute('component') === 'post/parent') return false;
|
|
460
167
|
return true;
|
|
461
168
|
});
|
|
462
169
|
}
|
|
463
170
|
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
467
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
468
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
469
|
-
|
|
470
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
471
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
472
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
473
|
-
return 'other';
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function isAdjacentAd(target) {
|
|
477
|
-
if (!target) return false;
|
|
478
|
-
const next = target.nextElementSibling;
|
|
479
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
480
|
-
const prev = target.previousElementSibling;
|
|
481
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function findWrap(kindClass, afterPos) {
|
|
486
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ---------------- placeholder pool ----------------
|
|
171
|
+
function getTopicItems() { return Array.from(document.querySelectorAll(SELECTORS.topicItem)); }
|
|
172
|
+
function getCategoryItems() { return Array.from(document.querySelectorAll(SELECTORS.categoryItem)); }
|
|
490
173
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Calcule l'ancre stable d'un élément : on préfère data-pid (posts) puis
|
|
176
|
+
* data-index, sinon on retombe sur l'index dans le tableau.
|
|
177
|
+
* La clé est préfixée par kindClass pour éviter les collisions.
|
|
178
|
+
*/
|
|
179
|
+
function getAnchorKey(kindClass, el, fallbackIndex) {
|
|
180
|
+
const pid = el.getAttribute('data-pid');
|
|
181
|
+
const index = el.getAttribute('data-index') ?? el.getAttribute('data-idx');
|
|
182
|
+
const id = pid ?? index ?? String(fallbackIndex);
|
|
183
|
+
return `${kindClass}:${id}`;
|
|
499
184
|
}
|
|
500
185
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
if (!Array.isArray(allIds) || !allIds.length) return;
|
|
504
|
-
const pool = getPoolEl();
|
|
505
|
-
for (const id of allIds) {
|
|
506
|
-
if (!id) continue;
|
|
507
|
-
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
508
|
-
if (document.getElementById(domId)) continue;
|
|
509
|
-
const ph = document.createElement('div');
|
|
510
|
-
ph.id = domId;
|
|
511
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
512
|
-
pool.appendChild(ph);
|
|
513
|
-
}
|
|
514
|
-
} catch (e) {}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function isInPool(ph) {
|
|
518
|
-
try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function releaseWrapNode(wrap) {
|
|
522
|
-
try {
|
|
523
|
-
const ph = wrap && wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
524
|
-
if (ph) {
|
|
525
|
-
try { getPoolEl().appendChild(ph); } catch (e) {}
|
|
526
|
-
try { state.io && state.io.unobserve(ph); } catch (e) {}
|
|
527
|
-
}
|
|
528
|
-
} catch (e) {}
|
|
529
|
-
try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// ---------------- network warmup ----------------
|
|
533
|
-
|
|
534
|
-
const _warmLinksDone = new Set();
|
|
535
|
-
function warmUpNetwork() {
|
|
536
|
-
try {
|
|
537
|
-
const head = document.head || document.getElementsByTagName('head')[0];
|
|
538
|
-
if (!head) return;
|
|
539
|
-
const links = [
|
|
540
|
-
// Ezoic
|
|
541
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
542
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
543
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
544
|
-
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
545
|
-
// Google ads
|
|
546
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
547
|
-
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
548
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
549
|
-
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
550
|
-
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
551
|
-
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
552
|
-
];
|
|
553
|
-
for (const [rel, href, cors] of links) {
|
|
554
|
-
const key = `${rel}|${href}`;
|
|
555
|
-
if (_warmLinksDone.has(key)) continue;
|
|
556
|
-
_warmLinksDone.add(key);
|
|
557
|
-
const link = document.createElement('link');
|
|
558
|
-
link.rel = rel;
|
|
559
|
-
link.href = href;
|
|
560
|
-
if (cors) link.crossOrigin = 'anonymous';
|
|
561
|
-
head.appendChild(link);
|
|
562
|
-
}
|
|
563
|
-
} catch (e) {}
|
|
186
|
+
function findWrapByAnchor(anchorKey) {
|
|
187
|
+
return document.querySelector(`.${WRAP_CLASS}[${ANCHOR_ATTR}="${CSS.escape(anchorKey)}"]`);
|
|
564
188
|
}
|
|
565
189
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
573
|
-
const ez = window.ezstandalone;
|
|
574
|
-
if (window.__nodebbEzoicPatched) return;
|
|
575
|
-
if (typeof ez.showAds !== 'function') return;
|
|
576
|
-
|
|
577
|
-
window.__nodebbEzoicPatched = true;
|
|
578
|
-
const orig = ez.showAds;
|
|
579
|
-
|
|
580
|
-
ez.showAds = function (...args) {
|
|
581
|
-
if (isBlocked()) return;
|
|
582
|
-
|
|
583
|
-
let ids = [];
|
|
584
|
-
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
585
|
-
else ids = args;
|
|
586
|
-
|
|
587
|
-
const seen = new Set();
|
|
588
|
-
for (const v of ids) {
|
|
589
|
-
const id = parseInt(v, 10);
|
|
590
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
591
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
592
|
-
if (!ph || !ph.isConnected) continue;
|
|
593
|
-
seen.add(id);
|
|
594
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
} catch (e) {}
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
applyPatch();
|
|
601
|
-
if (!window.__nodebbEzoicPatched) {
|
|
602
|
-
try {
|
|
603
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
604
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
605
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
606
|
-
} catch (e) {}
|
|
607
|
-
}
|
|
190
|
+
function hasAdjacentWrap(el) {
|
|
191
|
+
const next = el.nextElementSibling;
|
|
192
|
+
if (next?.classList?.contains(WRAP_CLASS)) return true;
|
|
193
|
+
const prev = el.previousElementSibling;
|
|
194
|
+
if (prev?.classList?.contains(WRAP_CLASS)) return true;
|
|
195
|
+
return false;
|
|
608
196
|
}
|
|
609
197
|
|
|
610
|
-
//
|
|
198
|
+
// ─── ID pool / rotation ───────────────────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Retourne le prochain id disponible du pool (non déjà monté dans le DOM),
|
|
201
|
+
* en avançant le curseur rotatif.
|
|
202
|
+
*/
|
|
203
|
+
function pickId(poolKey) {
|
|
204
|
+
const pool = state.pools[poolKey];
|
|
205
|
+
const n = pool.length;
|
|
206
|
+
if (!n) return null;
|
|
611
207
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
if (!
|
|
617
|
-
state.cfg = await res.json();
|
|
618
|
-
return state.cfg;
|
|
619
|
-
} catch (e) {
|
|
620
|
-
return null;
|
|
208
|
+
for (let tries = 0; tries < n; tries++) {
|
|
209
|
+
const idx = state.cursors[poolKey] % n;
|
|
210
|
+
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % n;
|
|
211
|
+
const id = pool[idx];
|
|
212
|
+
if (!state.mountedIds.has(id)) return id;
|
|
621
213
|
}
|
|
214
|
+
return null; // Tous les IDs déjà montés
|
|
622
215
|
}
|
|
623
216
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
|
|
627
|
-
if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
628
|
-
if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
629
|
-
|
|
630
|
-
// IMPORTANT:
|
|
631
|
-
// We do NOT prime a DOM pool anymore.
|
|
632
|
-
// Keeping placeholders connected (even offscreen) can lead Ezoic/GPT to
|
|
633
|
-
// pre-define slots, which then causes "Placeholder Id X has already been defined".
|
|
634
|
-
// Instead, we create the placeholder element only when we actually inject its wrapper.
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// ---------------- insertion primitives ----------------
|
|
638
|
-
|
|
639
|
-
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
217
|
+
// ─── Wrap construction ────────────────────────────────────────────────────
|
|
218
|
+
function buildWrap(id, kindClass, anchorKey) {
|
|
640
219
|
const wrap = document.createElement('div');
|
|
641
220
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
642
|
-
wrap.setAttribute(
|
|
643
|
-
wrap.setAttribute(
|
|
644
|
-
wrap.setAttribute(
|
|
645
|
-
|
|
646
|
-
if (afterPos === 1) {
|
|
647
|
-
wrap.setAttribute('data-ezoic-pin', '1');
|
|
648
|
-
}
|
|
649
|
-
wrap.style.width = '100%';
|
|
221
|
+
wrap.setAttribute(ANCHOR_ATTR, anchorKey);
|
|
222
|
+
wrap.setAttribute(WRAPID_ATTR, String(id));
|
|
223
|
+
wrap.setAttribute(CREATED_ATTR, String(now()));
|
|
224
|
+
wrap.style.cssText = 'width:100%;display:block;';
|
|
650
225
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
wrap.appendChild(ph);
|
|
656
|
-
}
|
|
226
|
+
const ph = document.createElement('div');
|
|
227
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
228
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
229
|
+
wrap.appendChild(ph);
|
|
657
230
|
|
|
658
231
|
return wrap;
|
|
659
232
|
}
|
|
660
233
|
|
|
661
|
-
function
|
|
662
|
-
if (!
|
|
663
|
-
if (
|
|
664
|
-
if (
|
|
234
|
+
function insertWrapAfter(el, id, kindClass, anchorKey) {
|
|
235
|
+
if (!el?.insertAdjacentElement) return null;
|
|
236
|
+
if (findWrapByAnchor(anchorKey)) return null; // déjà présent
|
|
237
|
+
if (state.mountedIds.has(id)) return null; // id déjà monté
|
|
665
238
|
|
|
666
239
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
671
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
672
|
-
|
|
673
|
-
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
674
|
-
if (existingPh) {
|
|
675
|
-
try {
|
|
676
|
-
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
677
|
-
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
678
|
-
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
679
|
-
} catch (e) {}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return wrap;
|
|
683
|
-
} finally {
|
|
684
|
-
insertingIds.delete(id);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function pickIdFromAll(allIds, cursorKey) {
|
|
689
|
-
const n = allIds.length;
|
|
690
|
-
if (!n) return null;
|
|
691
|
-
|
|
692
|
-
for (let tries = 0; tries < n; tries++) {
|
|
693
|
-
const idx = state[cursorKey] % n;
|
|
694
|
-
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
695
|
-
const id = allIds[idx];
|
|
696
|
-
|
|
697
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
698
|
-
if (ph && ph.isConnected && !isInPool(ph)) continue;
|
|
699
|
-
|
|
700
|
-
return id;
|
|
240
|
+
if (existingPh?.isConnected) {
|
|
241
|
+
// Cet id a déjà un placeholder dans le DOM → on ne peut pas le dupliquer
|
|
242
|
+
return null;
|
|
701
243
|
}
|
|
702
244
|
|
|
703
|
-
|
|
245
|
+
const wrap = buildWrap(id, kindClass, anchorKey);
|
|
246
|
+
withInternalMutation(() => el.insertAdjacentElement('afterend', wrap));
|
|
247
|
+
state.mountedIds.add(id);
|
|
248
|
+
return wrap;
|
|
704
249
|
}
|
|
705
250
|
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
251
|
+
function removeWrap(wrap) {
|
|
252
|
+
try {
|
|
253
|
+
const id = parseInt(wrap.getAttribute(WRAPID_ATTR), 10);
|
|
254
|
+
if (Number.isFinite(id)) state.mountedIds.delete(id);
|
|
255
|
+
wrap.remove();
|
|
256
|
+
} catch (_) {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Prune & Decluster ────────────────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
262
|
+
* On supprime proprement (remove) plutôt que de cacher → libère l'id.
|
|
263
|
+
*/
|
|
264
|
+
function pruneOrphans(kindClass) {
|
|
713
265
|
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
714
266
|
let removed = 0;
|
|
715
267
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
268
|
+
wraps.forEach(wrap => {
|
|
269
|
+
// Ne jamais pruner un wrap tout juste créé (fill lent côté Ezoic)
|
|
270
|
+
const created = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
271
|
+
if (created && (now() - created) < EMPTY_WRAP_TTL_MS) return;
|
|
719
272
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
prev = prev.previousElementSibling;
|
|
273
|
+
const anchorKey = wrap.getAttribute(ANCHOR_ATTR);
|
|
274
|
+
if (!anchorKey) {
|
|
275
|
+
withInternalMutation(() => removeWrap(wrap));
|
|
276
|
+
removed++;
|
|
277
|
+
return;
|
|
726
278
|
}
|
|
727
|
-
let next = wrap.nextElementSibling;
|
|
728
|
-
for (let i = 0; i < 14 && next; i++) {
|
|
729
|
-
if (itemSet.has(next)) return true;
|
|
730
|
-
next = next.nextElementSibling;
|
|
731
|
-
}
|
|
732
|
-
return false;
|
|
733
|
-
};
|
|
734
279
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
} catch (e) {}
|
|
280
|
+
// L'ancre existe-t-elle encore dans le DOM ?
|
|
281
|
+
const [, anchorId] = anchorKey.split(':');
|
|
282
|
+
const isPost = kindClass === 'ezoic-ad-message';
|
|
283
|
+
let anchorEl = null;
|
|
740
284
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
try {
|
|
748
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
749
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
750
|
-
} catch (e) {}
|
|
751
|
-
|
|
752
|
-
if (hasNearbyItem(wrap)) {
|
|
753
|
-
try { wrap.classList && wrap.classList.remove('ez-orphan-hidden'); wrap.style && (wrap.style.display = ''); } catch (e) {}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// If the anchor item is no longer in the DOM (virtualized), hide the wrap so ads never "stack"
|
|
758
|
-
// back-to-back while scrolling. We'll recycle it when its anchor comes back.
|
|
759
|
-
try { wrap.classList && wrap.classList.add('ez-orphan-hidden'); wrap.style && (wrap.style.display = 'none'); } catch (e) {}
|
|
760
|
-
|
|
761
|
-
// For message ads: only release if far offscreen to avoid perceived "vanishing" during fast scroll.
|
|
762
|
-
if (isMessage) {
|
|
763
|
-
try {
|
|
764
|
-
const r = wrap.getBoundingClientRect();
|
|
765
|
-
const vh = Math.max(1, window.innerHeight || 1);
|
|
766
|
-
const farAbove = r.bottom < -vh * 2;
|
|
767
|
-
const farBelow = r.top > vh * 4;
|
|
768
|
-
if (!farAbove && !farBelow) return;
|
|
769
|
-
} catch (e) {
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
285
|
+
if (isPost) {
|
|
286
|
+
anchorEl = document.querySelector(`[component="post"][data-pid="${CSS.escape(anchorId)}"]`);
|
|
287
|
+
} else {
|
|
288
|
+
anchorEl = document.querySelector(`${SELECTORS.topicItem}[data-index="${CSS.escape(anchorId)}"]`)
|
|
289
|
+
?? document.querySelector(`${SELECTORS.categoryItem}[data-index="${CSS.escape(anchorId)}"]`);
|
|
290
|
+
}
|
|
773
291
|
|
|
774
|
-
|
|
775
|
-
|
|
292
|
+
if (!anchorEl || !anchorEl.isConnected) {
|
|
293
|
+
// Ancre disparue → si rempli on garde (scrolling back), si vide on supprime
|
|
294
|
+
if (!isFilledNode(wrap)) {
|
|
295
|
+
withInternalMutation(() => removeWrap(wrap));
|
|
296
|
+
removed++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
776
299
|
});
|
|
777
300
|
|
|
778
301
|
return removed;
|
|
779
302
|
}
|
|
780
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Si deux wraps se retrouvent consécutifs (sans item entre eux),
|
|
306
|
+
* on supprime le plus récent des deux s'il est vide.
|
|
307
|
+
*/
|
|
781
308
|
function decluster(kindClass) {
|
|
782
|
-
// Remove "near-consecutive" wraps (keep the first). Be tolerant of spacer nodes.
|
|
783
309
|
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
784
|
-
if (wraps.length < 2) return 0;
|
|
785
|
-
|
|
786
|
-
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
787
|
-
|
|
788
|
-
const isFilled = (wrap) => {
|
|
789
|
-
return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
const isFresh = (wrap) => {
|
|
793
|
-
try {
|
|
794
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
795
|
-
return created && (now() - created) < keepEmptyWrapMs();
|
|
796
|
-
} catch (e) {
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
|
|
801
310
|
let removed = 0;
|
|
802
|
-
for (const w of wraps) {
|
|
803
|
-
// Never decluster pinned placements.
|
|
804
|
-
try {
|
|
805
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
806
|
-
} catch (e) {}
|
|
807
|
-
|
|
808
|
-
let prev = w.previousElementSibling;
|
|
809
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
810
|
-
if (isWrap(prev)) {
|
|
811
|
-
// If the previous wrap is pinned, keep this one (spacing is intentional).
|
|
812
|
-
try {
|
|
813
|
-
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
814
|
-
} catch (e) {}
|
|
815
|
-
|
|
816
|
-
// Never remove a wrap that is already filled; otherwise it looks like
|
|
817
|
-
// ads "disappear" while scrolling. Only remove the empty neighbour.
|
|
818
|
-
const prevFilled = isFilled(prev);
|
|
819
|
-
const curFilled = isFilled(w);
|
|
820
|
-
|
|
821
|
-
if (curFilled) {
|
|
822
|
-
// If the previous one is empty (and not fresh), drop the previous instead.
|
|
823
|
-
if (!prevFilled && !isFresh(prev)) {
|
|
824
|
-
withInternalDomChange(() => releaseWrapNode(prev));
|
|
825
|
-
removed++;
|
|
826
|
-
}
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
311
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
312
|
+
for (const wrap of wraps) {
|
|
313
|
+
let prev = wrap.previousElementSibling;
|
|
314
|
+
let steps = 0;
|
|
315
|
+
while (prev && steps < 3) {
|
|
316
|
+
if (prev.classList?.contains(WRAP_CLASS)) {
|
|
317
|
+
// Deux wraps consécutifs : supprimer le plus vide/récent
|
|
318
|
+
const wCreated = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
319
|
+
const pCreated = parseInt(prev.getAttribute(CREATED_ATTR) || '0', 10);
|
|
320
|
+
const wFilled = isFilledNode(wrap);
|
|
321
|
+
const pFilled = isFilledNode(prev);
|
|
322
|
+
|
|
323
|
+
if (!wFilled) {
|
|
324
|
+
withInternalMutation(() => removeWrap(wrap));
|
|
325
|
+
removed++;
|
|
326
|
+
} else if (!pFilled) {
|
|
327
|
+
withInternalMutation(() => removeWrap(prev));
|
|
835
328
|
removed++;
|
|
836
329
|
}
|
|
330
|
+
// Si les deux sont remplis, laisser en place
|
|
837
331
|
break;
|
|
838
332
|
}
|
|
839
333
|
prev = prev.previousElementSibling;
|
|
334
|
+
steps++;
|
|
840
335
|
}
|
|
841
336
|
}
|
|
337
|
+
|
|
842
338
|
return removed;
|
|
843
339
|
}
|
|
844
340
|
|
|
845
|
-
//
|
|
341
|
+
// ─── Injection ────────────────────────────────────────────────────────────
|
|
342
|
+
function computeTargetIndices(count, interval, showFirst) {
|
|
343
|
+
const targets = new Set();
|
|
344
|
+
if (showFirst && count > 0) targets.add(0);
|
|
345
|
+
for (let i = interval - 1; i < count; i += interval) targets.add(i);
|
|
346
|
+
return [...targets].sort((a, b) => a - b);
|
|
347
|
+
}
|
|
846
348
|
|
|
847
|
-
function
|
|
848
|
-
|
|
849
|
-
|
|
349
|
+
function injectBetween(kindClass, items, interval, showFirst, poolKey) {
|
|
350
|
+
if (!items.length) return 0;
|
|
351
|
+
|
|
352
|
+
const targets = computeTargetIndices(items.length, interval, showFirst);
|
|
353
|
+
const maxIns = MAX_INSERTS_PER_RUN + (isBoosted() ? 2 : 0);
|
|
354
|
+
let inserted = 0;
|
|
355
|
+
|
|
356
|
+
for (const idx of targets) {
|
|
357
|
+
if (inserted >= maxIns) break;
|
|
358
|
+
|
|
359
|
+
const el = items[idx];
|
|
360
|
+
if (!el?.isConnected) continue;
|
|
361
|
+
if (hasAdjacentWrap(el)) continue;
|
|
850
362
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
363
|
+
const anchorKey = getAnchorKey(kindClass, el, idx);
|
|
364
|
+
if (findWrapByAnchor(anchorKey)) continue; // déjà là
|
|
365
|
+
|
|
366
|
+
const id = pickId(poolKey);
|
|
367
|
+
if (!id) break; // pool épuisé pour cette passe
|
|
368
|
+
|
|
369
|
+
const wrap = insertWrapAfter(el, id, kindClass, anchorKey);
|
|
370
|
+
if (!wrap) continue;
|
|
371
|
+
|
|
372
|
+
observePlaceholder(id);
|
|
373
|
+
inserted++;
|
|
854
374
|
}
|
|
855
375
|
|
|
376
|
+
return inserted;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── Preload / Show ───────────────────────────────────────────────────────
|
|
380
|
+
function getPreloadMargin() {
|
|
381
|
+
const m = isMobile() ? 'mobile' : 'desktop';
|
|
382
|
+
return PRELOAD_MARGIN[m + (isBoosted() ? 'Boosted' : '')];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getMaxInflight() {
|
|
386
|
+
return (isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP) + (isBoosted() ? 1 : 0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function ensurePreloadObserver() {
|
|
390
|
+
const margin = getPreloadMargin();
|
|
391
|
+
if (state.io && state.ioMargin === margin) return state.io;
|
|
392
|
+
|
|
393
|
+
state.io?.disconnect();
|
|
394
|
+
state.io = null;
|
|
395
|
+
|
|
856
396
|
try {
|
|
857
|
-
state.io = new IntersectionObserver(
|
|
397
|
+
state.io = new IntersectionObserver(entries => {
|
|
858
398
|
for (const ent of entries) {
|
|
859
399
|
if (!ent.isIntersecting) continue;
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
864
|
-
const id = parseInt(idAttr, 10);
|
|
400
|
+
state.io?.unobserve(ent.target);
|
|
401
|
+
const id = parseInt(ent.target.getAttribute('data-ezoic-id'), 10);
|
|
865
402
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
866
403
|
}
|
|
867
|
-
}, { root: null, rootMargin:
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
} catch (e) {
|
|
871
|
-
state.io = null;
|
|
872
|
-
state.ioMargin = null;
|
|
873
|
-
}
|
|
404
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
405
|
+
state.ioMargin = margin;
|
|
406
|
+
} catch (_) { state.io = null; state.ioMargin = null; }
|
|
874
407
|
|
|
875
|
-
//
|
|
408
|
+
// Ré-observer les placeholders déjà dans le DOM
|
|
876
409
|
try {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
} catch (e) {}
|
|
410
|
+
document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`).forEach(n => {
|
|
411
|
+
try { state.io?.observe(n); } catch (_) {}
|
|
412
|
+
});
|
|
413
|
+
} catch (_) {}
|
|
882
414
|
|
|
883
415
|
return state.io;
|
|
884
416
|
}
|
|
885
417
|
|
|
886
418
|
function observePlaceholder(id) {
|
|
887
419
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
888
|
-
if (!ph
|
|
889
|
-
|
|
420
|
+
if (!ph?.isConnected) return;
|
|
421
|
+
try { state.io?.observe(ph); } catch (_) {}
|
|
890
422
|
|
|
891
|
-
|
|
892
|
-
try { io && io.observe(ph); } catch (e) {}
|
|
893
|
-
|
|
894
|
-
// If already near viewport, fire immediately.
|
|
895
|
-
// Mobile tends to scroll faster + has slower auctions, so we fire earlier.
|
|
423
|
+
// Si déjà proche du viewport → show immédiat
|
|
896
424
|
try {
|
|
897
425
|
const r = ph.getBoundingClientRect();
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
? (mobile ? -2600 : -1500)
|
|
904
|
-
: (mobile ? -1400 : -800);
|
|
905
|
-
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
906
|
-
} catch (e) {}
|
|
426
|
+
const screens = isBoosted() ? 6 : (isMobile() ? 4 : 2.5);
|
|
427
|
+
if (r.top < window.innerHeight * screens && r.bottom > -window.innerHeight * 2) {
|
|
428
|
+
enqueueShow(id);
|
|
429
|
+
}
|
|
430
|
+
} catch (_) {}
|
|
907
431
|
}
|
|
908
432
|
|
|
909
433
|
function enqueueShow(id) {
|
|
910
434
|
if (!id || isBlocked()) return;
|
|
911
|
-
|
|
912
|
-
// per-id throttle
|
|
913
435
|
const t = now();
|
|
914
|
-
|
|
915
|
-
if (t - last < 900) return;
|
|
436
|
+
if (t - (state.lastShowById.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
916
437
|
|
|
917
|
-
|
|
918
|
-
if (state.inflight >= max) {
|
|
438
|
+
if (state.inflight >= getMaxInflight()) {
|
|
919
439
|
if (!state.pendingSet.has(id)) {
|
|
920
440
|
state.pending.push(id);
|
|
921
441
|
state.pendingSet.add(id);
|
|
922
442
|
}
|
|
923
443
|
return;
|
|
924
444
|
}
|
|
925
|
-
|
|
926
445
|
startShow(id);
|
|
927
446
|
}
|
|
928
447
|
|
|
929
448
|
function drainQueue() {
|
|
930
449
|
if (isBlocked()) return;
|
|
931
|
-
|
|
932
|
-
while (state.inflight < max && state.pending.length) {
|
|
450
|
+
while (state.inflight < getMaxInflight() && state.pending.length) {
|
|
933
451
|
const id = state.pending.shift();
|
|
934
452
|
state.pendingSet.delete(id);
|
|
935
453
|
startShow(id);
|
|
936
454
|
}
|
|
937
455
|
}
|
|
938
456
|
|
|
939
|
-
function markEmptyWrapper(id) {
|
|
940
|
-
// If still empty after delay, mark empty for CSS (1px)
|
|
941
|
-
try {
|
|
942
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
943
|
-
if (!ph || !ph.isConnected) return;
|
|
944
|
-
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
945
|
-
if (!wrap) return;
|
|
946
|
-
|
|
947
|
-
setTimeout(() => {
|
|
948
|
-
try {
|
|
949
|
-
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
950
|
-
if (!ph2 || !ph2.isConnected) return;
|
|
951
|
-
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
952
|
-
if (!w2) return;
|
|
953
|
-
|
|
954
|
-
// Don't collapse "fresh" placements; slow auctions/CMP can fill late.
|
|
955
|
-
try {
|
|
956
|
-
const created = parseInt(w2.getAttribute('data-created') || '0', 10);
|
|
957
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
958
|
-
} catch (e) {}
|
|
959
|
-
|
|
960
|
-
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
961
|
-
if (!hasAd) {
|
|
962
|
-
w2.classList.add('is-empty');
|
|
963
|
-
watchWrapForFill(w2);
|
|
964
|
-
} else {
|
|
965
|
-
w2.classList.remove('is-empty');
|
|
966
|
-
tightenEzoicMinHeight(w2);
|
|
967
|
-
}
|
|
968
|
-
} catch (e) {}
|
|
969
|
-
}, 15000);
|
|
970
|
-
} catch (e) {}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
457
|
function startShow(id) {
|
|
974
458
|
if (!id || isBlocked()) return;
|
|
975
|
-
|
|
976
459
|
state.inflight++;
|
|
977
|
-
let
|
|
460
|
+
let done = false;
|
|
461
|
+
|
|
978
462
|
const release = () => {
|
|
979
|
-
if (
|
|
980
|
-
|
|
463
|
+
if (done) return;
|
|
464
|
+
done = true;
|
|
981
465
|
state.inflight = Math.max(0, state.inflight - 1);
|
|
982
466
|
drainQueue();
|
|
983
467
|
};
|
|
984
468
|
|
|
985
|
-
const
|
|
469
|
+
const timeout = setTimeout(release, 6500);
|
|
986
470
|
|
|
987
471
|
requestAnimationFrame(() => {
|
|
988
472
|
try {
|
|
989
|
-
if (isBlocked()) return;
|
|
990
|
-
|
|
473
|
+
if (isBlocked()) return release();
|
|
991
474
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
992
|
-
if (!ph
|
|
993
|
-
|
|
994
|
-
// If the placeholder already has creative, avoid re-showing.
|
|
995
|
-
// Re-showing is a common source of "Placeholder Id X has already been defined".
|
|
996
|
-
try {
|
|
997
|
-
if (ph.querySelector && ph.querySelector('iframe, ins, img, video, [data-google-container-id]')) {
|
|
998
|
-
clearTimeout(hardTimer);
|
|
999
|
-
release();
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
} catch (e) {}
|
|
475
|
+
if (!ph?.isConnected) return release();
|
|
476
|
+
if (isFilledNode(ph)) { clearTimeout(timeout); return release(); }
|
|
1003
477
|
|
|
1004
478
|
const t = now();
|
|
1005
|
-
|
|
1006
|
-
if (t - last < 900) return;
|
|
479
|
+
if (t - (state.lastShowById.get(id) ?? 0) < SHOW_THROTTLE_MS) return release();
|
|
1007
480
|
state.lastShowById.set(id, t);
|
|
1008
481
|
|
|
1009
482
|
window.ezstandalone = window.ezstandalone || {};
|
|
1010
483
|
const ez = window.ezstandalone;
|
|
1011
484
|
|
|
1012
485
|
const doShow = () => {
|
|
1013
|
-
try { ez.showAds(id); } catch (
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const phw = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
1017
|
-
const ww = phw && phw.closest ? phw.closest(`.${WRAP_CLASS}`) : null;
|
|
1018
|
-
if (ww) {
|
|
1019
|
-
watchWrapForFill(ww);
|
|
1020
|
-
setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 900);
|
|
1021
|
-
setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 2200);
|
|
1022
|
-
}
|
|
1023
|
-
} catch (e) {}
|
|
1024
|
-
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
486
|
+
try { ez.showAds(id); } catch (_) {}
|
|
487
|
+
scheduleEmptyCheck(id);
|
|
488
|
+
setTimeout(release, 650);
|
|
1025
489
|
};
|
|
1026
490
|
|
|
1027
|
-
if (Array.isArray(ez.cmd))
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
doShow();
|
|
1031
|
-
}
|
|
1032
|
-
} finally {
|
|
1033
|
-
// hardTimer releases on early return
|
|
1034
|
-
}
|
|
491
|
+
if (Array.isArray(ez.cmd)) ez.cmd.push(doShow);
|
|
492
|
+
else doShow();
|
|
493
|
+
} finally { /* timeout covers us */ }
|
|
1035
494
|
});
|
|
1036
495
|
}
|
|
1037
496
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const n = parseInt(di, 10);
|
|
1046
|
-
if (Number.isFinite(n) && n >= 0) return n + 1;
|
|
1047
|
-
}
|
|
1048
|
-
const d1 = el.getAttribute('data-idx') || el.getAttribute('data-position') || (el.dataset && (el.dataset.idx || el.dataset.position));
|
|
1049
|
-
if (d1 !== null && d1 !== undefined && d1 !== '' && !isNaN(d1)) {
|
|
1050
|
-
const n = parseInt(d1, 10);
|
|
1051
|
-
if (Number.isFinite(n) && n > 0) return n;
|
|
1052
|
-
}
|
|
1053
|
-
} catch (e) {}
|
|
1054
|
-
return fallbackIndex + 1;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function buildOrdinalMap(items) {
|
|
1058
|
-
const map = new Map();
|
|
1059
|
-
let max = 0;
|
|
1060
|
-
for (let i = 0; i < items.length; i++) {
|
|
1061
|
-
const el = items[i];
|
|
1062
|
-
const ord = getItemOrdinal(el, i);
|
|
1063
|
-
map.set(ord, el);
|
|
1064
|
-
if (ord > max) max = ord;
|
|
1065
|
-
}
|
|
1066
|
-
return { map, max };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
function computeTargets(count, interval, showFirst) {
|
|
1071
|
-
const out = [];
|
|
1072
|
-
if (count <= 0) return out;
|
|
1073
|
-
if (showFirst) out.push(1);
|
|
1074
|
-
for (let i = 1; i <= count; i++) {
|
|
1075
|
-
if (i % interval === 0) out.push(i);
|
|
1076
|
-
}
|
|
1077
|
-
// unique + sorted
|
|
1078
|
-
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
1082
|
-
if (!items.length) return 0;
|
|
1083
|
-
|
|
1084
|
-
const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
|
|
1085
|
-
const targets = computeTargets(maxOrdinal, interval, showFirst);
|
|
1086
|
-
let inserted = 0;
|
|
1087
|
-
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
1088
|
-
|
|
1089
|
-
for (const afterPos of targets) {
|
|
1090
|
-
if (inserted >= maxInserts) break;
|
|
1091
|
-
const el = ordinalMap.get(afterPos);
|
|
1092
|
-
if (!el) continue;
|
|
1093
|
-
if (!el || !el.isConnected) continue;
|
|
1094
|
-
if (isAdjacentAd(el)) continue;
|
|
1095
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
1096
|
-
|
|
1097
|
-
let id = pickIdFromAll(allIds, cursorKey);
|
|
1098
|
-
let recycledWrap = null;
|
|
1099
|
-
|
|
1100
|
-
// If the pool is exhausted (all placeholder ids already mounted), recycle a wrap that is far
|
|
1101
|
-
// above the viewport by moving it to the new target instead of creating a new placeholder.
|
|
1102
|
-
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1103
|
-
// appearing on very long infinite scroll sessions.
|
|
1104
|
-
if (!id) {
|
|
1105
|
-
// Safe mode: disable recycling for topic message ads to prevent visual "jumping"
|
|
1106
|
-
// (ads seemingly moving back under the first post during virtualized scroll).
|
|
1107
|
-
const allowRecycle = kindClass !== 'ezoic-ad-message';
|
|
1108
|
-
recycledWrap = (allowRecycle && scrollDir > 0) ? pickRecyclableWrap(kindClass) : null;
|
|
1109
|
-
if (recycledWrap) {
|
|
1110
|
-
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (!id) break;
|
|
1115
|
-
|
|
1116
|
-
const wrap = recycledWrap
|
|
1117
|
-
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1118
|
-
: insertAfter(el, id, kindClass, afterPos);
|
|
1119
|
-
if (!wrap) continue;
|
|
497
|
+
function scheduleEmptyCheck(id) {
|
|
498
|
+
setTimeout(() => {
|
|
499
|
+
try {
|
|
500
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
501
|
+
if (!ph?.isConnected) return;
|
|
502
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
503
|
+
if (!wrap) return;
|
|
1120
504
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
observePlaceholder(id);
|
|
1124
|
-
inserted++;
|
|
1125
|
-
}
|
|
505
|
+
const created = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
506
|
+
if (created && (now() - created) < EMPTY_WRAP_TTL_MS) return;
|
|
1126
507
|
|
|
1127
|
-
|
|
508
|
+
if (!isFilledNode(ph)) wrap.classList.add('is-empty');
|
|
509
|
+
else wrap.classList.remove('is-empty');
|
|
510
|
+
} catch (_) {}
|
|
511
|
+
}, 15_000);
|
|
1128
512
|
}
|
|
1129
513
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const vh = Math.max(300, window.innerHeight || 800);
|
|
1138
|
-
// Recycle only when the wrapper is far above the viewport.
|
|
1139
|
-
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1140
|
-
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1141
|
-
|
|
1142
|
-
let best = null;
|
|
1143
|
-
let bestBottom = Infinity;
|
|
1144
|
-
for (const w of wraps) {
|
|
1145
|
-
if (!w || !w.isConnected) continue;
|
|
1146
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
1147
|
-
const rect = w.getBoundingClientRect();
|
|
1148
|
-
if (rect.bottom < threshold) {
|
|
1149
|
-
if (rect.bottom < bestBottom) {
|
|
1150
|
-
bestBottom = rect.bottom;
|
|
1151
|
-
best = w;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return best;
|
|
1156
|
-
}
|
|
514
|
+
// ─── Patch Ezoic showAds ──────────────────────────────────────────────────
|
|
515
|
+
function patchShowAds() {
|
|
516
|
+
const apply = () => {
|
|
517
|
+
try {
|
|
518
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
519
|
+
const ez = window.ezstandalone;
|
|
520
|
+
if (window.__nodebbEzoicPatched || typeof ez.showAds !== 'function') return;
|
|
1157
521
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
522
|
+
window.__nodebbEzoicPatched = true;
|
|
523
|
+
const orig = ez.showAds.bind(ez);
|
|
1161
524
|
|
|
1162
|
-
|
|
1163
|
-
|
|
525
|
+
ez.showAds = function (...args) {
|
|
526
|
+
if (isBlocked()) return;
|
|
527
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
528
|
+
const seen = new Set();
|
|
529
|
+
for (const v of ids) {
|
|
530
|
+
const id = parseInt(v, 10);
|
|
531
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
532
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
533
|
+
if (!ph?.isConnected) continue;
|
|
534
|
+
seen.add(id);
|
|
535
|
+
try { orig(id); } catch (_) {}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
} catch (_) {}
|
|
539
|
+
};
|
|
1164
540
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
return null;
|
|
541
|
+
apply();
|
|
542
|
+
if (!window.__nodebbEzoicPatched) {
|
|
543
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
544
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
545
|
+
window.ezstandalone.cmd.push(apply);
|
|
1171
546
|
}
|
|
1172
547
|
}
|
|
1173
548
|
|
|
549
|
+
// ─── Core run ─────────────────────────────────────────────────────────────
|
|
1174
550
|
async function runCore() {
|
|
1175
551
|
if (isBlocked()) return 0;
|
|
1176
|
-
|
|
1177
552
|
patchShowAds();
|
|
1178
553
|
|
|
1179
|
-
const cfg = await
|
|
554
|
+
const cfg = await fetchConfig();
|
|
1180
555
|
if (!cfg || cfg.excluded) return 0;
|
|
1181
556
|
initPools(cfg);
|
|
1182
557
|
|
|
1183
558
|
const kind = getKind();
|
|
1184
559
|
let inserted = 0;
|
|
1185
560
|
|
|
561
|
+
const run = (kindClass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
562
|
+
if (!normBool(cfgEnable)) return 0;
|
|
563
|
+
const items = getItems();
|
|
564
|
+
pruneOrphans(kindClass);
|
|
565
|
+
const n = injectBetween(kindClass, items, Math.max(1, parseInt(cfgInterval, 10) || 3), normBool(cfgShowFirst), poolKey);
|
|
566
|
+
if (n) decluster(kindClass);
|
|
567
|
+
return n;
|
|
568
|
+
};
|
|
569
|
+
|
|
1186
570
|
if (kind === 'topic') {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
pruneOrphanWraps('ezoic-ad-message', items);
|
|
1190
|
-
inserted += injectBetween(
|
|
1191
|
-
'ezoic-ad-message',
|
|
1192
|
-
items,
|
|
1193
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
1194
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
1195
|
-
state.allPosts,
|
|
1196
|
-
'curPosts'
|
|
1197
|
-
);
|
|
1198
|
-
decluster('ezoic-ad-message');
|
|
1199
|
-
}
|
|
571
|
+
inserted += run('ezoic-ad-message', getPostContainers,
|
|
572
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
1200
573
|
} else if (kind === 'categoryTopics') {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
pruneOrphanWraps('ezoic-ad-between', items);
|
|
1204
|
-
inserted += injectBetween(
|
|
1205
|
-
'ezoic-ad-between',
|
|
1206
|
-
items,
|
|
1207
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
1208
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
1209
|
-
state.allTopics,
|
|
1210
|
-
'curTopics'
|
|
1211
|
-
);
|
|
1212
|
-
decluster('ezoic-ad-between');
|
|
1213
|
-
}
|
|
574
|
+
inserted += run('ezoic-ad-between', getTopicItems,
|
|
575
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
1214
576
|
} else if (kind === 'categories') {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
1218
|
-
inserted += injectBetween(
|
|
1219
|
-
'ezoic-ad-categories',
|
|
1220
|
-
items,
|
|
1221
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
1222
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
1223
|
-
state.allCategories,
|
|
1224
|
-
'curCategories'
|
|
1225
|
-
);
|
|
1226
|
-
decluster('ezoic-ad-categories');
|
|
1227
|
-
}
|
|
577
|
+
inserted += run('ezoic-ad-categories', getCategoryItems,
|
|
578
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
1228
579
|
}
|
|
1229
580
|
|
|
1230
581
|
return inserted;
|
|
1231
582
|
}
|
|
1232
583
|
|
|
1233
|
-
|
|
1234
|
-
if (state.heroDoneForPage || isBlocked()) return;
|
|
1235
|
-
|
|
1236
|
-
const cfg = await fetchConfigOnce();
|
|
1237
|
-
if (!cfg || cfg.excluded) return;
|
|
1238
|
-
initPools(cfg);
|
|
1239
|
-
|
|
1240
|
-
const kind = getKind();
|
|
1241
|
-
|
|
1242
|
-
let items = [];
|
|
1243
|
-
let allIds = [];
|
|
1244
|
-
let cursorKey = '';
|
|
1245
|
-
let kindClass = '';
|
|
1246
|
-
let showFirst = false;
|
|
1247
|
-
|
|
1248
|
-
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
1249
|
-
items = getPostContainers();
|
|
1250
|
-
allIds = state.allPosts;
|
|
1251
|
-
cursorKey = 'curPosts';
|
|
1252
|
-
kindClass = 'ezoic-ad-message';
|
|
1253
|
-
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
1254
|
-
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
1255
|
-
items = getTopicItems();
|
|
1256
|
-
allIds = state.allTopics;
|
|
1257
|
-
cursorKey = 'curTopics';
|
|
1258
|
-
kindClass = 'ezoic-ad-between';
|
|
1259
|
-
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
1260
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
1261
|
-
items = getCategoryItems();
|
|
1262
|
-
allIds = state.allCategories;
|
|
1263
|
-
cursorKey = 'curCategories';
|
|
1264
|
-
kindClass = 'ezoic-ad-categories';
|
|
1265
|
-
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
1266
|
-
} else {
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
if (!items.length) return;
|
|
1271
|
-
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
1272
|
-
|
|
1273
|
-
const afterPos = 1;
|
|
1274
|
-
const el = items[0];
|
|
1275
|
-
if (!el || !el.isConnected) return;
|
|
1276
|
-
if (isAdjacentAd(el)) return;
|
|
1277
|
-
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
1278
|
-
|
|
1279
|
-
const id = pickIdFromAll(allIds, cursorKey);
|
|
1280
|
-
if (!id) return;
|
|
1281
|
-
|
|
1282
|
-
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
1283
|
-
if (!wrap) return;
|
|
1284
|
-
|
|
1285
|
-
state.heroDoneForPage = true;
|
|
1286
|
-
observePlaceholder(id);
|
|
1287
|
-
// Hero placement is expected to be visible right away (after first item),
|
|
1288
|
-
// so kick a fill request immediately instead of waiting only for IO callbacks.
|
|
1289
|
-
enqueueShow(id);
|
|
1290
|
-
startShowQueue();
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// ---------------- scheduler ----------------
|
|
1294
|
-
|
|
584
|
+
// ─── Scheduler / Burst ────────────────────────────────────────────────────
|
|
1295
585
|
function scheduleRun(delayMs = 0, cb) {
|
|
1296
586
|
if (state.runQueued) return;
|
|
1297
587
|
state.runQueued = true;
|
|
1298
588
|
|
|
1299
589
|
const run = async () => {
|
|
1300
590
|
state.runQueued = false;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
try {
|
|
1305
|
-
try { cb && cb(inserted); } catch (e) {}
|
|
591
|
+
if (state.pageKey && getPageKey() !== state.pageKey) return;
|
|
592
|
+
let n = 0;
|
|
593
|
+
try { n = await runCore(); } catch (_) {}
|
|
594
|
+
try { cb?.(n); } catch (_) {}
|
|
1306
595
|
};
|
|
1307
596
|
|
|
1308
597
|
const doRun = () => requestAnimationFrame(run);
|
|
@@ -1312,83 +601,69 @@ function buildOrdinalMap(items) {
|
|
|
1312
601
|
|
|
1313
602
|
function requestBurst() {
|
|
1314
603
|
if (isBlocked()) return;
|
|
1315
|
-
|
|
1316
604
|
const t = now();
|
|
1317
|
-
if (t - state.lastBurstReqTs <
|
|
605
|
+
if (t - state.lastBurstReqTs < 100) return;
|
|
1318
606
|
state.lastBurstReqTs = t;
|
|
1319
607
|
|
|
1320
608
|
const pk = getPageKey();
|
|
1321
609
|
state.pageKey = pk;
|
|
1322
|
-
|
|
1323
610
|
state.burstDeadline = t + 1800;
|
|
1324
|
-
if (state.burstActive) return;
|
|
1325
611
|
|
|
612
|
+
if (state.burstActive) return;
|
|
1326
613
|
state.burstActive = true;
|
|
1327
|
-
state.burstCount
|
|
614
|
+
state.burstCount = 0;
|
|
1328
615
|
|
|
1329
616
|
const step = () => {
|
|
1330
|
-
if (getPageKey() !== pk)
|
|
1331
|
-
if (isBlocked())
|
|
617
|
+
if (getPageKey() !== pk) { state.burstActive = false; return; }
|
|
618
|
+
if (isBlocked()) { state.burstActive = false; return; }
|
|
1332
619
|
if (now() > state.burstDeadline) { state.burstActive = false; return; }
|
|
1333
|
-
if (state.burstCount >= 8)
|
|
620
|
+
if (state.burstCount >= 8) { state.burstActive = false; return; }
|
|
1334
621
|
|
|
1335
622
|
state.burstCount++;
|
|
1336
|
-
scheduleRun(0, (
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (!hasWork) { state.burstActive = false; return; }
|
|
1340
|
-
// Short delay keeps UI smooth while catching late DOM waves.
|
|
1341
|
-
setTimeout(step, inserted > 0 ? 120 : 220);
|
|
623
|
+
scheduleRun(0, (n) => {
|
|
624
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
625
|
+
setTimeout(step, n > 0 ? 120 : 250);
|
|
1342
626
|
});
|
|
1343
627
|
};
|
|
1344
628
|
|
|
1345
629
|
step();
|
|
1346
630
|
}
|
|
1347
631
|
|
|
1348
|
-
//
|
|
1349
|
-
|
|
632
|
+
// ─── Cleanup (ajaxify navigation) ─────────────────────────────────────────
|
|
1350
633
|
function cleanup() {
|
|
1351
|
-
|
|
634
|
+
// Bloquer toute injection pendant la transition
|
|
635
|
+
blockedUntil = now() + 1500;
|
|
1352
636
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
state.cfg = null;
|
|
1358
|
-
state.allTopics = [];
|
|
1359
|
-
state.allPosts = [];
|
|
1360
|
-
state.allCategories = [];
|
|
1361
|
-
state.curTopics = 0;
|
|
1362
|
-
state.curPosts = 0;
|
|
1363
|
-
state.curCategories = 0;
|
|
1364
|
-
state.lastShowById.clear();
|
|
637
|
+
// Supprimer tous les wraps injectés → libère les IDs
|
|
638
|
+
withInternalMutation(() => {
|
|
639
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach(removeWrap);
|
|
640
|
+
});
|
|
1365
641
|
|
|
1366
|
-
|
|
1367
|
-
state.
|
|
642
|
+
// Réinitialiser l'état complet
|
|
643
|
+
state.cfg = null;
|
|
644
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
645
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
646
|
+
state.mountedIds.clear();
|
|
647
|
+
state.lastShowById.clear();
|
|
648
|
+
state.inflight = 0;
|
|
649
|
+
state.pending = [];
|
|
1368
650
|
state.pendingSet.clear();
|
|
1369
651
|
|
|
1370
|
-
state.
|
|
1371
|
-
|
|
1372
|
-
// keep observers alive
|
|
652
|
+
state.burstActive = false;
|
|
653
|
+
state.runQueued = false;
|
|
1373
654
|
}
|
|
1374
655
|
|
|
1375
|
-
|
|
1376
|
-
|
|
656
|
+
// ─── DOM Observer ─────────────────────────────────────────────────────────
|
|
657
|
+
function shouldReact(mutations) {
|
|
1377
658
|
for (const m of mutations) {
|
|
1378
|
-
if (!m.addedNodes
|
|
659
|
+
if (!m.addedNodes?.length) continue;
|
|
1379
660
|
for (const n of m.addedNodes) {
|
|
1380
|
-
if (
|
|
1381
|
-
const el = /** @type {Element} */ (n);
|
|
661
|
+
if (n.nodeType !== 1) continue;
|
|
1382
662
|
if (
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
el.querySelector?.(SELECTORS.topicItem) ||
|
|
1388
|
-
el.querySelector?.(SELECTORS.categoryItem)
|
|
1389
|
-
) {
|
|
1390
|
-
return true;
|
|
1391
|
-
}
|
|
663
|
+
n.matches?.(SELECTORS.postItem) || n.querySelector?.(SELECTORS.postItem) ||
|
|
664
|
+
n.matches?.(SELECTORS.topicItem) || n.querySelector?.(SELECTORS.topicItem) ||
|
|
665
|
+
n.matches?.(SELECTORS.categoryItem) || n.querySelector?.(SELECTORS.categoryItem)
|
|
666
|
+
) return true;
|
|
1392
667
|
}
|
|
1393
668
|
}
|
|
1394
669
|
return false;
|
|
@@ -1396,20 +671,72 @@ function buildOrdinalMap(items) {
|
|
|
1396
671
|
|
|
1397
672
|
function ensureDomObserver() {
|
|
1398
673
|
if (state.domObs) return;
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
if (state.internalDomChange > 0) return;
|
|
674
|
+
state.domObs = new MutationObserver(mutations => {
|
|
675
|
+
if (state.internalMutation > 0) return;
|
|
1402
676
|
if (isBlocked()) return;
|
|
1403
|
-
if (!
|
|
677
|
+
if (!shouldReact(mutations)) return;
|
|
1404
678
|
requestBurst();
|
|
1405
679
|
});
|
|
680
|
+
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
681
|
+
}
|
|
1406
682
|
|
|
683
|
+
// ─── Utilities: console mute + TCF + network warm ─────────────────────────
|
|
684
|
+
function muteNoisyConsole() {
|
|
685
|
+
if (window.__nodebbEzoicConsoleMuted) return;
|
|
686
|
+
window.__nodebbEzoicConsoleMuted = true;
|
|
687
|
+
const MUTED = [
|
|
688
|
+
'[EzoicAds JS]: Placeholder Id',
|
|
689
|
+
'Debugger iframe already exists',
|
|
690
|
+
'HTML element with id ezoic-pub-ad-placeholder-',
|
|
691
|
+
];
|
|
692
|
+
['log', 'info', 'warn', 'error'].forEach(m => {
|
|
693
|
+
const orig = console[m];
|
|
694
|
+
if (typeof orig !== 'function') return;
|
|
695
|
+
console[m] = function (...args) {
|
|
696
|
+
const s = typeof args[0] === 'string' ? args[0] : '';
|
|
697
|
+
if (MUTED.some(p => s.includes(p))) return;
|
|
698
|
+
orig.apply(console, args);
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function ensureTcfLocator() {
|
|
1407
704
|
try {
|
|
1408
|
-
|
|
1409
|
-
|
|
705
|
+
if (!window.__tcfapi && !window.__cmp) return;
|
|
706
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
707
|
+
const f = Object.assign(document.createElement('iframe'), {
|
|
708
|
+
style: 'display:none', id: '__tcfapiLocator', name: '__tcfapiLocator',
|
|
709
|
+
});
|
|
710
|
+
(document.body || document.documentElement).appendChild(f);
|
|
711
|
+
} catch (_) {}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const _warmedLinks = new Set();
|
|
715
|
+
function warmNetwork() {
|
|
716
|
+
const head = document.head;
|
|
717
|
+
if (!head) return;
|
|
718
|
+
const links = [
|
|
719
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
720
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
721
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
722
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
723
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
724
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
725
|
+
];
|
|
726
|
+
for (const [rel, href, cors] of links) {
|
|
727
|
+
const key = `${rel}|${href}`;
|
|
728
|
+
if (_warmedLinks.has(key)) continue;
|
|
729
|
+
_warmedLinks.add(key);
|
|
730
|
+
const link = document.createElement('link');
|
|
731
|
+
link.rel = rel; link.href = href;
|
|
732
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
733
|
+
head.appendChild(link);
|
|
734
|
+
}
|
|
1410
735
|
}
|
|
1411
736
|
|
|
737
|
+
// ─── NodeBB + Scroll bindings ─────────────────────────────────────────────
|
|
1412
738
|
function bindNodeBB() {
|
|
739
|
+
const $ = window.jQuery;
|
|
1413
740
|
if (!$) return;
|
|
1414
741
|
|
|
1415
742
|
$(window).off('.ezoicInfinite');
|
|
@@ -1420,275 +747,77 @@ function buildOrdinalMap(items) {
|
|
|
1420
747
|
|
|
1421
748
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
1422
749
|
state.pageKey = getPageKey();
|
|
1423
|
-
blockedUntil
|
|
1424
|
-
|
|
750
|
+
blockedUntil = 0;
|
|
1425
751
|
muteNoisyConsole();
|
|
1426
|
-
|
|
1427
|
-
|
|
752
|
+
ensureTcfLocator();
|
|
753
|
+
warmNetwork();
|
|
1428
754
|
patchShowAds();
|
|
1429
|
-
globalGapFixInit();
|
|
1430
755
|
ensurePreloadObserver();
|
|
1431
756
|
ensureDomObserver();
|
|
1432
|
-
|
|
1433
|
-
insertHeroAdEarly().catch(() => {});
|
|
1434
757
|
requestBurst();
|
|
1435
758
|
});
|
|
1436
759
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
760
|
+
const burstEvents = [
|
|
761
|
+
'action:ajaxify.contentLoaded',
|
|
762
|
+
'action:posts.loaded',
|
|
763
|
+
'action:topics.loaded',
|
|
764
|
+
'action:categories.loaded',
|
|
765
|
+
'action:category.loaded',
|
|
766
|
+
'action:topic.loaded',
|
|
767
|
+
].map(e => `${e}.ezoicInfinite`).join(' ');
|
|
1442
768
|
|
|
1443
|
-
$(window).on(
|
|
1444
|
-
'action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:categories.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite',
|
|
1445
|
-
() => {
|
|
1446
|
-
if (isBlocked()) return;
|
|
1447
|
-
requestBurst();
|
|
1448
|
-
}
|
|
1449
|
-
);
|
|
769
|
+
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1450
770
|
|
|
1451
|
-
//
|
|
771
|
+
// Hooks AMD (NodeBB 4.x)
|
|
1452
772
|
try {
|
|
1453
|
-
require(['hooks'],
|
|
1454
|
-
if (
|
|
1455
|
-
[
|
|
1456
|
-
|
|
773
|
+
require(['hooks'], hooks => {
|
|
774
|
+
if (typeof hooks?.on !== 'function') return;
|
|
775
|
+
[
|
|
776
|
+
'action:ajaxify.end', 'action:ajaxify.contentLoaded',
|
|
777
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
778
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
779
|
+
].forEach(ev => {
|
|
780
|
+
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
1457
781
|
});
|
|
1458
782
|
});
|
|
1459
|
-
} catch (
|
|
783
|
+
} catch (_) {}
|
|
1460
784
|
}
|
|
1461
785
|
|
|
1462
786
|
function bindScroll() {
|
|
1463
787
|
let ticking = false;
|
|
1464
788
|
window.addEventListener('scroll', () => {
|
|
1465
|
-
//
|
|
789
|
+
// Scroll boost
|
|
1466
790
|
try {
|
|
1467
|
-
const t
|
|
1468
|
-
const y
|
|
791
|
+
const t = now();
|
|
792
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
1469
793
|
if (state.lastScrollTs) {
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (
|
|
1475
|
-
const wasBoosted = isBoosted();
|
|
1476
|
-
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, t + BOOST_DURATION_MS);
|
|
1477
|
-
if (!wasBoosted) ensurePreloadObserver();
|
|
1478
|
-
}
|
|
794
|
+
const speed = Math.abs(y - state.lastScrollY) / Math.max(1, t - state.lastScrollTs);
|
|
795
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
796
|
+
const wasBoosted = isBoosted();
|
|
797
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil, t + BOOST_DURATION_MS);
|
|
798
|
+
if (!wasBoosted) ensurePreloadObserver();
|
|
1479
799
|
}
|
|
1480
800
|
}
|
|
1481
|
-
state.lastScrollY
|
|
801
|
+
state.lastScrollY = y;
|
|
1482
802
|
state.lastScrollTs = t;
|
|
1483
|
-
} catch (
|
|
803
|
+
} catch (_) {}
|
|
1484
804
|
|
|
1485
805
|
if (ticking) return;
|
|
1486
806
|
ticking = true;
|
|
1487
|
-
requestAnimationFrame(() => {
|
|
1488
|
-
ticking = false;
|
|
1489
|
-
requestBurst();
|
|
1490
|
-
});
|
|
807
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1491
808
|
}, { passive: true });
|
|
1492
809
|
}
|
|
1493
810
|
|
|
1494
|
-
//
|
|
1495
|
-
|
|
811
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────
|
|
1496
812
|
state.pageKey = getPageKey();
|
|
1497
813
|
muteNoisyConsole();
|
|
1498
|
-
|
|
1499
|
-
|
|
814
|
+
ensureTcfLocator();
|
|
815
|
+
warmNetwork();
|
|
1500
816
|
patchShowAds();
|
|
1501
817
|
ensurePreloadObserver();
|
|
1502
818
|
ensureDomObserver();
|
|
1503
|
-
|
|
1504
819
|
bindNodeBB();
|
|
1505
820
|
bindScroll();
|
|
1506
|
-
|
|
1507
821
|
blockedUntil = 0;
|
|
1508
|
-
insertHeroAdEarly().catch(() => {});
|
|
1509
822
|
requestBurst();
|
|
1510
823
|
})();
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
// ===== V17 minimal pile-fix (no insert hooks) =====
|
|
1515
|
-
(function () {
|
|
1516
|
-
// Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
|
|
1517
|
-
var TOPIC_LI_SEL = 'li[component="category/topic"]';
|
|
1518
|
-
var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
|
|
1519
|
-
var HOST_CLASS = 'nodebb-ezoic-host';
|
|
1520
|
-
|
|
1521
|
-
var scheduled = false;
|
|
1522
|
-
var lastRun = 0;
|
|
1523
|
-
var COOLDOWN = 180;
|
|
1524
|
-
|
|
1525
|
-
function getTopicList() {
|
|
1526
|
-
try {
|
|
1527
|
-
var li = document.querySelector(TOPIC_LI_SEL);
|
|
1528
|
-
if (!li) return null;
|
|
1529
|
-
return li.closest ? li.closest('ul,ol') : null;
|
|
1530
|
-
} catch (e) { return null; }
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
function isHost(node) {
|
|
1534
|
-
return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function ensureHostForWrap(wrap, ul) {
|
|
1538
|
-
try {
|
|
1539
|
-
if (!wrap || wrap.nodeType !== 1) return null;
|
|
1540
|
-
if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
|
|
1541
|
-
|
|
1542
|
-
var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
|
|
1543
|
-
if (host) return host;
|
|
1544
|
-
|
|
1545
|
-
if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
|
|
1546
|
-
if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
|
|
1547
|
-
|
|
1548
|
-
// Only wrap if direct child of list (invalid / fragile)
|
|
1549
|
-
if (wrap.parentElement === ul) {
|
|
1550
|
-
host = document.createElement('li');
|
|
1551
|
-
host.className = HOST_CLASS;
|
|
1552
|
-
host.setAttribute('role', 'listitem');
|
|
1553
|
-
host.style.listStyle = 'none';
|
|
1554
|
-
host.style.width = '100%';
|
|
1555
|
-
ul.insertBefore(host, wrap);
|
|
1556
|
-
host.appendChild(wrap);
|
|
1557
|
-
try { wrap.style.width = '100%'; } catch (e) {}
|
|
1558
|
-
return host;
|
|
1559
|
-
}
|
|
1560
|
-
} catch (e) {}
|
|
1561
|
-
return null;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function previousTopicLi(node) {
|
|
1565
|
-
try {
|
|
1566
|
-
var prev = node.previousElementSibling;
|
|
1567
|
-
while (prev) {
|
|
1568
|
-
if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
|
|
1569
|
-
// skip other hosts/wraps
|
|
1570
|
-
prev = prev.previousElementSibling;
|
|
1571
|
-
}
|
|
1572
|
-
} catch (e) {}
|
|
1573
|
-
return null;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function detectPileUp(ul) {
|
|
1577
|
-
// Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
|
|
1578
|
-
try {
|
|
1579
|
-
var kids = ul.children;
|
|
1580
|
-
var run = 0;
|
|
1581
|
-
var maxRun = 0;
|
|
1582
|
-
for (var i = 0; i < kids.length; i++) {
|
|
1583
|
-
var el = kids[i];
|
|
1584
|
-
var isBetween = false;
|
|
1585
|
-
if (isHost(el)) {
|
|
1586
|
-
isBetween = !!(el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
|
|
1587
|
-
} else if (el.matches && el.matches(BETWEEN_WRAP_SEL)) {
|
|
1588
|
-
isBetween = true;
|
|
1589
|
-
}
|
|
1590
|
-
if (isBetween) {
|
|
1591
|
-
run++;
|
|
1592
|
-
if (run > maxRun) maxRun = run;
|
|
1593
|
-
} else if (el.matches && el.matches(TOPIC_LI_SEL)) {
|
|
1594
|
-
run = 0;
|
|
1595
|
-
} else {
|
|
1596
|
-
// other nodes reset lightly
|
|
1597
|
-
run = 0;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
return maxRun >= 2;
|
|
1601
|
-
} catch (e) {}
|
|
1602
|
-
return false;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
function redistribute(ul) {
|
|
1606
|
-
try {
|
|
1607
|
-
if (!ul) return;
|
|
1608
|
-
|
|
1609
|
-
// Step 1: wrap any direct child between DIVs into LI hosts (makes list stable)
|
|
1610
|
-
ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); });
|
|
1611
|
-
|
|
1612
|
-
// Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
|
|
1613
|
-
if (!detectPileUp(ul)) return;
|
|
1614
|
-
|
|
1615
|
-
// Move each host to immediately after the closest previous topic LI at its current position.
|
|
1616
|
-
var hosts = ul.querySelectorAll(':scope > li.' + HOST_CLASS);
|
|
1617
|
-
hosts.forEach(function(host){
|
|
1618
|
-
try {
|
|
1619
|
-
var wrap = host.querySelector && host.querySelector(BETWEEN_WRAP_SEL);
|
|
1620
|
-
if (!wrap) return;
|
|
1621
|
-
|
|
1622
|
-
var anchor = previousTopicLi(host);
|
|
1623
|
-
if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
|
|
1624
|
-
|
|
1625
|
-
if (host.previousElementSibling !== anchor) {
|
|
1626
|
-
anchor.insertAdjacentElement('afterend', host);
|
|
1627
|
-
}
|
|
1628
|
-
} catch (e) {}
|
|
1629
|
-
});
|
|
1630
|
-
} catch (e) {}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
function schedule(reason) {
|
|
1634
|
-
var now = Date.now();
|
|
1635
|
-
if (now - lastRun < COOLDOWN) return;
|
|
1636
|
-
if (scheduled) return;
|
|
1637
|
-
scheduled = true;
|
|
1638
|
-
requestAnimationFrame(function () {
|
|
1639
|
-
scheduled = false;
|
|
1640
|
-
lastRun = Date.now();
|
|
1641
|
-
try {
|
|
1642
|
-
var ul = getTopicList();
|
|
1643
|
-
if (!ul) return;
|
|
1644
|
-
redistribute(ul);
|
|
1645
|
-
} catch (e) {}
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
function init() {
|
|
1650
|
-
schedule('init');
|
|
1651
|
-
|
|
1652
|
-
// Observe only the topic list once available
|
|
1653
|
-
try {
|
|
1654
|
-
if (typeof MutationObserver !== 'undefined') {
|
|
1655
|
-
var observeList = function(ul){
|
|
1656
|
-
if (!ul) return;
|
|
1657
|
-
var mo = new MutationObserver(function(muts){
|
|
1658
|
-
// schedule on any change; redistribute() itself is guarded by pile-up detection
|
|
1659
|
-
schedule('mo');
|
|
1660
|
-
});
|
|
1661
|
-
mo.observe(ul, { childList: true, subtree: true });
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
var ul = getTopicList();
|
|
1665
|
-
if (ul) observeList(ul);
|
|
1666
|
-
else {
|
|
1667
|
-
var mo2 = new MutationObserver(function(){
|
|
1668
|
-
var u2 = getTopicList();
|
|
1669
|
-
if (u2) {
|
|
1670
|
-
try { observeList(u2); } catch(e){}
|
|
1671
|
-
try { mo2.disconnect(); } catch(e){}
|
|
1672
|
-
}
|
|
1673
|
-
});
|
|
1674
|
-
mo2.observe(document.documentElement || document.body, { childList: true, subtree: true });
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
} catch (e) {}
|
|
1678
|
-
|
|
1679
|
-
// NodeBB events: run after infinite scroll batches
|
|
1680
|
-
if (window.jQuery) {
|
|
1681
|
-
try {
|
|
1682
|
-
window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
|
|
1683
|
-
setTimeout(function(){ schedule('event'); }, 50);
|
|
1684
|
-
setTimeout(function(){ schedule('event2'); }, 400);
|
|
1685
|
-
});
|
|
1686
|
-
} catch (e) {}
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
1691
|
-
else init();
|
|
1692
|
-
})();
|
|
1693
|
-
// ===== /V17 =====
|
|
1694
|
-
|