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