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