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