nodebb-plugin-ezoic-infinite 1.6.66 → 1.6.68
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 +626 -930
package/public/client.js
CHANGED
|
@@ -1,751 +1,460 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// ───
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
lastScrollY = window.scrollY || 0;
|
|
10
|
-
window.addEventListener('scroll', () => {
|
|
11
|
-
const y = window.scrollY || 0;
|
|
12
|
-
const d = y - lastScrollY;
|
|
13
|
-
if (Math.abs(d) > 4) { scrollDir = d > 0 ? 1 : -1; lastScrollY = y; }
|
|
14
|
-
}, { passive: true });
|
|
15
|
-
} catch (e) {}
|
|
4
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
5
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
6
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
7
|
+
const HOST_CLASS = 'nodebb-ezoic-host';
|
|
8
|
+
const MAX_INSERTS_PER_RUN = 8;
|
|
16
9
|
|
|
17
|
-
|
|
10
|
+
// IntersectionObserver preload margins (large = request fills early).
|
|
11
|
+
const IO_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
12
|
+
const IO_MARGIN_MOBILE = '3200px 0px 3200px 0px';
|
|
18
13
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
14
|
+
// Fast-scroll boost: temporarily widen IO margins.
|
|
15
|
+
const IO_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
|
|
16
|
+
const IO_MARGIN_MOBILE_BOOST = '5200px 0px 5200px 0px';
|
|
17
|
+
const BOOST_DURATION_MS = 2500;
|
|
18
|
+
const BOOST_SPEED_PX_PER_MS = 2.2;
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const BOOST_DURATION_MS = 2500;
|
|
32
|
-
const BOOST_SPEED_PX_PER_MS = 2.2;
|
|
33
|
-
const MAX_INFLIGHT_DESKTOP = 4;
|
|
34
|
-
const MAX_INFLIGHT_MOBILE = 3;
|
|
35
|
-
|
|
36
|
-
const SELECTORS = {
|
|
37
|
-
topicItem: 'li[component="category/topic"]',
|
|
38
|
-
postItem: '[component="post"][data-pid]',
|
|
39
|
-
categoryItem: 'li[component="categories/category"]',
|
|
20
|
+
const MAX_INFLIGHT = { desktop: 4, mobile: 3 };
|
|
21
|
+
|
|
22
|
+
const SEL = {
|
|
23
|
+
topic: 'li[component="category/topic"]',
|
|
24
|
+
post: '[component="post"][data-pid]',
|
|
25
|
+
category: 'li[component="categories/category"]',
|
|
40
26
|
};
|
|
41
27
|
|
|
42
|
-
// ───
|
|
28
|
+
// ─── Utils ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
43
31
|
|
|
44
32
|
function now() { return Date.now(); }
|
|
45
33
|
function isMobile() { try { return window.innerWidth < 768; } catch (e) { return false; } }
|
|
46
|
-
function isBoosted(){ return now() < (
|
|
34
|
+
function isBoosted(){ return now() < (st.boostUntil || 0); }
|
|
35
|
+
function isBlocked(){ return now() < blockedUntil; }
|
|
47
36
|
|
|
48
|
-
function
|
|
37
|
+
function normBool(v) {
|
|
49
38
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
50
39
|
}
|
|
51
40
|
|
|
52
|
-
|
|
41
|
+
function parsePool(raw) {
|
|
42
|
+
if (!raw) return [];
|
|
43
|
+
const seen = new Set(), out = [];
|
|
44
|
+
for (const v of String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
45
|
+
const n = parseInt(v, 10);
|
|
46
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
53
51
|
function isFilled(node) {
|
|
54
52
|
return !!(node && node.querySelector &&
|
|
55
53
|
node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
function getMaxInflight() {
|
|
57
|
+
return (isMobile() ? MAX_INFLIGHT.mobile : MAX_INFLIGHT.desktop) + (isBoosted() ? 1 : 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getIoMargin() {
|
|
61
|
+
return isMobile()
|
|
62
|
+
? (isBoosted() ? IO_MARGIN_MOBILE_BOOST : IO_MARGIN_MOBILE)
|
|
63
|
+
: (isBoosted() ? IO_MARGIN_DESKTOP_BOOST : IO_MARGIN_DESKTOP);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPageKey() {
|
|
60
67
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
const d = window.ajaxify && window.ajaxify.data;
|
|
69
|
+
if (d) {
|
|
70
|
+
if (d.tid) return 'topic:' + d.tid;
|
|
71
|
+
if (d.cid) return 'category:' + d.cid;
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {}
|
|
74
|
+
return window.location.pathname;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getKind() {
|
|
78
|
+
const p = window.location.pathname || '';
|
|
79
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
80
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
81
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
82
|
+
if (document.querySelector(SEL.category)) return 'categories';
|
|
83
|
+
if (document.querySelector(SEL.post)) return 'topic';
|
|
84
|
+
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
85
|
+
return 'other';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function withInternal(fn) {
|
|
89
|
+
st.internalMut++;
|
|
90
|
+
try { fn(); } finally { st.internalMut--; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
const st = {
|
|
96
|
+
pageKey: null,
|
|
97
|
+
cfg: null,
|
|
98
|
+
|
|
99
|
+
pools: { topic: [], post: [], category: [] },
|
|
100
|
+
cursors: { topic: 0, post: 0, category: 0 },
|
|
101
|
+
|
|
102
|
+
lastShow: new Map(), // id → timestamp
|
|
103
|
+
inflight: 0,
|
|
104
|
+
pending: [],
|
|
105
|
+
pendingSet: new Set(),
|
|
106
|
+
|
|
107
|
+
io: null,
|
|
108
|
+
ioMargin: null,
|
|
109
|
+
domObs: null,
|
|
110
|
+
|
|
111
|
+
internalMut: 0,
|
|
112
|
+
boostUntil: 0,
|
|
113
|
+
lastScrollY: 0,
|
|
114
|
+
lastScrollTs: 0,
|
|
115
|
+
|
|
116
|
+
heroDone: false,
|
|
117
|
+
runQueued: false,
|
|
118
|
+
burstActive: false,
|
|
119
|
+
burstDeadline:0,
|
|
120
|
+
burstCount: 0,
|
|
121
|
+
lastBurstTs: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let blockedUntil = 0;
|
|
125
|
+
const inserting = new Set();
|
|
126
|
+
|
|
127
|
+
// ─── Console mute (Ezoic SPA spam) ───────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function muteConsole() {
|
|
130
|
+
try {
|
|
131
|
+
if (window.__nbbEzoicMuted) return;
|
|
132
|
+
window.__nbbEzoicMuted = true;
|
|
133
|
+
const bad = (a) => {
|
|
134
|
+
const s = typeof a[0] === 'string' ? a[0] : '';
|
|
135
|
+
return (s.includes('[EzoicAds JS]: Placeholder Id') && s.includes('has already been defined'))
|
|
136
|
+
|| s.includes('Debugger iframe already exists')
|
|
137
|
+
|| (s.includes('HTML element with id ezoic-pub-ad-placeholder-') && s.includes('does not exist'));
|
|
77
138
|
};
|
|
78
|
-
['log','info','warn','error']
|
|
139
|
+
for (const m of ['log','info','warn','error']) {
|
|
140
|
+
const o = console[m];
|
|
141
|
+
if (typeof o !== 'function') continue;
|
|
142
|
+
console[m] = function(...a) { if (!bad(a)) o.apply(console, a); };
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Network warmup ──────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
const _warmed = new Set();
|
|
150
|
+
function warmNetwork() {
|
|
151
|
+
const head = document.head;
|
|
152
|
+
if (!head) return;
|
|
153
|
+
for (const [rel, href, cors] of [
|
|
154
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
155
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
156
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
157
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
158
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
159
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
160
|
+
]) {
|
|
161
|
+
const k = rel + href;
|
|
162
|
+
if (_warmed.has(k)) continue;
|
|
163
|
+
_warmed.add(k);
|
|
164
|
+
const l = document.createElement('link');
|
|
165
|
+
l.rel = rel; l.href = href;
|
|
166
|
+
if (cors) l.crossOrigin = 'anonymous';
|
|
167
|
+
head.appendChild(l);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Ezoic bridge ────────────────────────────────────────────────────────────
|
|
172
|
+
// Patch showAds so it silently skips IDs whose placeholder is not in the DOM.
|
|
173
|
+
|
|
174
|
+
function patchShowAds() {
|
|
175
|
+
const patch = () => {
|
|
176
|
+
try {
|
|
177
|
+
const ez = window.ezstandalone = window.ezstandalone || {};
|
|
178
|
+
if (window.__nbbEzoicPatched || typeof ez.showAds !== 'function') return;
|
|
179
|
+
window.__nbbEzoicPatched = true;
|
|
180
|
+
const orig = ez.showAds;
|
|
181
|
+
ez.showAds = function (...args) {
|
|
182
|
+
if (isBlocked()) return;
|
|
183
|
+
const ids = Array.isArray(args[0]) ? args[0] : args;
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
for (const v of ids) {
|
|
186
|
+
const id = parseInt(v, 10);
|
|
187
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
188
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
189
|
+
if (!ph || !ph.isConnected) continue;
|
|
190
|
+
seen.add(id);
|
|
191
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
} catch (e) {}
|
|
195
|
+
};
|
|
196
|
+
patch();
|
|
197
|
+
if (!window.__nbbEzoicPatched) {
|
|
198
|
+
try {
|
|
199
|
+
(window.ezstandalone = window.ezstandalone || {}).cmd =
|
|
200
|
+
window.ezstandalone.cmd || [];
|
|
201
|
+
window.ezstandalone.cmd.push(patch);
|
|
202
|
+
} catch (e) {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
async function fetchConfig() {
|
|
209
|
+
if (st.cfg) return st.cfg;
|
|
210
|
+
try {
|
|
211
|
+
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
212
|
+
if (r.ok) st.cfg = await r.json();
|
|
79
213
|
} catch (e) {}
|
|
214
|
+
return st.cfg;
|
|
80
215
|
}
|
|
81
216
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// ─── Ezoic min-height tightener
|
|
90
|
-
// Ezoic injects `min-height:400px !important`
|
|
91
|
-
|
|
92
|
-
function
|
|
217
|
+
function initPools(cfg) {
|
|
218
|
+
if (!cfg) return;
|
|
219
|
+
if (!st.pools.topic.length) st.pools.topic = parsePool(cfg.placeholderIds);
|
|
220
|
+
if (!st.pools.post.length) st.pools.post = parsePool(cfg.messagePlaceholderIds);
|
|
221
|
+
if (!st.pools.category.length) st.pools.category = parsePool(cfg.categoryPlaceholderIds);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Ezoic min-height tightener ──────────────────────────────────────────────
|
|
225
|
+
// Ezoic injects `min-height:400px !important` via inline style; we override it.
|
|
226
|
+
|
|
227
|
+
function tightenMinHeight(wrap) {
|
|
93
228
|
try {
|
|
94
|
-
if (!wrap
|
|
229
|
+
if (!wrap) return;
|
|
95
230
|
const iframes = wrap.querySelectorAll('iframe');
|
|
96
231
|
if (!iframes.length) return;
|
|
97
232
|
|
|
98
|
-
|
|
233
|
+
// Find the nearest ezoic-ad ancestor with the 400px inline min-height.
|
|
234
|
+
let ref = null;
|
|
99
235
|
let p = iframes[0].parentElement;
|
|
100
236
|
while (p && p !== wrap) {
|
|
101
237
|
if (p.classList && p.classList.contains('ezoic-ad')) {
|
|
102
238
|
const st = (p.getAttribute('style') || '').toLowerCase();
|
|
103
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400')) {
|
|
239
|
+
if (st.includes('min-height:400') || st.includes('min-height: 400')) { ref = p; break; }
|
|
104
240
|
}
|
|
105
241
|
p = p.parentElement;
|
|
106
242
|
}
|
|
107
|
-
|
|
243
|
+
ref = ref || wrap.querySelector('.ezoic-ad-adaptive, .ezoic-ad') || wrap;
|
|
108
244
|
|
|
109
245
|
let refTop = 0;
|
|
110
|
-
try { refTop =
|
|
246
|
+
try { refTop = ref.getBoundingClientRect().top; } catch (e) {}
|
|
111
247
|
|
|
112
|
-
let
|
|
113
|
-
iframes.forEach(
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
maxBottom = Math.max(maxBottom, rect.bottom - refTop);
|
|
248
|
+
let maxH = 0;
|
|
249
|
+
iframes.forEach(f => {
|
|
250
|
+
const r = f.getBoundingClientRect();
|
|
251
|
+
if (r.width > 1 && r.height > 1) maxH = Math.max(maxH, r.bottom - refTop);
|
|
117
252
|
});
|
|
118
|
-
if (!
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!maxBottom) return;
|
|
253
|
+
if (!maxH) iframes.forEach(f => {
|
|
254
|
+
maxH = Math.max(maxH,
|
|
255
|
+
parseInt(f.getAttribute('height') || '0', 10), f.offsetHeight || 0);
|
|
256
|
+
});
|
|
257
|
+
if (!maxH) return;
|
|
124
258
|
|
|
125
|
-
const h = Math.max(1, Math.ceil(
|
|
126
|
-
const
|
|
259
|
+
const h = Math.max(1, Math.ceil(maxH)) + 'px';
|
|
260
|
+
const set = (n) => {
|
|
127
261
|
if (!n || !n.style) return;
|
|
128
|
-
try { n.style.setProperty('min-height', h
|
|
262
|
+
try { n.style.setProperty('min-height', h, 'important'); } catch (e) { n.style.minHeight = h; }
|
|
129
263
|
try { n.style.setProperty('height', 'auto', 'important'); } catch (e) {}
|
|
130
264
|
try { n.style.setProperty('line-height', '0', 'important'); } catch (e) {}
|
|
131
265
|
};
|
|
132
266
|
|
|
133
|
-
let cur =
|
|
267
|
+
let cur = ref;
|
|
134
268
|
while (cur && cur !== wrap) {
|
|
135
269
|
if (cur.classList && cur.classList.contains('ezoic-ad')) {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
270
|
+
const s = (cur.getAttribute('style') || '').toLowerCase();
|
|
271
|
+
if (s.includes('min-height:400') || s.includes('min-height: 400')) set(cur);
|
|
138
272
|
}
|
|
139
273
|
cur = cur.parentElement;
|
|
140
274
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
275
|
+
set(ref);
|
|
276
|
+
ref.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive').forEach(n => {
|
|
277
|
+
const s = (n.getAttribute('style') || '').toLowerCase();
|
|
278
|
+
if (s.includes('min-height:400') || s.includes('min-height: 400')) set(n);
|
|
145
279
|
});
|
|
280
|
+
|
|
146
281
|
if (isMobile()) {
|
|
147
|
-
try {
|
|
148
|
-
try {
|
|
149
|
-
try {
|
|
282
|
+
try { ref.style.setProperty('width', '100%', 'important'); } catch (e) {}
|
|
283
|
+
try { ref.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
|
|
284
|
+
try { ref.style.setProperty('min-width', '0', 'important'); } catch (e) {}
|
|
150
285
|
}
|
|
151
286
|
} catch (e) {}
|
|
152
287
|
}
|
|
153
288
|
|
|
154
|
-
function
|
|
289
|
+
function watchFill(wrap) {
|
|
155
290
|
try {
|
|
156
291
|
if (!wrap || wrap.__ezFillObs) return;
|
|
157
292
|
const start = now();
|
|
158
|
-
const
|
|
159
|
-
try {
|
|
160
|
-
if (now() - start < 6000) setTimeout(
|
|
293
|
+
const burst = () => {
|
|
294
|
+
try { tightenMinHeight(wrap); } catch (e) {}
|
|
295
|
+
if (now() - start < 6000) setTimeout(burst, 350);
|
|
161
296
|
};
|
|
162
|
-
const obs = new MutationObserver(
|
|
163
|
-
if (isFilled(wrap)) { wrap.classList.remove('is-empty');
|
|
164
|
-
for (const m of muts)
|
|
165
|
-
if (m.type === 'attributes' && m.attributeName === 'style') {
|
|
166
|
-
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (now() - start > 7000) { try { obs.disconnect(); } catch (e) {} wrap.__ezFillObs = null; }
|
|
297
|
+
const obs = new MutationObserver(muts => {
|
|
298
|
+
if (isFilled(wrap)) { wrap.classList.remove('is-empty'); burst(); }
|
|
299
|
+
for (const m of muts)
|
|
300
|
+
if (m.type === 'attributes' && m.attributeName === 'style') { tightenMinHeight(wrap); break; }
|
|
301
|
+
if (now() - start > 7000) { obs.disconnect(); wrap.__ezFillObs = null; }
|
|
171
302
|
});
|
|
172
303
|
obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
173
304
|
wrap.__ezFillObs = obs;
|
|
174
305
|
} catch (e) {}
|
|
175
306
|
}
|
|
176
307
|
|
|
177
|
-
|
|
308
|
+
// Global safety net: catch Ezoic 400px min-height inside posts.
|
|
309
|
+
function globalGapFix() {
|
|
178
310
|
try {
|
|
179
|
-
if (window.
|
|
180
|
-
window.
|
|
311
|
+
if (window.__nbbEzoicGapFix) return;
|
|
312
|
+
window.__nbbEzoicGapFix = true;
|
|
181
313
|
const root = document.getElementById('content') ||
|
|
182
314
|
document.querySelector('[component="content"], #panel') || document.body;
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
const maybeFix = (r) => {
|
|
315
|
+
const inPost = el => !!(el && el.closest &&
|
|
316
|
+
el.closest('[component="post"], .topic, .posts, [component="topic"]'));
|
|
317
|
+
const fix = r => {
|
|
188
318
|
if (!r || !r.querySelectorAll) return;
|
|
189
319
|
r.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]')
|
|
190
|
-
.forEach(
|
|
191
|
-
|
|
192
|
-
if (!
|
|
193
|
-
|
|
194
|
-
try { tightenEzoicMinHeight(n.closest('.' + WRAP_CLASS) || n.parentElement || n); } catch (e) {}
|
|
320
|
+
.forEach(n => {
|
|
321
|
+
if (!(n.getAttribute('style') || '').toLowerCase().includes('min-height:400')) return;
|
|
322
|
+
if (!inPost(n)) return;
|
|
323
|
+
tightenMinHeight(n.closest('.' + WRAP_CLASS) || n.parentElement || n);
|
|
195
324
|
});
|
|
196
325
|
};
|
|
197
|
-
requestAnimationFrame(() =>
|
|
326
|
+
requestAnimationFrame(() => fix(root));
|
|
198
327
|
const pending = new Set();
|
|
199
|
-
let
|
|
200
|
-
|
|
201
|
-
scheduled = false;
|
|
202
|
-
pending.forEach((n) => { try { maybeFix(n); } catch (e) {} });
|
|
203
|
-
pending.clear();
|
|
204
|
-
};
|
|
205
|
-
new MutationObserver((muts) => {
|
|
328
|
+
let sched = false;
|
|
329
|
+
new MutationObserver(muts => {
|
|
206
330
|
for (const m of muts) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
331
|
+
const t = m.type === 'attributes'
|
|
332
|
+
? (m.target.nodeType === 1 ? m.target : m.target.parentElement)
|
|
333
|
+
: null;
|
|
334
|
+
if (t) pending.add(t);
|
|
335
|
+
else m.addedNodes && m.addedNodes.forEach(n => n.nodeType === 1 && pending.add(n));
|
|
336
|
+
}
|
|
337
|
+
if (pending.size && !sched) {
|
|
338
|
+
sched = true;
|
|
339
|
+
requestAnimationFrame(() => { sched = false; pending.forEach(n => fix(n)); pending.clear(); });
|
|
213
340
|
}
|
|
214
|
-
if (pending.size && !scheduled) { scheduled = true; requestAnimationFrame(flush); }
|
|
215
341
|
}).observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
|
|
216
342
|
} catch (e) {}
|
|
217
343
|
}
|
|
218
344
|
|
|
219
|
-
// ───
|
|
220
|
-
const state = {
|
|
221
|
-
pageKey: null, cfg: null,
|
|
222
|
-
allTopics: [], allPosts: [], allCategories: [],
|
|
223
|
-
curTopics: 0, curPosts: 0, curCategories: 0,
|
|
224
|
-
lastShowById: new Map(),
|
|
225
|
-
domObs: null, io: null, ioMargin: null,
|
|
226
|
-
internalDomChange: 0,
|
|
227
|
-
inflight: 0, pending: [], pendingSet: new Set(),
|
|
228
|
-
scrollBoostUntil: 0, lastScrollY: 0, lastScrollTs: 0,
|
|
229
|
-
heroDoneForPage: false,
|
|
230
|
-
runQueued: false, burstActive: false, burstDeadline: 0, burstCount: 0, lastBurstReqTs: 0,
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
let blockedUntil = 0;
|
|
234
|
-
const insertingIds = new Set();
|
|
235
|
-
function isBlocked() { return now() < blockedUntil; }
|
|
345
|
+
// ─── DOM helpers ─────────────────────────────────────────────────────────────
|
|
236
346
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!raw) return [];
|
|
240
|
-
const seen = new Set(), out = [];
|
|
241
|
-
String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean).forEach((v) => {
|
|
242
|
-
const n = parseInt(v, 10);
|
|
243
|
-
if (Number.isFinite(n) && n > 0 && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
244
|
-
});
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function getPageKey() {
|
|
249
|
-
try {
|
|
250
|
-
const ax = window.ajaxify;
|
|
251
|
-
if (ax && ax.data) {
|
|
252
|
-
if (ax.data.tid) return 'topic:' + ax.data.tid;
|
|
253
|
-
if (ax.data.cid) return 'cid:' + ax.data.cid + ':' + window.location.pathname;
|
|
254
|
-
}
|
|
255
|
-
} catch (e) {}
|
|
256
|
-
return window.location.pathname;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getPreloadRootMargin() {
|
|
260
|
-
return isMobile()
|
|
261
|
-
? (isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE)
|
|
262
|
-
: (isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function getMaxInflight() {
|
|
266
|
-
return (isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP) + (isBoosted() ? 1 : 0);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function withInternalDomChange(fn) {
|
|
270
|
-
state.internalDomChange++;
|
|
271
|
-
try { fn(); } finally { state.internalDomChange--; }
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ─── DOM helpers ────────────────────────────────────────────────────────────
|
|
275
|
-
function getTopicItems() { return Array.from(document.querySelectorAll(SELECTORS.topicItem)); }
|
|
276
|
-
function getCategoryItems() { return Array.from(document.querySelectorAll(SELECTORS.categoryItem)); }
|
|
277
|
-
|
|
278
|
-
function getPostContainers() {
|
|
279
|
-
return Array.from(document.querySelectorAll(SELECTORS.postItem)).filter((el) => {
|
|
347
|
+
function getItems(kind) {
|
|
348
|
+
if (kind === 'topic') return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
280
349
|
if (!el.isConnected) return false;
|
|
281
350
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
282
|
-
const pp = el.parentElement && el.parentElement.closest(
|
|
283
|
-
|
|
284
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
285
|
-
return true;
|
|
351
|
+
const pp = el.parentElement && el.parentElement.closest(SEL.post);
|
|
352
|
+
return !(pp && pp !== el) && el.getAttribute('component') !== 'post/parent';
|
|
286
353
|
});
|
|
354
|
+
if (kind === 'categoryTopics') return Array.from(document.querySelectorAll(SEL.topic));
|
|
355
|
+
if (kind === 'categories') return Array.from(document.querySelectorAll(SEL.category));
|
|
356
|
+
return [];
|
|
287
357
|
}
|
|
288
358
|
|
|
289
|
-
function
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
293
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
294
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
295
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
296
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
297
|
-
return 'other';
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function isAdjacentAd(target) {
|
|
301
|
-
if (!target) return false;
|
|
302
|
-
const next = target.nextElementSibling;
|
|
303
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
304
|
-
const prev = target.previousElementSibling;
|
|
305
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function findWrap(kindClass, afterPos) {
|
|
310
|
-
return document.querySelector('.' + WRAP_CLASS + '.' + kindClass + '[data-ezoic-after="' + afterPos + '"]');
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ─── Network warmup ─────────────────────────────────────────────────────────
|
|
314
|
-
const _warmLinksDone = new Set();
|
|
315
|
-
function warmUpNetwork() {
|
|
316
|
-
try {
|
|
317
|
-
const head = document.head || document.getElementsByTagName('head')[0];
|
|
318
|
-
if (!head) return;
|
|
319
|
-
[
|
|
320
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
321
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
322
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
323
|
-
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
324
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
325
|
-
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
326
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
327
|
-
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
328
|
-
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
329
|
-
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
330
|
-
].forEach(([rel, href, cors]) => {
|
|
331
|
-
const key = rel + '|' + href;
|
|
332
|
-
if (_warmLinksDone.has(key)) return;
|
|
333
|
-
_warmLinksDone.add(key);
|
|
334
|
-
const link = document.createElement('link');
|
|
335
|
-
link.rel = rel; link.href = href;
|
|
336
|
-
if (cors) link.crossOrigin = 'anonymous';
|
|
337
|
-
head.appendChild(link);
|
|
338
|
-
});
|
|
339
|
-
} catch (e) {}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ─── Ezoic bridge ───────────────────────────────────────────────────────────
|
|
343
|
-
// Patch showAds so it silently skips IDs whose placeholder is not in the DOM.
|
|
344
|
-
function patchShowAds() {
|
|
345
|
-
const applyPatch = () => {
|
|
346
|
-
try {
|
|
347
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
348
|
-
const ez = window.ezstandalone;
|
|
349
|
-
if (window.__nodebbEzoicPatched) return;
|
|
350
|
-
if (typeof ez.showAds !== 'function') return;
|
|
351
|
-
window.__nodebbEzoicPatched = true;
|
|
352
|
-
const orig = ez.showAds;
|
|
353
|
-
ez.showAds = function (...args) {
|
|
354
|
-
if (isBlocked()) return;
|
|
355
|
-
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
356
|
-
const seen = new Set();
|
|
357
|
-
for (const v of ids) {
|
|
358
|
-
const id = parseInt(v, 10);
|
|
359
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
360
|
-
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
361
|
-
if (!ph || !ph.isConnected) continue;
|
|
362
|
-
seen.add(id);
|
|
363
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
} catch (e) {}
|
|
367
|
-
};
|
|
368
|
-
applyPatch();
|
|
369
|
-
if (!window.__nodebbEzoicPatched) {
|
|
370
|
-
try {
|
|
371
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
372
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
373
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
374
|
-
} catch (e) {}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
379
|
-
async function fetchConfigOnce() {
|
|
380
|
-
if (state.cfg) return state.cfg;
|
|
381
|
-
try {
|
|
382
|
-
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
383
|
-
if (!res.ok) return null;
|
|
384
|
-
state.cfg = await res.json();
|
|
385
|
-
return state.cfg;
|
|
386
|
-
} catch (e) { return null; }
|
|
359
|
+
function isAdjacentWrap(el) {
|
|
360
|
+
const n = el.nextElementSibling, p = el.previousElementSibling;
|
|
361
|
+
return (n && n.classList.contains(WRAP_CLASS)) || (p && p.classList.contains(WRAP_CLASS));
|
|
387
362
|
}
|
|
388
363
|
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// causes Ezoic to pre-define slots → "already defined" errors on SPA navigations.
|
|
393
|
-
if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
|
|
394
|
-
if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
395
|
-
if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
364
|
+
function findWrapAt(kindClass, afterPos) {
|
|
365
|
+
return document.querySelector('.' + WRAP_CLASS + '.' + kindClass +
|
|
366
|
+
'[data-ezoic-after="' + afterPos + '"]');
|
|
396
367
|
}
|
|
397
368
|
|
|
398
|
-
// ───
|
|
369
|
+
// ─── Wrap creation / destruction ─────────────────────────────────────────────
|
|
399
370
|
|
|
400
|
-
function buildWrap(id, kindClass, afterPos
|
|
371
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
401
372
|
const wrap = document.createElement('div');
|
|
402
|
-
wrap.className
|
|
373
|
+
wrap.className = WRAP_CLASS + ' ' + kindClass;
|
|
403
374
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
404
375
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
405
376
|
wrap.setAttribute('data-created', String(now()));
|
|
406
377
|
if (afterPos === 1) wrap.setAttribute('data-ezoic-pin', '1');
|
|
407
378
|
wrap.style.width = '100%';
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
wrap.appendChild(ph);
|
|
413
|
-
}
|
|
379
|
+
const ph = document.createElement('div');
|
|
380
|
+
ph.id = PLACEHOLDER_PREFIX + id;
|
|
381
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
382
|
+
wrap.appendChild(ph);
|
|
414
383
|
return wrap;
|
|
415
384
|
}
|
|
416
385
|
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const existingPh = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
422
|
-
insertingIds.add(id);
|
|
386
|
+
function dropWrap(wrap) {
|
|
387
|
+
// Unobserve placeholder, then remove the whole wrap node.
|
|
388
|
+
// The placeholder ID is freed back to the pool automatically because
|
|
389
|
+
// it no longer has a DOM element — pickId will pick it again.
|
|
423
390
|
try {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
if (existingPh) {
|
|
427
|
-
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
428
|
-
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
429
|
-
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
430
|
-
}
|
|
431
|
-
return wrap;
|
|
432
|
-
} finally {
|
|
433
|
-
insertingIds.delete(id);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function releaseWrapNode(wrap) {
|
|
438
|
-
try {
|
|
439
|
-
const ph = wrap && wrap.querySelector && wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
440
|
-
if (ph) {
|
|
441
|
-
try { (document.body || document.documentElement).appendChild(ph); } catch (e) {}
|
|
442
|
-
try { state.io && state.io.unobserve(ph); } catch (e) {}
|
|
443
|
-
}
|
|
391
|
+
const ph = wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
392
|
+
if (ph) try { st.io && st.io.unobserve(ph); } catch (e) {}
|
|
444
393
|
} catch (e) {}
|
|
445
|
-
try { wrap
|
|
394
|
+
try { wrap.remove(); } catch (e) {}
|
|
446
395
|
}
|
|
447
396
|
|
|
448
|
-
function
|
|
449
|
-
const
|
|
450
|
-
if (!
|
|
397
|
+
function pickId(poolKey, cursorKey) {
|
|
398
|
+
const pool = st.pools[poolKey];
|
|
399
|
+
if (!pool.length) return null;
|
|
400
|
+
const n = pool.length;
|
|
451
401
|
for (let tries = 0; tries < n; tries++) {
|
|
452
|
-
const idx =
|
|
453
|
-
|
|
454
|
-
const id
|
|
455
|
-
const ph
|
|
456
|
-
//
|
|
457
|
-
if (ph
|
|
458
|
-
return id;
|
|
402
|
+
const idx = st.cursors[cursorKey] % n;
|
|
403
|
+
st.cursors[cursorKey] = (idx + 1) % n;
|
|
404
|
+
const id = pool[idx];
|
|
405
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
406
|
+
// ID is available if its placeholder is absent from DOM or is floating detached.
|
|
407
|
+
if (!ph || !ph.isConnected || !ph.closest('.' + WRAP_CLASS)) return id;
|
|
459
408
|
}
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ─── Orphan / cluster management ────────────────────────────────────────────
|
|
464
|
-
function pruneOrphanWraps(kindClass, items) {
|
|
465
|
-
if (!items || !items.length) return 0;
|
|
466
|
-
const itemSet = new Set(items);
|
|
467
|
-
const isMessage = kindClass === 'ezoic-ad-message';
|
|
468
|
-
// Between/category ads are NEVER fully released — only hidden/shown.
|
|
469
|
-
// Releasing frees IDs into the pool → re-injection at wrong positions.
|
|
470
|
-
const allowRelease = isMessage;
|
|
471
|
-
let removed = 0;
|
|
472
|
-
|
|
473
|
-
const hasNearbyItem = (wrap) => {
|
|
474
|
-
// If wrap is inside a li.nodebb-ezoic-host, check the HOST's siblings.
|
|
475
|
-
const pivot = (wrap.parentElement && wrap.parentElement.classList &&
|
|
476
|
-
wrap.parentElement.classList.contains(HOST_CLASS))
|
|
477
|
-
? wrap.parentElement : wrap;
|
|
478
|
-
let prev = pivot.previousElementSibling;
|
|
479
|
-
for (let i = 0; i < 14 && prev; i++) {
|
|
480
|
-
if (itemSet.has(prev)) return true;
|
|
481
|
-
prev = prev.previousElementSibling;
|
|
482
|
-
}
|
|
483
|
-
let next = pivot.nextElementSibling;
|
|
484
|
-
for (let i = 0; i < 14 && next; i++) {
|
|
485
|
-
if (itemSet.has(next)) return true;
|
|
486
|
-
next = next.nextElementSibling;
|
|
487
|
-
}
|
|
488
|
-
return false;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
|
|
492
|
-
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
493
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
494
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
495
|
-
|
|
496
|
-
if (hasNearbyItem(wrap)) {
|
|
497
|
-
try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Anchor gone → hide.
|
|
502
|
-
try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
|
|
503
|
-
if (!allowRelease) return;
|
|
504
|
-
|
|
505
|
-
// Message-ads only: release when far off-screen.
|
|
506
|
-
try {
|
|
507
|
-
const r = wrap.getBoundingClientRect();
|
|
508
|
-
const vh = Math.max(1, window.innerHeight || 1);
|
|
509
|
-
if (r.bottom > -vh * 2 && r.top < vh * 4) return;
|
|
510
|
-
} catch (e) { return; }
|
|
511
|
-
|
|
512
|
-
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
513
|
-
removed++;
|
|
514
|
-
});
|
|
515
|
-
return removed;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function decluster(kindClass) {
|
|
519
|
-
const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
|
|
520
|
-
if (wraps.length < 2) return 0;
|
|
521
|
-
// Between/category ads: hide the duplicate rather than releasing it.
|
|
522
|
-
// Releasing frees IDs into the pool → re-injection at wrong positions on scroll-up.
|
|
523
|
-
const allowRelease = kindClass === 'ezoic-ad-message';
|
|
524
|
-
const evict = (n) => {
|
|
525
|
-
if (allowRelease) { withInternalDomChange(() => releaseWrapNode(n)); }
|
|
526
|
-
else { try { n.classList.add('ez-orphan-hidden'); n.style.display = 'none'; } catch (e) {} }
|
|
527
|
-
};
|
|
528
|
-
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
529
|
-
const isFresh = (w) => {
|
|
530
|
-
const c = parseInt(w.getAttribute('data-created') || '0', 10);
|
|
531
|
-
return c && (now() - c) < keepEmptyWrapMs();
|
|
532
|
-
};
|
|
533
|
-
let removed = 0;
|
|
534
|
-
for (const w of wraps) {
|
|
535
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
536
|
-
if (w.classList && w.classList.contains('ez-orphan-hidden')) continue;
|
|
537
|
-
let prev = w.previousElementSibling;
|
|
538
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
539
|
-
if (isWrap(prev)) {
|
|
540
|
-
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
541
|
-
if (prev.classList && prev.classList.contains('ez-orphan-hidden')) break;
|
|
542
|
-
const prevFilled = isFilled(prev);
|
|
543
|
-
const curFilled = isFilled(w);
|
|
544
|
-
if (curFilled) {
|
|
545
|
-
if (!prevFilled && !isFresh(prev)) { evict(prev); removed++; }
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
if (prevFilled || !isFresh(w)) { evict(w); removed++; }
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
prev = prev.previousElementSibling;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return removed;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ─── Pile-up repair for between-ads ─────────────────────────────────────────
|
|
558
|
-
// When NodeBB's DOM structure requires between-ad DIVs to live inside a <ul>,
|
|
559
|
-
// they must be wrapped in a <li> host. This module also corrects position
|
|
560
|
-
// drift that occurs after infinite-scroll mutations.
|
|
561
|
-
//
|
|
562
|
-
// FIX (v18): replaced the previous blind `previousTopicLi()` DOM walk with
|
|
563
|
-
// anchor lookup via `data-ezoic-after` + topic `data-index`. The old approach
|
|
564
|
-
// was the ROOT CAUSE of ads piling below the first topic: during pile-up
|
|
565
|
-
// detection it moved ALL piled wraps to after the SAME first topic it found.
|
|
566
|
-
//
|
|
567
|
-
// Now each wrap is moved to after the specific topic whose ordinal matches
|
|
568
|
-
// the wrap's own `data-ezoic-after` attribute.
|
|
569
|
-
|
|
570
|
-
function pileFixGetTopicList() {
|
|
571
|
-
try {
|
|
572
|
-
const li = document.querySelector(SELECTORS.topicItem);
|
|
573
|
-
if (!li) return null;
|
|
574
|
-
return li.closest ? li.closest('ul,ol') : null;
|
|
575
|
-
} catch (e) { return null; }
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function ensureHostForWrap(wrap, ul) {
|
|
579
|
-
try {
|
|
580
|
-
if (!wrap || wrap.nodeType !== 1) return null;
|
|
581
|
-
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
582
|
-
if (!wrap.matches || !wrap.matches(wrapSel)) return null;
|
|
583
|
-
const existing = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
|
|
584
|
-
if (existing) return existing;
|
|
585
|
-
if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
|
|
586
|
-
if (!ul || !/^(UL|OL)$/i.test(ul.tagName)) return null;
|
|
587
|
-
if (wrap.parentElement !== ul) return null;
|
|
588
|
-
const host = document.createElement('li');
|
|
589
|
-
host.className = HOST_CLASS;
|
|
590
|
-
host.setAttribute('role', 'listitem');
|
|
591
|
-
host.style.cssText = 'list-style:none;width:100%;';
|
|
592
|
-
ul.insertBefore(host, wrap);
|
|
593
|
-
host.appendChild(wrap);
|
|
594
|
-
return host;
|
|
595
|
-
} catch (e) { return null; }
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function detectPileUp(ul) {
|
|
599
|
-
try {
|
|
600
|
-
let run = 0, maxRun = 0;
|
|
601
|
-
const kids = ul.children;
|
|
602
|
-
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
603
|
-
for (let i = 0; i < kids.length; i++) {
|
|
604
|
-
const el = kids[i];
|
|
605
|
-
const isHost = el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS);
|
|
606
|
-
const isBetween = (isHost && el.querySelector && el.querySelector(wrapSel)) ||
|
|
607
|
-
(el.matches && el.matches(wrapSel));
|
|
608
|
-
if (isBetween) { maxRun = Math.max(maxRun, ++run); }
|
|
609
|
-
else if (el.matches && el.matches(SELECTORS.topicItem)) { run = 0; }
|
|
610
|
-
else { run = 0; }
|
|
611
|
-
}
|
|
612
|
-
return maxRun >= 2;
|
|
613
|
-
} catch (e) { return false; }
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Move each pile-up host to after its correct anchor topic.
|
|
618
|
-
*
|
|
619
|
-
* Anchor is determined by matching the wrap's `data-ezoic-after` (1-based ordinal)
|
|
620
|
-
* against each topic LI's `data-index` (0-based) → ordinal = data-index + 1.
|
|
621
|
-
*/
|
|
622
|
-
function redistributePileUp(ul) {
|
|
623
|
-
try {
|
|
624
|
-
if (!ul) return;
|
|
625
|
-
// Step 1: wrap bare between-ad DIVs that are direct <ul> children.
|
|
626
|
-
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
627
|
-
const directs = Array.from(ul.querySelectorAll(':scope > ' + wrapSel));
|
|
628
|
-
directs.forEach((w) => ensureHostForWrap(w, ul));
|
|
629
|
-
|
|
630
|
-
// Step 2: bail if no pile-up.
|
|
631
|
-
if (!detectPileUp(ul)) return;
|
|
632
|
-
|
|
633
|
-
// Step 3: build ordinal → topic LI map.
|
|
634
|
-
const topicLis = Array.from(ul.querySelectorAll(':scope > ' + SELECTORS.topicItem));
|
|
635
|
-
const ordinalMap = new Map();
|
|
636
|
-
topicLis.forEach((li, i) => {
|
|
637
|
-
const di = li.dataset && (li.dataset.index !== undefined ? li.dataset.index : null);
|
|
638
|
-
const raw = di !== null && di !== undefined ? di : li.getAttribute('data-index');
|
|
639
|
-
const ord = (raw !== null && raw !== undefined && raw !== '' && !isNaN(raw))
|
|
640
|
-
? parseInt(raw, 10) + 1 // data-index is 0-based; ordinals are 1-based
|
|
641
|
-
: i + 1;
|
|
642
|
-
ordinalMap.set(ord, li);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// Step 4: move each host to immediately after its correct anchor topic.
|
|
646
|
-
const hosts = Array.from(ul.querySelectorAll(':scope > li.' + HOST_CLASS));
|
|
647
|
-
hosts.forEach((host) => {
|
|
648
|
-
try {
|
|
649
|
-
const wrap = host.querySelector && host.querySelector(wrapSel);
|
|
650
|
-
if (!wrap) return;
|
|
651
|
-
|
|
652
|
-
const afterPos = parseInt(wrap.getAttribute('data-ezoic-after') || '0', 10);
|
|
653
|
-
if (!afterPos) return;
|
|
654
|
-
|
|
655
|
-
const anchor = ordinalMap.get(afterPos);
|
|
656
|
-
if (!anchor) return; // anchor topic not loaded yet — leave in place
|
|
657
|
-
|
|
658
|
-
if (host.previousElementSibling !== anchor) {
|
|
659
|
-
anchor.insertAdjacentElement('afterend', host);
|
|
660
|
-
}
|
|
661
|
-
} catch (e) {}
|
|
662
|
-
});
|
|
663
|
-
} catch (e) {}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
let _pileFixScheduled = false;
|
|
667
|
-
let _pileFixLastRun = 0;
|
|
668
|
-
const PILE_FIX_COOLDOWN = 180;
|
|
669
|
-
|
|
670
|
-
function schedulePileFix() {
|
|
671
|
-
if (now() - _pileFixLastRun < PILE_FIX_COOLDOWN) return;
|
|
672
|
-
if (_pileFixScheduled) return;
|
|
673
|
-
_pileFixScheduled = true;
|
|
674
|
-
requestAnimationFrame(() => {
|
|
675
|
-
_pileFixScheduled = false;
|
|
676
|
-
_pileFixLastRun = now();
|
|
677
|
-
try { const ul = pileFixGetTopicList(); if (ul) redistributePileUp(ul); } catch (e) {}
|
|
678
|
-
});
|
|
409
|
+
return null; // all IDs currently mounted
|
|
679
410
|
}
|
|
680
411
|
|
|
681
|
-
|
|
682
|
-
try {
|
|
683
|
-
if (typeof MutationObserver === 'undefined') return;
|
|
684
|
-
const observe = (ul) => {
|
|
685
|
-
new MutationObserver(() => schedulePileFix())
|
|
686
|
-
.observe(ul, { childList: true, subtree: true });
|
|
687
|
-
};
|
|
688
|
-
const ul = pileFixGetTopicList();
|
|
689
|
-
if (ul) {
|
|
690
|
-
observe(ul);
|
|
691
|
-
} else {
|
|
692
|
-
const sentinel = new MutationObserver(() => {
|
|
693
|
-
const u = pileFixGetTopicList();
|
|
694
|
-
if (u) { try { sentinel.disconnect(); } catch (e) {} observe(u); }
|
|
695
|
-
});
|
|
696
|
-
sentinel.observe(document.documentElement || document.body, { childList: true, subtree: true });
|
|
697
|
-
}
|
|
698
|
-
} catch (e) {}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ─── IntersectionObserver (preload / fast fill) ─────────────────────────────
|
|
702
|
-
function ensurePreloadObserver() {
|
|
703
|
-
const desiredMargin = getPreloadRootMargin();
|
|
704
|
-
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
705
|
-
|
|
706
|
-
try { state.io && state.io.disconnect(); } catch (e) {}
|
|
707
|
-
state.io = null;
|
|
412
|
+
// ─── IntersectionObserver ────────────────────────────────────────────────────
|
|
708
413
|
|
|
414
|
+
function ensureIO() {
|
|
415
|
+
const margin = getIoMargin();
|
|
416
|
+
if (st.io && st.ioMargin === margin) return st.io;
|
|
417
|
+
try { st.io && st.io.disconnect(); } catch (e) {}
|
|
418
|
+
st.io = null;
|
|
709
419
|
try {
|
|
710
|
-
|
|
711
|
-
for (const
|
|
712
|
-
if (!
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
420
|
+
st.io = new IntersectionObserver(entries => {
|
|
421
|
+
for (const e of entries) {
|
|
422
|
+
if (!e.isIntersecting) continue;
|
|
423
|
+
try { st.io && st.io.unobserve(e.target); } catch (x) {}
|
|
424
|
+
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
425
|
+
if (id > 0) enqueueShow(id);
|
|
717
426
|
}
|
|
718
|
-
}, {
|
|
719
|
-
|
|
427
|
+
}, { rootMargin: margin, threshold: 0 });
|
|
428
|
+
st.ioMargin = margin;
|
|
429
|
+
// Re-observe all live placeholders.
|
|
720
430
|
document.querySelectorAll('[id^="' + PLACEHOLDER_PREFIX + '"]')
|
|
721
|
-
.forEach(
|
|
722
|
-
} catch (e) {
|
|
723
|
-
|
|
724
|
-
return state.io;
|
|
431
|
+
.forEach(ph => { try { st.io.observe(ph); } catch (e) {} });
|
|
432
|
+
} catch (e) { st.io = st.ioMargin = null; }
|
|
433
|
+
return st.io;
|
|
725
434
|
}
|
|
726
435
|
|
|
727
|
-
function
|
|
436
|
+
function observePh(id) {
|
|
728
437
|
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
729
438
|
if (!ph || !ph.isConnected) return;
|
|
730
439
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
731
|
-
try {
|
|
440
|
+
try { ensureIO() && ensureIO().observe(ph); } catch (e) {}
|
|
441
|
+
// Immediate fire if already near viewport.
|
|
732
442
|
try {
|
|
733
443
|
const r = ph.getBoundingClientRect();
|
|
734
|
-
const
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
if (r.top < window.innerHeight *
|
|
444
|
+
const mob = isMobile(), boost = isBoosted();
|
|
445
|
+
const scr = boost ? (mob ? 9 : 5) : (mob ? 6 : 3);
|
|
446
|
+
const bot = boost ? (mob ? -2600 : -1500) : (mob ? -1400 : -800);
|
|
447
|
+
if (r.top < window.innerHeight * scr && r.bottom > bot) enqueueShow(id);
|
|
738
448
|
} catch (e) {}
|
|
739
449
|
}
|
|
740
450
|
|
|
741
|
-
// ─── Show queue
|
|
451
|
+
// ─── Show queue ──────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
742
453
|
function enqueueShow(id) {
|
|
743
454
|
if (!id || isBlocked()) return;
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
if (state.inflight >= max) {
|
|
748
|
-
if (!state.pendingSet.has(id)) { state.pending.push(id); state.pendingSet.add(id); }
|
|
455
|
+
if (now() - (st.lastShow.get(id) || 0) < 900) return;
|
|
456
|
+
if (st.inflight >= getMaxInflight()) {
|
|
457
|
+
if (!st.pendingSet.has(id)) { st.pending.push(id); st.pendingSet.add(id); }
|
|
749
458
|
return;
|
|
750
459
|
}
|
|
751
460
|
startShow(id);
|
|
@@ -753,414 +462,399 @@
|
|
|
753
462
|
|
|
754
463
|
function drainQueue() {
|
|
755
464
|
if (isBlocked()) return;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
state.pendingSet.delete(id);
|
|
465
|
+
while (st.inflight < getMaxInflight() && st.pending.length) {
|
|
466
|
+
const id = st.pending.shift();
|
|
467
|
+
st.pendingSet.delete(id);
|
|
760
468
|
startShow(id);
|
|
761
469
|
}
|
|
762
470
|
}
|
|
763
471
|
|
|
764
|
-
function
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (!w2) return;
|
|
776
|
-
const created = parseInt(w2.getAttribute('data-created') || '0', 10);
|
|
777
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
778
|
-
if (!isFilled(ph2)) { w2.classList.add('is-empty'); watchWrapForFill(w2); }
|
|
779
|
-
else { w2.classList.remove('is-empty'); tightenEzoicMinHeight(w2); }
|
|
780
|
-
} catch (e) {}
|
|
781
|
-
}, 15_000);
|
|
782
|
-
} catch (e) {}
|
|
472
|
+
function scheduleMarkEmpty(id) {
|
|
473
|
+
setTimeout(() => {
|
|
474
|
+
try {
|
|
475
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
476
|
+
if (!ph || !ph.isConnected) return;
|
|
477
|
+
const w = ph.closest('.' + WRAP_CLASS);
|
|
478
|
+
if (!w) return;
|
|
479
|
+
if (isFilled(ph)) { w.classList.remove('is-empty'); tightenMinHeight(w); }
|
|
480
|
+
else { w.classList.add('is-empty'); watchFill(w); }
|
|
481
|
+
} catch (e) {}
|
|
482
|
+
}, 15_000);
|
|
783
483
|
}
|
|
784
484
|
|
|
785
485
|
function startShow(id) {
|
|
786
486
|
if (!id || isBlocked()) return;
|
|
787
|
-
|
|
788
|
-
let
|
|
789
|
-
const
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
state.inflight = Math.max(0, state.inflight - 1);
|
|
487
|
+
st.inflight++;
|
|
488
|
+
let done = false;
|
|
489
|
+
const finish = () => {
|
|
490
|
+
if (done) return; done = true;
|
|
491
|
+
st.inflight = Math.max(0, st.inflight - 1);
|
|
793
492
|
drainQueue();
|
|
794
493
|
};
|
|
795
|
-
const
|
|
494
|
+
const timeout = setTimeout(finish, 6500);
|
|
495
|
+
|
|
796
496
|
requestAnimationFrame(() => {
|
|
797
497
|
try {
|
|
798
498
|
if (isBlocked()) return;
|
|
799
499
|
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
800
500
|
if (!ph || !ph.isConnected) return;
|
|
801
|
-
if (isFilled(ph)) { clearTimeout(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
806
|
-
const ez = window.ezstandalone;
|
|
501
|
+
if (isFilled(ph)) { clearTimeout(timeout); finish(); return; }
|
|
502
|
+
if (now() - (st.lastShow.get(id) || 0) < 900) return;
|
|
503
|
+
st.lastShow.set(id, now());
|
|
504
|
+
|
|
505
|
+
const ez = window.ezstandalone = window.ezstandalone || {};
|
|
807
506
|
const doShow = () => {
|
|
808
507
|
try { ez.showAds(id); } catch (e) {}
|
|
809
|
-
|
|
508
|
+
scheduleMarkEmpty(id);
|
|
810
509
|
try {
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
setTimeout(() =>
|
|
816
|
-
setTimeout(() =>
|
|
510
|
+
const w = document.getElementById(PLACEHOLDER_PREFIX + id)
|
|
511
|
+
?.closest('.' + WRAP_CLASS);
|
|
512
|
+
if (w) {
|
|
513
|
+
watchFill(w);
|
|
514
|
+
setTimeout(() => tightenMinHeight(w), 900);
|
|
515
|
+
setTimeout(() => tightenMinHeight(w), 2200);
|
|
817
516
|
}
|
|
818
517
|
} catch (e) {}
|
|
819
|
-
setTimeout(() => { clearTimeout(
|
|
518
|
+
setTimeout(() => { clearTimeout(timeout); finish(); }, 650);
|
|
820
519
|
};
|
|
821
|
-
if (Array.isArray(ez.cmd))
|
|
520
|
+
if (Array.isArray(ez.cmd)) try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
822
521
|
else doShow();
|
|
823
|
-
} finally {
|
|
824
|
-
// hardTimer covers early returns.
|
|
825
|
-
}
|
|
522
|
+
} finally {} // timeout covers early returns
|
|
826
523
|
});
|
|
827
524
|
}
|
|
828
525
|
|
|
829
|
-
// ─── Core injection
|
|
830
|
-
|
|
526
|
+
// ─── Core injection ──────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
function getOrdinal(el, idx) {
|
|
831
529
|
try {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
(el.dataset && (el.dataset.index || el.dataset.postIndex));
|
|
835
|
-
if (di != null && di !== '' && !isNaN(di)) {
|
|
836
|
-
const n = parseInt(di, 10);
|
|
837
|
-
if (Number.isFinite(n) && n >= 0) return n + 1;
|
|
838
|
-
}
|
|
839
|
-
const d1 = el.getAttribute('data-idx') || el.getAttribute('data-position') ||
|
|
840
|
-
(el.dataset && (el.dataset.idx || el.dataset.position));
|
|
841
|
-
if (d1 != null && d1 !== '' && !isNaN(d1)) {
|
|
842
|
-
const n = parseInt(d1, 10);
|
|
843
|
-
if (Number.isFinite(n) && n > 0) return n;
|
|
844
|
-
}
|
|
530
|
+
const v = el.getAttribute('data-index') || (el.dataset && el.dataset.index);
|
|
531
|
+
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10) + 1;
|
|
845
532
|
} catch (e) {}
|
|
846
|
-
return
|
|
533
|
+
return idx + 1;
|
|
847
534
|
}
|
|
848
535
|
|
|
849
|
-
function
|
|
536
|
+
function computeTargets(items, interval, showFirst) {
|
|
537
|
+
// Build ordinal map and determine target positions.
|
|
850
538
|
const map = new Map();
|
|
851
539
|
let max = 0;
|
|
852
|
-
|
|
853
|
-
const ord =
|
|
854
|
-
map.set(ord,
|
|
540
|
+
items.forEach((el, i) => {
|
|
541
|
+
const ord = getOrdinal(el, i);
|
|
542
|
+
map.set(ord, el);
|
|
855
543
|
if (ord > max) max = ord;
|
|
856
|
-
}
|
|
857
|
-
|
|
544
|
+
});
|
|
545
|
+
const targets = [];
|
|
546
|
+
if (showFirst && max >= 1) targets.push(1);
|
|
547
|
+
for (let i = interval; i <= max; i += interval) targets.push(i);
|
|
548
|
+
return { map, targets: [...new Set(targets)].sort((a, b) => a - b) };
|
|
858
549
|
}
|
|
859
550
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
551
|
+
/**
|
|
552
|
+
* Remove all wraps whose anchor item is no longer in the live item set.
|
|
553
|
+
*
|
|
554
|
+
* Strategy: always fully remove (not just hide).
|
|
555
|
+
* - The placeholder ID returns to the pool naturally (no DOM element → pickId skips).
|
|
556
|
+
* - When the user scrolls back down and those items reload, fresh wraps are injected.
|
|
557
|
+
* - The viewport guard in injectWraps (rect.bottom < 0) prevents re-injection
|
|
558
|
+
* on items that are above the fold (just loaded by NodeBB during upward scroll).
|
|
559
|
+
*/
|
|
560
|
+
function removeOrphanWraps(kindClass, itemSet) {
|
|
561
|
+
document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach(wrap => {
|
|
562
|
+
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
867
563
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
// These are likely to be visible again when the user scrolls back up.
|
|
879
|
-
const afterPos = parseInt(w.getAttribute('data-ezoic-after') || '0', 10);
|
|
880
|
-
if (afterPos > 0 && afterPos <= 6) continue;
|
|
881
|
-
const rect = w.getBoundingClientRect();
|
|
882
|
-
if (rect.bottom < threshold && rect.bottom < bestBottom) {
|
|
883
|
-
bestBottom = rect.bottom;
|
|
884
|
-
best = w;
|
|
564
|
+
// Check if anchor is still alive using DOM proximity.
|
|
565
|
+
const pivot = (wrap.parentElement && wrap.parentElement.classList.contains(HOST_CLASS))
|
|
566
|
+
? wrap.parentElement : wrap;
|
|
567
|
+
let found = false, el = pivot.previousElementSibling;
|
|
568
|
+
for (let i = 0; i < 4 && el; i++, el = el.previousElementSibling)
|
|
569
|
+
if (itemSet.has(el)) { found = true; break; }
|
|
570
|
+
if (!found) {
|
|
571
|
+
el = pivot.nextElementSibling;
|
|
572
|
+
for (let i = 0; i < 4 && el; i++, el = el.nextElementSibling)
|
|
573
|
+
if (itemSet.has(el)) { found = true; break; }
|
|
885
574
|
}
|
|
886
|
-
}
|
|
887
|
-
return best;
|
|
888
|
-
}
|
|
889
575
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
893
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
894
|
-
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
895
|
-
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
896
|
-
return wrap;
|
|
897
|
-
} catch (e) { return null; }
|
|
576
|
+
if (!found) withInternal(() => dropWrap(wrap));
|
|
577
|
+
});
|
|
898
578
|
}
|
|
899
579
|
|
|
900
|
-
function
|
|
580
|
+
function injectWraps(kindClass, items, interval, showFirst, poolKey, cursorKey) {
|
|
901
581
|
if (!items.length) return 0;
|
|
902
|
-
const { map
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
let inserted = 0;
|
|
582
|
+
const { map, targets } = computeTargets(items, interval, showFirst);
|
|
583
|
+
const max = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
584
|
+
let n = 0;
|
|
906
585
|
|
|
907
|
-
for (const
|
|
908
|
-
if (
|
|
909
|
-
const el =
|
|
586
|
+
for (const pos of targets) {
|
|
587
|
+
if (n >= max) break;
|
|
588
|
+
const el = map.get(pos);
|
|
910
589
|
if (!el || !el.isConnected) continue;
|
|
911
590
|
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
try {
|
|
918
|
-
if (el.getBoundingClientRect().bottom < 0) continue;
|
|
919
|
-
} catch (e) {}
|
|
591
|
+
// ── Viewport guard ────────────────────────────────────────────────────────
|
|
592
|
+
// Never inject after an element whose bottom is above the viewport top.
|
|
593
|
+
// NodeBB loads items above the fold when the user scrolls up; those items
|
|
594
|
+
// have rect.bottom < 0 at load time. Injecting there causes the pile-up.
|
|
595
|
+
try { if (el.getBoundingClientRect().bottom < 0) continue; } catch (e) {}
|
|
920
596
|
|
|
921
|
-
if (
|
|
922
|
-
if (
|
|
597
|
+
if (isAdjacentWrap(el)) continue;
|
|
598
|
+
if (findWrapAt(kindClass, pos)) continue;
|
|
923
599
|
|
|
924
|
-
|
|
925
|
-
|
|
600
|
+
const id = pickId(poolKey, cursorKey);
|
|
601
|
+
if (!id) break;
|
|
926
602
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
603
|
+
const wrap = buildWrap(id, kindClass, pos);
|
|
604
|
+
withInternal(() => el.insertAdjacentElement('afterend', wrap));
|
|
605
|
+
observePh(id);
|
|
606
|
+
n++;
|
|
607
|
+
}
|
|
608
|
+
return n;
|
|
609
|
+
}
|
|
935
610
|
|
|
936
|
-
|
|
611
|
+
// ─── Pile-up fix for category topic lists ────────────────────────────────────
|
|
612
|
+
// If the skin requires ad divs inside a <ul>, wrap them in <li> hosts and
|
|
613
|
+
// correct their position after NodeBB re-renders.
|
|
937
614
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
615
|
+
let _pileFixSched = false, _pileFixLast = 0;
|
|
616
|
+
function schedulePileFix() {
|
|
617
|
+
if (now() - _pileFixLast < 180 || _pileFixSched) return;
|
|
618
|
+
_pileFixSched = true;
|
|
619
|
+
requestAnimationFrame(() => {
|
|
620
|
+
_pileFixSched = false; _pileFixLast = now();
|
|
621
|
+
try {
|
|
622
|
+
const li = document.querySelector(SEL.topic);
|
|
623
|
+
const ul = li && li.closest('ul,ol');
|
|
624
|
+
if (!ul) return;
|
|
625
|
+
|
|
626
|
+
// Wrap bare ad divs that are direct <ul> children into <li> hosts.
|
|
627
|
+
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
628
|
+
ul.querySelectorAll(':scope > ' + wrapSel).forEach(w => {
|
|
629
|
+
if (w.parentElement !== ul) return;
|
|
630
|
+
const host = document.createElement('li');
|
|
631
|
+
host.className = HOST_CLASS;
|
|
632
|
+
host.setAttribute('role', 'listitem');
|
|
633
|
+
host.style.cssText = 'list-style:none;width:100%;';
|
|
634
|
+
ul.insertBefore(host, w);
|
|
635
|
+
host.appendChild(w);
|
|
636
|
+
});
|
|
942
637
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
638
|
+
// Detect pile-up: 2+ between-ad hosts adjacent with no topic between them.
|
|
639
|
+
let run = 0, maxRun = 0;
|
|
640
|
+
for (const c of ul.children) {
|
|
641
|
+
const isBetween = (c.tagName === 'LI' && c.classList.contains(HOST_CLASS) &&
|
|
642
|
+
c.querySelector(wrapSel)) ||
|
|
643
|
+
(c.matches && c.matches(wrapSel));
|
|
644
|
+
if (isBetween) maxRun = Math.max(maxRun, ++run);
|
|
645
|
+
else if (c.matches && c.matches(SEL.topic)) run = 0;
|
|
646
|
+
else run = 0;
|
|
647
|
+
}
|
|
648
|
+
if (maxRun < 2) return;
|
|
649
|
+
|
|
650
|
+
// Build ordinal → topic LI map.
|
|
651
|
+
const ordMap = new Map();
|
|
652
|
+
Array.from(ul.querySelectorAll(':scope > ' + SEL.topic)).forEach((li, i) => {
|
|
653
|
+
const di = li.dataset && li.dataset.index;
|
|
654
|
+
const ord = (di != null && di !== '' && !isNaN(di)) ? parseInt(di, 10) + 1 : i + 1;
|
|
655
|
+
ordMap.set(ord, li);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Move each host to after its correct anchor topic.
|
|
659
|
+
ul.querySelectorAll(':scope > li.' + HOST_CLASS).forEach(host => {
|
|
660
|
+
const wrap = host.querySelector(wrapSel);
|
|
661
|
+
if (!wrap) return;
|
|
662
|
+
const pos = parseInt(wrap.getAttribute('data-ezoic-after') || '0', 10);
|
|
663
|
+
const anchor = pos && ordMap.get(pos);
|
|
664
|
+
if (anchor && host.previousElementSibling !== anchor)
|
|
665
|
+
anchor.insertAdjacentElement('afterend', host);
|
|
666
|
+
});
|
|
667
|
+
} catch (e) {}
|
|
668
|
+
});
|
|
947
669
|
}
|
|
948
670
|
|
|
949
|
-
// ─── runCore
|
|
671
|
+
// ─── runCore ─────────────────────────────────────────────────────────────────
|
|
672
|
+
|
|
950
673
|
async function runCore() {
|
|
951
674
|
if (isBlocked()) return 0;
|
|
952
675
|
patchShowAds();
|
|
953
|
-
const cfg = await
|
|
676
|
+
const cfg = await fetchConfig();
|
|
954
677
|
if (!cfg || cfg.excluded) return 0;
|
|
955
678
|
initPools(cfg);
|
|
956
679
|
|
|
957
680
|
const kind = getKind();
|
|
958
681
|
let inserted = 0;
|
|
959
682
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
} else if (kind === '
|
|
978
|
-
const items =
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
decluster('ezoic-ad-between');
|
|
985
|
-
schedulePileFix();
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
989
|
-
const items = getCategoryItems();
|
|
990
|
-
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
991
|
-
if (canInject) {
|
|
992
|
-
inserted += injectBetween('ezoic-ad-categories', items,
|
|
993
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
994
|
-
normalizeBool(cfg.showFirstCategoryAd), state.allCategories, 'curCategories');
|
|
995
|
-
decluster('ezoic-ad-categories');
|
|
996
|
-
}
|
|
683
|
+
if (kind === 'topic' && normBool(cfg.enableMessageAds)) {
|
|
684
|
+
const items = getItems('topic');
|
|
685
|
+
const set = new Set(items);
|
|
686
|
+
removeOrphanWraps('ezoic-ad-message', set);
|
|
687
|
+
inserted += injectWraps('ezoic-ad-message', items,
|
|
688
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
689
|
+
normBool(cfg.showFirstMessageAd), 'post', 'post');
|
|
690
|
+
|
|
691
|
+
} else if (kind === 'categoryTopics' && normBool(cfg.enableBetweenAds)) {
|
|
692
|
+
const items = getItems('categoryTopics');
|
|
693
|
+
const set = new Set(items);
|
|
694
|
+
removeOrphanWraps('ezoic-ad-between', set);
|
|
695
|
+
inserted += injectWraps('ezoic-ad-between', items,
|
|
696
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
697
|
+
normBool(cfg.showFirstTopicAd), 'topic', 'topic');
|
|
698
|
+
schedulePileFix();
|
|
699
|
+
|
|
700
|
+
} else if (kind === 'categories' && normBool(cfg.enableCategoryAds)) {
|
|
701
|
+
const items = getItems('categories');
|
|
702
|
+
const set = new Set(items);
|
|
703
|
+
removeOrphanWraps('ezoic-ad-categories', set);
|
|
704
|
+
inserted += injectWraps('ezoic-ad-categories', items,
|
|
705
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
706
|
+
normBool(cfg.showFirstCategoryAd), 'category', 'category');
|
|
997
707
|
}
|
|
998
708
|
|
|
999
709
|
return inserted;
|
|
1000
710
|
}
|
|
1001
711
|
|
|
1002
|
-
// ─── Hero ad (first item
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
712
|
+
// ─── Hero ad (inject immediately after first item) ───────────────────────────
|
|
713
|
+
|
|
714
|
+
async function heroAd() {
|
|
715
|
+
if (st.heroDone || isBlocked()) return;
|
|
716
|
+
const cfg = await fetchConfig();
|
|
1006
717
|
if (!cfg || cfg.excluded) return;
|
|
1007
718
|
initPools(cfg);
|
|
1008
719
|
|
|
1009
720
|
const kind = getKind();
|
|
1010
|
-
let items = [],
|
|
1011
|
-
|
|
1012
|
-
if (kind === 'topic' &&
|
|
1013
|
-
items =
|
|
1014
|
-
kindClass = 'ezoic-ad-message';
|
|
1015
|
-
} else if (kind === 'categoryTopics' &&
|
|
1016
|
-
items =
|
|
1017
|
-
kindClass = 'ezoic-ad-between';
|
|
1018
|
-
} else if (kind === 'categories' &&
|
|
1019
|
-
items =
|
|
1020
|
-
kindClass = 'ezoic-ad-categories';
|
|
721
|
+
let items = [], showFirst = false, poolKey = '', cursorKey = '', kindClass = '';
|
|
722
|
+
|
|
723
|
+
if (kind === 'topic' && normBool(cfg.enableMessageAds)) {
|
|
724
|
+
items = getItems('topic'); showFirst = normBool(cfg.showFirstMessageAd);
|
|
725
|
+
poolKey = cursorKey = 'post'; kindClass = 'ezoic-ad-message';
|
|
726
|
+
} else if (kind === 'categoryTopics' && normBool(cfg.enableBetweenAds)) {
|
|
727
|
+
items = getItems('categoryTopics'); showFirst = normBool(cfg.showFirstTopicAd);
|
|
728
|
+
poolKey = cursorKey = 'topic'; kindClass = 'ezoic-ad-between';
|
|
729
|
+
} else if (kind === 'categories' && normBool(cfg.enableCategoryAds)) {
|
|
730
|
+
items = getItems('categories'); showFirst = normBool(cfg.showFirstCategoryAd);
|
|
731
|
+
poolKey = cursorKey = 'category'; kindClass = 'ezoic-ad-categories';
|
|
1021
732
|
} else { return; }
|
|
1022
733
|
|
|
1023
|
-
if (!items.length || !showFirst) {
|
|
734
|
+
if (!items.length || !showFirst) { st.heroDone = true; return; }
|
|
1024
735
|
|
|
1025
736
|
const el = items[0];
|
|
1026
|
-
if (!el || !el.isConnected ||
|
|
1027
|
-
|
|
737
|
+
if (!el || !el.isConnected || isAdjacentWrap(el) || findWrapAt(kindClass, 1))
|
|
738
|
+
{ st.heroDone = true; return; }
|
|
1028
739
|
|
|
1029
|
-
const id =
|
|
740
|
+
const id = pickId(poolKey, cursorKey);
|
|
1030
741
|
if (!id) return;
|
|
1031
742
|
|
|
1032
|
-
const wrap =
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
observePlaceholder(id);
|
|
743
|
+
const wrap = buildWrap(id, kindClass, 1);
|
|
744
|
+
withInternal(() => el.insertAdjacentElement('afterend', wrap));
|
|
745
|
+
st.heroDone = true;
|
|
746
|
+
observePh(id);
|
|
1037
747
|
enqueueShow(id);
|
|
1038
|
-
drainQueue();
|
|
748
|
+
drainQueue();
|
|
1039
749
|
}
|
|
1040
750
|
|
|
1041
|
-
// ───
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
751
|
+
// ─── Burst scheduler ─────────────────────────────────────────────────────────
|
|
752
|
+
|
|
753
|
+
function scheduleRun(delay, cb) {
|
|
754
|
+
if (st.runQueued) return;
|
|
755
|
+
st.runQueued = true;
|
|
1045
756
|
const run = async () => {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
try {
|
|
1051
|
-
try { cb && cb(inserted); } catch (e) {}
|
|
757
|
+
st.runQueued = false;
|
|
758
|
+
if (getPageKey() !== st.pageKey) return;
|
|
759
|
+
let n = 0;
|
|
760
|
+
try { n = await runCore(); } catch (e) {}
|
|
761
|
+
try { cb && cb(n); } catch (e) {}
|
|
1052
762
|
};
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
else doRun();
|
|
763
|
+
const go = () => requestAnimationFrame(run);
|
|
764
|
+
delay > 0 ? setTimeout(go, delay) : go();
|
|
1056
765
|
}
|
|
1057
766
|
|
|
1058
|
-
function
|
|
767
|
+
function burst() {
|
|
1059
768
|
if (isBlocked()) return;
|
|
1060
769
|
const t = now();
|
|
1061
|
-
if (t -
|
|
1062
|
-
|
|
1063
|
-
const pk = getPageKey();
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
state.burstCount = 0;
|
|
770
|
+
if (t - st.lastBurstTs < 120) return;
|
|
771
|
+
st.lastBurstTs = t;
|
|
772
|
+
const pk = st.pageKey = getPageKey();
|
|
773
|
+
st.burstDeadline = t + 1800;
|
|
774
|
+
if (st.burstActive) return;
|
|
775
|
+
st.burstActive = true;
|
|
776
|
+
st.burstCount = 0;
|
|
1069
777
|
const step = () => {
|
|
1070
|
-
if (getPageKey() !== pk || isBlocked() ||
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
scheduleRun(0,
|
|
1075
|
-
if (!
|
|
1076
|
-
setTimeout(step,
|
|
778
|
+
if (getPageKey() !== pk || isBlocked() ||
|
|
779
|
+
now() > st.burstDeadline || st.burstCount >= 8)
|
|
780
|
+
{ st.burstActive = false; return; }
|
|
781
|
+
st.burstCount++;
|
|
782
|
+
scheduleRun(0, n => {
|
|
783
|
+
if (!n && !st.pending.length) { st.burstActive = false; return; }
|
|
784
|
+
setTimeout(step, n > 0 ? 120 : 220);
|
|
1077
785
|
});
|
|
1078
786
|
};
|
|
1079
787
|
step();
|
|
1080
788
|
}
|
|
1081
789
|
|
|
1082
|
-
// ─── Lifecycle
|
|
790
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
791
|
+
|
|
1083
792
|
function cleanup() {
|
|
1084
793
|
blockedUntil = now() + 1200;
|
|
1085
|
-
try { document.querySelectorAll('.' + WRAP_CLASS).forEach(
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
794
|
+
try { document.querySelectorAll('.' + WRAP_CLASS).forEach(dropWrap); } catch (e) {}
|
|
795
|
+
st.cfg = null;
|
|
796
|
+
st.pools = { topic: [], post: [], category: [] };
|
|
797
|
+
st.cursors = { topic: 0, post: 0, category: 0 };
|
|
798
|
+
st.lastShow.clear();
|
|
799
|
+
st.inflight = 0;
|
|
800
|
+
st.pending = []; st.pendingSet.clear();
|
|
801
|
+
st.heroDone = false;
|
|
802
|
+
// IO and domObs kept alive intentionally.
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function shouldReact(mutations) {
|
|
1096
806
|
for (const m of mutations) {
|
|
1097
807
|
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
1098
808
|
for (const n of m.addedNodes) {
|
|
1099
809
|
if (!n || n.nodeType !== 1) continue;
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
el.matches(SELECTORS.postItem) ||
|
|
1104
|
-
el.matches(SELECTORS.topicItem) ||
|
|
1105
|
-
el.matches(SELECTORS.categoryItem)
|
|
1106
|
-
)) ||
|
|
1107
|
-
(el.querySelector && (
|
|
1108
|
-
el.querySelector(SELECTORS.postItem) ||
|
|
1109
|
-
el.querySelector(SELECTORS.topicItem) ||
|
|
1110
|
-
el.querySelector(SELECTORS.categoryItem)
|
|
1111
|
-
))
|
|
1112
|
-
) return true;
|
|
810
|
+
if ((n.matches && (n.matches(SEL.post) || n.matches(SEL.topic) || n.matches(SEL.category))) ||
|
|
811
|
+
(n.querySelector && (n.querySelector(SEL.post) || n.querySelector(SEL.topic) || n.querySelector(SEL.category))))
|
|
812
|
+
return true;
|
|
1113
813
|
}
|
|
1114
814
|
}
|
|
1115
815
|
return false;
|
|
1116
816
|
}
|
|
1117
817
|
|
|
1118
|
-
function
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1122
|
-
if (
|
|
1123
|
-
requestBurst();
|
|
818
|
+
function ensureDomObs() {
|
|
819
|
+
if (st.domObs) return;
|
|
820
|
+
st.domObs = new MutationObserver(muts => {
|
|
821
|
+
if (st.internalMut > 0 || isBlocked()) return;
|
|
822
|
+
if (shouldReact(muts)) burst();
|
|
1124
823
|
});
|
|
1125
|
-
try {
|
|
824
|
+
try { st.domObs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
1126
825
|
}
|
|
1127
826
|
|
|
1128
|
-
// ─── NodeBB
|
|
827
|
+
// ─── NodeBB event bindings ───────────────────────────────────────────────────
|
|
828
|
+
|
|
1129
829
|
function bindNodeBB() {
|
|
1130
830
|
if (!$) return;
|
|
1131
|
-
$(window).off('.
|
|
1132
|
-
$(window).on('action:ajaxify.start.
|
|
1133
|
-
$(window).on('action:ajaxify.end.
|
|
1134
|
-
|
|
831
|
+
$(window).off('.ezoicNbb');
|
|
832
|
+
$(window).on('action:ajaxify.start.ezoicNbb', () => cleanup());
|
|
833
|
+
$(window).on('action:ajaxify.end.ezoicNbb', () => {
|
|
834
|
+
st.pageKey = getPageKey();
|
|
1135
835
|
blockedUntil = 0;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
836
|
+
muteConsole(); warmNetwork(); patchShowAds();
|
|
837
|
+
globalGapFix(); ensureIO(); ensureDomObs();
|
|
838
|
+
heroAd().catch(() => {});
|
|
839
|
+
burst();
|
|
1140
840
|
setTimeout(schedulePileFix, 80);
|
|
1141
841
|
setTimeout(schedulePileFix, 500);
|
|
1142
842
|
});
|
|
1143
|
-
$(window).on('action:ajaxify.contentLoaded.
|
|
1144
|
-
if (!isBlocked()) requestBurst();
|
|
1145
|
-
});
|
|
843
|
+
$(window).on('action:ajaxify.contentLoaded.ezoicNbb', () => { if (!isBlocked()) burst(); });
|
|
1146
844
|
$(window).on([
|
|
1147
845
|
'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded',
|
|
1148
846
|
'action:category.loaded', 'action:topic.loaded', 'action:infiniteScroll.loaded',
|
|
1149
|
-
].map(
|
|
1150
|
-
if (!isBlocked()) {
|
|
1151
|
-
requestBurst();
|
|
1152
|
-
setTimeout(schedulePileFix, 80);
|
|
1153
|
-
setTimeout(schedulePileFix, 500);
|
|
1154
|
-
}
|
|
847
|
+
].map(e => e + '.ezoicNbb').join(' '), () => {
|
|
848
|
+
if (!isBlocked()) { burst(); setTimeout(schedulePileFix, 80); setTimeout(schedulePileFix, 500); }
|
|
1155
849
|
});
|
|
1156
850
|
try {
|
|
1157
|
-
require(['hooks'],
|
|
1158
|
-
if (
|
|
1159
|
-
[
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
});
|
|
851
|
+
require(['hooks'], hooks => {
|
|
852
|
+
if (typeof hooks.on !== 'function') return;
|
|
853
|
+
for (const ev of [
|
|
854
|
+
'action:ajaxify.end', 'action:ajaxify.contentLoaded',
|
|
855
|
+
'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded',
|
|
856
|
+
'action:category.loaded', 'action:topic.loaded', 'action:infiniteScroll.loaded',
|
|
857
|
+
]) try { hooks.on(ev, () => { if (!isBlocked()) burst(); }); } catch (e) {}
|
|
1164
858
|
});
|
|
1165
859
|
} catch (e) {}
|
|
1166
860
|
}
|
|
@@ -1168,33 +862,35 @@
|
|
|
1168
862
|
function bindScroll() {
|
|
1169
863
|
let ticking = false;
|
|
1170
864
|
window.addEventListener('scroll', () => {
|
|
865
|
+
// Fast-scroll boost: widen IO margins.
|
|
1171
866
|
try {
|
|
1172
|
-
const t = now(), y = window.scrollY ||
|
|
1173
|
-
if (
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (!wasBoosted) ensurePreloadObserver();
|
|
867
|
+
const t = now(), y = window.scrollY || 0;
|
|
868
|
+
if (st.lastScrollTs) {
|
|
869
|
+
const speed = Math.abs(y - st.lastScrollY) / (t - st.lastScrollTs);
|
|
870
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
871
|
+
const was = isBoosted();
|
|
872
|
+
st.boostUntil = Math.max(st.boostUntil, t + BOOST_DURATION_MS);
|
|
873
|
+
if (!was) ensureIO();
|
|
1180
874
|
}
|
|
1181
875
|
}
|
|
1182
|
-
|
|
876
|
+
st.lastScrollY = y; st.lastScrollTs = t;
|
|
1183
877
|
} catch (e) {}
|
|
878
|
+
|
|
1184
879
|
if (ticking) return;
|
|
1185
880
|
ticking = true;
|
|
1186
|
-
requestAnimationFrame(() => { ticking = false;
|
|
881
|
+
requestAnimationFrame(() => { ticking = false; burst(); });
|
|
1187
882
|
}, { passive: true });
|
|
1188
883
|
}
|
|
1189
884
|
|
|
1190
885
|
// ─── Boot ────────────────────────────────────────────────────────────────────
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
886
|
+
|
|
887
|
+
st.pageKey = getPageKey();
|
|
888
|
+
muteConsole(); warmNetwork(); patchShowAds();
|
|
889
|
+
ensureIO(); ensureDomObs();
|
|
1194
890
|
bindNodeBB(); bindScroll();
|
|
1195
891
|
blockedUntil = 0;
|
|
1196
|
-
|
|
1197
|
-
|
|
892
|
+
heroAd().catch(() => {});
|
|
893
|
+
burst();
|
|
1198
894
|
schedulePileFix();
|
|
1199
895
|
|
|
1200
896
|
})();
|