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