nodebb-plugin-ezoic-infinite 1.8.46 → 1.8.47
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 +21 -5
- package/package.json +1 -1
- package/public/client.js +179 -239
package/library.js
CHANGED
|
@@ -141,12 +141,12 @@ 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>',
|
|
145
144
|
'<script>',
|
|
145
|
+
'window._ezaq = window._ezaq || {};',
|
|
146
146
|
'window.ezstandalone = window.ezstandalone || {};',
|
|
147
|
-
|
|
148
|
-
'window.ezstandalone.cmd = window.ezstandalone.cmd || [];',
|
|
147
|
+
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
149
148
|
'</script>',
|
|
149
|
+
'<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
150
150
|
].join('\n');
|
|
151
151
|
|
|
152
152
|
// ── Hooks ────────────────────────────────────────────────────────────────────
|
|
@@ -174,7 +174,24 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
174
174
|
const settings = await getSettings();
|
|
175
175
|
const uid = data.req?.uid ?? 0;
|
|
176
176
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
177
|
-
|
|
177
|
+
|
|
178
|
+
if (excluded) {
|
|
179
|
+
// Even for excluded users, inject a minimal stub so that any script
|
|
180
|
+
// referencing ezstandalone doesn't throw a ReferenceError, plus
|
|
181
|
+
// the config with excluded=true so client.js bails early.
|
|
182
|
+
const cfg = buildClientConfig(settings, true);
|
|
183
|
+
const stub = [
|
|
184
|
+
'<script>',
|
|
185
|
+
'window._ezaq = window._ezaq || {};',
|
|
186
|
+
'window.ezstandalone = window.ezstandalone || {};',
|
|
187
|
+
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
188
|
+
'</script>',
|
|
189
|
+
].join('\n');
|
|
190
|
+
data.templateData.customHTML =
|
|
191
|
+
stub + '\n' +
|
|
192
|
+
serializeInlineConfig(cfg) +
|
|
193
|
+
(data.templateData.customHTML || '');
|
|
194
|
+
} else {
|
|
178
195
|
const cfg = buildClientConfig(settings, false);
|
|
179
196
|
data.templateData.customHTML =
|
|
180
197
|
HEAD_PRECONNECTS + '\n' +
|
|
@@ -183,7 +200,6 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
183
200
|
(data.templateData.customHTML || '');
|
|
184
201
|
}
|
|
185
202
|
} catch (err) {
|
|
186
|
-
// Log but don't break rendering
|
|
187
203
|
console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
|
|
188
204
|
}
|
|
189
205
|
return data;
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js v2.
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* - showAds batching: microtask-based flush instead of setTimeout
|
|
11
|
-
* - Warm network: runs once per session, not per navigation
|
|
12
|
-
* - State machine: clear lifecycle for placeholders (idle → observed → queued → shown → recycled)
|
|
4
|
+
* Based on working v2-fixed with targeted fixes:
|
|
5
|
+
* - Ezoic API: showAds() + destroyPlaceholders() only (per official docs)
|
|
6
|
+
* - cleanup(): destroyAll() before DOM removal, only on real page change
|
|
7
|
+
* - Recycling: conservative threshold (-3vh), age/inflight guards
|
|
8
|
+
* - aria-hidden: MutationObserver protection
|
|
9
|
+
* - Empty check: more conservative timing and GPT slot awareness
|
|
13
10
|
*/
|
|
14
11
|
(function nbbEzoicInfinite() {
|
|
15
12
|
'use strict';
|
|
@@ -19,7 +16,6 @@
|
|
|
19
16
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
20
17
|
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
18
|
|
|
22
|
-
// Data attributes
|
|
23
19
|
const ATTR = {
|
|
24
20
|
ANCHOR: 'data-ezoic-anchor',
|
|
25
21
|
WRAPID: 'data-ezoic-wrapid',
|
|
@@ -27,21 +23,19 @@
|
|
|
27
23
|
SHOWN: 'data-ezoic-shown',
|
|
28
24
|
};
|
|
29
25
|
|
|
30
|
-
// Timing
|
|
31
26
|
const TIMING = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
EMPTY_CHECK_EARLY_MS: 30_000,
|
|
28
|
+
EMPTY_CHECK_LATE_MS: 60_000,
|
|
29
|
+
MIN_PRUNE_AGE_MS: 8_000,
|
|
30
|
+
SHOW_THROTTLE_MS: 900,
|
|
31
|
+
BURST_COOLDOWN_MS: 200,
|
|
32
|
+
BLOCK_DURATION_MS: 1_500,
|
|
33
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
34
|
+
SHOW_RELEASE_MS: 700,
|
|
35
|
+
BATCH_FLUSH_MS: 80,
|
|
36
|
+
RECYCLE_DELAY_MS: 450,
|
|
42
37
|
};
|
|
43
38
|
|
|
44
|
-
// Limits
|
|
45
39
|
const LIMITS = {
|
|
46
40
|
MAX_INSERTS_RUN: 6,
|
|
47
41
|
MAX_INFLIGHT: 4,
|
|
@@ -55,22 +49,20 @@
|
|
|
55
49
|
MOBILE: '3500px 0px 3500px 0px',
|
|
56
50
|
};
|
|
57
51
|
|
|
58
|
-
// Selectors
|
|
59
52
|
const SEL = {
|
|
60
53
|
post: '[component="post"][data-pid]',
|
|
61
54
|
topic: 'li[component="category/topic"]',
|
|
62
55
|
category: 'li[component="categories/category"]',
|
|
63
56
|
};
|
|
64
57
|
|
|
65
|
-
// Kind configuration table — single source of truth per ad type
|
|
66
58
|
const KIND = {
|
|
67
59
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
68
60
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
69
61
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
70
62
|
};
|
|
71
63
|
|
|
72
|
-
// Selector for detecting filled ad slots
|
|
73
64
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
65
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
74
66
|
|
|
75
67
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
76
68
|
|
|
@@ -103,45 +95,37 @@
|
|
|
103
95
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
104
96
|
|
|
105
97
|
const state = {
|
|
106
|
-
// Page context
|
|
107
98
|
pageKey: null,
|
|
108
99
|
kind: null,
|
|
109
100
|
cfg: null,
|
|
110
101
|
|
|
111
|
-
// Pools
|
|
112
102
|
poolsReady: false,
|
|
113
103
|
pools: { topics: [], posts: [], categories: [] },
|
|
114
104
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
115
105
|
|
|
116
|
-
// Mounted placeholders
|
|
117
106
|
mountedIds: new Set(),
|
|
118
107
|
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
119
108
|
lastShow: new Map(), // id → timestamp
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
110
|
+
wrapByKey: new Map(),
|
|
111
|
+
wrapsByClass: new Map(),
|
|
124
112
|
|
|
125
|
-
// Observers
|
|
126
113
|
io: null,
|
|
127
114
|
domObs: null,
|
|
128
115
|
|
|
129
|
-
|
|
130
|
-
mutGuard: 0,
|
|
116
|
+
mutGuard: 0,
|
|
131
117
|
blockedUntil: 0,
|
|
132
118
|
|
|
133
|
-
// Show queue
|
|
134
119
|
inflight: 0,
|
|
135
120
|
pending: [],
|
|
136
121
|
pendingSet: new Set(),
|
|
137
122
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
burstActive: false,
|
|
123
|
+
runQueued: false,
|
|
124
|
+
burstActive: false,
|
|
141
125
|
burstDeadline: 0,
|
|
142
|
-
burstCount:
|
|
143
|
-
lastBurstTs:
|
|
144
|
-
firstShown:
|
|
126
|
+
burstCount: 0,
|
|
127
|
+
lastBurstTs: 0,
|
|
128
|
+
firstShown: false,
|
|
145
129
|
};
|
|
146
130
|
|
|
147
131
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -155,7 +139,6 @@
|
|
|
155
139
|
|
|
156
140
|
async function fetchConfig() {
|
|
157
141
|
if (state.cfg) return state.cfg;
|
|
158
|
-
// Prefer inline config injected by server (zero latency)
|
|
159
142
|
try {
|
|
160
143
|
const inline = window.__nbbEzoicCfg;
|
|
161
144
|
if (inline && typeof inline === 'object') {
|
|
@@ -163,7 +146,6 @@
|
|
|
163
146
|
return state.cfg;
|
|
164
147
|
}
|
|
165
148
|
} catch (_) {}
|
|
166
|
-
// Fallback to API
|
|
167
149
|
try {
|
|
168
150
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
169
151
|
if (r.ok) state.cfg = await r.json();
|
|
@@ -195,7 +177,6 @@
|
|
|
195
177
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
196
178
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
197
179
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
198
|
-
// DOM fallback
|
|
199
180
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
200
181
|
if (document.querySelector(SEL.post)) return 'topic';
|
|
201
182
|
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
@@ -215,7 +196,6 @@
|
|
|
215
196
|
const el = all[i];
|
|
216
197
|
if (!el.isConnected) continue;
|
|
217
198
|
if (!el.querySelector('[component="post/content"]')) continue;
|
|
218
|
-
// Skip nested quotes / parent posts
|
|
219
199
|
const parent = el.parentElement?.closest(SEL.post);
|
|
220
200
|
if (parent && parent !== el) continue;
|
|
221
201
|
if (el.getAttribute('component') === 'post/parent') continue;
|
|
@@ -235,7 +215,6 @@
|
|
|
235
215
|
const v = el.getAttribute(attr);
|
|
236
216
|
if (v != null && v !== '') return v;
|
|
237
217
|
}
|
|
238
|
-
// Positional fallback
|
|
239
218
|
const children = el.parentElement?.children;
|
|
240
219
|
if (!children) return 'i0';
|
|
241
220
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -257,20 +236,12 @@
|
|
|
257
236
|
return set;
|
|
258
237
|
}
|
|
259
238
|
|
|
260
|
-
// ──
|
|
261
|
-
//
|
|
262
|
-
// NodeBB can remove large portions of the DOM during infinite scrolling
|
|
263
|
-
// (especially posts in topics). If a wrap is removed outside of our own
|
|
264
|
-
// dropWrap(), we must free its placeholder id; otherwise the pool appears
|
|
265
|
-
// exhausted and ads won't re-insert when the user scrolls back up.
|
|
239
|
+
// ── GC disconnected wraps (NodeBB virtualization) ──────────────────────────
|
|
266
240
|
|
|
267
241
|
function gcDisconnectedWraps() {
|
|
268
|
-
// 1) Clean wrapByKey
|
|
269
242
|
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
270
243
|
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
271
244
|
}
|
|
272
|
-
|
|
273
|
-
// 2) Clean wrapsByClass sets and free ids
|
|
274
245
|
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
275
246
|
for (const w of Array.from(set)) {
|
|
276
247
|
if (w?.isConnected) continue;
|
|
@@ -286,15 +257,12 @@
|
|
|
286
257
|
}
|
|
287
258
|
if (!set.size) state.wrapsByClass.delete(klass);
|
|
288
259
|
}
|
|
289
|
-
|
|
290
|
-
// 3) Authoritative rebuild of mountedIds from the live DOM
|
|
291
260
|
try {
|
|
292
261
|
const live = new Set();
|
|
293
262
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
294
263
|
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
295
264
|
if (id > 0) live.add(id);
|
|
296
265
|
}
|
|
297
|
-
// Drop any ids that are no longer live
|
|
298
266
|
for (const id of Array.from(state.mountedIds)) {
|
|
299
267
|
if (!live.has(id)) {
|
|
300
268
|
state.mountedIds.delete(id);
|
|
@@ -307,26 +275,16 @@
|
|
|
307
275
|
|
|
308
276
|
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
309
277
|
|
|
310
|
-
/**
|
|
311
|
-
* Check if a wrap element still has its corresponding anchor in the DOM.
|
|
312
|
-
* Uses O(1) registry lookup first, then sibling scan, then global querySelector.
|
|
313
|
-
*/
|
|
314
278
|
function wrapIsLive(wrap) {
|
|
315
279
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
316
280
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
317
281
|
if (!key) return false;
|
|
318
|
-
|
|
319
|
-
// Fast path: registry match
|
|
320
282
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
321
|
-
|
|
322
|
-
// Parse key
|
|
323
283
|
const colonIdx = key.indexOf(':');
|
|
324
284
|
const klass = key.slice(0, colonIdx);
|
|
325
285
|
const anchorId = key.slice(colonIdx + 1);
|
|
326
286
|
const cfg = KIND[klass];
|
|
327
287
|
if (!cfg) return false;
|
|
328
|
-
|
|
329
|
-
// Sibling scan (cheap for adjacent anchors)
|
|
330
288
|
const parent = wrap.parentElement;
|
|
331
289
|
if (parent) {
|
|
332
290
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
@@ -336,8 +294,6 @@
|
|
|
336
294
|
}
|
|
337
295
|
}
|
|
338
296
|
}
|
|
339
|
-
|
|
340
|
-
// Global fallback (expensive, rare)
|
|
341
297
|
try {
|
|
342
298
|
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
343
299
|
} catch (_) { return false; }
|
|
@@ -361,11 +317,8 @@
|
|
|
361
317
|
|
|
362
318
|
function scheduleUncollapseChecks(wrap) {
|
|
363
319
|
if (!wrap) return;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
setTimeout(() => {
|
|
367
|
-
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
368
|
-
}, ms);
|
|
320
|
+
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
321
|
+
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
369
322
|
}
|
|
370
323
|
}
|
|
371
324
|
|
|
@@ -384,19 +337,18 @@
|
|
|
384
337
|
}
|
|
385
338
|
|
|
386
339
|
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
340
|
+
//
|
|
341
|
+
// Per Ezoic docs: destroyPlaceholders(id) → remove old HTML →
|
|
342
|
+
// recreate fresh placeholder → showAds(id).
|
|
387
343
|
|
|
388
|
-
/**
|
|
389
|
-
* When pool is exhausted, recycle a wrap far above the viewport.
|
|
390
|
-
* Sequence: destroy → delay → re-observe → enqueueShow
|
|
391
|
-
*/
|
|
392
344
|
function recycleWrap(klass, targetEl, newKey) {
|
|
393
345
|
const ez = window.ezstandalone;
|
|
394
346
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
395
|
-
typeof ez?.
|
|
396
|
-
typeof ez?.displayMore !== 'function') return null;
|
|
347
|
+
typeof ez?.showAds !== 'function') return null;
|
|
397
348
|
|
|
398
349
|
const vh = window.innerHeight || 800;
|
|
399
|
-
const threshold = -vh;
|
|
350
|
+
const threshold = -(3 * vh);
|
|
351
|
+
const t = now();
|
|
400
352
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
401
353
|
let bestFull = null, bestFullY = Infinity;
|
|
402
354
|
|
|
@@ -405,6 +357,13 @@
|
|
|
405
357
|
|
|
406
358
|
for (const wrap of wraps) {
|
|
407
359
|
try {
|
|
360
|
+
// Skip young wraps (ad might still be loading)
|
|
361
|
+
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
|
+
|
|
408
367
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
409
368
|
if (bottom > threshold) continue;
|
|
410
369
|
if (!isFilled(wrap)) {
|
|
@@ -423,47 +382,48 @@
|
|
|
423
382
|
|
|
424
383
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
425
384
|
|
|
426
|
-
// Unobserve before moving
|
|
385
|
+
// Unobserve before moving
|
|
427
386
|
try {
|
|
428
387
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
429
388
|
if (ph) state.io?.unobserve(ph);
|
|
430
389
|
} catch (_) {}
|
|
431
390
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
435
|
-
best.setAttribute(ATTR.CREATED, String(now()));
|
|
436
|
-
best.setAttribute(ATTR.SHOWN, '0');
|
|
437
|
-
best.classList.remove('is-empty');
|
|
438
|
-
best.replaceChildren();
|
|
439
|
-
|
|
440
|
-
const fresh = document.createElement('div');
|
|
441
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
442
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
443
|
-
fresh.style.minHeight = '1px';
|
|
444
|
-
best.appendChild(fresh);
|
|
445
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Update registry
|
|
449
|
-
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
450
|
-
state.wrapByKey.set(newKey, best);
|
|
451
|
-
|
|
452
|
-
// Ezoic recycle sequence
|
|
453
|
-
const doDestroy = () => {
|
|
391
|
+
// Ezoic recycle: destroy → new DOM → showAds
|
|
392
|
+
const doRecycle = () => {
|
|
454
393
|
state.phState.set(id, 'destroyed');
|
|
455
|
-
try { ez.destroyPlaceholders(id); } catch (_) {
|
|
456
|
-
|
|
457
|
-
}
|
|
394
|
+
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
395
|
+
|
|
458
396
|
setTimeout(() => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
397
|
+
// Recreate fresh placeholder DOM at new position
|
|
398
|
+
mutate(() => {
|
|
399
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
400
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
401
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
402
|
+
best.classList.remove('is-empty');
|
|
403
|
+
best.replaceChildren();
|
|
404
|
+
|
|
405
|
+
const fresh = document.createElement('div');
|
|
406
|
+
fresh.id = `${PH_PREFIX}${id}`;
|
|
407
|
+
fresh.setAttribute('data-ezoic-id', String(id));
|
|
408
|
+
fresh.style.minHeight = '1px';
|
|
409
|
+
best.appendChild(fresh);
|
|
410
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
414
|
+
state.wrapByKey.set(newKey, best);
|
|
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);
|
|
462
422
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
463
423
|
};
|
|
464
424
|
|
|
465
425
|
try {
|
|
466
|
-
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(
|
|
426
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle();
|
|
467
427
|
} catch (_) {}
|
|
468
428
|
|
|
469
429
|
return { id, wrap: best };
|
|
@@ -492,7 +452,6 @@
|
|
|
492
452
|
if (!el?.insertAdjacentElement) return null;
|
|
493
453
|
if (findWrap(key)) return null;
|
|
494
454
|
if (state.mountedIds.has(id)) return null;
|
|
495
|
-
// Ensure no duplicate DOM element with same placeholder ID
|
|
496
455
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
497
456
|
if (existing?.isConnected) return null;
|
|
498
457
|
|
|
@@ -509,17 +468,13 @@
|
|
|
509
468
|
try {
|
|
510
469
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
511
470
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
512
|
-
|
|
513
471
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
514
472
|
if (Number.isFinite(id)) {
|
|
515
473
|
state.mountedIds.delete(id);
|
|
516
474
|
state.phState.delete(id);
|
|
517
475
|
}
|
|
518
|
-
|
|
519
476
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
520
477
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
521
|
-
|
|
522
|
-
// Find the kind class to unregister
|
|
523
478
|
for (const cls of w.classList) {
|
|
524
479
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
525
480
|
state.wrapsByClass.get(cls)?.delete(w);
|
|
@@ -531,9 +486,6 @@
|
|
|
531
486
|
}
|
|
532
487
|
|
|
533
488
|
// ── Prune (category topic lists only) ──────────────────────────────────────
|
|
534
|
-
//
|
|
535
|
-
// Safe for category topic lists: NodeBB does NOT virtualize topics in categories.
|
|
536
|
-
// NOT safe for posts: NodeBB virtualizes posts off-viewport.
|
|
537
489
|
|
|
538
490
|
function pruneOrphansBetween() {
|
|
539
491
|
const klass = 'ezoic-ad-between';
|
|
@@ -541,7 +493,6 @@
|
|
|
541
493
|
const wraps = state.wrapsByClass.get(klass);
|
|
542
494
|
if (!wraps?.size) return;
|
|
543
495
|
|
|
544
|
-
// Build set of live anchor IDs
|
|
545
496
|
const liveAnchors = new Set();
|
|
546
497
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
547
498
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
@@ -552,7 +503,6 @@
|
|
|
552
503
|
for (const w of wraps) {
|
|
553
504
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
554
505
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
555
|
-
|
|
556
506
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
557
507
|
const sid = key.slice(klass.length + 1);
|
|
558
508
|
if (!sid || !liveAnchors.has(sid)) {
|
|
@@ -569,7 +519,6 @@
|
|
|
569
519
|
const v = el.getAttribute(attr);
|
|
570
520
|
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
571
521
|
}
|
|
572
|
-
// Positional fallback
|
|
573
522
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
574
523
|
let i = 0;
|
|
575
524
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -604,7 +553,7 @@
|
|
|
604
553
|
}
|
|
605
554
|
} else {
|
|
606
555
|
const recycled = recycleWrap(klass, el, key);
|
|
607
|
-
if (!recycled) break;
|
|
556
|
+
if (!recycled) break;
|
|
608
557
|
inserted++;
|
|
609
558
|
}
|
|
610
559
|
}
|
|
@@ -728,25 +677,30 @@
|
|
|
728
677
|
}
|
|
729
678
|
|
|
730
679
|
function scheduleEmptyCheck(id, showTs) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
680
|
+
// Two-pass check: conservative to avoid collapsing slow-loading ads
|
|
681
|
+
for (const delay of [TIMING.EMPTY_CHECK_EARLY_MS, TIMING.EMPTY_CHECK_LATE_MS]) {
|
|
682
|
+
setTimeout(() => {
|
|
683
|
+
try {
|
|
684
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
685
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
686
|
+
if (!wrap || !ph?.isConnected) return;
|
|
687
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
688
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
689
|
+
// Don't collapse if a GPT slot exists (might still be loading)
|
|
690
|
+
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
691
|
+
// Don't collapse if placeholder has meaningful height
|
|
692
|
+
if (ph.offsetHeight > 10) return;
|
|
693
|
+
wrap.classList.add('is-empty');
|
|
694
|
+
} catch (_) {}
|
|
695
|
+
}, delay);
|
|
696
|
+
}
|
|
742
697
|
}
|
|
743
698
|
|
|
744
699
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
745
700
|
//
|
|
746
|
-
// Intercepts ez.showAds() to
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// - batch calls for efficiency
|
|
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.
|
|
750
704
|
|
|
751
705
|
function patchShowAds() {
|
|
752
706
|
const apply = () => {
|
|
@@ -763,17 +717,14 @@
|
|
|
763
717
|
const flush = () => {
|
|
764
718
|
flushTimer = null;
|
|
765
719
|
if (isBlocked() || !queue.size) return;
|
|
766
|
-
|
|
767
720
|
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
768
721
|
queue.clear();
|
|
769
|
-
|
|
770
722
|
const valid = ids.filter(id => {
|
|
771
723
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
772
724
|
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
773
725
|
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
774
726
|
return true;
|
|
775
727
|
});
|
|
776
|
-
|
|
777
728
|
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
778
729
|
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
779
730
|
try { orig(...chunk); } catch (_) {
|
|
@@ -783,6 +734,10 @@
|
|
|
783
734
|
};
|
|
784
735
|
|
|
785
736
|
ez.showAds = function (...args) {
|
|
737
|
+
// No-arg call = Ezoic page refresh — pass through unmodified
|
|
738
|
+
if (args.length === 0) {
|
|
739
|
+
return orig();
|
|
740
|
+
}
|
|
786
741
|
if (isBlocked()) return;
|
|
787
742
|
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
788
743
|
for (const v of ids) {
|
|
@@ -812,9 +767,6 @@
|
|
|
812
767
|
|
|
813
768
|
async function runCore() {
|
|
814
769
|
if (isBlocked()) return 0;
|
|
815
|
-
|
|
816
|
-
// Keep internal pools in sync with NodeBB DOM virtualization/removals
|
|
817
|
-
// so ads can re-insert correctly when users scroll back up.
|
|
818
770
|
try { gcDisconnectedWraps(); } catch (_) {}
|
|
819
771
|
|
|
820
772
|
const cfg = await fetchConfig();
|
|
@@ -836,7 +788,6 @@
|
|
|
836
788
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
837
789
|
);
|
|
838
790
|
}
|
|
839
|
-
|
|
840
791
|
if (kind === 'categoryTopics') {
|
|
841
792
|
pruneOrphansBetween();
|
|
842
793
|
return exec(
|
|
@@ -844,7 +795,6 @@
|
|
|
844
795
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
845
796
|
);
|
|
846
797
|
}
|
|
847
|
-
|
|
848
798
|
return exec(
|
|
849
799
|
'ezoic-ad-categories', getCategories,
|
|
850
800
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
@@ -872,11 +822,9 @@
|
|
|
872
822
|
state.lastBurstTs = t;
|
|
873
823
|
state.pageKey = pageKey();
|
|
874
824
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
875
|
-
|
|
876
825
|
if (state.burstActive) return;
|
|
877
826
|
state.burstActive = true;
|
|
878
827
|
state.burstCount = 0;
|
|
879
|
-
|
|
880
828
|
const step = () => {
|
|
881
829
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
882
830
|
state.burstActive = false;
|
|
@@ -895,10 +843,18 @@
|
|
|
895
843
|
|
|
896
844
|
function cleanup() {
|
|
897
845
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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();
|
|
901
853
|
}
|
|
854
|
+
} catch (_) {}
|
|
855
|
+
|
|
856
|
+
mutate(() => {
|
|
857
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
902
858
|
});
|
|
903
859
|
state.cfg = null;
|
|
904
860
|
state.poolsReady = false;
|
|
@@ -919,8 +875,6 @@
|
|
|
919
875
|
}
|
|
920
876
|
|
|
921
877
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
922
|
-
//
|
|
923
|
-
// Scoped to detect: (1) ad fill events in wraps, (2) new content items
|
|
924
878
|
|
|
925
879
|
function ensureDomObserver() {
|
|
926
880
|
if (state.domObs) return;
|
|
@@ -929,19 +883,17 @@
|
|
|
929
883
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
930
884
|
|
|
931
885
|
let needsBurst = false;
|
|
932
|
-
|
|
933
|
-
// Determine relevant selectors for current page kind
|
|
934
886
|
const kind = getKind();
|
|
935
887
|
const relevantSels =
|
|
936
|
-
kind === 'topic'
|
|
937
|
-
kind === 'categoryTopics'? [SEL.topic] :
|
|
938
|
-
kind === 'categories'
|
|
939
|
-
|
|
888
|
+
kind === 'topic' ? [SEL.post] :
|
|
889
|
+
kind === 'categoryTopics' ? [SEL.topic] :
|
|
890
|
+
kind === 'categories' ? [SEL.category] :
|
|
891
|
+
[SEL.post, SEL.topic, SEL.category];
|
|
940
892
|
|
|
941
893
|
for (const m of muts) {
|
|
942
894
|
if (m.type !== 'childList') continue;
|
|
943
895
|
|
|
944
|
-
//
|
|
896
|
+
// Free IDs from wraps removed by NodeBB virtualization
|
|
945
897
|
for (const node of m.removedNodes) {
|
|
946
898
|
if (!(node instanceof Element)) continue;
|
|
947
899
|
try {
|
|
@@ -949,9 +901,7 @@
|
|
|
949
901
|
dropWrap(node);
|
|
950
902
|
} else {
|
|
951
903
|
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
952
|
-
if (wraps?.length) {
|
|
953
|
-
for (const w of wraps) dropWrap(w);
|
|
954
|
-
}
|
|
904
|
+
if (wraps?.length) { for (const w of wraps) dropWrap(w); }
|
|
955
905
|
}
|
|
956
906
|
} catch (_) {}
|
|
957
907
|
}
|
|
@@ -959,7 +909,7 @@
|
|
|
959
909
|
for (const node of m.addedNodes) {
|
|
960
910
|
if (!(node instanceof Element)) continue;
|
|
961
911
|
|
|
962
|
-
//
|
|
912
|
+
// Ad fill detection
|
|
963
913
|
try {
|
|
964
914
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
965
915
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -968,14 +918,24 @@
|
|
|
968
918
|
}
|
|
969
919
|
} catch (_) {}
|
|
970
920
|
|
|
971
|
-
//
|
|
921
|
+
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
922
|
+
try {
|
|
923
|
+
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
924
|
+
? [node]
|
|
925
|
+
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
926
|
+
for (const wrap of wraps) {
|
|
927
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
928
|
+
if (!id || id <= 0) continue;
|
|
929
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
930
|
+
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
931
|
+
}
|
|
932
|
+
} catch (_) {}
|
|
933
|
+
|
|
934
|
+
// New content detection
|
|
972
935
|
if (!needsBurst) {
|
|
973
936
|
for (const sel of relevantSels) {
|
|
974
937
|
try {
|
|
975
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
976
|
-
needsBurst = true;
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
938
|
+
if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
|
|
979
939
|
} catch (_) {}
|
|
980
940
|
}
|
|
981
941
|
}
|
|
@@ -991,52 +951,25 @@
|
|
|
991
951
|
} catch (_) {}
|
|
992
952
|
}
|
|
993
953
|
|
|
994
|
-
// ── TCF / CMP Protection
|
|
995
|
-
//
|
|
996
|
-
// Root cause of the CMP errors:
|
|
997
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
998
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
999
|
-
//
|
|
1000
|
-
// The CMP (Gatekeeper Consent) communicates via postMessage on the
|
|
1001
|
-
// __tcfapiLocator iframe's contentWindow. During NodeBB ajaxify navigation,
|
|
1002
|
-
// jQuery's html() or empty() on the content area can cascade and remove
|
|
1003
|
-
// iframes from <body>. The CMP then calls getTCData on a stale reference
|
|
1004
|
-
// where contentWindow is null.
|
|
1005
|
-
//
|
|
1006
|
-
// Strategy (3 layers):
|
|
1007
|
-
//
|
|
1008
|
-
// 1. PROTECT: Move the locator iframe into <head> where ajaxify never
|
|
1009
|
-
// touches it. The TCF spec only requires the iframe to exist in the
|
|
1010
|
-
// document with name="__tcfapiLocator" — it works from <head>.
|
|
1011
|
-
//
|
|
1012
|
-
// 2. GUARD: Wrap __tcfapi and __cmp with a safety layer that catches
|
|
1013
|
-
// errors in the CMP's internal getTCData, preventing the uncaught
|
|
1014
|
-
// TypeError from propagating.
|
|
1015
|
-
//
|
|
1016
|
-
// 3. RESTORE: MutationObserver on <body> childList (not subtree) to
|
|
1017
|
-
// immediately re-create the locator if something still removes it.
|
|
954
|
+
// ── TCF / CMP Protection (3 layers) ────────────────────────────────────────
|
|
1018
955
|
|
|
1019
956
|
function ensureTcfLocator() {
|
|
1020
957
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1021
958
|
|
|
1022
959
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
1023
960
|
|
|
1024
|
-
// Create or relocate the locator iframe into <head> for protection
|
|
1025
961
|
const ensureInHead = () => {
|
|
1026
962
|
let existing = document.getElementById(LOCATOR_ID);
|
|
1027
963
|
if (existing) {
|
|
1028
|
-
// If it's in <body>, move it to <head> where ajaxify can't reach it
|
|
1029
964
|
if (existing.parentElement !== document.head) {
|
|
1030
965
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
1031
966
|
}
|
|
1032
967
|
return existing;
|
|
1033
968
|
}
|
|
1034
|
-
// Create fresh
|
|
1035
969
|
const f = document.createElement('iframe');
|
|
1036
970
|
f.style.display = 'none';
|
|
1037
971
|
f.id = f.name = LOCATOR_ID;
|
|
1038
972
|
try { document.head.appendChild(f); } catch (_) {
|
|
1039
|
-
// Fallback to body if head insertion fails
|
|
1040
973
|
(document.body || document.documentElement).appendChild(f);
|
|
1041
974
|
}
|
|
1042
975
|
return f;
|
|
@@ -1044,11 +977,8 @@
|
|
|
1044
977
|
|
|
1045
978
|
ensureInHead();
|
|
1046
979
|
|
|
1047
|
-
// Layer 2: Guard the CMP API calls against null contentWindow
|
|
1048
980
|
if (!window.__nbbCmpGuarded) {
|
|
1049
981
|
window.__nbbCmpGuarded = true;
|
|
1050
|
-
|
|
1051
|
-
// Wrap __tcfapi
|
|
1052
982
|
if (typeof window.__tcfapi === 'function') {
|
|
1053
983
|
const origTcf = window.__tcfapi;
|
|
1054
984
|
window.__tcfapi = function (cmd, version, cb, param) {
|
|
@@ -1057,23 +987,18 @@
|
|
|
1057
987
|
try { cb?.(...args); } catch (_) {}
|
|
1058
988
|
}, param);
|
|
1059
989
|
} catch (e) {
|
|
1060
|
-
// If the error is the null postMessage/addtlConsent, swallow it
|
|
1061
990
|
if (e?.message?.includes('null')) {
|
|
1062
|
-
// Re-ensure locator exists, then retry once
|
|
1063
991
|
ensureInHead();
|
|
1064
992
|
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
1065
993
|
}
|
|
1066
994
|
}
|
|
1067
995
|
};
|
|
1068
996
|
}
|
|
1069
|
-
|
|
1070
|
-
// Wrap __cmp (legacy CMP v1 API)
|
|
1071
997
|
if (typeof window.__cmp === 'function') {
|
|
1072
998
|
const origCmp = window.__cmp;
|
|
1073
999
|
window.__cmp = function (...args) {
|
|
1074
|
-
try {
|
|
1075
|
-
|
|
1076
|
-
} catch (e) {
|
|
1000
|
+
try { return origCmp.apply(this, args); }
|
|
1001
|
+
catch (e) {
|
|
1077
1002
|
if (e?.message?.includes('null')) {
|
|
1078
1003
|
ensureInHead();
|
|
1079
1004
|
try { return origCmp.apply(this, args); } catch (_) {}
|
|
@@ -1083,37 +1008,47 @@
|
|
|
1083
1008
|
}
|
|
1084
1009
|
}
|
|
1085
1010
|
|
|
1086
|
-
// Layer 3: MutationObserver to immediately restore if removed
|
|
1087
1011
|
if (!window.__nbbTcfObs) {
|
|
1088
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
1089
|
-
|
|
1090
|
-
if (document.getElementById(LOCATOR_ID)) return;
|
|
1091
|
-
// Something removed it — restore immediately (no debounce)
|
|
1092
|
-
ensureInHead();
|
|
1012
|
+
window.__nbbTcfObs = new MutationObserver(() => {
|
|
1013
|
+
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
1093
1014
|
});
|
|
1094
|
-
// Observe body direct children only (the most likely removal point)
|
|
1095
1015
|
try {
|
|
1096
1016
|
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1097
|
-
childList: true,
|
|
1098
|
-
subtree: false,
|
|
1017
|
+
childList: true, subtree: false,
|
|
1099
1018
|
});
|
|
1100
1019
|
} catch (_) {}
|
|
1101
|
-
// Also observe <head> in case something cleans it
|
|
1102
1020
|
try {
|
|
1103
1021
|
if (document.head) {
|
|
1104
1022
|
window.__nbbTcfObs.observe(document.head, {
|
|
1105
|
-
childList: true,
|
|
1106
|
-
subtree: false,
|
|
1023
|
+
childList: true, subtree: false,
|
|
1107
1024
|
});
|
|
1108
1025
|
}
|
|
1109
1026
|
} catch (_) {}
|
|
1110
1027
|
}
|
|
1111
1028
|
}
|
|
1112
1029
|
|
|
1030
|
+
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
1031
|
+
|
|
1032
|
+
function protectAriaHidden() {
|
|
1033
|
+
if (window.__nbbAriaObs) return;
|
|
1034
|
+
const remove = () => {
|
|
1035
|
+
try {
|
|
1036
|
+
if (document.body.getAttribute('aria-hidden') === 'true') {
|
|
1037
|
+
document.body.removeAttribute('aria-hidden');
|
|
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 (_) {}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1113
1051
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
1114
|
-
//
|
|
1115
|
-
// Mute noisy Ezoic warnings that are expected in infinite scroll context.
|
|
1116
|
-
// Uses startsWith checks instead of includes for performance.
|
|
1117
1052
|
|
|
1118
1053
|
function muteConsole() {
|
|
1119
1054
|
if (window.__nbbEzMuted) return;
|
|
@@ -1127,8 +1062,14 @@
|
|
|
1127
1062
|
'Debugger iframe already exists',
|
|
1128
1063
|
'[CMP] Error in custom getTCData',
|
|
1129
1064
|
'vignette: no interstitial API',
|
|
1065
|
+
'Ezoic JS-Enable should only ever',
|
|
1066
|
+
];
|
|
1067
|
+
const PATTERNS = [
|
|
1068
|
+
`with id ${PH_PREFIX}`,
|
|
1069
|
+
'adsbygoogle.push() error: All',
|
|
1070
|
+
'has already been defined',
|
|
1071
|
+
'bad response. Status',
|
|
1130
1072
|
];
|
|
1131
|
-
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
1132
1073
|
|
|
1133
1074
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
1134
1075
|
const orig = console[method];
|
|
@@ -1136,10 +1077,8 @@
|
|
|
1136
1077
|
console[method] = function (...args) {
|
|
1137
1078
|
if (typeof args[0] === 'string') {
|
|
1138
1079
|
const msg = args[0];
|
|
1139
|
-
for (const
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
if (msg.includes(PH_PATTERN)) return;
|
|
1080
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
1081
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
1143
1082
|
}
|
|
1144
1083
|
return orig.apply(console, args);
|
|
1145
1084
|
};
|
|
@@ -1147,17 +1086,14 @@
|
|
|
1147
1086
|
}
|
|
1148
1087
|
|
|
1149
1088
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1150
|
-
// Run once per session — preconnect hints are in <head> via server-side injection
|
|
1151
1089
|
|
|
1152
1090
|
let _networkWarmed = false;
|
|
1153
1091
|
|
|
1154
1092
|
function warmNetwork() {
|
|
1155
1093
|
if (_networkWarmed) return;
|
|
1156
1094
|
_networkWarmed = true;
|
|
1157
|
-
|
|
1158
1095
|
const head = document.head;
|
|
1159
1096
|
if (!head) return;
|
|
1160
|
-
|
|
1161
1097
|
const hints = [
|
|
1162
1098
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
1163
1099
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
@@ -1166,7 +1102,6 @@
|
|
|
1166
1102
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1167
1103
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1168
1104
|
];
|
|
1169
|
-
|
|
1170
1105
|
for (const [rel, href, cors] of hints) {
|
|
1171
1106
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1172
1107
|
const link = document.createElement('link');
|
|
@@ -1185,18 +1120,24 @@
|
|
|
1185
1120
|
|
|
1186
1121
|
$(window).off('.nbbEzoic');
|
|
1187
1122
|
|
|
1188
|
-
|
|
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
|
+
});
|
|
1189
1132
|
|
|
1190
1133
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1191
1134
|
state.pageKey = pageKey();
|
|
1192
1135
|
state.kind = null;
|
|
1193
1136
|
state.blockedUntil = 0;
|
|
1194
1137
|
|
|
1195
|
-
// Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
|
|
1196
|
-
try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1197
|
-
|
|
1198
1138
|
muteConsole();
|
|
1199
1139
|
ensureTcfLocator();
|
|
1140
|
+
protectAriaHidden();
|
|
1200
1141
|
warmNetwork();
|
|
1201
1142
|
patchShowAds();
|
|
1202
1143
|
getIO();
|
|
@@ -1204,7 +1145,6 @@
|
|
|
1204
1145
|
requestBurst();
|
|
1205
1146
|
});
|
|
1206
1147
|
|
|
1207
|
-
// Content-loaded events trigger burst
|
|
1208
1148
|
const burstEvents = [
|
|
1209
1149
|
'action:ajaxify.contentLoaded',
|
|
1210
1150
|
'action:posts.loaded',
|
|
@@ -1216,7 +1156,6 @@
|
|
|
1216
1156
|
|
|
1217
1157
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1218
1158
|
|
|
1219
|
-
// Also bind via NodeBB hooks module (for compatibility)
|
|
1220
1159
|
try {
|
|
1221
1160
|
require(['hooks'], hooks => {
|
|
1222
1161
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -1250,6 +1189,7 @@
|
|
|
1250
1189
|
state.pageKey = pageKey();
|
|
1251
1190
|
muteConsole();
|
|
1252
1191
|
ensureTcfLocator();
|
|
1192
|
+
protectAriaHidden();
|
|
1253
1193
|
warmNetwork();
|
|
1254
1194
|
patchShowAds();
|
|
1255
1195
|
getIO();
|