nodebb-plugin-ezoic-infinite 1.8.39 → 1.8.41
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 +628 -338
package/public/client.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Complete rewrite: performance optimization, CMP/TCF fix, clean architecture.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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)
|
|
13
13
|
*/
|
|
14
14
|
(function nbbEzoicInfinite() {
|
|
15
15
|
'use strict';
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
20
20
|
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
21
|
|
|
22
|
+
// Data attributes
|
|
22
23
|
const ATTR = {
|
|
23
24
|
ANCHOR: 'data-ezoic-anchor',
|
|
24
25
|
WRAPID: 'data-ezoic-wrapid',
|
|
@@ -26,19 +27,25 @@
|
|
|
26
27
|
SHOWN: 'data-ezoic-shown',
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
// Timing
|
|
29
31
|
const TIMING = {
|
|
30
32
|
EMPTY_CHECK_MS: 20_000,
|
|
31
33
|
MIN_PRUNE_AGE_MS: 8_000,
|
|
32
34
|
SHOW_THROTTLE_MS: 900,
|
|
33
35
|
BURST_COOLDOWN_MS: 200,
|
|
34
36
|
BLOCK_DURATION_MS: 1_500,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
38
|
+
SHOW_RELEASE_MS: 700,
|
|
39
|
+
BATCH_FLUSH_MS: 80,
|
|
40
|
+
RECYCLE_DELAY_MS: 450,
|
|
41
|
+
|
|
38
42
|
};
|
|
39
43
|
|
|
44
|
+
// Limits
|
|
40
45
|
const LIMITS = {
|
|
41
46
|
MAX_INSERTS_RUN: 6,
|
|
47
|
+
MAX_INFLIGHT: 4,
|
|
48
|
+
BATCH_SIZE: 3,
|
|
42
49
|
MAX_BURST_STEPS: 8,
|
|
43
50
|
BURST_WINDOW_MS: 2_000,
|
|
44
51
|
};
|
|
@@ -48,39 +55,47 @@
|
|
|
48
55
|
MOBILE: '3500px 0px 3500px 0px',
|
|
49
56
|
};
|
|
50
57
|
|
|
58
|
+
// Selectors
|
|
51
59
|
const SEL = {
|
|
52
60
|
post: '[component="post"][data-pid]',
|
|
53
61
|
topic: 'li[component="category/topic"]',
|
|
54
62
|
category: 'li[component="categories/category"]',
|
|
55
63
|
};
|
|
56
64
|
|
|
65
|
+
// Kind configuration table — single source of truth per ad type
|
|
57
66
|
const KIND = {
|
|
58
67
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
59
68
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
60
69
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
61
70
|
};
|
|
62
71
|
|
|
72
|
+
// Selector for detecting filled ad slots
|
|
63
73
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
64
74
|
|
|
65
|
-
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
66
|
-
|
|
67
75
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
68
76
|
|
|
69
77
|
const now = () => Date.now();
|
|
70
78
|
const isMobile = () => window.innerWidth < 768;
|
|
71
79
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
72
80
|
|
|
73
|
-
function isFilled(node)
|
|
81
|
+
function isFilled(node) {
|
|
82
|
+
return node?.querySelector?.(FILL_SEL) != null;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
function isPlaceholderUsed(ph) {
|
|
75
86
|
if (!ph?.isConnected) return false;
|
|
76
87
|
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
function parseIds(raw) {
|
|
80
|
-
const out = []
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
81
93
|
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
82
94
|
const n = parseInt(line, 10);
|
|
83
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
95
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
96
|
+
seen.add(n);
|
|
97
|
+
out.push(n);
|
|
98
|
+
}
|
|
84
99
|
}
|
|
85
100
|
return out;
|
|
86
101
|
}
|
|
@@ -88,30 +103,45 @@
|
|
|
88
103
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
89
104
|
|
|
90
105
|
const state = {
|
|
91
|
-
|
|
106
|
+
// Page context
|
|
107
|
+
pageKey: null,
|
|
108
|
+
kind: null,
|
|
109
|
+
cfg: null,
|
|
92
110
|
|
|
111
|
+
// Pools
|
|
93
112
|
poolsReady: false,
|
|
94
|
-
pools:
|
|
95
|
-
cursors:
|
|
113
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
114
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
115
|
+
|
|
116
|
+
// Mounted placeholders
|
|
117
|
+
mountedIds: new Set(),
|
|
118
|
+
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
119
|
+
lastShow: new Map(), // id → timestamp
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
lastShow: new Map(),
|
|
101
|
-
wrapByKey: new Map(),
|
|
102
|
-
wrapsByClass: new Map(),
|
|
121
|
+
// Wrap registry
|
|
122
|
+
wrapByKey: new Map(), // anchorKey → wrap element
|
|
123
|
+
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
// Observers
|
|
126
|
+
io: null,
|
|
127
|
+
domObs: null,
|
|
106
128
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
// Guards
|
|
130
|
+
mutGuard: 0,
|
|
131
|
+
blockedUntil: 0,
|
|
110
132
|
|
|
111
|
-
//
|
|
112
|
-
|
|
133
|
+
// Show queue
|
|
134
|
+
inflight: 0,
|
|
135
|
+
pending: [],
|
|
136
|
+
pendingSet: new Set(),
|
|
137
|
+
|
|
138
|
+
// Scheduler
|
|
113
139
|
runQueued: false,
|
|
114
|
-
burstActive: false,
|
|
140
|
+
burstActive: false,
|
|
141
|
+
burstDeadline: 0,
|
|
142
|
+
burstCount: 0,
|
|
143
|
+
lastBurstTs: 0,
|
|
144
|
+
firstShown: false,
|
|
115
145
|
};
|
|
116
146
|
|
|
117
147
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -125,10 +155,15 @@
|
|
|
125
155
|
|
|
126
156
|
async function fetchConfig() {
|
|
127
157
|
if (state.cfg) return state.cfg;
|
|
158
|
+
// Prefer inline config injected by server (zero latency)
|
|
128
159
|
try {
|
|
129
160
|
const inline = window.__nbbEzoicCfg;
|
|
130
|
-
if (inline && typeof inline === 'object') {
|
|
161
|
+
if (inline && typeof inline === 'object') {
|
|
162
|
+
state.cfg = inline;
|
|
163
|
+
return state.cfg;
|
|
164
|
+
}
|
|
131
165
|
} catch (_) {}
|
|
166
|
+
// Fallback to API
|
|
132
167
|
try {
|
|
133
168
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
134
169
|
if (r.ok) state.cfg = await r.json();
|
|
@@ -160,22 +195,27 @@
|
|
|
160
195
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
161
196
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
162
197
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
198
|
+
// DOM fallback
|
|
163
199
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
164
200
|
if (document.querySelector(SEL.post)) return 'topic';
|
|
165
201
|
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
166
202
|
return 'other';
|
|
167
203
|
}
|
|
168
204
|
|
|
169
|
-
function getKind() {
|
|
205
|
+
function getKind() {
|
|
206
|
+
return state.kind || (state.kind = detectKind());
|
|
207
|
+
}
|
|
170
208
|
|
|
171
209
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
172
210
|
|
|
173
211
|
function getPosts() {
|
|
174
|
-
const all = document.querySelectorAll(SEL.post)
|
|
212
|
+
const all = document.querySelectorAll(SEL.post);
|
|
213
|
+
const out = [];
|
|
175
214
|
for (let i = 0; i < all.length; i++) {
|
|
176
215
|
const el = all[i];
|
|
177
216
|
if (!el.isConnected) continue;
|
|
178
217
|
if (!el.querySelector('[component="post/content"]')) continue;
|
|
218
|
+
// Skip nested quotes / parent posts
|
|
179
219
|
const parent = el.parentElement?.closest(SEL.post);
|
|
180
220
|
if (parent && parent !== el) continue;
|
|
181
221
|
if (el.getAttribute('component') === 'post/parent') continue;
|
|
@@ -183,6 +223,7 @@
|
|
|
183
223
|
}
|
|
184
224
|
return out;
|
|
185
225
|
}
|
|
226
|
+
|
|
186
227
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
187
228
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
188
229
|
|
|
@@ -190,10 +231,16 @@
|
|
|
190
231
|
|
|
191
232
|
function stableId(klass, el) {
|
|
192
233
|
const attr = KIND[klass]?.anchorAttr;
|
|
193
|
-
if (attr) {
|
|
234
|
+
if (attr) {
|
|
235
|
+
const v = el.getAttribute(attr);
|
|
236
|
+
if (v != null && v !== '') return v;
|
|
237
|
+
}
|
|
238
|
+
// Positional fallback
|
|
194
239
|
const children = el.parentElement?.children;
|
|
195
240
|
if (!children) return 'i0';
|
|
196
|
-
for (let i = 0; i < children.length; i++) {
|
|
241
|
+
for (let i = 0; i < children.length; i++) {
|
|
242
|
+
if (children[i] === el) return `i${i}`;
|
|
243
|
+
}
|
|
197
244
|
return 'i0';
|
|
198
245
|
}
|
|
199
246
|
|
|
@@ -205,30 +252,95 @@
|
|
|
205
252
|
}
|
|
206
253
|
|
|
207
254
|
function getWrapSet(klass) {
|
|
208
|
-
let
|
|
209
|
-
if (!
|
|
210
|
-
return
|
|
255
|
+
let set = state.wrapsByClass.get(klass);
|
|
256
|
+
if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
|
|
257
|
+
return set;
|
|
258
|
+
}
|
|
259
|
+
|
|
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.
|
|
266
|
+
|
|
267
|
+
function gcDisconnectedWraps() {
|
|
268
|
+
// 1) Clean wrapByKey
|
|
269
|
+
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
270
|
+
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2) Clean wrapsByClass sets and free ids
|
|
274
|
+
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
275
|
+
for (const w of Array.from(set)) {
|
|
276
|
+
if (w?.isConnected) continue;
|
|
277
|
+
set.delete(w);
|
|
278
|
+
try {
|
|
279
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
280
|
+
if (Number.isFinite(id)) {
|
|
281
|
+
state.mountedIds.delete(id);
|
|
282
|
+
state.phState.delete(id);
|
|
283
|
+
state.lastShow.delete(id);
|
|
284
|
+
}
|
|
285
|
+
} catch (_) {}
|
|
286
|
+
}
|
|
287
|
+
if (!set.size) state.wrapsByClass.delete(klass);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3) Authoritative rebuild of mountedIds from the live DOM
|
|
291
|
+
try {
|
|
292
|
+
const live = new Set();
|
|
293
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
294
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
295
|
+
if (id > 0) live.add(id);
|
|
296
|
+
}
|
|
297
|
+
// Drop any ids that are no longer live
|
|
298
|
+
for (const id of Array.from(state.mountedIds)) {
|
|
299
|
+
if (!live.has(id)) {
|
|
300
|
+
state.mountedIds.delete(id);
|
|
301
|
+
state.phState.delete(id);
|
|
302
|
+
state.lastShow.delete(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (_) {}
|
|
211
306
|
}
|
|
212
307
|
|
|
213
|
-
// ── Wrap lifecycle
|
|
308
|
+
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
214
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
|
+
*/
|
|
215
314
|
function wrapIsLive(wrap) {
|
|
216
315
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
217
316
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
218
317
|
if (!key) return false;
|
|
318
|
+
|
|
319
|
+
// Fast path: registry match
|
|
219
320
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
321
|
+
|
|
322
|
+
// Parse key
|
|
220
323
|
const colonIdx = key.indexOf(':');
|
|
221
|
-
const klass
|
|
324
|
+
const klass = key.slice(0, colonIdx);
|
|
325
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
222
326
|
const cfg = KIND[klass];
|
|
223
327
|
if (!cfg) return false;
|
|
328
|
+
|
|
329
|
+
// Sibling scan (cheap for adjacent anchors)
|
|
224
330
|
const parent = wrap.parentElement;
|
|
225
331
|
if (parent) {
|
|
226
332
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
227
333
|
for (const sib of parent.children) {
|
|
228
|
-
if (sib !== wrap) {
|
|
334
|
+
if (sib !== wrap) {
|
|
335
|
+
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
336
|
+
}
|
|
229
337
|
}
|
|
230
338
|
}
|
|
231
|
-
|
|
339
|
+
|
|
340
|
+
// Global fallback (expensive, rare)
|
|
341
|
+
try {
|
|
342
|
+
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
343
|
+
} catch (_) { return false; }
|
|
232
344
|
}
|
|
233
345
|
|
|
234
346
|
function adjacentWrap(el) {
|
|
@@ -243,18 +355,21 @@
|
|
|
243
355
|
if (!ph || !isFilled(ph)) return false;
|
|
244
356
|
wrap.classList.remove('is-empty');
|
|
245
357
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
246
|
-
if (id > 0) state.phState.set(id, '
|
|
358
|
+
if (id > 0) state.phState.set(id, 'shown');
|
|
247
359
|
return true;
|
|
248
360
|
}
|
|
249
361
|
|
|
250
362
|
function scheduleUncollapseChecks(wrap) {
|
|
251
363
|
if (!wrap) return;
|
|
252
|
-
|
|
253
|
-
|
|
364
|
+
const delays = [500, 1500, 3000, 7000, 15000];
|
|
365
|
+
for (const ms of delays) {
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
368
|
+
}, ms);
|
|
254
369
|
}
|
|
255
370
|
}
|
|
256
371
|
|
|
257
|
-
// ── Pool
|
|
372
|
+
// ── Pool management ────────────────────────────────────────────────────────
|
|
258
373
|
|
|
259
374
|
function pickId(poolKey) {
|
|
260
375
|
const pool = state.pools[poolKey];
|
|
@@ -268,141 +383,12 @@
|
|
|
268
383
|
return null;
|
|
269
384
|
}
|
|
270
385
|
|
|
271
|
-
// ──
|
|
272
|
-
//
|
|
273
|
-
// Correct Ezoic infinite scroll flow:
|
|
274
|
-
// First batch: ez.cmd.push(() => { ez.define(...ids); ez.enable(); })
|
|
275
|
-
// Next batches: ez.cmd.push(() => { ez.define(...ids); ez.displayMore(...ids); })
|
|
276
|
-
// Recycle: ez.cmd.push(() => { ez.destroyPlaceholders(id); })
|
|
277
|
-
// → wait → new DOM → ez.cmd.push(() => { ez.define(id); ez.displayMore(id); })
|
|
278
|
-
//
|
|
279
|
-
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
280
|
-
// Ezoic API on every single placeholder insertion.
|
|
281
|
-
|
|
282
|
-
function ezCmd(fn) {
|
|
283
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
284
|
-
const ez = window.ezstandalone;
|
|
285
|
-
if (Array.isArray(ez.cmd)) {
|
|
286
|
-
ez.cmd.push(fn);
|
|
287
|
-
} else {
|
|
288
|
-
try { fn(); } catch (_) {}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Queue a placeholder ID for the next batched define+displayMore call.
|
|
294
|
-
*/
|
|
295
|
-
function ezEnqueue(id) {
|
|
296
|
-
if (isBlocked()) return;
|
|
297
|
-
state.ezBatch.add(id);
|
|
298
|
-
if (!state.ezFlushTimer) {
|
|
299
|
-
state.ezFlushTimer = setTimeout(ezFlush, TIMING.BATCH_FLUSH_MS);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Flush: define all queued IDs, then enable (first time) or displayMore.
|
|
305
|
-
*/
|
|
306
|
-
function ezFlush() {
|
|
307
|
-
state.ezFlushTimer = null;
|
|
308
|
-
if (isBlocked() || !state.ezBatch.size) return;
|
|
309
|
-
|
|
310
|
-
// Filter to only valid, connected placeholders
|
|
311
|
-
const ids = [];
|
|
312
|
-
for (const id of state.ezBatch) {
|
|
313
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
314
|
-
if (!ph?.isConnected) { state.phState.delete(id); continue; }
|
|
315
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'displayed'); continue; }
|
|
316
|
-
ids.push(id);
|
|
317
|
-
}
|
|
318
|
-
state.ezBatch.clear();
|
|
319
|
-
if (!ids.length) return;
|
|
320
|
-
|
|
321
|
-
const isFirst = !state.enableCalled;
|
|
322
|
-
|
|
323
|
-
ezCmd(() => {
|
|
324
|
-
const ez = window.ezstandalone;
|
|
325
|
-
if (!ez) return;
|
|
326
|
-
|
|
327
|
-
// define() registers the placeholder IDs with Ezoic
|
|
328
|
-
try {
|
|
329
|
-
if (typeof ez.define === 'function') {
|
|
330
|
-
ez.define(...ids);
|
|
331
|
-
}
|
|
332
|
-
} catch (_) {}
|
|
333
|
-
|
|
334
|
-
for (const id of ids) state.phState.set(id, 'defined');
|
|
335
|
-
|
|
336
|
-
if (isFirst) {
|
|
337
|
-
// First batch on this page: use enable() which triggers the initial ad request
|
|
338
|
-
state.enableCalled = true;
|
|
339
|
-
try {
|
|
340
|
-
if (typeof ez.enable === 'function') {
|
|
341
|
-
ez.enable();
|
|
342
|
-
}
|
|
343
|
-
} catch (_) {}
|
|
344
|
-
} else {
|
|
345
|
-
// Subsequent batches: use displayMore() for infinite scroll
|
|
346
|
-
try {
|
|
347
|
-
if (typeof ez.displayMore === 'function') {
|
|
348
|
-
ez.displayMore(...ids);
|
|
349
|
-
}
|
|
350
|
-
} catch (_) {}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Mark as displayed and schedule fill checks
|
|
354
|
-
for (const id of ids) {
|
|
355
|
-
state.phState.set(id, 'displayed');
|
|
356
|
-
state.lastShow.set(id, now());
|
|
357
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
358
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
359
|
-
if (wrap) {
|
|
360
|
-
wrap.setAttribute(ATTR.SHOWN, String(now()));
|
|
361
|
-
scheduleUncollapseChecks(wrap);
|
|
362
|
-
}
|
|
363
|
-
scheduleEmptyCheck(id);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
}
|
|
386
|
+
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
367
387
|
|
|
368
388
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
389
|
+
* When pool is exhausted, recycle a wrap far above the viewport.
|
|
390
|
+
* Sequence: destroy → delay → re-observe → enqueueShow
|
|
371
391
|
*/
|
|
372
|
-
function ezDestroy(id) {
|
|
373
|
-
return new Promise(resolve => {
|
|
374
|
-
ezCmd(() => {
|
|
375
|
-
state.phState.set(id, 'destroyed');
|
|
376
|
-
const ez = window.ezstandalone;
|
|
377
|
-
try {
|
|
378
|
-
if (typeof ez?.destroyPlaceholders === 'function') {
|
|
379
|
-
ez.destroyPlaceholders(id);
|
|
380
|
-
}
|
|
381
|
-
} catch (_) {}
|
|
382
|
-
setTimeout(resolve, TIMING.RECYCLE_DESTROY_MS);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function scheduleEmptyCheck(id) {
|
|
388
|
-
const showTs = now();
|
|
389
|
-
setTimeout(() => {
|
|
390
|
-
try {
|
|
391
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
392
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
393
|
-
if (!wrap || !ph?.isConnected) return;
|
|
394
|
-
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
395
|
-
if (clearEmptyIfFilled(wrap)) return;
|
|
396
|
-
wrap.classList.add('is-empty');
|
|
397
|
-
} catch (_) {}
|
|
398
|
-
}, TIMING.EMPTY_CHECK_MS);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
402
|
-
//
|
|
403
|
-
// When pool is exhausted, find the farthest wrap above viewport,
|
|
404
|
-
// destroy its Ezoic placeholder, recreate the DOM, then define+displayMore.
|
|
405
|
-
|
|
406
392
|
function recycleWrap(klass, targetEl, newKey) {
|
|
407
393
|
const ez = window.ezstandalone;
|
|
408
394
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
@@ -410,8 +396,7 @@
|
|
|
410
396
|
typeof ez?.displayMore !== 'function') return null;
|
|
411
397
|
|
|
412
398
|
const vh = window.innerHeight || 800;
|
|
413
|
-
const threshold = -
|
|
414
|
-
const t = now();
|
|
399
|
+
const threshold = -vh;
|
|
415
400
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
416
401
|
let bestFull = null, bestFullY = Infinity;
|
|
417
402
|
|
|
@@ -420,12 +405,6 @@
|
|
|
420
405
|
|
|
421
406
|
for (const wrap of wraps) {
|
|
422
407
|
try {
|
|
423
|
-
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
424
|
-
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
425
|
-
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
426
|
-
const st = state.phState.get(wid);
|
|
427
|
-
// Don't recycle placeholders that are still being processed
|
|
428
|
-
if (st === 'new' || st === 'defined') continue;
|
|
429
408
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
430
409
|
if (bottom > threshold) continue;
|
|
431
410
|
if (!isFilled(wrap)) {
|
|
@@ -444,46 +423,48 @@
|
|
|
444
423
|
|
|
445
424
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
446
425
|
|
|
447
|
-
// Unobserve before moving
|
|
426
|
+
// Unobserve before moving to prevent stale showAds
|
|
448
427
|
try {
|
|
449
428
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
450
429
|
if (ph) state.io?.unobserve(ph);
|
|
451
430
|
} catch (_) {}
|
|
452
431
|
|
|
453
|
-
//
|
|
454
|
-
|
|
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
|
+
});
|
|
455
447
|
|
|
456
|
-
|
|
457
|
-
|
|
448
|
+
// Update registry
|
|
449
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
450
|
+
state.wrapByKey.set(newKey, best);
|
|
458
451
|
|
|
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
|
+
}
|
|
459
458
|
setTimeout(() => {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
best.classList.remove('is-empty');
|
|
466
|
-
best.replaceChildren();
|
|
467
|
-
|
|
468
|
-
const fresh = document.createElement('div');
|
|
469
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
470
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
471
|
-
fresh.style.minHeight = '1px';
|
|
472
|
-
best.appendChild(fresh);
|
|
473
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
477
|
-
state.wrapByKey.set(newKey, best);
|
|
459
|
+
try { observePlaceholder(id); } catch (_) {}
|
|
460
|
+
state.phState.set(id, 'new');
|
|
461
|
+
try { enqueueShow(id); } catch (_) {}
|
|
462
|
+
}, TIMING.RECYCLE_DELAY_MS);
|
|
463
|
+
};
|
|
478
464
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
observePlaceholder(id);
|
|
483
|
-
ezEnqueue(id);
|
|
484
|
-
}, TIMING.RECYCLE_DEFINE_MS);
|
|
485
|
-
}, TIMING.RECYCLE_DESTROY_MS);
|
|
486
|
-
});
|
|
465
|
+
try {
|
|
466
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy();
|
|
467
|
+
} catch (_) {}
|
|
487
468
|
|
|
488
469
|
return { id, wrap: best };
|
|
489
470
|
}
|
|
@@ -498,6 +479,7 @@
|
|
|
498
479
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
499
480
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
500
481
|
w.style.cssText = 'width:100%;display:block';
|
|
482
|
+
|
|
501
483
|
const ph = document.createElement('div');
|
|
502
484
|
ph.id = `${PH_PREFIX}${id}`;
|
|
503
485
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -510,8 +492,10 @@
|
|
|
510
492
|
if (!el?.insertAdjacentElement) return null;
|
|
511
493
|
if (findWrap(key)) return null;
|
|
512
494
|
if (state.mountedIds.has(id)) return null;
|
|
495
|
+
// Ensure no duplicate DOM element with same placeholder ID
|
|
513
496
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
514
497
|
if (existing?.isConnected) return null;
|
|
498
|
+
|
|
515
499
|
const w = makeWrap(id, klass, key);
|
|
516
500
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
517
501
|
state.mountedIds.add(id);
|
|
@@ -525,13 +509,21 @@
|
|
|
525
509
|
try {
|
|
526
510
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
527
511
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
512
|
+
|
|
528
513
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
529
|
-
if (Number.isFinite(id)) {
|
|
514
|
+
if (Number.isFinite(id)) {
|
|
515
|
+
state.mountedIds.delete(id);
|
|
516
|
+
state.phState.delete(id);
|
|
517
|
+
}
|
|
518
|
+
|
|
530
519
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
531
520
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
521
|
+
|
|
522
|
+
// Find the kind class to unregister
|
|
532
523
|
for (const cls of w.classList) {
|
|
533
524
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
534
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
525
|
+
state.wrapsByClass.get(cls)?.delete(w);
|
|
526
|
+
break;
|
|
535
527
|
}
|
|
536
528
|
}
|
|
537
529
|
w.remove();
|
|
@@ -539,24 +531,33 @@
|
|
|
539
531
|
}
|
|
540
532
|
|
|
541
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.
|
|
542
537
|
|
|
543
538
|
function pruneOrphansBetween() {
|
|
544
539
|
const klass = 'ezoic-ad-between';
|
|
545
540
|
const cfg = KIND[klass];
|
|
546
541
|
const wraps = state.wrapsByClass.get(klass);
|
|
547
542
|
if (!wraps?.size) return;
|
|
543
|
+
|
|
544
|
+
// Build set of live anchor IDs
|
|
548
545
|
const liveAnchors = new Set();
|
|
549
546
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
550
547
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
551
548
|
if (v) liveAnchors.add(v);
|
|
552
549
|
}
|
|
550
|
+
|
|
553
551
|
const t = now();
|
|
554
552
|
for (const w of wraps) {
|
|
555
553
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
556
554
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
555
|
+
|
|
557
556
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
558
557
|
const sid = key.slice(klass.length + 1);
|
|
559
|
-
if (!sid || !liveAnchors.has(sid))
|
|
558
|
+
if (!sid || !liveAnchors.has(sid)) {
|
|
559
|
+
mutate(() => dropWrap(w));
|
|
560
|
+
}
|
|
560
561
|
}
|
|
561
562
|
}
|
|
562
563
|
|
|
@@ -568,6 +569,7 @@
|
|
|
568
569
|
const v = el.getAttribute(attr);
|
|
569
570
|
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
570
571
|
}
|
|
572
|
+
// Positional fallback
|
|
571
573
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
572
574
|
let i = 0;
|
|
573
575
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -584,9 +586,11 @@
|
|
|
584
586
|
for (const el of items) {
|
|
585
587
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
586
588
|
if (!el?.isConnected) continue;
|
|
589
|
+
|
|
587
590
|
const ord = ordinal(klass, el);
|
|
588
591
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
589
592
|
if (adjacentWrap(el)) continue;
|
|
593
|
+
|
|
590
594
|
const key = anchorKey(klass, el);
|
|
591
595
|
if (findWrap(key)) continue;
|
|
592
596
|
|
|
@@ -595,13 +599,12 @@
|
|
|
595
599
|
const w = insertAfter(el, id, klass, key);
|
|
596
600
|
if (w) {
|
|
597
601
|
observePlaceholder(id);
|
|
598
|
-
|
|
599
|
-
ezEnqueue(id);
|
|
602
|
+
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
600
603
|
inserted++;
|
|
601
604
|
}
|
|
602
605
|
} else {
|
|
603
606
|
const recycled = recycleWrap(klass, el, key);
|
|
604
|
-
if (!recycled) break;
|
|
607
|
+
if (!recycled) break; // Pool truly exhausted
|
|
605
608
|
inserted++;
|
|
606
609
|
}
|
|
607
610
|
}
|
|
@@ -609,12 +612,6 @@
|
|
|
609
612
|
}
|
|
610
613
|
|
|
611
614
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
612
|
-
//
|
|
613
|
-
// The IO is used to eagerly observe placeholders so that when they enter
|
|
614
|
-
// the viewport margin, we can queue them for Ezoic. However, the actual
|
|
615
|
-
// Ezoic API calls (define/displayMore) happen in the batched flush.
|
|
616
|
-
// The IO callback is mainly useful for re-triggering after NodeBB
|
|
617
|
-
// virtualisation re-inserts posts.
|
|
618
615
|
|
|
619
616
|
function getIO() {
|
|
620
617
|
if (state.io) return state.io;
|
|
@@ -622,19 +619,9 @@
|
|
|
622
619
|
state.io = new IntersectionObserver(entries => {
|
|
623
620
|
for (const entry of entries) {
|
|
624
621
|
if (!entry.isIntersecting) continue;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (!id || id <= 0) continue;
|
|
629
|
-
const st = state.phState.get(id);
|
|
630
|
-
// Only enqueue if not yet processed by Ezoic
|
|
631
|
-
if (st === 'new') {
|
|
632
|
-
ezEnqueue(id);
|
|
633
|
-
} else if (st === 'displayed') {
|
|
634
|
-
// Already shown — check if the placeholder is actually filled.
|
|
635
|
-
// If not (Ezoic had no ad), don't re-trigger — it won't help.
|
|
636
|
-
// If yes, nothing to do.
|
|
637
|
-
}
|
|
622
|
+
if (entry.target instanceof Element) state.io?.unobserve(entry.target);
|
|
623
|
+
const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
|
|
624
|
+
if (id > 0) enqueueShow(id);
|
|
638
625
|
}
|
|
639
626
|
}, {
|
|
640
627
|
root: null,
|
|
@@ -647,16 +634,193 @@
|
|
|
647
634
|
|
|
648
635
|
function observePlaceholder(id) {
|
|
649
636
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
650
|
-
if (ph?.isConnected) {
|
|
637
|
+
if (ph?.isConnected) {
|
|
638
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
function enqueueShow(id) {
|
|
645
|
+
if (!id || isBlocked()) return;
|
|
646
|
+
const st = state.phState.get(id);
|
|
647
|
+
if (st === 'show-queued' || st === 'shown') return;
|
|
648
|
+
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
649
|
+
|
|
650
|
+
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
651
|
+
if (!state.pendingSet.has(id)) {
|
|
652
|
+
state.pending.push(id);
|
|
653
|
+
state.pendingSet.add(id);
|
|
654
|
+
state.phState.set(id, 'show-queued');
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
state.phState.set(id, 'show-queued');
|
|
659
|
+
startShow(id);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function drainQueue() {
|
|
663
|
+
if (isBlocked()) return;
|
|
664
|
+
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
665
|
+
const id = state.pending.shift();
|
|
666
|
+
state.pendingSet.delete(id);
|
|
667
|
+
startShow(id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function startShow(id) {
|
|
672
|
+
if (!id || isBlocked()) return;
|
|
673
|
+
state.inflight++;
|
|
674
|
+
|
|
675
|
+
let released = false;
|
|
676
|
+
const release = () => {
|
|
677
|
+
if (released) return;
|
|
678
|
+
released = true;
|
|
679
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
680
|
+
drainQueue();
|
|
681
|
+
};
|
|
682
|
+
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
683
|
+
|
|
684
|
+
requestAnimationFrame(() => {
|
|
685
|
+
try {
|
|
686
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
687
|
+
|
|
688
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
689
|
+
if (!ph?.isConnected) {
|
|
690
|
+
state.phState.delete(id);
|
|
691
|
+
clearTimeout(timer);
|
|
692
|
+
return release();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
696
|
+
state.phState.set(id, 'shown');
|
|
697
|
+
clearTimeout(timer);
|
|
698
|
+
return release();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const t = now();
|
|
702
|
+
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
703
|
+
clearTimeout(timer);
|
|
704
|
+
return release();
|
|
705
|
+
}
|
|
706
|
+
state.lastShow.set(id, t);
|
|
707
|
+
|
|
708
|
+
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
709
|
+
state.phState.set(id, 'shown');
|
|
710
|
+
|
|
711
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
712
|
+
const ez = window.ezstandalone;
|
|
713
|
+
|
|
714
|
+
const doShow = () => {
|
|
715
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
716
|
+
try { ez.showAds(id); } catch (_) {}
|
|
717
|
+
if (wrap) scheduleUncollapseChecks(wrap);
|
|
718
|
+
scheduleEmptyCheck(id, t);
|
|
719
|
+
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
723
|
+
} catch (_) {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
release();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
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);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
745
|
+
//
|
|
746
|
+
// Intercepts ez.showAds() to:
|
|
747
|
+
// - block calls during navigation transitions
|
|
748
|
+
// - filter out disconnected placeholders
|
|
749
|
+
// - batch calls for efficiency
|
|
750
|
+
|
|
751
|
+
function patchShowAds() {
|
|
752
|
+
const apply = () => {
|
|
753
|
+
try {
|
|
754
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
755
|
+
const ez = window.ezstandalone;
|
|
756
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
757
|
+
window.__nbbEzPatched = true;
|
|
758
|
+
|
|
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
|
+
|
|
785
|
+
ez.showAds = function (...args) {
|
|
786
|
+
if (isBlocked()) return;
|
|
787
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
788
|
+
for (const v of ids) {
|
|
789
|
+
const id = parseInt(v, 10);
|
|
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);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
} catch (_) {}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
apply();
|
|
805
|
+
if (!window.__nbbEzPatched) {
|
|
806
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
807
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
808
|
+
}
|
|
651
809
|
}
|
|
652
810
|
|
|
653
811
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
654
812
|
|
|
655
813
|
async function runCore() {
|
|
656
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.
|
|
818
|
+
try { gcDisconnectedWraps(); } catch (_) {}
|
|
819
|
+
|
|
657
820
|
const cfg = await fetchConfig();
|
|
658
821
|
if (!cfg || cfg.excluded) return 0;
|
|
659
822
|
initPools(cfg);
|
|
823
|
+
|
|
660
824
|
const kind = getKind();
|
|
661
825
|
if (kind === 'other') return 0;
|
|
662
826
|
|
|
@@ -667,16 +831,24 @@
|
|
|
667
831
|
};
|
|
668
832
|
|
|
669
833
|
if (kind === 'topic') {
|
|
670
|
-
return exec(
|
|
671
|
-
|
|
834
|
+
return exec(
|
|
835
|
+
'ezoic-ad-message', getPosts,
|
|
836
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
837
|
+
);
|
|
672
838
|
}
|
|
839
|
+
|
|
673
840
|
if (kind === 'categoryTopics') {
|
|
674
841
|
pruneOrphansBetween();
|
|
675
|
-
return exec(
|
|
676
|
-
|
|
842
|
+
return exec(
|
|
843
|
+
'ezoic-ad-between', getTopics,
|
|
844
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
845
|
+
);
|
|
677
846
|
}
|
|
678
|
-
|
|
679
|
-
|
|
847
|
+
|
|
848
|
+
return exec(
|
|
849
|
+
'ezoic-ad-categories', getCategories,
|
|
850
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
851
|
+
);
|
|
680
852
|
}
|
|
681
853
|
|
|
682
854
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -700,69 +872,94 @@
|
|
|
700
872
|
state.lastBurstTs = t;
|
|
701
873
|
state.pageKey = pageKey();
|
|
702
874
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
875
|
+
|
|
703
876
|
if (state.burstActive) return;
|
|
704
877
|
state.burstActive = true;
|
|
705
878
|
state.burstCount = 0;
|
|
879
|
+
|
|
706
880
|
const step = () => {
|
|
707
881
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
708
|
-
state.burstActive = false;
|
|
882
|
+
state.burstActive = false;
|
|
883
|
+
return;
|
|
709
884
|
}
|
|
710
885
|
state.burstCount++;
|
|
711
886
|
scheduleRun(n => {
|
|
712
|
-
if (!n && !state.
|
|
887
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
713
888
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
714
889
|
});
|
|
715
890
|
};
|
|
716
891
|
step();
|
|
717
892
|
}
|
|
718
893
|
|
|
719
|
-
// ── Cleanup
|
|
894
|
+
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
720
895
|
|
|
721
896
|
function cleanup() {
|
|
722
897
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
723
|
-
|
|
724
|
-
// Cancel pending Ezoic batch
|
|
725
|
-
if (state.ezFlushTimer) { clearTimeout(state.ezFlushTimer); state.ezFlushTimer = null; }
|
|
726
|
-
state.ezBatch.clear();
|
|
727
|
-
|
|
728
898
|
mutate(() => {
|
|
729
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
899
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
900
|
+
dropWrap(w);
|
|
901
|
+
}
|
|
730
902
|
});
|
|
731
|
-
state.cfg
|
|
732
|
-
state.poolsReady
|
|
733
|
-
state.pools
|
|
734
|
-
state.cursors
|
|
903
|
+
state.cfg = null;
|
|
904
|
+
state.poolsReady = false;
|
|
905
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
906
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
735
907
|
state.mountedIds.clear();
|
|
736
908
|
state.lastShow.clear();
|
|
737
909
|
state.wrapByKey.clear();
|
|
738
910
|
state.wrapsByClass.clear();
|
|
739
|
-
state.kind
|
|
911
|
+
state.kind = null;
|
|
740
912
|
state.phState.clear();
|
|
741
|
-
state.
|
|
913
|
+
state.inflight = 0;
|
|
914
|
+
state.pending = [];
|
|
915
|
+
state.pendingSet.clear();
|
|
742
916
|
state.burstActive = false;
|
|
743
917
|
state.runQueued = false;
|
|
918
|
+
state.firstShown = false;
|
|
744
919
|
}
|
|
745
920
|
|
|
746
921
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
922
|
+
//
|
|
923
|
+
// Scoped to detect: (1) ad fill events in wraps, (2) new content items
|
|
747
924
|
|
|
748
925
|
function ensureDomObserver() {
|
|
749
926
|
if (state.domObs) return;
|
|
927
|
+
|
|
750
928
|
state.domObs = new MutationObserver(muts => {
|
|
751
929
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
930
|
+
|
|
752
931
|
let needsBurst = false;
|
|
932
|
+
|
|
933
|
+
// Determine relevant selectors for current page kind
|
|
753
934
|
const kind = getKind();
|
|
754
935
|
const relevantSels =
|
|
755
|
-
kind === 'topic'
|
|
756
|
-
kind === 'categoryTopics'
|
|
757
|
-
kind === 'categories'
|
|
758
|
-
|
|
936
|
+
kind === 'topic' ? [SEL.post] :
|
|
937
|
+
kind === 'categoryTopics'? [SEL.topic] :
|
|
938
|
+
kind === 'categories' ? [SEL.category] :
|
|
939
|
+
[SEL.post, SEL.topic, SEL.category];
|
|
759
940
|
|
|
760
941
|
for (const m of muts) {
|
|
761
942
|
if (m.type !== 'childList') continue;
|
|
943
|
+
|
|
944
|
+
// If NodeBB removed wraps as part of virtualization, free ids immediately
|
|
945
|
+
for (const node of m.removedNodes) {
|
|
946
|
+
if (!(node instanceof Element)) continue;
|
|
947
|
+
try {
|
|
948
|
+
if (node.classList?.contains(WRAP_CLASS)) {
|
|
949
|
+
dropWrap(node);
|
|
950
|
+
} else {
|
|
951
|
+
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
952
|
+
if (wraps?.length) {
|
|
953
|
+
for (const w of wraps) dropWrap(w);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (_) {}
|
|
957
|
+
}
|
|
958
|
+
|
|
762
959
|
for (const node of m.addedNodes) {
|
|
763
960
|
if (!(node instanceof Element)) continue;
|
|
764
961
|
|
|
765
|
-
//
|
|
962
|
+
// Check for ad fill events in wraps
|
|
766
963
|
try {
|
|
767
964
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
768
965
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -771,121 +968,178 @@
|
|
|
771
968
|
}
|
|
772
969
|
} catch (_) {}
|
|
773
970
|
|
|
774
|
-
//
|
|
775
|
-
try {
|
|
776
|
-
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
777
|
-
? [node]
|
|
778
|
-
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
779
|
-
for (const wrap of wraps) {
|
|
780
|
-
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
781
|
-
if (!id || id <= 0) continue;
|
|
782
|
-
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
783
|
-
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
784
|
-
}
|
|
785
|
-
} catch (_) {}
|
|
786
|
-
|
|
787
|
-
// New content detection
|
|
971
|
+
// Check for new content items (posts, topics, categories)
|
|
788
972
|
if (!needsBurst) {
|
|
789
973
|
for (const sel of relevantSels) {
|
|
790
974
|
try {
|
|
791
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
975
|
+
if (node.matches(sel) || node.querySelector(sel)) {
|
|
976
|
+
needsBurst = true;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
792
979
|
} catch (_) {}
|
|
793
980
|
}
|
|
794
981
|
}
|
|
795
982
|
}
|
|
796
983
|
if (needsBurst) break;
|
|
797
984
|
}
|
|
985
|
+
|
|
798
986
|
if (needsBurst) requestBurst();
|
|
799
987
|
});
|
|
800
|
-
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
991
|
+
} catch (_) {}
|
|
801
992
|
}
|
|
802
993
|
|
|
803
|
-
// ── 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.
|
|
804
1018
|
|
|
805
1019
|
function ensureTcfLocator() {
|
|
806
1020
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1021
|
+
|
|
807
1022
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
1023
|
+
|
|
1024
|
+
// Create or relocate the locator iframe into <head> for protection
|
|
808
1025
|
const ensureInHead = () => {
|
|
809
1026
|
let existing = document.getElementById(LOCATOR_ID);
|
|
810
1027
|
if (existing) {
|
|
1028
|
+
// If it's in <body>, move it to <head> where ajaxify can't reach it
|
|
811
1029
|
if (existing.parentElement !== document.head) {
|
|
812
1030
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
813
1031
|
}
|
|
814
|
-
return;
|
|
1032
|
+
return existing;
|
|
815
1033
|
}
|
|
1034
|
+
// Create fresh
|
|
816
1035
|
const f = document.createElement('iframe');
|
|
817
|
-
f.style.display = 'none';
|
|
1036
|
+
f.style.display = 'none';
|
|
1037
|
+
f.id = f.name = LOCATOR_ID;
|
|
818
1038
|
try { document.head.appendChild(f); } catch (_) {
|
|
1039
|
+
// Fallback to body if head insertion fails
|
|
819
1040
|
(document.body || document.documentElement).appendChild(f);
|
|
820
1041
|
}
|
|
1042
|
+
return f;
|
|
821
1043
|
};
|
|
1044
|
+
|
|
822
1045
|
ensureInHead();
|
|
1046
|
+
|
|
1047
|
+
// Layer 2: Guard the CMP API calls against null contentWindow
|
|
823
1048
|
if (!window.__nbbCmpGuarded) {
|
|
824
1049
|
window.__nbbCmpGuarded = true;
|
|
1050
|
+
|
|
1051
|
+
// Wrap __tcfapi
|
|
825
1052
|
if (typeof window.__tcfapi === 'function') {
|
|
826
|
-
const
|
|
827
|
-
window.__tcfapi = function (cmd,
|
|
828
|
-
try {
|
|
829
|
-
|
|
1053
|
+
const origTcf = window.__tcfapi;
|
|
1054
|
+
window.__tcfapi = function (cmd, version, cb, param) {
|
|
1055
|
+
try {
|
|
1056
|
+
return origTcf.call(this, cmd, version, function (...args) {
|
|
1057
|
+
try { cb?.(...args); } catch (_) {}
|
|
1058
|
+
}, param);
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
// If the error is the null postMessage/addtlConsent, swallow it
|
|
1061
|
+
if (e?.message?.includes('null')) {
|
|
1062
|
+
// Re-ensure locator exists, then retry once
|
|
1063
|
+
ensureInHead();
|
|
1064
|
+
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
830
1067
|
};
|
|
831
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
// Wrap __cmp (legacy CMP v1 API)
|
|
832
1071
|
if (typeof window.__cmp === 'function') {
|
|
833
|
-
const
|
|
834
|
-
window.__cmp = function (...
|
|
835
|
-
try {
|
|
836
|
-
|
|
1072
|
+
const origCmp = window.__cmp;
|
|
1073
|
+
window.__cmp = function (...args) {
|
|
1074
|
+
try {
|
|
1075
|
+
return origCmp.apply(this, args);
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
if (e?.message?.includes('null')) {
|
|
1078
|
+
ensureInHead();
|
|
1079
|
+
try { return origCmp.apply(this, args); } catch (_) {}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
837
1082
|
};
|
|
838
1083
|
}
|
|
839
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
// Layer 3: MutationObserver to immediately restore if removed
|
|
840
1087
|
if (!window.__nbbTcfObs) {
|
|
841
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
842
|
-
|
|
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();
|
|
843
1093
|
});
|
|
844
|
-
|
|
845
|
-
try {
|
|
1094
|
+
// Observe body direct children only (the most likely removal point)
|
|
1095
|
+
try {
|
|
1096
|
+
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1097
|
+
childList: true,
|
|
1098
|
+
subtree: false,
|
|
1099
|
+
});
|
|
1100
|
+
} catch (_) {}
|
|
1101
|
+
// Also observe <head> in case something cleans it
|
|
1102
|
+
try {
|
|
1103
|
+
if (document.head) {
|
|
1104
|
+
window.__nbbTcfObs.observe(document.head, {
|
|
1105
|
+
childList: true,
|
|
1106
|
+
subtree: false,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
} catch (_) {}
|
|
846
1110
|
}
|
|
847
1111
|
}
|
|
848
1112
|
|
|
849
|
-
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
850
|
-
|
|
851
|
-
function protectAriaHidden() {
|
|
852
|
-
if (window.__nbbAriaObs) return;
|
|
853
|
-
const remove = () => {
|
|
854
|
-
try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
855
|
-
};
|
|
856
|
-
remove();
|
|
857
|
-
window.__nbbAriaObs = new MutationObserver(remove);
|
|
858
|
-
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
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.
|
|
862
1117
|
|
|
863
1118
|
function muteConsole() {
|
|
864
1119
|
if (window.__nbbEzMuted) return;
|
|
865
1120
|
window.__nbbEzMuted = true;
|
|
1121
|
+
|
|
866
1122
|
const PREFIXES = [
|
|
867
1123
|
'[EzoicAds JS]: Placeholder Id',
|
|
1124
|
+
'No valid placeholders for loadMore',
|
|
868
1125
|
'cannot call refresh on the same page',
|
|
869
1126
|
'no placeholders are currently defined in Refresh',
|
|
870
1127
|
'Debugger iframe already exists',
|
|
871
1128
|
'[CMP] Error in custom getTCData',
|
|
872
1129
|
'vignette: no interstitial API',
|
|
873
1130
|
];
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
'adsbygoogle.push() error: All',
|
|
877
|
-
'has already been defined',
|
|
878
|
-
'No valid placeholders for loadMore',
|
|
879
|
-
'bad response. Status',
|
|
880
|
-
];
|
|
1131
|
+
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
1132
|
+
|
|
881
1133
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
882
1134
|
const orig = console[method];
|
|
883
1135
|
if (typeof orig !== 'function') continue;
|
|
884
1136
|
console[method] = function (...args) {
|
|
885
1137
|
if (typeof args[0] === 'string') {
|
|
886
1138
|
const msg = args[0];
|
|
887
|
-
for (const
|
|
888
|
-
|
|
1139
|
+
for (const prefix of PREFIXES) {
|
|
1140
|
+
if (msg.startsWith(prefix)) return;
|
|
1141
|
+
}
|
|
1142
|
+
if (msg.includes(PH_PATTERN)) return;
|
|
889
1143
|
}
|
|
890
1144
|
return orig.apply(console, args);
|
|
891
1145
|
};
|
|
@@ -893,24 +1147,31 @@
|
|
|
893
1147
|
}
|
|
894
1148
|
|
|
895
1149
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1150
|
+
// Run once per session — preconnect hints are in <head> via server-side injection
|
|
896
1151
|
|
|
897
1152
|
let _networkWarmed = false;
|
|
1153
|
+
|
|
898
1154
|
function warmNetwork() {
|
|
899
1155
|
if (_networkWarmed) return;
|
|
900
1156
|
_networkWarmed = true;
|
|
1157
|
+
|
|
901
1158
|
const head = document.head;
|
|
902
1159
|
if (!head) return;
|
|
903
|
-
|
|
1160
|
+
|
|
1161
|
+
const hints = [
|
|
904
1162
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
905
1163
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
906
1164
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
907
1165
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
908
1166
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
909
1167
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
910
|
-
]
|
|
1168
|
+
];
|
|
1169
|
+
|
|
1170
|
+
for (const [rel, href, cors] of hints) {
|
|
911
1171
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
912
1172
|
const link = document.createElement('link');
|
|
913
|
-
link.rel = rel;
|
|
1173
|
+
link.rel = rel;
|
|
1174
|
+
link.href = href;
|
|
914
1175
|
if (cors) link.crossOrigin = 'anonymous';
|
|
915
1176
|
head.appendChild(link);
|
|
916
1177
|
}
|
|
@@ -921,25 +1182,51 @@
|
|
|
921
1182
|
function bindNodeBB() {
|
|
922
1183
|
const $ = window.jQuery;
|
|
923
1184
|
if (!$) return;
|
|
1185
|
+
|
|
924
1186
|
$(window).off('.nbbEzoic');
|
|
1187
|
+
|
|
925
1188
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1189
|
+
|
|
926
1190
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
927
|
-
state.pageKey
|
|
928
|
-
state.kind
|
|
1191
|
+
state.pageKey = pageKey();
|
|
1192
|
+
state.kind = null;
|
|
929
1193
|
state.blockedUntil = 0;
|
|
930
|
-
|
|
931
|
-
|
|
1194
|
+
|
|
1195
|
+
// Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
|
|
1196
|
+
try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1197
|
+
|
|
1198
|
+
muteConsole();
|
|
1199
|
+
ensureTcfLocator();
|
|
1200
|
+
warmNetwork();
|
|
1201
|
+
patchShowAds();
|
|
1202
|
+
getIO();
|
|
1203
|
+
ensureDomObserver();
|
|
1204
|
+
requestBurst();
|
|
932
1205
|
});
|
|
1206
|
+
|
|
1207
|
+
// Content-loaded events trigger burst
|
|
933
1208
|
const burstEvents = [
|
|
934
|
-
'action:ajaxify.contentLoaded',
|
|
935
|
-
'action:
|
|
1209
|
+
'action:ajaxify.contentLoaded',
|
|
1210
|
+
'action:posts.loaded',
|
|
1211
|
+
'action:topics.loaded',
|
|
1212
|
+
'action:categories.loaded',
|
|
1213
|
+
'action:category.loaded',
|
|
1214
|
+
'action:topic.loaded',
|
|
936
1215
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1216
|
+
|
|
937
1217
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1218
|
+
|
|
1219
|
+
// Also bind via NodeBB hooks module (for compatibility)
|
|
938
1220
|
try {
|
|
939
1221
|
require(['hooks'], hooks => {
|
|
940
1222
|
if (typeof hooks?.on !== 'function') return;
|
|
941
|
-
for (const ev of [
|
|
942
|
-
|
|
1223
|
+
for (const ev of [
|
|
1224
|
+
'action:ajaxify.end',
|
|
1225
|
+
'action:posts.loaded',
|
|
1226
|
+
'action:topics.loaded',
|
|
1227
|
+
'action:categories.loaded',
|
|
1228
|
+
'action:topic.loaded',
|
|
1229
|
+
]) {
|
|
943
1230
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
944
1231
|
}
|
|
945
1232
|
});
|
|
@@ -951,7 +1238,10 @@
|
|
|
951
1238
|
window.addEventListener('scroll', () => {
|
|
952
1239
|
if (ticking) return;
|
|
953
1240
|
ticking = true;
|
|
954
|
-
requestAnimationFrame(() => {
|
|
1241
|
+
requestAnimationFrame(() => {
|
|
1242
|
+
ticking = false;
|
|
1243
|
+
requestBurst();
|
|
1244
|
+
});
|
|
955
1245
|
}, { passive: true });
|
|
956
1246
|
}
|
|
957
1247
|
|
|
@@ -960,8 +1250,8 @@
|
|
|
960
1250
|
state.pageKey = pageKey();
|
|
961
1251
|
muteConsole();
|
|
962
1252
|
ensureTcfLocator();
|
|
963
|
-
protectAriaHidden();
|
|
964
1253
|
warmNetwork();
|
|
1254
|
+
patchShowAds();
|
|
965
1255
|
getIO();
|
|
966
1256
|
ensureDomObserver();
|
|
967
1257
|
bindNodeBB();
|