nodebb-plugin-ezoic-infinite 1.8.65 → 1.8.66
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 +1 -1
- package/package.json +1 -1
- package/public/client.js +423 -215
package/library.js
CHANGED
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js v2.
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - Ezoic API: showAds() + destroyPlaceholders() per official docs
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
4
|
+
* Based on working v2-fixed with targeted fixes:
|
|
5
|
+
* - Ezoic API: showAds() + destroyPlaceholders() only (per official docs)
|
|
6
|
+
* - cleanup(): destroyAll() before DOM removal, only on real page change
|
|
7
|
+
* - Recycling: conservative threshold (-3vh), age/inflight guards
|
|
8
|
+
* - aria-hidden: MutationObserver protection
|
|
9
|
+
* - Empty check: more conservative timing and GPT slot awareness
|
|
10
10
|
*/
|
|
11
11
|
(function nbbEzoicInfinite() {
|
|
12
12
|
'use strict';
|
|
@@ -24,25 +24,30 @@
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const TIMING = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
MIN_PRUNE_AGE_MS:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RECYCLE_DELAY_MS:
|
|
27
|
+
EMPTY_CHECK_EARLY_MS: 30_000,
|
|
28
|
+
EMPTY_CHECK_LATE_MS: 60_000,
|
|
29
|
+
MIN_PRUNE_AGE_MS: 8_000,
|
|
30
|
+
SHOW_THROTTLE_MS: 900,
|
|
31
|
+
BURST_COOLDOWN_MS: 200,
|
|
32
|
+
BLOCK_DURATION_MS: 1_500,
|
|
33
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
34
|
+
SHOW_RELEASE_MS: 700,
|
|
35
|
+
BATCH_FLUSH_MS: 80,
|
|
36
|
+
RECYCLE_DELAY_MS: 450,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const LIMITS = {
|
|
40
|
+
MAX_INSERTS_RUN: 6,
|
|
41
|
+
MAX_INFLIGHT: 4,
|
|
42
|
+
BATCH_SIZE: 3,
|
|
43
|
+
MAX_BURST_STEPS: 8,
|
|
44
|
+
BURST_WINDOW_MS: 2_000,
|
|
45
|
+
};
|
|
43
46
|
|
|
44
|
-
const
|
|
45
|
-
|
|
47
|
+
const IO_MARGIN = {
|
|
48
|
+
DESKTOP: '2500px 0px 2500px 0px',
|
|
49
|
+
MOBILE: '3500px 0px 3500px 0px',
|
|
50
|
+
};
|
|
46
51
|
|
|
47
52
|
const SEL = {
|
|
48
53
|
post: '[component="post"][data-pid]',
|
|
@@ -57,26 +62,39 @@
|
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
65
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
60
66
|
|
|
61
67
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
62
68
|
|
|
63
69
|
const now = () => Date.now();
|
|
64
70
|
const isMobile = () => window.innerWidth < 768;
|
|
65
71
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
66
|
-
|
|
72
|
+
|
|
73
|
+
function isFilled(node) {
|
|
74
|
+
return node?.querySelector?.(FILL_SEL) != null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPlaceholderUsed(ph) {
|
|
78
|
+
if (!ph?.isConnected) return false;
|
|
79
|
+
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
80
|
+
}
|
|
67
81
|
|
|
68
82
|
function parseIds(raw) {
|
|
69
|
-
const out = []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
86
|
+
const n = parseInt(line, 10);
|
|
87
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
88
|
+
seen.add(n);
|
|
89
|
+
out.push(n);
|
|
90
|
+
}
|
|
73
91
|
}
|
|
74
92
|
return out;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
78
96
|
|
|
79
|
-
const
|
|
97
|
+
const state = {
|
|
80
98
|
pageKey: null,
|
|
81
99
|
kind: null,
|
|
82
100
|
cfg: null,
|
|
@@ -85,17 +103,18 @@
|
|
|
85
103
|
pools: { topics: [], posts: [], categories: [] },
|
|
86
104
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
87
105
|
|
|
88
|
-
mountedIds:
|
|
89
|
-
|
|
106
|
+
mountedIds: new Set(),
|
|
107
|
+
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
108
|
+
lastShow: new Map(), // id → timestamp
|
|
90
109
|
|
|
91
|
-
wrapByKey: new Map(),
|
|
92
|
-
wrapsByClass: new Map(),
|
|
110
|
+
wrapByKey: new Map(),
|
|
111
|
+
wrapsByClass: new Map(),
|
|
93
112
|
|
|
94
113
|
io: null,
|
|
95
114
|
domObs: null,
|
|
96
115
|
|
|
97
|
-
mutGuard:
|
|
98
|
-
blockedUntil:
|
|
116
|
+
mutGuard: 0,
|
|
117
|
+
blockedUntil: 0,
|
|
99
118
|
|
|
100
119
|
inflight: 0,
|
|
101
120
|
pending: [],
|
|
@@ -106,36 +125,40 @@
|
|
|
106
125
|
burstDeadline: 0,
|
|
107
126
|
burstCount: 0,
|
|
108
127
|
lastBurstTs: 0,
|
|
128
|
+
firstShown: false,
|
|
109
129
|
};
|
|
110
130
|
|
|
111
|
-
const isBlocked = () => now() <
|
|
131
|
+
const isBlocked = () => now() < state.blockedUntil;
|
|
112
132
|
|
|
113
133
|
function mutate(fn) {
|
|
114
|
-
|
|
115
|
-
try { fn(); } finally {
|
|
134
|
+
state.mutGuard++;
|
|
135
|
+
try { fn(); } finally { state.mutGuard--; }
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
119
139
|
|
|
120
140
|
async function fetchConfig() {
|
|
121
|
-
if (
|
|
141
|
+
if (state.cfg) return state.cfg;
|
|
122
142
|
try {
|
|
123
143
|
const inline = window.__nbbEzoicCfg;
|
|
124
|
-
if (inline && typeof inline === 'object') {
|
|
144
|
+
if (inline && typeof inline === 'object') {
|
|
145
|
+
state.cfg = inline;
|
|
146
|
+
return state.cfg;
|
|
147
|
+
}
|
|
125
148
|
} catch (_) {}
|
|
126
149
|
try {
|
|
127
150
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
128
|
-
if (r.ok)
|
|
151
|
+
if (r.ok) state.cfg = await r.json();
|
|
129
152
|
} catch (_) {}
|
|
130
|
-
return
|
|
153
|
+
return state.cfg;
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function initPools(cfg) {
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
if (state.poolsReady) return;
|
|
158
|
+
state.pools.topics = parseIds(cfg.placeholderIds);
|
|
159
|
+
state.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
160
|
+
state.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
161
|
+
state.poolsReady = true;
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
@@ -161,7 +184,7 @@
|
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
function getKind() {
|
|
164
|
-
return
|
|
187
|
+
return state.kind || (state.kind = detectKind());
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
@@ -181,8 +204,8 @@
|
|
|
181
204
|
return out;
|
|
182
205
|
}
|
|
183
206
|
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
208
|
+
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
186
209
|
|
|
187
210
|
// ── Anchor keys & wrap registry ────────────────────────────────────────────
|
|
188
211
|
|
|
@@ -203,45 +226,60 @@
|
|
|
203
226
|
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
204
227
|
|
|
205
228
|
function findWrap(key) {
|
|
206
|
-
const w =
|
|
229
|
+
const w = state.wrapByKey.get(key);
|
|
207
230
|
return w?.isConnected ? w : null;
|
|
208
231
|
}
|
|
209
232
|
|
|
210
233
|
function getWrapSet(klass) {
|
|
211
|
-
let set =
|
|
212
|
-
if (!set) { set = new Set();
|
|
234
|
+
let set = state.wrapsByClass.get(klass);
|
|
235
|
+
if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
|
|
213
236
|
return set;
|
|
214
237
|
}
|
|
215
238
|
|
|
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.
|
|
239
|
+
// ── GC disconnected wraps (NodeBB virtualization) ──────────────────────────
|
|
219
240
|
|
|
220
241
|
function gcDisconnectedWraps() {
|
|
221
|
-
for (const [key, w] of
|
|
222
|
-
if (!w?.isConnected)
|
|
242
|
+
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
243
|
+
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
223
244
|
}
|
|
224
|
-
for (const [klass, set] of
|
|
225
|
-
for (const w of set) {
|
|
245
|
+
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
246
|
+
for (const w of Array.from(set)) {
|
|
226
247
|
if (w?.isConnected) continue;
|
|
227
248
|
set.delete(w);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
try {
|
|
250
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
251
|
+
if (Number.isFinite(id)) {
|
|
252
|
+
state.mountedIds.delete(id);
|
|
253
|
+
state.phState.delete(id);
|
|
254
|
+
state.lastShow.delete(id);
|
|
255
|
+
}
|
|
256
|
+
} catch (_) {}
|
|
233
257
|
}
|
|
234
|
-
if (!set.size)
|
|
258
|
+
if (!set.size) state.wrapsByClass.delete(klass);
|
|
235
259
|
}
|
|
260
|
+
try {
|
|
261
|
+
const live = new Set();
|
|
262
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
263
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
264
|
+
if (id > 0) live.add(id);
|
|
265
|
+
}
|
|
266
|
+
for (const id of Array.from(state.mountedIds)) {
|
|
267
|
+
if (!live.has(id)) {
|
|
268
|
+
state.mountedIds.delete(id);
|
|
269
|
+
state.phState.delete(id);
|
|
270
|
+
state.lastShow.delete(id);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (_) {}
|
|
236
274
|
}
|
|
237
275
|
|
|
238
|
-
// ── Wrap lifecycle
|
|
276
|
+
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
239
277
|
|
|
240
278
|
function wrapIsLive(wrap) {
|
|
241
279
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
242
280
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
243
281
|
if (!key) return false;
|
|
244
|
-
if (
|
|
282
|
+
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
245
283
|
const colonIdx = key.indexOf(':');
|
|
246
284
|
const klass = key.slice(0, colonIdx);
|
|
247
285
|
const anchorId = key.slice(colonIdx + 1);
|
|
@@ -272,12 +310,14 @@
|
|
|
272
310
|
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
311
|
if (!ph || !isFilled(ph)) return false;
|
|
274
312
|
wrap.classList.remove('is-empty');
|
|
313
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
314
|
+
if (id > 0) state.phState.set(id, 'shown');
|
|
275
315
|
return true;
|
|
276
316
|
}
|
|
277
317
|
|
|
278
318
|
function scheduleUncollapseChecks(wrap) {
|
|
279
319
|
if (!wrap) return;
|
|
280
|
-
for (const ms of [500, 3000,
|
|
320
|
+
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
281
321
|
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
282
322
|
}
|
|
283
323
|
}
|
|
@@ -285,19 +325,21 @@
|
|
|
285
325
|
// ── Pool management ────────────────────────────────────────────────────────
|
|
286
326
|
|
|
287
327
|
function pickId(poolKey) {
|
|
288
|
-
const pool =
|
|
328
|
+
const pool = state.pools[poolKey];
|
|
289
329
|
if (!pool.length) return null;
|
|
290
330
|
for (let t = 0; t < pool.length; t++) {
|
|
291
|
-
const idx =
|
|
292
|
-
|
|
331
|
+
const idx = state.cursors[poolKey] % pool.length;
|
|
332
|
+
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % pool.length;
|
|
293
333
|
const id = pool[idx];
|
|
294
|
-
if (!
|
|
334
|
+
if (!state.mountedIds.has(id)) return id;
|
|
295
335
|
}
|
|
296
336
|
return null;
|
|
297
337
|
}
|
|
298
338
|
|
|
299
339
|
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
300
|
-
//
|
|
340
|
+
//
|
|
341
|
+
// Per Ezoic docs: destroyPlaceholders(id) → remove old HTML →
|
|
342
|
+
// recreate fresh placeholder → showAds(id).
|
|
301
343
|
|
|
302
344
|
function recycleWrap(klass, targetEl, newKey) {
|
|
303
345
|
const ez = window.ezstandalone;
|
|
@@ -310,13 +352,18 @@
|
|
|
310
352
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
311
353
|
let bestFull = null, bestFullY = Infinity;
|
|
312
354
|
|
|
313
|
-
const wraps =
|
|
355
|
+
const wraps = state.wrapsByClass.get(klass);
|
|
314
356
|
if (!wraps) return null;
|
|
315
357
|
|
|
316
358
|
for (const wrap of wraps) {
|
|
317
359
|
try {
|
|
360
|
+
// Skip young wraps (ad might still be loading)
|
|
318
361
|
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
319
|
-
if (t - created <
|
|
362
|
+
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
363
|
+
// Skip wraps with inflight showAds
|
|
364
|
+
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
365
|
+
if (wid > 0 && state.phState.get(wid) === 'show-queued') continue;
|
|
366
|
+
|
|
320
367
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
321
368
|
if (bottom > threshold) continue;
|
|
322
369
|
if (!isFilled(wrap)) {
|
|
@@ -332,19 +379,22 @@
|
|
|
332
379
|
|
|
333
380
|
const id = parseInt(best.getAttribute(ATTR.WRAPID), 10);
|
|
334
381
|
if (!Number.isFinite(id)) return null;
|
|
382
|
+
|
|
335
383
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
336
384
|
|
|
337
385
|
// Unobserve before moving
|
|
338
386
|
try {
|
|
339
387
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
340
|
-
if (ph)
|
|
388
|
+
if (ph) state.io?.unobserve(ph);
|
|
341
389
|
} catch (_) {}
|
|
342
390
|
|
|
343
|
-
// Ezoic recycle: destroy →
|
|
391
|
+
// Ezoic recycle: destroy → new DOM → showAds
|
|
344
392
|
const doRecycle = () => {
|
|
393
|
+
state.phState.set(id, 'destroyed');
|
|
345
394
|
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
346
395
|
|
|
347
396
|
setTimeout(() => {
|
|
397
|
+
// Recreate fresh placeholder DOM at new position
|
|
348
398
|
mutate(() => {
|
|
349
399
|
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
350
400
|
best.setAttribute(ATTR.CREATED, String(now()));
|
|
@@ -355,21 +405,27 @@
|
|
|
355
405
|
const fresh = document.createElement('div');
|
|
356
406
|
fresh.id = `${PH_PREFIX}${id}`;
|
|
357
407
|
fresh.setAttribute('data-ezoic-id', String(id));
|
|
408
|
+
fresh.style.minHeight = '1px';
|
|
358
409
|
best.appendChild(fresh);
|
|
359
410
|
targetEl.insertAdjacentElement('afterend', best);
|
|
360
411
|
});
|
|
361
412
|
|
|
362
|
-
if (oldKey &&
|
|
363
|
-
|
|
413
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
414
|
+
state.wrapByKey.set(newKey, best);
|
|
364
415
|
|
|
416
|
+
// Re-show after DOM is settled
|
|
365
417
|
setTimeout(() => {
|
|
366
|
-
|
|
418
|
+
observePlaceholder(id);
|
|
419
|
+
state.phState.set(id, 'new');
|
|
367
420
|
enqueueShow(id);
|
|
368
421
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
369
422
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
370
423
|
};
|
|
371
424
|
|
|
372
|
-
try {
|
|
425
|
+
try {
|
|
426
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle();
|
|
427
|
+
} catch (_) {}
|
|
428
|
+
|
|
373
429
|
return { id, wrap: best };
|
|
374
430
|
}
|
|
375
431
|
|
|
@@ -383,9 +439,11 @@
|
|
|
383
439
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
384
440
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
385
441
|
w.style.cssText = 'width:100%;display:block';
|
|
442
|
+
|
|
386
443
|
const ph = document.createElement('div');
|
|
387
444
|
ph.id = `${PH_PREFIX}${id}`;
|
|
388
445
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
446
|
+
ph.style.minHeight = '1px';
|
|
389
447
|
w.appendChild(ph);
|
|
390
448
|
return w;
|
|
391
449
|
}
|
|
@@ -393,12 +451,15 @@
|
|
|
393
451
|
function insertAfter(el, id, klass, key) {
|
|
394
452
|
if (!el?.insertAdjacentElement) return null;
|
|
395
453
|
if (findWrap(key)) return null;
|
|
396
|
-
if (
|
|
397
|
-
|
|
454
|
+
if (state.mountedIds.has(id)) return null;
|
|
455
|
+
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
456
|
+
if (existing?.isConnected) return null;
|
|
457
|
+
|
|
398
458
|
const w = makeWrap(id, klass, key);
|
|
399
459
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
400
|
-
|
|
401
|
-
|
|
460
|
+
state.mountedIds.add(id);
|
|
461
|
+
state.phState.set(id, 'new');
|
|
462
|
+
state.wrapByKey.set(key, w);
|
|
402
463
|
getWrapSet(klass).add(w);
|
|
403
464
|
return w;
|
|
404
465
|
}
|
|
@@ -406,17 +467,17 @@
|
|
|
406
467
|
function dropWrap(w) {
|
|
407
468
|
try {
|
|
408
469
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
409
|
-
if (ph instanceof Element)
|
|
470
|
+
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
410
471
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
411
472
|
if (Number.isFinite(id)) {
|
|
412
|
-
|
|
413
|
-
|
|
473
|
+
state.mountedIds.delete(id);
|
|
474
|
+
state.phState.delete(id);
|
|
414
475
|
}
|
|
415
476
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
416
|
-
if (key &&
|
|
477
|
+
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
417
478
|
for (const cls of w.classList) {
|
|
418
479
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
419
|
-
|
|
480
|
+
state.wrapsByClass.get(cls)?.delete(w);
|
|
420
481
|
break;
|
|
421
482
|
}
|
|
422
483
|
}
|
|
@@ -429,7 +490,7 @@
|
|
|
429
490
|
function pruneOrphansBetween() {
|
|
430
491
|
const klass = 'ezoic-ad-between';
|
|
431
492
|
const cfg = KIND[klass];
|
|
432
|
-
const wraps =
|
|
493
|
+
const wraps = state.wrapsByClass.get(klass);
|
|
433
494
|
if (!wraps?.size) return;
|
|
434
495
|
|
|
435
496
|
const liveAnchors = new Set();
|
|
@@ -437,13 +498,16 @@
|
|
|
437
498
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
438
499
|
if (v) liveAnchors.add(v);
|
|
439
500
|
}
|
|
501
|
+
|
|
440
502
|
const t = now();
|
|
441
503
|
for (const w of wraps) {
|
|
442
504
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
443
505
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
444
506
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
445
507
|
const sid = key.slice(klass.length + 1);
|
|
446
|
-
if (!sid || !liveAnchors.has(sid))
|
|
508
|
+
if (!sid || !liveAnchors.has(sid)) {
|
|
509
|
+
mutate(() => dropWrap(w));
|
|
510
|
+
}
|
|
447
511
|
}
|
|
448
512
|
}
|
|
449
513
|
|
|
@@ -467,18 +531,26 @@
|
|
|
467
531
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
468
532
|
if (!items.length) return 0;
|
|
469
533
|
let inserted = 0;
|
|
534
|
+
|
|
470
535
|
for (const el of items) {
|
|
471
|
-
if (inserted >= MAX_INSERTS_RUN) break;
|
|
536
|
+
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
472
537
|
if (!el?.isConnected) continue;
|
|
538
|
+
|
|
473
539
|
const ord = ordinal(klass, el);
|
|
474
540
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
475
541
|
if (adjacentWrap(el)) continue;
|
|
542
|
+
|
|
476
543
|
const key = anchorKey(klass, el);
|
|
477
544
|
if (findWrap(key)) continue;
|
|
545
|
+
|
|
478
546
|
const id = pickId(poolKey);
|
|
479
547
|
if (id) {
|
|
480
548
|
const w = insertAfter(el, id, klass, key);
|
|
481
|
-
if (w) {
|
|
549
|
+
if (w) {
|
|
550
|
+
observePlaceholder(id);
|
|
551
|
+
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
552
|
+
inserted++;
|
|
553
|
+
}
|
|
482
554
|
} else {
|
|
483
555
|
const recycled = recycleWrap(klass, el, key);
|
|
484
556
|
if (!recycled) break;
|
|
@@ -491,58 +563,69 @@
|
|
|
491
563
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
492
564
|
|
|
493
565
|
function getIO() {
|
|
494
|
-
if (
|
|
566
|
+
if (state.io) return state.io;
|
|
495
567
|
try {
|
|
496
|
-
|
|
497
|
-
for (const
|
|
498
|
-
if (!
|
|
499
|
-
if (
|
|
500
|
-
const id = parseInt(
|
|
568
|
+
state.io = new IntersectionObserver(entries => {
|
|
569
|
+
for (const entry of entries) {
|
|
570
|
+
if (!entry.isIntersecting) continue;
|
|
571
|
+
if (entry.target instanceof Element) state.io?.unobserve(entry.target);
|
|
572
|
+
const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
|
|
501
573
|
if (id > 0) enqueueShow(id);
|
|
502
574
|
}
|
|
503
575
|
}, {
|
|
504
576
|
root: null,
|
|
505
|
-
rootMargin: isMobile() ?
|
|
577
|
+
rootMargin: isMobile() ? IO_MARGIN.MOBILE : IO_MARGIN.DESKTOP,
|
|
506
578
|
threshold: 0,
|
|
507
579
|
});
|
|
508
|
-
} catch (_) {
|
|
509
|
-
return
|
|
580
|
+
} catch (_) { state.io = null; }
|
|
581
|
+
return state.io;
|
|
510
582
|
}
|
|
511
583
|
|
|
512
|
-
function
|
|
584
|
+
function observePlaceholder(id) {
|
|
513
585
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
514
|
-
if (ph?.isConnected)
|
|
586
|
+
if (ph?.isConnected) {
|
|
587
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
588
|
+
}
|
|
515
589
|
}
|
|
516
590
|
|
|
517
591
|
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
518
592
|
|
|
519
593
|
function enqueueShow(id) {
|
|
520
594
|
if (!id || isBlocked()) return;
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
595
|
+
const st = state.phState.get(id);
|
|
596
|
+
if (st === 'show-queued' || st === 'shown') return;
|
|
597
|
+
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
598
|
+
|
|
599
|
+
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
600
|
+
if (!state.pendingSet.has(id)) {
|
|
601
|
+
state.pending.push(id);
|
|
602
|
+
state.pendingSet.add(id);
|
|
603
|
+
state.phState.set(id, 'show-queued');
|
|
604
|
+
}
|
|
524
605
|
return;
|
|
525
606
|
}
|
|
607
|
+
state.phState.set(id, 'show-queued');
|
|
526
608
|
startShow(id);
|
|
527
609
|
}
|
|
528
610
|
|
|
529
611
|
function drainQueue() {
|
|
530
612
|
if (isBlocked()) return;
|
|
531
|
-
while (
|
|
532
|
-
const id =
|
|
533
|
-
|
|
613
|
+
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
614
|
+
const id = state.pending.shift();
|
|
615
|
+
state.pendingSet.delete(id);
|
|
534
616
|
startShow(id);
|
|
535
617
|
}
|
|
536
618
|
}
|
|
537
619
|
|
|
538
620
|
function startShow(id) {
|
|
539
621
|
if (!id || isBlocked()) return;
|
|
540
|
-
|
|
541
|
-
|
|
622
|
+
state.inflight++;
|
|
623
|
+
|
|
624
|
+
let released = false;
|
|
542
625
|
const release = () => {
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
626
|
+
if (released) return;
|
|
627
|
+
released = true;
|
|
628
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
546
629
|
drainQueue();
|
|
547
630
|
};
|
|
548
631
|
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
@@ -550,31 +633,52 @@
|
|
|
550
633
|
requestAnimationFrame(() => {
|
|
551
634
|
try {
|
|
552
635
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
636
|
+
|
|
553
637
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
554
|
-
if (!ph?.isConnected
|
|
638
|
+
if (!ph?.isConnected) {
|
|
639
|
+
state.phState.delete(id);
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
return release();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
645
|
+
state.phState.set(id, 'shown');
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
return release();
|
|
648
|
+
}
|
|
555
649
|
|
|
556
650
|
const t = now();
|
|
557
|
-
if (t - (
|
|
558
|
-
|
|
651
|
+
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
return release();
|
|
654
|
+
}
|
|
655
|
+
state.lastShow.set(id, t);
|
|
559
656
|
|
|
560
|
-
|
|
561
|
-
|
|
657
|
+
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
658
|
+
state.phState.set(id, 'shown');
|
|
562
659
|
|
|
563
660
|
window.ezstandalone = window.ezstandalone || {};
|
|
564
661
|
const ez = window.ezstandalone;
|
|
662
|
+
|
|
565
663
|
const doShow = () => {
|
|
664
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
566
665
|
try { ez.showAds(id); } catch (_) {}
|
|
567
666
|
if (wrap) scheduleUncollapseChecks(wrap);
|
|
568
667
|
scheduleEmptyCheck(id, t);
|
|
569
668
|
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
570
669
|
};
|
|
670
|
+
|
|
571
671
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
572
|
-
} catch (_) {
|
|
672
|
+
} catch (_) {
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
release();
|
|
675
|
+
}
|
|
573
676
|
});
|
|
574
677
|
}
|
|
575
678
|
|
|
576
679
|
function scheduleEmptyCheck(id, showTs) {
|
|
577
|
-
|
|
680
|
+
// Two-pass check: conservative to avoid collapsing slow-loading ads
|
|
681
|
+
for (const delay of [TIMING.EMPTY_CHECK_EARLY_MS, TIMING.EMPTY_CHECK_LATE_MS]) {
|
|
578
682
|
setTimeout(() => {
|
|
579
683
|
try {
|
|
580
684
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
@@ -582,8 +686,9 @@
|
|
|
582
686
|
if (!wrap || !ph?.isConnected) return;
|
|
583
687
|
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
584
688
|
if (clearEmptyIfFilled(wrap)) return;
|
|
585
|
-
// Don't collapse if GPT slot exists (still loading)
|
|
689
|
+
// Don't collapse if a GPT slot exists (might still be loading)
|
|
586
690
|
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
691
|
+
// Don't collapse if placeholder has meaningful height
|
|
587
692
|
if (ph.offsetHeight > 10) return;
|
|
588
693
|
wrap.classList.add('is-empty');
|
|
589
694
|
} catch (_) {}
|
|
@@ -592,7 +697,10 @@
|
|
|
592
697
|
}
|
|
593
698
|
|
|
594
699
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
595
|
-
//
|
|
700
|
+
//
|
|
701
|
+
// Intercepts ez.showAds() to batch calls and filter disconnected placeholders.
|
|
702
|
+
// IMPORTANT: no-arg showAds() calls (used by Ezoic for page transitions)
|
|
703
|
+
// are passed through unmodified.
|
|
596
704
|
|
|
597
705
|
function patchShowAds() {
|
|
598
706
|
const apply = () => {
|
|
@@ -601,22 +709,53 @@
|
|
|
601
709
|
const ez = window.ezstandalone;
|
|
602
710
|
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
603
711
|
window.__nbbEzPatched = true;
|
|
712
|
+
|
|
604
713
|
const orig = ez.showAds.bind(ez);
|
|
714
|
+
const queue = new Set();
|
|
715
|
+
let flushTimer = null;
|
|
716
|
+
|
|
717
|
+
const flush = () => {
|
|
718
|
+
flushTimer = null;
|
|
719
|
+
if (isBlocked() || !queue.size) return;
|
|
720
|
+
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
721
|
+
queue.clear();
|
|
722
|
+
const valid = ids.filter(id => {
|
|
723
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
724
|
+
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
725
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
726
|
+
return true;
|
|
727
|
+
});
|
|
728
|
+
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
729
|
+
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
730
|
+
try { orig(...chunk); } catch (_) {
|
|
731
|
+
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
|
|
605
736
|
ez.showAds = function (...args) {
|
|
606
|
-
|
|
737
|
+
// No-arg call = Ezoic page refresh — pass through unmodified
|
|
738
|
+
if (args.length === 0) {
|
|
739
|
+
return orig();
|
|
740
|
+
}
|
|
607
741
|
if (isBlocked()) return;
|
|
608
742
|
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
609
|
-
const seen = new Set();
|
|
610
743
|
for (const v of ids) {
|
|
611
744
|
const id = parseInt(v, 10);
|
|
612
|
-
if (!Number.isFinite(id) || id <= 0
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
745
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
746
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
747
|
+
if (!ph?.isConnected) continue;
|
|
748
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
|
|
749
|
+
state.phState.set(id, 'show-queued');
|
|
750
|
+
queue.add(id);
|
|
751
|
+
}
|
|
752
|
+
if (queue.size && !flushTimer) {
|
|
753
|
+
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
616
754
|
}
|
|
617
755
|
};
|
|
618
756
|
} catch (_) {}
|
|
619
757
|
};
|
|
758
|
+
|
|
620
759
|
apply();
|
|
621
760
|
if (!window.__nbbEzPatched) {
|
|
622
761
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -628,7 +767,6 @@
|
|
|
628
767
|
|
|
629
768
|
async function runCore() {
|
|
630
769
|
if (isBlocked()) return 0;
|
|
631
|
-
patchShowAds();
|
|
632
770
|
try { gcDisconnectedWraps(); } catch (_) {}
|
|
633
771
|
|
|
634
772
|
const cfg = await fetchConfig();
|
|
@@ -645,26 +783,32 @@
|
|
|
645
783
|
};
|
|
646
784
|
|
|
647
785
|
if (kind === 'topic') {
|
|
648
|
-
return exec(
|
|
649
|
-
|
|
786
|
+
return exec(
|
|
787
|
+
'ezoic-ad-message', getPosts,
|
|
788
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
789
|
+
);
|
|
650
790
|
}
|
|
651
791
|
if (kind === 'categoryTopics') {
|
|
652
792
|
pruneOrphansBetween();
|
|
653
|
-
return exec(
|
|
654
|
-
|
|
793
|
+
return exec(
|
|
794
|
+
'ezoic-ad-between', getTopics,
|
|
795
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
796
|
+
);
|
|
655
797
|
}
|
|
656
|
-
return exec(
|
|
657
|
-
|
|
798
|
+
return exec(
|
|
799
|
+
'ezoic-ad-categories', getCategories,
|
|
800
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
801
|
+
);
|
|
658
802
|
}
|
|
659
803
|
|
|
660
804
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
661
805
|
|
|
662
806
|
function scheduleRun(cb) {
|
|
663
|
-
if (
|
|
664
|
-
|
|
807
|
+
if (state.runQueued) return;
|
|
808
|
+
state.runQueued = true;
|
|
665
809
|
requestAnimationFrame(async () => {
|
|
666
|
-
|
|
667
|
-
if (
|
|
810
|
+
state.runQueued = false;
|
|
811
|
+
if (state.pageKey && pageKey() !== state.pageKey) return;
|
|
668
812
|
let n = 0;
|
|
669
813
|
try { n = await runCore(); } catch (_) {}
|
|
670
814
|
try { cb?.(n); } catch (_) {}
|
|
@@ -674,20 +818,21 @@
|
|
|
674
818
|
function requestBurst() {
|
|
675
819
|
if (isBlocked()) return;
|
|
676
820
|
const t = now();
|
|
677
|
-
if (t -
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
821
|
+
if (t - state.lastBurstTs < TIMING.BURST_COOLDOWN_MS) return;
|
|
822
|
+
state.lastBurstTs = t;
|
|
823
|
+
state.pageKey = pageKey();
|
|
824
|
+
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
825
|
+
if (state.burstActive) return;
|
|
826
|
+
state.burstActive = true;
|
|
827
|
+
state.burstCount = 0;
|
|
684
828
|
const step = () => {
|
|
685
|
-
if (pageKey() !==
|
|
686
|
-
|
|
829
|
+
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
830
|
+
state.burstActive = false;
|
|
831
|
+
return;
|
|
687
832
|
}
|
|
688
|
-
|
|
833
|
+
state.burstCount++;
|
|
689
834
|
scheduleRun(n => {
|
|
690
|
-
if (!n && !
|
|
835
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
691
836
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
692
837
|
});
|
|
693
838
|
};
|
|
@@ -697,32 +842,46 @@
|
|
|
697
842
|
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
698
843
|
|
|
699
844
|
function cleanup() {
|
|
700
|
-
|
|
845
|
+
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
846
|
+
|
|
847
|
+
// Tell Ezoic to destroy all placeholders BEFORE we remove DOM elements.
|
|
848
|
+
// This prevents GPT slotDestroyed events and Ezoic 400 errors.
|
|
849
|
+
try {
|
|
850
|
+
const ez = window.ezstandalone;
|
|
851
|
+
if (typeof ez?.destroyAll === 'function') {
|
|
852
|
+
ez.destroyAll();
|
|
853
|
+
}
|
|
854
|
+
} catch (_) {}
|
|
855
|
+
|
|
701
856
|
mutate(() => {
|
|
702
857
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
703
858
|
});
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
859
|
+
state.cfg = null;
|
|
860
|
+
state.poolsReady = false;
|
|
861
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
862
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
863
|
+
state.mountedIds.clear();
|
|
864
|
+
state.lastShow.clear();
|
|
865
|
+
state.wrapByKey.clear();
|
|
866
|
+
state.wrapsByClass.clear();
|
|
867
|
+
state.kind = null;
|
|
868
|
+
state.phState.clear();
|
|
869
|
+
state.inflight = 0;
|
|
870
|
+
state.pending = [];
|
|
871
|
+
state.pendingSet.clear();
|
|
872
|
+
state.burstActive = false;
|
|
873
|
+
state.runQueued = false;
|
|
874
|
+
state.firstShown = false;
|
|
718
875
|
}
|
|
719
876
|
|
|
720
877
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
721
878
|
|
|
722
879
|
function ensureDomObserver() {
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
880
|
+
if (state.domObs) return;
|
|
881
|
+
|
|
882
|
+
state.domObs = new MutationObserver(muts => {
|
|
883
|
+
if (state.mutGuard > 0 || isBlocked()) return;
|
|
884
|
+
|
|
726
885
|
let needsBurst = false;
|
|
727
886
|
const kind = getKind();
|
|
728
887
|
const relevantSels =
|
|
@@ -730,8 +889,10 @@
|
|
|
730
889
|
kind === 'categoryTopics' ? [SEL.topic] :
|
|
731
890
|
kind === 'categories' ? [SEL.category] :
|
|
732
891
|
[SEL.post, SEL.topic, SEL.category];
|
|
892
|
+
|
|
733
893
|
for (const m of muts) {
|
|
734
894
|
if (m.type !== 'childList') continue;
|
|
895
|
+
|
|
735
896
|
// Free IDs from wraps removed by NodeBB virtualization
|
|
736
897
|
for (const node of m.removedNodes) {
|
|
737
898
|
if (!(node instanceof Element)) continue;
|
|
@@ -740,13 +901,15 @@
|
|
|
740
901
|
dropWrap(node);
|
|
741
902
|
} else {
|
|
742
903
|
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
743
|
-
if (wraps?.length) for (const w of wraps) dropWrap(w);
|
|
904
|
+
if (wraps?.length) { for (const w of wraps) dropWrap(w); }
|
|
744
905
|
}
|
|
745
906
|
} catch (_) {}
|
|
746
907
|
}
|
|
908
|
+
|
|
747
909
|
for (const node of m.addedNodes) {
|
|
748
910
|
if (!(node instanceof Element)) continue;
|
|
749
|
-
|
|
911
|
+
|
|
912
|
+
// Ad fill detection
|
|
750
913
|
try {
|
|
751
914
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
752
915
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -754,19 +917,20 @@
|
|
|
754
917
|
if (wrap) clearEmptyIfFilled(wrap);
|
|
755
918
|
}
|
|
756
919
|
} catch (_) {}
|
|
920
|
+
|
|
757
921
|
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
758
922
|
try {
|
|
759
|
-
const
|
|
923
|
+
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
760
924
|
? [node]
|
|
761
925
|
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
762
|
-
for (const wrap of
|
|
926
|
+
for (const wrap of wraps) {
|
|
763
927
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
764
|
-
if (id
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
928
|
+
if (!id || id <= 0) continue;
|
|
929
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
930
|
+
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
768
931
|
}
|
|
769
932
|
} catch (_) {}
|
|
933
|
+
|
|
770
934
|
// New content detection
|
|
771
935
|
if (!needsBurst) {
|
|
772
936
|
for (const sel of relevantSels) {
|
|
@@ -778,23 +942,29 @@
|
|
|
778
942
|
}
|
|
779
943
|
if (needsBurst) break;
|
|
780
944
|
}
|
|
945
|
+
|
|
781
946
|
if (needsBurst) requestBurst();
|
|
782
947
|
});
|
|
783
|
-
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
951
|
+
} catch (_) {}
|
|
784
952
|
}
|
|
785
953
|
|
|
786
|
-
// ── TCF / CMP Protection
|
|
954
|
+
// ── TCF / CMP Protection (3 layers) ────────────────────────────────────────
|
|
787
955
|
|
|
788
956
|
function ensureTcfLocator() {
|
|
789
957
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
958
|
+
|
|
790
959
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
960
|
+
|
|
791
961
|
const ensureInHead = () => {
|
|
792
962
|
let existing = document.getElementById(LOCATOR_ID);
|
|
793
963
|
if (existing) {
|
|
794
964
|
if (existing.parentElement !== document.head) {
|
|
795
965
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
796
966
|
}
|
|
797
|
-
return;
|
|
967
|
+
return existing;
|
|
798
968
|
}
|
|
799
969
|
const f = document.createElement('iframe');
|
|
800
970
|
f.style.display = 'none';
|
|
@@ -802,7 +972,9 @@
|
|
|
802
972
|
try { document.head.appendChild(f); } catch (_) {
|
|
803
973
|
(document.body || document.documentElement).appendChild(f);
|
|
804
974
|
}
|
|
975
|
+
return f;
|
|
805
976
|
};
|
|
977
|
+
|
|
806
978
|
ensureInHead();
|
|
807
979
|
|
|
808
980
|
if (!window.__nbbCmpGuarded) {
|
|
@@ -844,8 +1016,12 @@
|
|
|
844
1016
|
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
845
1017
|
childList: true, subtree: false,
|
|
846
1018
|
});
|
|
1019
|
+
} catch (_) {}
|
|
1020
|
+
try {
|
|
847
1021
|
if (document.head) {
|
|
848
|
-
window.__nbbTcfObs.observe(document.head, {
|
|
1022
|
+
window.__nbbTcfObs.observe(document.head, {
|
|
1023
|
+
childList: true, subtree: false,
|
|
1024
|
+
});
|
|
849
1025
|
}
|
|
850
1026
|
} catch (_) {}
|
|
851
1027
|
}
|
|
@@ -866,7 +1042,8 @@
|
|
|
866
1042
|
window.__nbbAriaObs = new MutationObserver(remove);
|
|
867
1043
|
try {
|
|
868
1044
|
window.__nbbAriaObs.observe(document.body, {
|
|
869
|
-
attributes: true,
|
|
1045
|
+
attributes: true,
|
|
1046
|
+
attributeFilter: ['aria-hidden'],
|
|
870
1047
|
});
|
|
871
1048
|
} catch (_) {}
|
|
872
1049
|
}
|
|
@@ -876,7 +1053,8 @@
|
|
|
876
1053
|
function muteConsole() {
|
|
877
1054
|
if (window.__nbbEzMuted) return;
|
|
878
1055
|
window.__nbbEzMuted = true;
|
|
879
|
-
|
|
1056
|
+
|
|
1057
|
+
const PREFIXES = [
|
|
880
1058
|
'[EzoicAds JS]: Placeholder Id',
|
|
881
1059
|
'No valid placeholders for loadMore',
|
|
882
1060
|
'cannot call refresh on the same page',
|
|
@@ -885,22 +1063,22 @@
|
|
|
885
1063
|
'[CMP] Error in custom getTCData',
|
|
886
1064
|
'vignette: no interstitial API',
|
|
887
1065
|
'Ezoic JS-Enable should only ever',
|
|
888
|
-
|
|
889
|
-
|
|
1066
|
+
];
|
|
1067
|
+
const PATTERNS = [
|
|
890
1068
|
`with id ${PH_PREFIX}`,
|
|
891
1069
|
'adsbygoogle.push() error: All',
|
|
892
1070
|
'has already been defined',
|
|
893
1071
|
'bad response. Status',
|
|
894
|
-
'slotDestroyed event',
|
|
895
|
-
'identity bridging',
|
|
896
1072
|
];
|
|
1073
|
+
|
|
897
1074
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
898
1075
|
const orig = console[method];
|
|
899
1076
|
if (typeof orig !== 'function') continue;
|
|
900
1077
|
console[method] = function (...args) {
|
|
901
1078
|
if (typeof args[0] === 'string') {
|
|
902
1079
|
const msg = args[0];
|
|
903
|
-
for (const p of
|
|
1080
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
1081
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
904
1082
|
}
|
|
905
1083
|
return orig.apply(console, args);
|
|
906
1084
|
};
|
|
@@ -909,25 +1087,28 @@
|
|
|
909
1087
|
|
|
910
1088
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
911
1089
|
|
|
912
|
-
|
|
1090
|
+
let _networkWarmed = false;
|
|
1091
|
+
|
|
913
1092
|
function warmNetwork() {
|
|
1093
|
+
if (_networkWarmed) return;
|
|
1094
|
+
_networkWarmed = true;
|
|
914
1095
|
const head = document.head;
|
|
915
1096
|
if (!head) return;
|
|
916
|
-
|
|
1097
|
+
const hints = [
|
|
917
1098
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
918
1099
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
919
1100
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
920
1101
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
921
1102
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
922
1103
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
923
|
-
]
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (cors)
|
|
930
|
-
head.appendChild(
|
|
1104
|
+
];
|
|
1105
|
+
for (const [rel, href, cors] of hints) {
|
|
1106
|
+
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1107
|
+
const link = document.createElement('link');
|
|
1108
|
+
link.rel = rel;
|
|
1109
|
+
link.href = href;
|
|
1110
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
1111
|
+
head.appendChild(link);
|
|
931
1112
|
}
|
|
932
1113
|
}
|
|
933
1114
|
|
|
@@ -936,30 +1117,54 @@
|
|
|
936
1117
|
function bindNodeBB() {
|
|
937
1118
|
const $ = window.jQuery;
|
|
938
1119
|
if (!$) return;
|
|
1120
|
+
|
|
939
1121
|
$(window).off('.nbbEzoic');
|
|
940
|
-
|
|
1122
|
+
|
|
1123
|
+
// Only cleanup on actual page change, not same-page pagination
|
|
1124
|
+
$(window).on('action:ajaxify.start.nbbEzoic', (ev, data) => {
|
|
1125
|
+
const targetUrl = data?.url || data?.tpl_url || '';
|
|
1126
|
+
const currentPath = location.pathname.replace(/^\//, '');
|
|
1127
|
+
if (targetUrl && targetUrl.replace(/[?#].*$/, '') === currentPath.replace(/[?#].*$/, '')) {
|
|
1128
|
+
return; // Same page — skip cleanup
|
|
1129
|
+
}
|
|
1130
|
+
cleanup();
|
|
1131
|
+
});
|
|
1132
|
+
|
|
941
1133
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1134
|
+
state.pageKey = pageKey();
|
|
1135
|
+
state.kind = null;
|
|
1136
|
+
state.blockedUntil = 0;
|
|
1137
|
+
|
|
1138
|
+
muteConsole();
|
|
1139
|
+
ensureTcfLocator();
|
|
1140
|
+
protectAriaHidden();
|
|
1141
|
+
warmNetwork();
|
|
1142
|
+
patchShowAds();
|
|
1143
|
+
getIO();
|
|
1144
|
+
ensureDomObserver();
|
|
947
1145
|
requestBurst();
|
|
948
1146
|
});
|
|
949
1147
|
|
|
950
1148
|
const burstEvents = [
|
|
951
|
-
'action:ajaxify.contentLoaded',
|
|
952
|
-
'action:
|
|
953
|
-
'action:
|
|
1149
|
+
'action:ajaxify.contentLoaded',
|
|
1150
|
+
'action:posts.loaded',
|
|
1151
|
+
'action:topics.loaded',
|
|
1152
|
+
'action:categories.loaded',
|
|
1153
|
+
'action:category.loaded',
|
|
1154
|
+
'action:topic.loaded',
|
|
954
1155
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1156
|
+
|
|
955
1157
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
956
1158
|
|
|
957
1159
|
try {
|
|
958
1160
|
require(['hooks'], hooks => {
|
|
959
1161
|
if (typeof hooks?.on !== 'function') return;
|
|
960
1162
|
for (const ev of [
|
|
961
|
-
'action:ajaxify.end',
|
|
962
|
-
'action:
|
|
1163
|
+
'action:ajaxify.end',
|
|
1164
|
+
'action:posts.loaded',
|
|
1165
|
+
'action:topics.loaded',
|
|
1166
|
+
'action:categories.loaded',
|
|
1167
|
+
'action:topic.loaded',
|
|
963
1168
|
]) {
|
|
964
1169
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
965
1170
|
}
|
|
@@ -972,13 +1177,16 @@
|
|
|
972
1177
|
window.addEventListener('scroll', () => {
|
|
973
1178
|
if (ticking) return;
|
|
974
1179
|
ticking = true;
|
|
975
|
-
requestAnimationFrame(() => {
|
|
1180
|
+
requestAnimationFrame(() => {
|
|
1181
|
+
ticking = false;
|
|
1182
|
+
requestBurst();
|
|
1183
|
+
});
|
|
976
1184
|
}, { passive: true });
|
|
977
1185
|
}
|
|
978
1186
|
|
|
979
1187
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
980
1188
|
|
|
981
|
-
|
|
1189
|
+
state.pageKey = pageKey();
|
|
982
1190
|
muteConsole();
|
|
983
1191
|
ensureTcfLocator();
|
|
984
1192
|
protectAriaHidden();
|
|
@@ -988,7 +1196,7 @@
|
|
|
988
1196
|
ensureDomObserver();
|
|
989
1197
|
bindNodeBB();
|
|
990
1198
|
bindScroll();
|
|
991
|
-
|
|
1199
|
+
state.blockedUntil = 0;
|
|
992
1200
|
requestBurst();
|
|
993
1201
|
|
|
994
1202
|
})();
|