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