nodebb-plugin-ezoic-infinite 1.8.51 → 1.8.53
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/library.js +8 -21
- package/package.json +1 -1
- package/public/client.js +296 -564
package/public/client.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js v2.
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.3.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* - showAds batching: microtask-based flush instead of setTimeout
|
|
11
|
-
* - Warm network: runs once per session, not per navigation
|
|
12
|
-
* - State machine: clear lifecycle for placeholders (idle → observed → queued → shown → recycled)
|
|
4
|
+
* Architecture based on battle-tested v50, with targeted improvements:
|
|
5
|
+
* - Ezoic API: showAds() + destroyPlaceholders() per official docs
|
|
6
|
+
* - wrapsByClass Set for O(1) recycle lookup (no querySelectorAll)
|
|
7
|
+
* - MutationObserver: ad fill detection + virtualization re-observe
|
|
8
|
+
* - Conservative empty check (30s/60s, GPT-aware)
|
|
9
|
+
* - aria-hidden + TCF locator protection
|
|
13
10
|
*/
|
|
14
11
|
(function nbbEzoicInfinite() {
|
|
15
12
|
'use strict';
|
|
@@ -19,7 +16,6 @@
|
|
|
19
16
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
20
17
|
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
18
|
|
|
22
|
-
// Data attributes
|
|
23
19
|
const ATTR = {
|
|
24
20
|
ANCHOR: 'data-ezoic-anchor',
|
|
25
21
|
WRAPID: 'data-ezoic-wrapid',
|
|
@@ -27,49 +23,39 @@
|
|
|
27
23
|
SHOWN: 'data-ezoic-shown',
|
|
28
24
|
};
|
|
29
25
|
|
|
30
|
-
// Timing
|
|
31
26
|
const TIMING = {
|
|
32
|
-
|
|
27
|
+
EMPTY_CHECK_MS_1: 30_000,
|
|
28
|
+
EMPTY_CHECK_MS_2: 60_000,
|
|
33
29
|
MIN_PRUNE_AGE_MS: 8_000,
|
|
30
|
+
RECYCLE_MIN_AGE_MS: 5_000,
|
|
34
31
|
SHOW_THROTTLE_MS: 900,
|
|
35
32
|
BURST_COOLDOWN_MS: 200,
|
|
36
33
|
BLOCK_DURATION_MS: 1_500,
|
|
37
34
|
SHOW_TIMEOUT_MS: 7_000,
|
|
38
35
|
SHOW_RELEASE_MS: 700,
|
|
39
|
-
BATCH_FLUSH_MS: 80,
|
|
40
36
|
RECYCLE_DELAY_MS: 450,
|
|
41
|
-
|
|
42
37
|
};
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
BATCH_SIZE: 3,
|
|
49
|
-
MAX_BURST_STEPS: 8,
|
|
50
|
-
BURST_WINDOW_MS: 2_000,
|
|
51
|
-
};
|
|
39
|
+
const MAX_INSERTS_RUN = 6;
|
|
40
|
+
const MAX_INFLIGHT = 4;
|
|
41
|
+
const MAX_BURST_STEPS = 8;
|
|
42
|
+
const BURST_WINDOW_MS = 2_000;
|
|
52
43
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
MOBILE: '3500px 0px 3500px 0px',
|
|
56
|
-
};
|
|
44
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
45
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
57
46
|
|
|
58
|
-
// Selectors
|
|
59
47
|
const SEL = {
|
|
60
48
|
post: '[component="post"][data-pid]',
|
|
61
49
|
topic: 'li[component="category/topic"]',
|
|
62
50
|
category: 'li[component="categories/category"]',
|
|
63
51
|
};
|
|
64
52
|
|
|
65
|
-
// Kind configuration table — single source of truth per ad type
|
|
66
53
|
const KIND = {
|
|
67
54
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
68
55
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
69
56
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
70
57
|
};
|
|
71
58
|
|
|
72
|
-
// Selector for detecting filled ad slots
|
|
73
59
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
74
60
|
|
|
75
61
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
@@ -77,106 +63,79 @@
|
|
|
77
63
|
const now = () => Date.now();
|
|
78
64
|
const isMobile = () => window.innerWidth < 768;
|
|
79
65
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
80
|
-
|
|
81
|
-
function isFilled(node) {
|
|
82
|
-
return node?.querySelector?.(FILL_SEL) != null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isPlaceholderUsed(ph) {
|
|
86
|
-
if (!ph?.isConnected) return false;
|
|
87
|
-
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
88
|
-
}
|
|
66
|
+
const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
|
|
89
67
|
|
|
90
68
|
function parseIds(raw) {
|
|
91
|
-
const out = [];
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
96
|
-
seen.add(n);
|
|
97
|
-
out.push(n);
|
|
98
|
-
}
|
|
69
|
+
const out = [], seen = new Set();
|
|
70
|
+
for (const v of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
71
|
+
const n = parseInt(v, 10);
|
|
72
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
99
73
|
}
|
|
100
74
|
return out;
|
|
101
75
|
}
|
|
102
76
|
|
|
103
77
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
104
78
|
|
|
105
|
-
const
|
|
106
|
-
// Page context
|
|
79
|
+
const S = {
|
|
107
80
|
pageKey: null,
|
|
108
81
|
kind: null,
|
|
109
82
|
cfg: null,
|
|
110
83
|
|
|
111
|
-
// Pools
|
|
112
84
|
poolsReady: false,
|
|
113
85
|
pools: { topics: [], posts: [], categories: [] },
|
|
114
86
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
115
87
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
119
|
-
lastShow: new Map(), // id → timestamp
|
|
88
|
+
mountedIds: new Set(),
|
|
89
|
+
lastShow: new Map(),
|
|
120
90
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
91
|
+
wrapByKey: new Map(), // anchorKey → wrap element
|
|
92
|
+
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
124
93
|
|
|
125
|
-
// Observers
|
|
126
94
|
io: null,
|
|
127
95
|
domObs: null,
|
|
128
96
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
blockedUntil: 0,
|
|
97
|
+
mutGuard: 0,
|
|
98
|
+
blockedUntil: 0,
|
|
132
99
|
|
|
133
|
-
// Show queue
|
|
134
100
|
inflight: 0,
|
|
135
101
|
pending: [],
|
|
136
102
|
pendingSet: new Set(),
|
|
137
103
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
burstActive: false,
|
|
104
|
+
runQueued: false,
|
|
105
|
+
burstActive: false,
|
|
141
106
|
burstDeadline: 0,
|
|
142
|
-
burstCount:
|
|
143
|
-
lastBurstTs:
|
|
144
|
-
firstShown: false,
|
|
107
|
+
burstCount: 0,
|
|
108
|
+
lastBurstTs: 0,
|
|
145
109
|
};
|
|
146
110
|
|
|
147
|
-
const isBlocked = () => now() <
|
|
111
|
+
const isBlocked = () => now() < S.blockedUntil;
|
|
148
112
|
|
|
149
113
|
function mutate(fn) {
|
|
150
|
-
|
|
151
|
-
try { fn(); } finally {
|
|
114
|
+
S.mutGuard++;
|
|
115
|
+
try { fn(); } finally { S.mutGuard--; }
|
|
152
116
|
}
|
|
153
117
|
|
|
154
118
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
155
119
|
|
|
156
120
|
async function fetchConfig() {
|
|
157
|
-
if (
|
|
158
|
-
// Prefer inline config injected by server (zero latency)
|
|
121
|
+
if (S.cfg) return S.cfg;
|
|
159
122
|
try {
|
|
160
123
|
const inline = window.__nbbEzoicCfg;
|
|
161
|
-
if (inline && typeof inline === 'object') {
|
|
162
|
-
state.cfg = inline;
|
|
163
|
-
return state.cfg;
|
|
164
|
-
}
|
|
124
|
+
if (inline && typeof inline === 'object') { S.cfg = inline; return S.cfg; }
|
|
165
125
|
} catch (_) {}
|
|
166
|
-
// Fallback to API
|
|
167
126
|
try {
|
|
168
127
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
169
|
-
if (r.ok)
|
|
128
|
+
if (r.ok) S.cfg = await r.json();
|
|
170
129
|
} catch (_) {}
|
|
171
|
-
return
|
|
130
|
+
return S.cfg;
|
|
172
131
|
}
|
|
173
132
|
|
|
174
133
|
function initPools(cfg) {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
134
|
+
if (S.poolsReady) return;
|
|
135
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
136
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
137
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
138
|
+
S.poolsReady = true;
|
|
180
139
|
}
|
|
181
140
|
|
|
182
141
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
@@ -195,7 +154,6 @@
|
|
|
195
154
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
196
155
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
197
156
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
198
|
-
// DOM fallback
|
|
199
157
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
200
158
|
if (document.querySelector(SEL.post)) return 'topic';
|
|
201
159
|
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
@@ -203,7 +161,7 @@
|
|
|
203
161
|
}
|
|
204
162
|
|
|
205
163
|
function getKind() {
|
|
206
|
-
return
|
|
164
|
+
return S.kind || (S.kind = detectKind());
|
|
207
165
|
}
|
|
208
166
|
|
|
209
167
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
@@ -215,7 +173,6 @@
|
|
|
215
173
|
const el = all[i];
|
|
216
174
|
if (!el.isConnected) continue;
|
|
217
175
|
if (!el.querySelector('[component="post/content"]')) continue;
|
|
218
|
-
// Skip nested quotes / parent posts
|
|
219
176
|
const parent = el.parentElement?.closest(SEL.post);
|
|
220
177
|
if (parent && parent !== el) continue;
|
|
221
178
|
if (el.getAttribute('component') === 'post/parent') continue;
|
|
@@ -224,8 +181,8 @@
|
|
|
224
181
|
return out;
|
|
225
182
|
}
|
|
226
183
|
|
|
227
|
-
|
|
228
|
-
|
|
184
|
+
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
185
|
+
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
229
186
|
|
|
230
187
|
// ── Anchor keys & wrap registry ────────────────────────────────────────────
|
|
231
188
|
|
|
@@ -235,7 +192,6 @@
|
|
|
235
192
|
const v = el.getAttribute(attr);
|
|
236
193
|
if (v != null && v !== '') return v;
|
|
237
194
|
}
|
|
238
|
-
// Positional fallback
|
|
239
195
|
const children = el.parentElement?.children;
|
|
240
196
|
if (!children) return 'i0';
|
|
241
197
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -247,86 +203,50 @@
|
|
|
247
203
|
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
248
204
|
|
|
249
205
|
function findWrap(key) {
|
|
250
|
-
const w =
|
|
206
|
+
const w = S.wrapByKey.get(key);
|
|
251
207
|
return w?.isConnected ? w : null;
|
|
252
208
|
}
|
|
253
209
|
|
|
254
210
|
function getWrapSet(klass) {
|
|
255
|
-
let set =
|
|
256
|
-
if (!set) { set = new Set();
|
|
211
|
+
let set = S.wrapsByClass.get(klass);
|
|
212
|
+
if (!set) { set = new Set(); S.wrapsByClass.set(klass, set); }
|
|
257
213
|
return set;
|
|
258
214
|
}
|
|
259
215
|
|
|
260
|
-
// ──
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// (especially posts in topics). If a wrap is removed outside of our own
|
|
264
|
-
// dropWrap(), we must free its placeholder id; otherwise the pool appears
|
|
265
|
-
// exhausted and ads won't re-insert when the user scrolls back up.
|
|
216
|
+
// ── GC disconnected wraps ──────────────────────────────────────────────────
|
|
217
|
+
// NodeBB virtualizes posts off-viewport. The MutationObserver catches most
|
|
218
|
+
// removals, but this is a safety net for edge cases.
|
|
266
219
|
|
|
267
220
|
function gcDisconnectedWraps() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
221
|
+
for (const [key, w] of S.wrapByKey) {
|
|
222
|
+
if (!w?.isConnected) S.wrapByKey.delete(key);
|
|
271
223
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
275
|
-
for (const w of Array.from(set)) {
|
|
224
|
+
for (const [klass, set] of S.wrapsByClass) {
|
|
225
|
+
for (const w of set) {
|
|
276
226
|
if (w?.isConnected) continue;
|
|
277
227
|
set.delete(w);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
state.phState.delete(id);
|
|
283
|
-
state.lastShow.delete(id);
|
|
284
|
-
}
|
|
285
|
-
} catch (_) {}
|
|
286
|
-
}
|
|
287
|
-
if (!set.size) state.wrapsByClass.delete(klass);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 3) Authoritative rebuild of mountedIds from the live DOM
|
|
291
|
-
try {
|
|
292
|
-
const live = new Set();
|
|
293
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
294
|
-
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
295
|
-
if (id > 0) live.add(id);
|
|
296
|
-
}
|
|
297
|
-
// Drop any ids that are no longer live
|
|
298
|
-
for (const id of Array.from(state.mountedIds)) {
|
|
299
|
-
if (!live.has(id)) {
|
|
300
|
-
state.mountedIds.delete(id);
|
|
301
|
-
state.phState.delete(id);
|
|
302
|
-
state.lastShow.delete(id);
|
|
228
|
+
const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
|
|
229
|
+
if (Number.isFinite(id)) {
|
|
230
|
+
S.mountedIds.delete(id);
|
|
231
|
+
S.lastShow.delete(id);
|
|
303
232
|
}
|
|
304
233
|
}
|
|
305
|
-
|
|
234
|
+
if (!set.size) S.wrapsByClass.delete(klass);
|
|
235
|
+
}
|
|
306
236
|
}
|
|
307
237
|
|
|
308
|
-
// ── Wrap lifecycle
|
|
238
|
+
// ── Wrap lifecycle ─────────────────────────────────────────────────────────
|
|
309
239
|
|
|
310
|
-
/**
|
|
311
|
-
* Check if a wrap element still has its corresponding anchor in the DOM.
|
|
312
|
-
* Uses O(1) registry lookup first, then sibling scan, then global querySelector.
|
|
313
|
-
*/
|
|
314
240
|
function wrapIsLive(wrap) {
|
|
315
241
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
316
242
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
317
243
|
if (!key) return false;
|
|
318
|
-
|
|
319
|
-
// Fast path: registry match
|
|
320
|
-
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
321
|
-
|
|
322
|
-
// Parse key
|
|
244
|
+
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
323
245
|
const colonIdx = key.indexOf(':');
|
|
324
246
|
const klass = key.slice(0, colonIdx);
|
|
325
247
|
const anchorId = key.slice(colonIdx + 1);
|
|
326
248
|
const cfg = KIND[klass];
|
|
327
249
|
if (!cfg) return false;
|
|
328
|
-
|
|
329
|
-
// Sibling scan (cheap for adjacent anchors)
|
|
330
250
|
const parent = wrap.parentElement;
|
|
331
251
|
if (parent) {
|
|
332
252
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
@@ -336,8 +256,6 @@
|
|
|
336
256
|
}
|
|
337
257
|
}
|
|
338
258
|
}
|
|
339
|
-
|
|
340
|
-
// Global fallback (expensive, rare)
|
|
341
259
|
try {
|
|
342
260
|
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
343
261
|
} catch (_) { return false; }
|
|
@@ -354,57 +272,51 @@
|
|
|
354
272
|
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
355
273
|
if (!ph || !isFilled(ph)) return false;
|
|
356
274
|
wrap.classList.remove('is-empty');
|
|
357
|
-
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
358
|
-
if (id > 0) state.phState.set(id, 'shown');
|
|
359
275
|
return true;
|
|
360
276
|
}
|
|
361
277
|
|
|
362
278
|
function scheduleUncollapseChecks(wrap) {
|
|
363
279
|
if (!wrap) return;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
setTimeout(() => {
|
|
367
|
-
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
368
|
-
}, ms);
|
|
280
|
+
for (const ms of [500, 3000, 10000]) {
|
|
281
|
+
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
369
282
|
}
|
|
370
283
|
}
|
|
371
284
|
|
|
372
285
|
// ── Pool management ────────────────────────────────────────────────────────
|
|
373
286
|
|
|
374
287
|
function pickId(poolKey) {
|
|
375
|
-
const pool =
|
|
288
|
+
const pool = S.pools[poolKey];
|
|
376
289
|
if (!pool.length) return null;
|
|
377
290
|
for (let t = 0; t < pool.length; t++) {
|
|
378
|
-
const idx =
|
|
379
|
-
|
|
291
|
+
const idx = S.cursors[poolKey] % pool.length;
|
|
292
|
+
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
380
293
|
const id = pool[idx];
|
|
381
|
-
if (!
|
|
294
|
+
if (!S.mountedIds.has(id)) return id;
|
|
382
295
|
}
|
|
383
296
|
return null;
|
|
384
297
|
}
|
|
385
298
|
|
|
386
299
|
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
300
|
+
// Per Ezoic docs: destroyPlaceholders(id) → remove HTML → fresh placeholder → showAds(id)
|
|
387
301
|
|
|
388
|
-
/**
|
|
389
|
-
* When pool is exhausted, recycle a wrap far above the viewport.
|
|
390
|
-
* Sequence: destroy → delay → re-observe → enqueueShow
|
|
391
|
-
*/
|
|
392
302
|
function recycleWrap(klass, targetEl, newKey) {
|
|
393
303
|
const ez = window.ezstandalone;
|
|
394
304
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
395
|
-
typeof ez?.
|
|
396
|
-
typeof ez?.displayMore !== 'function') return null;
|
|
305
|
+
typeof ez?.showAds !== 'function') return null;
|
|
397
306
|
|
|
398
307
|
const vh = window.innerHeight || 800;
|
|
399
|
-
const threshold = -vh;
|
|
308
|
+
const threshold = -(3 * vh);
|
|
309
|
+
const t = now();
|
|
400
310
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
401
311
|
let bestFull = null, bestFullY = Infinity;
|
|
402
312
|
|
|
403
|
-
const wraps =
|
|
313
|
+
const wraps = S.wrapsByClass.get(klass);
|
|
404
314
|
if (!wraps) return null;
|
|
405
315
|
|
|
406
316
|
for (const wrap of wraps) {
|
|
407
317
|
try {
|
|
318
|
+
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
319
|
+
if (t - created < TIMING.RECYCLE_MIN_AGE_MS) continue;
|
|
408
320
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
409
321
|
if (bottom > threshold) continue;
|
|
410
322
|
if (!isFilled(wrap)) {
|
|
@@ -420,52 +332,44 @@
|
|
|
420
332
|
|
|
421
333
|
const id = parseInt(best.getAttribute(ATTR.WRAPID), 10);
|
|
422
334
|
if (!Number.isFinite(id)) return null;
|
|
423
|
-
|
|
424
335
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
425
336
|
|
|
426
|
-
// Unobserve before moving
|
|
337
|
+
// Unobserve before moving
|
|
427
338
|
try {
|
|
428
339
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
429
|
-
if (ph)
|
|
340
|
+
if (ph) S.io?.unobserve(ph);
|
|
430
341
|
} catch (_) {}
|
|
431
342
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
best.setAttribute(ATTR.CREATED, String(now()));
|
|
436
|
-
best.setAttribute(ATTR.SHOWN, '0');
|
|
437
|
-
best.classList.remove('is-empty');
|
|
438
|
-
best.replaceChildren();
|
|
439
|
-
|
|
440
|
-
const fresh = document.createElement('div');
|
|
441
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
442
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
443
|
-
fresh.style.minHeight = '1px';
|
|
444
|
-
best.appendChild(fresh);
|
|
445
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Update registry
|
|
449
|
-
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
450
|
-
state.wrapByKey.set(newKey, best);
|
|
343
|
+
// Ezoic recycle: destroy → fresh DOM → showAds
|
|
344
|
+
const doRecycle = () => {
|
|
345
|
+
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
451
346
|
|
|
452
|
-
// Ezoic recycle sequence
|
|
453
|
-
const doDestroy = () => {
|
|
454
|
-
state.phState.set(id, 'destroyed');
|
|
455
|
-
try { ez.destroyPlaceholders(id); } catch (_) {
|
|
456
|
-
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
457
|
-
}
|
|
458
347
|
setTimeout(() => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
348
|
+
mutate(() => {
|
|
349
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
350
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
351
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
352
|
+
best.classList.remove('is-empty');
|
|
353
|
+
best.replaceChildren();
|
|
354
|
+
|
|
355
|
+
const fresh = document.createElement('div');
|
|
356
|
+
fresh.id = `${PH_PREFIX}${id}`;
|
|
357
|
+
fresh.setAttribute('data-ezoic-id', String(id));
|
|
358
|
+
best.appendChild(fresh);
|
|
359
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
363
|
+
S.wrapByKey.set(newKey, best);
|
|
364
|
+
|
|
365
|
+
setTimeout(() => {
|
|
366
|
+
observePh(id);
|
|
367
|
+
enqueueShow(id);
|
|
368
|
+
}, TIMING.RECYCLE_DELAY_MS);
|
|
462
369
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
463
370
|
};
|
|
464
371
|
|
|
465
|
-
try {
|
|
466
|
-
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy();
|
|
467
|
-
} catch (_) {}
|
|
468
|
-
|
|
372
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle(); } catch (_) {}
|
|
469
373
|
return { id, wrap: best };
|
|
470
374
|
}
|
|
471
375
|
|
|
@@ -479,11 +383,9 @@
|
|
|
479
383
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
480
384
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
481
385
|
w.style.cssText = 'width:100%;display:block';
|
|
482
|
-
|
|
483
386
|
const ph = document.createElement('div');
|
|
484
387
|
ph.id = `${PH_PREFIX}${id}`;
|
|
485
388
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
486
|
-
ph.style.minHeight = '1px';
|
|
487
389
|
w.appendChild(ph);
|
|
488
390
|
return w;
|
|
489
391
|
}
|
|
@@ -491,16 +393,12 @@
|
|
|
491
393
|
function insertAfter(el, id, klass, key) {
|
|
492
394
|
if (!el?.insertAdjacentElement) return null;
|
|
493
395
|
if (findWrap(key)) return null;
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
497
|
-
if (existing?.isConnected) return null;
|
|
498
|
-
|
|
396
|
+
if (S.mountedIds.has(id)) return null;
|
|
397
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
499
398
|
const w = makeWrap(id, klass, key);
|
|
500
399
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
state.wrapByKey.set(key, w);
|
|
400
|
+
S.mountedIds.add(id);
|
|
401
|
+
S.wrapByKey.set(key, w);
|
|
504
402
|
getWrapSet(klass).add(w);
|
|
505
403
|
return w;
|
|
506
404
|
}
|
|
@@ -508,21 +406,17 @@
|
|
|
508
406
|
function dropWrap(w) {
|
|
509
407
|
try {
|
|
510
408
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
511
|
-
if (ph instanceof Element)
|
|
512
|
-
|
|
409
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
513
410
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
514
411
|
if (Number.isFinite(id)) {
|
|
515
|
-
|
|
516
|
-
|
|
412
|
+
S.mountedIds.delete(id);
|
|
413
|
+
S.lastShow.delete(id);
|
|
517
414
|
}
|
|
518
|
-
|
|
519
415
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
520
|
-
if (key &&
|
|
521
|
-
|
|
522
|
-
// Find the kind class to unregister
|
|
416
|
+
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
523
417
|
for (const cls of w.classList) {
|
|
524
418
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
525
|
-
|
|
419
|
+
S.wrapsByClass.get(cls)?.delete(w);
|
|
526
420
|
break;
|
|
527
421
|
}
|
|
528
422
|
}
|
|
@@ -531,33 +425,25 @@
|
|
|
531
425
|
}
|
|
532
426
|
|
|
533
427
|
// ── Prune (category topic lists only) ──────────────────────────────────────
|
|
534
|
-
//
|
|
535
|
-
// Safe for category topic lists: NodeBB does NOT virtualize topics in categories.
|
|
536
|
-
// NOT safe for posts: NodeBB virtualizes posts off-viewport.
|
|
537
428
|
|
|
538
429
|
function pruneOrphansBetween() {
|
|
539
430
|
const klass = 'ezoic-ad-between';
|
|
540
431
|
const cfg = KIND[klass];
|
|
541
|
-
const wraps =
|
|
432
|
+
const wraps = S.wrapsByClass.get(klass);
|
|
542
433
|
if (!wraps?.size) return;
|
|
543
434
|
|
|
544
|
-
// Build set of live anchor IDs
|
|
545
435
|
const liveAnchors = new Set();
|
|
546
436
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
547
437
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
548
438
|
if (v) liveAnchors.add(v);
|
|
549
439
|
}
|
|
550
|
-
|
|
551
440
|
const t = now();
|
|
552
441
|
for (const w of wraps) {
|
|
553
442
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
554
443
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
555
|
-
|
|
556
444
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
557
445
|
const sid = key.slice(klass.length + 1);
|
|
558
|
-
if (!sid || !liveAnchors.has(sid))
|
|
559
|
-
mutate(() => dropWrap(w));
|
|
560
|
-
}
|
|
446
|
+
if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
561
447
|
}
|
|
562
448
|
}
|
|
563
449
|
|
|
@@ -569,7 +455,6 @@
|
|
|
569
455
|
const v = el.getAttribute(attr);
|
|
570
456
|
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
571
457
|
}
|
|
572
|
-
// Positional fallback
|
|
573
458
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
574
459
|
let i = 0;
|
|
575
460
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -582,29 +467,21 @@
|
|
|
582
467
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
583
468
|
if (!items.length) return 0;
|
|
584
469
|
let inserted = 0;
|
|
585
|
-
|
|
586
470
|
for (const el of items) {
|
|
587
|
-
if (inserted >=
|
|
471
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
588
472
|
if (!el?.isConnected) continue;
|
|
589
|
-
|
|
590
473
|
const ord = ordinal(klass, el);
|
|
591
474
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
592
475
|
if (adjacentWrap(el)) continue;
|
|
593
|
-
|
|
594
476
|
const key = anchorKey(klass, el);
|
|
595
477
|
if (findWrap(key)) continue;
|
|
596
|
-
|
|
597
478
|
const id = pickId(poolKey);
|
|
598
479
|
if (id) {
|
|
599
480
|
const w = insertAfter(el, id, klass, key);
|
|
600
|
-
if (w) {
|
|
601
|
-
observePlaceholder(id);
|
|
602
|
-
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
603
|
-
inserted++;
|
|
604
|
-
}
|
|
481
|
+
if (w) { observePh(id); inserted++; }
|
|
605
482
|
} else {
|
|
606
483
|
const recycled = recycleWrap(klass, el, key);
|
|
607
|
-
if (!recycled) break;
|
|
484
|
+
if (!recycled) break;
|
|
608
485
|
inserted++;
|
|
609
486
|
}
|
|
610
487
|
}
|
|
@@ -614,69 +491,58 @@
|
|
|
614
491
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
615
492
|
|
|
616
493
|
function getIO() {
|
|
617
|
-
if (
|
|
494
|
+
if (S.io) return S.io;
|
|
618
495
|
try {
|
|
619
|
-
|
|
620
|
-
for (const
|
|
621
|
-
if (!
|
|
622
|
-
if (
|
|
623
|
-
const id = parseInt(
|
|
496
|
+
S.io = new IntersectionObserver(entries => {
|
|
497
|
+
for (const e of entries) {
|
|
498
|
+
if (!e.isIntersecting) continue;
|
|
499
|
+
if (e.target instanceof Element) S.io?.unobserve(e.target);
|
|
500
|
+
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
624
501
|
if (id > 0) enqueueShow(id);
|
|
625
502
|
}
|
|
626
503
|
}, {
|
|
627
504
|
root: null,
|
|
628
|
-
rootMargin: isMobile() ?
|
|
505
|
+
rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP,
|
|
629
506
|
threshold: 0,
|
|
630
507
|
});
|
|
631
|
-
} catch (_) {
|
|
632
|
-
return
|
|
508
|
+
} catch (_) { S.io = null; }
|
|
509
|
+
return S.io;
|
|
633
510
|
}
|
|
634
511
|
|
|
635
|
-
function
|
|
512
|
+
function observePh(id) {
|
|
636
513
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
637
|
-
if (ph?.isConnected) {
|
|
638
|
-
try { getIO()?.observe(ph); } catch (_) {}
|
|
639
|
-
}
|
|
514
|
+
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
640
515
|
}
|
|
641
516
|
|
|
642
517
|
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
643
518
|
|
|
644
519
|
function enqueueShow(id) {
|
|
645
520
|
if (!id || isBlocked()) return;
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
651
|
-
if (!state.pendingSet.has(id)) {
|
|
652
|
-
state.pending.push(id);
|
|
653
|
-
state.pendingSet.add(id);
|
|
654
|
-
state.phState.set(id, 'show-queued');
|
|
655
|
-
}
|
|
521
|
+
if (now() - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
522
|
+
if (S.inflight >= MAX_INFLIGHT) {
|
|
523
|
+
if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
|
|
656
524
|
return;
|
|
657
525
|
}
|
|
658
|
-
state.phState.set(id, 'show-queued');
|
|
659
526
|
startShow(id);
|
|
660
527
|
}
|
|
661
528
|
|
|
662
529
|
function drainQueue() {
|
|
663
530
|
if (isBlocked()) return;
|
|
664
|
-
while (
|
|
665
|
-
const id =
|
|
666
|
-
|
|
531
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
532
|
+
const id = S.pending.shift();
|
|
533
|
+
S.pendingSet.delete(id);
|
|
667
534
|
startShow(id);
|
|
668
535
|
}
|
|
669
536
|
}
|
|
670
537
|
|
|
671
538
|
function startShow(id) {
|
|
672
539
|
if (!id || isBlocked()) return;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
let released = false;
|
|
540
|
+
S.inflight++;
|
|
541
|
+
let done = false;
|
|
676
542
|
const release = () => {
|
|
677
|
-
if (
|
|
678
|
-
|
|
679
|
-
|
|
543
|
+
if (done) return;
|
|
544
|
+
done = true;
|
|
545
|
+
S.inflight = Math.max(0, S.inflight - 1);
|
|
680
546
|
drainQueue();
|
|
681
547
|
};
|
|
682
548
|
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
@@ -684,69 +550,49 @@
|
|
|
684
550
|
requestAnimationFrame(() => {
|
|
685
551
|
try {
|
|
686
552
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
687
|
-
|
|
688
553
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
689
|
-
if (!ph?.isConnected) {
|
|
690
|
-
state.phState.delete(id);
|
|
691
|
-
clearTimeout(timer);
|
|
692
|
-
return release();
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
696
|
-
state.phState.set(id, 'shown');
|
|
697
|
-
clearTimeout(timer);
|
|
698
|
-
return release();
|
|
699
|
-
}
|
|
554
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
700
555
|
|
|
701
556
|
const t = now();
|
|
702
|
-
if (t - (
|
|
703
|
-
|
|
704
|
-
return release();
|
|
705
|
-
}
|
|
706
|
-
state.lastShow.set(id, t);
|
|
557
|
+
if (t - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
558
|
+
S.lastShow.set(id, t);
|
|
707
559
|
|
|
708
|
-
|
|
709
|
-
|
|
560
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
561
|
+
try { wrap?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
710
562
|
|
|
711
563
|
window.ezstandalone = window.ezstandalone || {};
|
|
712
564
|
const ez = window.ezstandalone;
|
|
713
|
-
|
|
714
565
|
const doShow = () => {
|
|
715
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
716
566
|
try { ez.showAds(id); } catch (_) {}
|
|
717
567
|
if (wrap) scheduleUncollapseChecks(wrap);
|
|
718
568
|
scheduleEmptyCheck(id, t);
|
|
719
569
|
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
720
570
|
};
|
|
721
|
-
|
|
722
571
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
723
|
-
} catch (_) {
|
|
724
|
-
clearTimeout(timer);
|
|
725
|
-
release();
|
|
726
|
-
}
|
|
572
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
727
573
|
});
|
|
728
574
|
}
|
|
729
575
|
|
|
730
576
|
function scheduleEmptyCheck(id, showTs) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
577
|
+
for (const delay of [TIMING.EMPTY_CHECK_MS_1, TIMING.EMPTY_CHECK_MS_2]) {
|
|
578
|
+
setTimeout(() => {
|
|
579
|
+
try {
|
|
580
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
581
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
582
|
+
if (!wrap || !ph?.isConnected) return;
|
|
583
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
584
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
585
|
+
// Don't collapse if GPT slot exists (still loading)
|
|
586
|
+
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
587
|
+
if (ph.offsetHeight > 10) return;
|
|
588
|
+
wrap.classList.add('is-empty');
|
|
589
|
+
} catch (_) {}
|
|
590
|
+
}, delay);
|
|
591
|
+
}
|
|
742
592
|
}
|
|
743
593
|
|
|
744
594
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
745
|
-
//
|
|
746
|
-
// Intercepts ez.showAds() to:
|
|
747
|
-
// - block calls during navigation transitions
|
|
748
|
-
// - filter out disconnected placeholders
|
|
749
|
-
// - batch calls for efficiency
|
|
595
|
+
// Matches v50: individual calls, no batching, pass-through no-arg calls.
|
|
750
596
|
|
|
751
597
|
function patchShowAds() {
|
|
752
598
|
const apply = () => {
|
|
@@ -755,52 +601,22 @@
|
|
|
755
601
|
const ez = window.ezstandalone;
|
|
756
602
|
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
757
603
|
window.__nbbEzPatched = true;
|
|
758
|
-
|
|
759
604
|
const orig = ez.showAds.bind(ez);
|
|
760
|
-
const queue = new Set();
|
|
761
|
-
let flushTimer = null;
|
|
762
|
-
|
|
763
|
-
const flush = () => {
|
|
764
|
-
flushTimer = null;
|
|
765
|
-
if (isBlocked() || !queue.size) return;
|
|
766
|
-
|
|
767
|
-
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
768
|
-
queue.clear();
|
|
769
|
-
|
|
770
|
-
const valid = ids.filter(id => {
|
|
771
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
772
|
-
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
773
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
774
|
-
return true;
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
778
|
-
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
779
|
-
try { orig(...chunk); } catch (_) {
|
|
780
|
-
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
};
|
|
784
|
-
|
|
785
605
|
ez.showAds = function (...args) {
|
|
606
|
+
if (args.length === 0) return orig();
|
|
786
607
|
if (isBlocked()) return;
|
|
787
608
|
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
609
|
+
const seen = new Set();
|
|
788
610
|
for (const v of ids) {
|
|
789
611
|
const id = parseInt(v, 10);
|
|
790
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
state.phState.set(id, 'show-queued');
|
|
795
|
-
queue.add(id);
|
|
796
|
-
}
|
|
797
|
-
if (queue.size && !flushTimer) {
|
|
798
|
-
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
612
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
613
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
|
|
614
|
+
seen.add(id);
|
|
615
|
+
try { orig(id); } catch (_) {}
|
|
799
616
|
}
|
|
800
617
|
};
|
|
801
618
|
} catch (_) {}
|
|
802
619
|
};
|
|
803
|
-
|
|
804
620
|
apply();
|
|
805
621
|
if (!window.__nbbEzPatched) {
|
|
806
622
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -812,9 +628,7 @@
|
|
|
812
628
|
|
|
813
629
|
async function runCore() {
|
|
814
630
|
if (isBlocked()) return 0;
|
|
815
|
-
|
|
816
|
-
// Keep internal pools in sync with NodeBB DOM virtualization/removals
|
|
817
|
-
// so ads can re-insert correctly when users scroll back up.
|
|
631
|
+
patchShowAds();
|
|
818
632
|
try { gcDisconnectedWraps(); } catch (_) {}
|
|
819
633
|
|
|
820
634
|
const cfg = await fetchConfig();
|
|
@@ -831,34 +645,26 @@
|
|
|
831
645
|
};
|
|
832
646
|
|
|
833
647
|
if (kind === 'topic') {
|
|
834
|
-
return exec(
|
|
835
|
-
|
|
836
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
837
|
-
);
|
|
648
|
+
return exec('ezoic-ad-message', getPosts,
|
|
649
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
838
650
|
}
|
|
839
|
-
|
|
840
651
|
if (kind === 'categoryTopics') {
|
|
841
652
|
pruneOrphansBetween();
|
|
842
|
-
return exec(
|
|
843
|
-
|
|
844
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
845
|
-
);
|
|
653
|
+
return exec('ezoic-ad-between', getTopics,
|
|
654
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
846
655
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
'ezoic-ad-categories', getCategories,
|
|
850
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
851
|
-
);
|
|
656
|
+
return exec('ezoic-ad-categories', getCategories,
|
|
657
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
852
658
|
}
|
|
853
659
|
|
|
854
660
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
855
661
|
|
|
856
662
|
function scheduleRun(cb) {
|
|
857
|
-
if (
|
|
858
|
-
|
|
663
|
+
if (S.runQueued) return;
|
|
664
|
+
S.runQueued = true;
|
|
859
665
|
requestAnimationFrame(async () => {
|
|
860
|
-
|
|
861
|
-
if (
|
|
666
|
+
S.runQueued = false;
|
|
667
|
+
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
862
668
|
let n = 0;
|
|
863
669
|
try { n = await runCore(); } catch (_) {}
|
|
864
670
|
try { cb?.(n); } catch (_) {}
|
|
@@ -868,23 +674,20 @@
|
|
|
868
674
|
function requestBurst() {
|
|
869
675
|
if (isBlocked()) return;
|
|
870
676
|
const t = now();
|
|
871
|
-
if (t -
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
state.burstCount = 0;
|
|
879
|
-
|
|
677
|
+
if (t - S.lastBurstTs < TIMING.BURST_COOLDOWN_MS) return;
|
|
678
|
+
S.lastBurstTs = t;
|
|
679
|
+
S.pageKey = pageKey();
|
|
680
|
+
S.burstDeadline = t + BURST_WINDOW_MS;
|
|
681
|
+
if (S.burstActive) return;
|
|
682
|
+
S.burstActive = true;
|
|
683
|
+
S.burstCount = 0;
|
|
880
684
|
const step = () => {
|
|
881
|
-
if (pageKey() !==
|
|
882
|
-
|
|
883
|
-
return;
|
|
685
|
+
if (pageKey() !== S.pageKey || isBlocked() || now() > S.burstDeadline || S.burstCount >= MAX_BURST_STEPS) {
|
|
686
|
+
S.burstActive = false; return;
|
|
884
687
|
}
|
|
885
|
-
|
|
688
|
+
S.burstCount++;
|
|
886
689
|
scheduleRun(n => {
|
|
887
|
-
if (!n && !
|
|
690
|
+
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
888
691
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
889
692
|
});
|
|
890
693
|
};
|
|
@@ -894,54 +697,42 @@
|
|
|
894
697
|
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
895
698
|
|
|
896
699
|
function cleanup() {
|
|
897
|
-
|
|
700
|
+
S.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
898
701
|
mutate(() => {
|
|
899
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
900
|
-
dropWrap(w);
|
|
901
|
-
}
|
|
702
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
902
703
|
});
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
state.runQueued = false;
|
|
918
|
-
state.firstShown = false;
|
|
704
|
+
S.cfg = null;
|
|
705
|
+
S.poolsReady = false;
|
|
706
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
707
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
708
|
+
S.mountedIds.clear();
|
|
709
|
+
S.lastShow.clear();
|
|
710
|
+
S.wrapByKey.clear();
|
|
711
|
+
S.wrapsByClass.clear();
|
|
712
|
+
S.kind = null;
|
|
713
|
+
S.inflight = 0;
|
|
714
|
+
S.pending = [];
|
|
715
|
+
S.pendingSet.clear();
|
|
716
|
+
S.burstActive = false;
|
|
717
|
+
S.runQueued = false;
|
|
919
718
|
}
|
|
920
719
|
|
|
921
720
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
922
|
-
//
|
|
923
|
-
// Scoped to detect: (1) ad fill events in wraps, (2) new content items
|
|
924
721
|
|
|
925
722
|
function ensureDomObserver() {
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (state.mutGuard > 0 || isBlocked()) return;
|
|
930
|
-
|
|
723
|
+
if (S.domObs) return;
|
|
724
|
+
S.domObs = new MutationObserver(muts => {
|
|
725
|
+
if (S.mutGuard > 0 || isBlocked()) return;
|
|
931
726
|
let needsBurst = false;
|
|
932
|
-
|
|
933
|
-
// Determine relevant selectors for current page kind
|
|
934
727
|
const kind = getKind();
|
|
935
728
|
const relevantSels =
|
|
936
|
-
kind === 'topic'
|
|
937
|
-
kind === 'categoryTopics'? [SEL.topic] :
|
|
938
|
-
kind === 'categories'
|
|
939
|
-
|
|
940
|
-
|
|
729
|
+
kind === 'topic' ? [SEL.post] :
|
|
730
|
+
kind === 'categoryTopics' ? [SEL.topic] :
|
|
731
|
+
kind === 'categories' ? [SEL.category] :
|
|
732
|
+
[SEL.post, SEL.topic, SEL.category];
|
|
941
733
|
for (const m of muts) {
|
|
942
734
|
if (m.type !== 'childList') continue;
|
|
943
|
-
|
|
944
|
-
// If NodeBB removed wraps as part of virtualization, free ids immediately
|
|
735
|
+
// Free IDs from wraps removed by NodeBB virtualization
|
|
945
736
|
for (const node of m.removedNodes) {
|
|
946
737
|
if (!(node instanceof Element)) continue;
|
|
947
738
|
try {
|
|
@@ -949,17 +740,13 @@
|
|
|
949
740
|
dropWrap(node);
|
|
950
741
|
} else {
|
|
951
742
|
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
952
|
-
if (wraps?.length)
|
|
953
|
-
for (const w of wraps) dropWrap(w);
|
|
954
|
-
}
|
|
743
|
+
if (wraps?.length) for (const w of wraps) dropWrap(w);
|
|
955
744
|
}
|
|
956
745
|
} catch (_) {}
|
|
957
746
|
}
|
|
958
|
-
|
|
959
747
|
for (const node of m.addedNodes) {
|
|
960
748
|
if (!(node instanceof Element)) continue;
|
|
961
|
-
|
|
962
|
-
// Check for ad fill events in wraps
|
|
749
|
+
// Ad fill detection → uncollapse
|
|
963
750
|
try {
|
|
964
751
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
965
752
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -967,88 +754,59 @@
|
|
|
967
754
|
if (wrap) clearEmptyIfFilled(wrap);
|
|
968
755
|
}
|
|
969
756
|
} catch (_) {}
|
|
970
|
-
|
|
971
|
-
|
|
757
|
+
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
758
|
+
try {
|
|
759
|
+
const reinserted = node.classList?.contains(WRAP_CLASS)
|
|
760
|
+
? [node]
|
|
761
|
+
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
762
|
+
for (const wrap of reinserted) {
|
|
763
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
764
|
+
if (id > 0) {
|
|
765
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
766
|
+
if (ph) try { getIO()?.observe(ph); } catch (_) {}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch (_) {}
|
|
770
|
+
// New content detection
|
|
972
771
|
if (!needsBurst) {
|
|
973
772
|
for (const sel of relevantSels) {
|
|
974
773
|
try {
|
|
975
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
976
|
-
needsBurst = true;
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
774
|
+
if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
|
|
979
775
|
} catch (_) {}
|
|
980
776
|
}
|
|
981
777
|
}
|
|
982
778
|
}
|
|
983
779
|
if (needsBurst) break;
|
|
984
780
|
}
|
|
985
|
-
|
|
986
781
|
if (needsBurst) requestBurst();
|
|
987
782
|
});
|
|
988
|
-
|
|
989
|
-
try {
|
|
990
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
991
|
-
} catch (_) {}
|
|
783
|
+
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
992
784
|
}
|
|
993
785
|
|
|
994
|
-
// ── TCF / CMP Protection
|
|
995
|
-
//
|
|
996
|
-
// Root cause of the CMP errors:
|
|
997
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
998
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
999
|
-
//
|
|
1000
|
-
// The CMP (Gatekeeper Consent) communicates via postMessage on the
|
|
1001
|
-
// __tcfapiLocator iframe's contentWindow. During NodeBB ajaxify navigation,
|
|
1002
|
-
// jQuery's html() or empty() on the content area can cascade and remove
|
|
1003
|
-
// iframes from <body>. The CMP then calls getTCData on a stale reference
|
|
1004
|
-
// where contentWindow is null.
|
|
1005
|
-
//
|
|
1006
|
-
// Strategy (3 layers):
|
|
1007
|
-
//
|
|
1008
|
-
// 1. PROTECT: Move the locator iframe into <head> where ajaxify never
|
|
1009
|
-
// touches it. The TCF spec only requires the iframe to exist in the
|
|
1010
|
-
// document with name="__tcfapiLocator" — it works from <head>.
|
|
1011
|
-
//
|
|
1012
|
-
// 2. GUARD: Wrap __tcfapi and __cmp with a safety layer that catches
|
|
1013
|
-
// errors in the CMP's internal getTCData, preventing the uncaught
|
|
1014
|
-
// TypeError from propagating.
|
|
1015
|
-
//
|
|
1016
|
-
// 3. RESTORE: MutationObserver on <body> childList (not subtree) to
|
|
1017
|
-
// immediately re-create the locator if something still removes it.
|
|
786
|
+
// ── TCF / CMP Protection ───────────────────────────────────────────────────
|
|
1018
787
|
|
|
1019
788
|
function ensureTcfLocator() {
|
|
1020
789
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1021
|
-
|
|
1022
790
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
1023
|
-
|
|
1024
|
-
// Create or relocate the locator iframe into <head> for protection
|
|
1025
791
|
const ensureInHead = () => {
|
|
1026
792
|
let existing = document.getElementById(LOCATOR_ID);
|
|
1027
793
|
if (existing) {
|
|
1028
|
-
// If it's in <body>, move it to <head> where ajaxify can't reach it
|
|
1029
794
|
if (existing.parentElement !== document.head) {
|
|
1030
795
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
1031
796
|
}
|
|
1032
|
-
return
|
|
797
|
+
return;
|
|
1033
798
|
}
|
|
1034
|
-
// Create fresh
|
|
1035
799
|
const f = document.createElement('iframe');
|
|
1036
800
|
f.style.display = 'none';
|
|
1037
801
|
f.id = f.name = LOCATOR_ID;
|
|
1038
802
|
try { document.head.appendChild(f); } catch (_) {
|
|
1039
|
-
// Fallback to body if head insertion fails
|
|
1040
803
|
(document.body || document.documentElement).appendChild(f);
|
|
1041
804
|
}
|
|
1042
|
-
return f;
|
|
1043
805
|
};
|
|
1044
|
-
|
|
1045
806
|
ensureInHead();
|
|
1046
807
|
|
|
1047
|
-
// Layer 2: Guard the CMP API calls against null contentWindow
|
|
1048
808
|
if (!window.__nbbCmpGuarded) {
|
|
1049
809
|
window.__nbbCmpGuarded = true;
|
|
1050
|
-
|
|
1051
|
-
// Wrap __tcfapi
|
|
1052
810
|
if (typeof window.__tcfapi === 'function') {
|
|
1053
811
|
const origTcf = window.__tcfapi;
|
|
1054
812
|
window.__tcfapi = function (cmd, version, cb, param) {
|
|
@@ -1057,23 +815,18 @@
|
|
|
1057
815
|
try { cb?.(...args); } catch (_) {}
|
|
1058
816
|
}, param);
|
|
1059
817
|
} catch (e) {
|
|
1060
|
-
// If the error is the null postMessage/addtlConsent, swallow it
|
|
1061
818
|
if (e?.message?.includes('null')) {
|
|
1062
|
-
// Re-ensure locator exists, then retry once
|
|
1063
819
|
ensureInHead();
|
|
1064
820
|
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
1065
821
|
}
|
|
1066
822
|
}
|
|
1067
823
|
};
|
|
1068
824
|
}
|
|
1069
|
-
|
|
1070
|
-
// Wrap __cmp (legacy CMP v1 API)
|
|
1071
825
|
if (typeof window.__cmp === 'function') {
|
|
1072
826
|
const origCmp = window.__cmp;
|
|
1073
827
|
window.__cmp = function (...args) {
|
|
1074
|
-
try {
|
|
1075
|
-
|
|
1076
|
-
} catch (e) {
|
|
828
|
+
try { return origCmp.apply(this, args); }
|
|
829
|
+
catch (e) {
|
|
1077
830
|
if (e?.message?.includes('null')) {
|
|
1078
831
|
ensureInHead();
|
|
1079
832
|
try { return origCmp.apply(this, args); } catch (_) {}
|
|
@@ -1083,43 +836,47 @@
|
|
|
1083
836
|
}
|
|
1084
837
|
}
|
|
1085
838
|
|
|
1086
|
-
// Layer 3: MutationObserver to immediately restore if removed
|
|
1087
839
|
if (!window.__nbbTcfObs) {
|
|
1088
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
1089
|
-
|
|
1090
|
-
if (document.getElementById(LOCATOR_ID)) return;
|
|
1091
|
-
// Something removed it — restore immediately (no debounce)
|
|
1092
|
-
ensureInHead();
|
|
840
|
+
window.__nbbTcfObs = new MutationObserver(() => {
|
|
841
|
+
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
1093
842
|
});
|
|
1094
|
-
// Observe body direct children only (the most likely removal point)
|
|
1095
843
|
try {
|
|
1096
844
|
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1097
|
-
childList: true,
|
|
1098
|
-
subtree: false,
|
|
845
|
+
childList: true, subtree: false,
|
|
1099
846
|
});
|
|
1100
|
-
} catch (_) {}
|
|
1101
|
-
// Also observe <head> in case something cleans it
|
|
1102
|
-
try {
|
|
1103
847
|
if (document.head) {
|
|
1104
|
-
window.__nbbTcfObs.observe(document.head, {
|
|
1105
|
-
childList: true,
|
|
1106
|
-
subtree: false,
|
|
1107
|
-
});
|
|
848
|
+
window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false });
|
|
1108
849
|
}
|
|
1109
850
|
} catch (_) {}
|
|
1110
851
|
}
|
|
1111
852
|
}
|
|
1112
853
|
|
|
854
|
+
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
855
|
+
|
|
856
|
+
function protectAriaHidden() {
|
|
857
|
+
if (window.__nbbAriaObs) return;
|
|
858
|
+
const remove = () => {
|
|
859
|
+
try {
|
|
860
|
+
if (document.body.getAttribute('aria-hidden') === 'true') {
|
|
861
|
+
document.body.removeAttribute('aria-hidden');
|
|
862
|
+
}
|
|
863
|
+
} catch (_) {}
|
|
864
|
+
};
|
|
865
|
+
remove();
|
|
866
|
+
window.__nbbAriaObs = new MutationObserver(remove);
|
|
867
|
+
try {
|
|
868
|
+
window.__nbbAriaObs.observe(document.body, {
|
|
869
|
+
attributes: true, attributeFilter: ['aria-hidden'],
|
|
870
|
+
});
|
|
871
|
+
} catch (_) {}
|
|
872
|
+
}
|
|
873
|
+
|
|
1113
874
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
1114
|
-
//
|
|
1115
|
-
// Mute noisy Ezoic warnings that are expected in infinite scroll context.
|
|
1116
|
-
// Uses startsWith checks instead of includes for performance.
|
|
1117
875
|
|
|
1118
876
|
function muteConsole() {
|
|
1119
877
|
if (window.__nbbEzMuted) return;
|
|
1120
878
|
window.__nbbEzMuted = true;
|
|
1121
|
-
|
|
1122
|
-
const PREFIXES = [
|
|
879
|
+
const MUTED = [
|
|
1123
880
|
'[EzoicAds JS]: Placeholder Id',
|
|
1124
881
|
'No valid placeholders for loadMore',
|
|
1125
882
|
'cannot call refresh on the same page',
|
|
@@ -1127,19 +884,23 @@
|
|
|
1127
884
|
'Debugger iframe already exists',
|
|
1128
885
|
'[CMP] Error in custom getTCData',
|
|
1129
886
|
'vignette: no interstitial API',
|
|
887
|
+
'Ezoic JS-Enable should only ever',
|
|
888
|
+
'### sending slotDestroyed',
|
|
889
|
+
'Error loading identity bridging',
|
|
890
|
+
`with id ${PH_PREFIX}`,
|
|
891
|
+
'adsbygoogle.push() error: All',
|
|
892
|
+
'has already been defined',
|
|
893
|
+
'bad response. Status',
|
|
894
|
+
'slotDestroyed event',
|
|
895
|
+
'identity bridging',
|
|
1130
896
|
];
|
|
1131
|
-
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
1132
|
-
|
|
1133
897
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
1134
898
|
const orig = console[method];
|
|
1135
899
|
if (typeof orig !== 'function') continue;
|
|
1136
900
|
console[method] = function (...args) {
|
|
1137
901
|
if (typeof args[0] === 'string') {
|
|
1138
902
|
const msg = args[0];
|
|
1139
|
-
for (const
|
|
1140
|
-
if (msg.startsWith(prefix)) return;
|
|
1141
|
-
}
|
|
1142
|
-
if (msg.includes(PH_PATTERN)) return;
|
|
903
|
+
for (const p of MUTED) { if (msg.includes(p)) return; }
|
|
1143
904
|
}
|
|
1144
905
|
return orig.apply(console, args);
|
|
1145
906
|
};
|
|
@@ -1147,33 +908,26 @@
|
|
|
1147
908
|
}
|
|
1148
909
|
|
|
1149
910
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1150
|
-
// Run once per session — preconnect hints are in <head> via server-side injection
|
|
1151
|
-
|
|
1152
|
-
let _networkWarmed = false;
|
|
1153
911
|
|
|
912
|
+
const _warmed = new Set();
|
|
1154
913
|
function warmNetwork() {
|
|
1155
|
-
if (_networkWarmed) return;
|
|
1156
|
-
_networkWarmed = true;
|
|
1157
|
-
|
|
1158
914
|
const head = document.head;
|
|
1159
915
|
if (!head) return;
|
|
1160
|
-
|
|
1161
|
-
const hints = [
|
|
916
|
+
for (const [rel, href, cors] of [
|
|
1162
917
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
1163
918
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
1164
919
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
1165
920
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
1166
921
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1167
922
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1168
|
-
]
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
head.appendChild(link);
|
|
923
|
+
]) {
|
|
924
|
+
const k = `${rel}|${href}`;
|
|
925
|
+
if (_warmed.has(k)) continue;
|
|
926
|
+
_warmed.add(k);
|
|
927
|
+
const l = document.createElement('link');
|
|
928
|
+
l.rel = rel; l.href = href;
|
|
929
|
+
if (cors) l.crossOrigin = 'anonymous';
|
|
930
|
+
head.appendChild(l);
|
|
1177
931
|
}
|
|
1178
932
|
}
|
|
1179
933
|
|
|
@@ -1182,50 +936,30 @@
|
|
|
1182
936
|
function bindNodeBB() {
|
|
1183
937
|
const $ = window.jQuery;
|
|
1184
938
|
if (!$) return;
|
|
1185
|
-
|
|
1186
939
|
$(window).off('.nbbEzoic');
|
|
1187
|
-
|
|
1188
940
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1189
|
-
|
|
1190
941
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1197
|
-
|
|
1198
|
-
muteConsole();
|
|
1199
|
-
ensureTcfLocator();
|
|
1200
|
-
warmNetwork();
|
|
1201
|
-
patchShowAds();
|
|
1202
|
-
getIO();
|
|
1203
|
-
ensureDomObserver();
|
|
942
|
+
S.pageKey = pageKey();
|
|
943
|
+
S.kind = null;
|
|
944
|
+
S.blockedUntil = 0;
|
|
945
|
+
muteConsole(); ensureTcfLocator(); protectAriaHidden();
|
|
946
|
+
warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
|
|
1204
947
|
requestBurst();
|
|
1205
948
|
});
|
|
1206
949
|
|
|
1207
|
-
// Content-loaded events trigger burst
|
|
1208
950
|
const burstEvents = [
|
|
1209
|
-
'action:ajaxify.contentLoaded',
|
|
1210
|
-
'action:
|
|
1211
|
-
'action:
|
|
1212
|
-
'action:categories.loaded',
|
|
1213
|
-
'action:category.loaded',
|
|
1214
|
-
'action:topic.loaded',
|
|
951
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
952
|
+
'action:topics.loaded', 'action:categories.loaded',
|
|
953
|
+
'action:category.loaded', 'action:topic.loaded',
|
|
1215
954
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1216
|
-
|
|
1217
955
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1218
956
|
|
|
1219
|
-
// Also bind via NodeBB hooks module (for compatibility)
|
|
1220
957
|
try {
|
|
1221
958
|
require(['hooks'], hooks => {
|
|
1222
959
|
if (typeof hooks?.on !== 'function') return;
|
|
1223
960
|
for (const ev of [
|
|
1224
|
-
'action:ajaxify.end',
|
|
1225
|
-
'action:
|
|
1226
|
-
'action:topics.loaded',
|
|
1227
|
-
'action:categories.loaded',
|
|
1228
|
-
'action:topic.loaded',
|
|
961
|
+
'action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
962
|
+
'action:categories.loaded', 'action:topic.loaded',
|
|
1229
963
|
]) {
|
|
1230
964
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
1231
965
|
}
|
|
@@ -1238,25 +972,23 @@
|
|
|
1238
972
|
window.addEventListener('scroll', () => {
|
|
1239
973
|
if (ticking) return;
|
|
1240
974
|
ticking = true;
|
|
1241
|
-
requestAnimationFrame(() => {
|
|
1242
|
-
ticking = false;
|
|
1243
|
-
requestBurst();
|
|
1244
|
-
});
|
|
975
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1245
976
|
}, { passive: true });
|
|
1246
977
|
}
|
|
1247
978
|
|
|
1248
979
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
1249
980
|
|
|
1250
|
-
|
|
981
|
+
S.pageKey = pageKey();
|
|
1251
982
|
muteConsole();
|
|
1252
983
|
ensureTcfLocator();
|
|
984
|
+
protectAriaHidden();
|
|
1253
985
|
warmNetwork();
|
|
1254
986
|
patchShowAds();
|
|
1255
987
|
getIO();
|
|
1256
988
|
ensureDomObserver();
|
|
1257
989
|
bindNodeBB();
|
|
1258
990
|
bindScroll();
|
|
1259
|
-
|
|
991
|
+
S.blockedUntil = 0;
|
|
1260
992
|
requestBurst();
|
|
1261
993
|
|
|
1262
994
|
})();
|