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