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