nodebb-plugin-ezoic-infinite 1.8.45 → 1.8.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/client.js +491 -257
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* → ez.cmd.push(() => ez.showAds(id))
|
|
11
|
-
*
|
|
12
|
-
* phState per placeholder ID:
|
|
13
|
-
* new → displayed → [destroyed → new] (recycle loop)
|
|
4
|
+
* Based on working v2-fixed with targeted fixes:
|
|
5
|
+
* - Ezoic API: showAds() + destroyPlaceholders() only (per official docs)
|
|
6
|
+
* - cleanup(): destroyAll() before DOM removal, only on real page change
|
|
7
|
+
* - Recycling: conservative threshold (-3vh), age/inflight guards
|
|
8
|
+
* - aria-hidden: MutationObserver protection
|
|
9
|
+
* - Empty check: more conservative timing and GPT slot awareness
|
|
14
10
|
*/
|
|
15
11
|
(function nbbEzoicInfinite() {
|
|
16
12
|
'use strict';
|
|
@@ -28,20 +24,22 @@
|
|
|
28
24
|
};
|
|
29
25
|
|
|
30
26
|
const TIMING = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
EMPTY_CHECK_EARLY_MS: 30_000,
|
|
28
|
+
EMPTY_CHECK_LATE_MS: 60_000,
|
|
29
|
+
MIN_PRUNE_AGE_MS: 8_000,
|
|
30
|
+
SHOW_THROTTLE_MS: 900,
|
|
31
|
+
BURST_COOLDOWN_MS: 200,
|
|
32
|
+
BLOCK_DURATION_MS: 1_500,
|
|
33
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
34
|
+
SHOW_RELEASE_MS: 700,
|
|
35
|
+
BATCH_FLUSH_MS: 80,
|
|
36
|
+
RECYCLE_DELAY_MS: 450,
|
|
41
37
|
};
|
|
42
38
|
|
|
43
39
|
const LIMITS = {
|
|
44
40
|
MAX_INSERTS_RUN: 6,
|
|
41
|
+
MAX_INFLIGHT: 4,
|
|
42
|
+
BATCH_SIZE: 3,
|
|
45
43
|
MAX_BURST_STEPS: 8,
|
|
46
44
|
BURST_WINDOW_MS: 2_000,
|
|
47
45
|
};
|
|
@@ -64,9 +62,7 @@
|
|
|
64
62
|
};
|
|
65
63
|
|
|
66
64
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
67
|
-
|
|
68
|
-
// Reduce "ad popping" from aggressive ID recycling.
|
|
69
|
-
const RECYCLE_MIN_AGE_MS = 20_000;
|
|
65
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
70
66
|
|
|
71
67
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
72
68
|
|
|
@@ -74,17 +70,24 @@
|
|
|
74
70
|
const isMobile = () => window.innerWidth < 768;
|
|
75
71
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
76
72
|
|
|
77
|
-
function isFilled(node)
|
|
73
|
+
function isFilled(node) {
|
|
74
|
+
return node?.querySelector?.(FILL_SEL) != null;
|
|
75
|
+
}
|
|
76
|
+
|
|
78
77
|
function isPlaceholderUsed(ph) {
|
|
79
78
|
if (!ph?.isConnected) return false;
|
|
80
79
|
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
function parseIds(raw) {
|
|
84
|
-
const out = []
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = new Set();
|
|
85
85
|
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
86
86
|
const n = parseInt(line, 10);
|
|
87
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
87
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
88
|
+
seen.add(n);
|
|
89
|
+
out.push(n);
|
|
90
|
+
}
|
|
88
91
|
}
|
|
89
92
|
return out;
|
|
90
93
|
}
|
|
@@ -92,29 +95,37 @@
|
|
|
92
95
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
93
96
|
|
|
94
97
|
const state = {
|
|
95
|
-
pageKey:
|
|
98
|
+
pageKey: null,
|
|
99
|
+
kind: null,
|
|
100
|
+
cfg: null,
|
|
96
101
|
|
|
97
102
|
poolsReady: false,
|
|
98
|
-
pools:
|
|
99
|
-
cursors:
|
|
103
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
104
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
105
|
+
|
|
106
|
+
mountedIds: new Set(),
|
|
107
|
+
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
108
|
+
lastShow: new Map(), // id → timestamp
|
|
100
109
|
|
|
101
|
-
mountedIds: new Set(),
|
|
102
|
-
// phState: 'new' | 'displayed' | 'destroyed'
|
|
103
|
-
phState: new Map(),
|
|
104
|
-
lastShow: new Map(),
|
|
105
110
|
wrapByKey: new Map(),
|
|
106
111
|
wrapsByClass: new Map(),
|
|
107
112
|
|
|
108
|
-
io:
|
|
109
|
-
|
|
113
|
+
io: null,
|
|
114
|
+
domObs: null,
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
ezFlushTimer: null,
|
|
116
|
+
mutGuard: 0,
|
|
117
|
+
blockedUntil: 0,
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
inflight: 0,
|
|
120
|
+
pending: [],
|
|
121
|
+
pendingSet: new Set(),
|
|
122
|
+
|
|
123
|
+
runQueued: false,
|
|
124
|
+
burstActive: false,
|
|
125
|
+
burstDeadline: 0,
|
|
126
|
+
burstCount: 0,
|
|
127
|
+
lastBurstTs: 0,
|
|
128
|
+
firstShown: false,
|
|
118
129
|
};
|
|
119
130
|
|
|
120
131
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -130,7 +141,10 @@
|
|
|
130
141
|
if (state.cfg) return state.cfg;
|
|
131
142
|
try {
|
|
132
143
|
const inline = window.__nbbEzoicCfg;
|
|
133
|
-
if (inline && typeof inline === 'object') {
|
|
144
|
+
if (inline && typeof inline === 'object') {
|
|
145
|
+
state.cfg = inline;
|
|
146
|
+
return state.cfg;
|
|
147
|
+
}
|
|
134
148
|
} catch (_) {}
|
|
135
149
|
try {
|
|
136
150
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
@@ -169,12 +183,15 @@
|
|
|
169
183
|
return 'other';
|
|
170
184
|
}
|
|
171
185
|
|
|
172
|
-
function getKind() {
|
|
186
|
+
function getKind() {
|
|
187
|
+
return state.kind || (state.kind = detectKind());
|
|
188
|
+
}
|
|
173
189
|
|
|
174
190
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
175
191
|
|
|
176
192
|
function getPosts() {
|
|
177
|
-
const all = document.querySelectorAll(SEL.post)
|
|
193
|
+
const all = document.querySelectorAll(SEL.post);
|
|
194
|
+
const out = [];
|
|
178
195
|
for (let i = 0; i < all.length; i++) {
|
|
179
196
|
const el = all[i];
|
|
180
197
|
if (!el.isConnected) continue;
|
|
@@ -186,6 +203,7 @@
|
|
|
186
203
|
}
|
|
187
204
|
return out;
|
|
188
205
|
}
|
|
206
|
+
|
|
189
207
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
190
208
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
191
209
|
|
|
@@ -193,10 +211,15 @@
|
|
|
193
211
|
|
|
194
212
|
function stableId(klass, el) {
|
|
195
213
|
const attr = KIND[klass]?.anchorAttr;
|
|
196
|
-
if (attr) {
|
|
214
|
+
if (attr) {
|
|
215
|
+
const v = el.getAttribute(attr);
|
|
216
|
+
if (v != null && v !== '') return v;
|
|
217
|
+
}
|
|
197
218
|
const children = el.parentElement?.children;
|
|
198
219
|
if (!children) return 'i0';
|
|
199
|
-
for (let i = 0; i < children.length; i++) {
|
|
220
|
+
for (let i = 0; i < children.length; i++) {
|
|
221
|
+
if (children[i] === el) return `i${i}`;
|
|
222
|
+
}
|
|
200
223
|
return 'i0';
|
|
201
224
|
}
|
|
202
225
|
|
|
@@ -208,12 +231,49 @@
|
|
|
208
231
|
}
|
|
209
232
|
|
|
210
233
|
function getWrapSet(klass) {
|
|
211
|
-
let
|
|
212
|
-
if (!
|
|
213
|
-
return
|
|
234
|
+
let set = state.wrapsByClass.get(klass);
|
|
235
|
+
if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
|
|
236
|
+
return set;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── GC disconnected wraps (NodeBB virtualization) ──────────────────────────
|
|
240
|
+
|
|
241
|
+
function gcDisconnectedWraps() {
|
|
242
|
+
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
243
|
+
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
244
|
+
}
|
|
245
|
+
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
246
|
+
for (const w of Array.from(set)) {
|
|
247
|
+
if (w?.isConnected) continue;
|
|
248
|
+
set.delete(w);
|
|
249
|
+
try {
|
|
250
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
251
|
+
if (Number.isFinite(id)) {
|
|
252
|
+
state.mountedIds.delete(id);
|
|
253
|
+
state.phState.delete(id);
|
|
254
|
+
state.lastShow.delete(id);
|
|
255
|
+
}
|
|
256
|
+
} catch (_) {}
|
|
257
|
+
}
|
|
258
|
+
if (!set.size) state.wrapsByClass.delete(klass);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const live = new Set();
|
|
262
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
263
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
264
|
+
if (id > 0) live.add(id);
|
|
265
|
+
}
|
|
266
|
+
for (const id of Array.from(state.mountedIds)) {
|
|
267
|
+
if (!live.has(id)) {
|
|
268
|
+
state.mountedIds.delete(id);
|
|
269
|
+
state.phState.delete(id);
|
|
270
|
+
state.lastShow.delete(id);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (_) {}
|
|
214
274
|
}
|
|
215
275
|
|
|
216
|
-
// ── Wrap lifecycle
|
|
276
|
+
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
217
277
|
|
|
218
278
|
function wrapIsLive(wrap) {
|
|
219
279
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
@@ -221,17 +281,22 @@
|
|
|
221
281
|
if (!key) return false;
|
|
222
282
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
223
283
|
const colonIdx = key.indexOf(':');
|
|
224
|
-
const klass
|
|
284
|
+
const klass = key.slice(0, colonIdx);
|
|
285
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
225
286
|
const cfg = KIND[klass];
|
|
226
287
|
if (!cfg) return false;
|
|
227
288
|
const parent = wrap.parentElement;
|
|
228
289
|
if (parent) {
|
|
229
290
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
230
291
|
for (const sib of parent.children) {
|
|
231
|
-
if (sib !== wrap) {
|
|
292
|
+
if (sib !== wrap) {
|
|
293
|
+
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
294
|
+
}
|
|
232
295
|
}
|
|
233
296
|
}
|
|
234
|
-
try {
|
|
297
|
+
try {
|
|
298
|
+
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
299
|
+
} catch (_) { return false; }
|
|
235
300
|
}
|
|
236
301
|
|
|
237
302
|
function adjacentWrap(el) {
|
|
@@ -246,7 +311,7 @@
|
|
|
246
311
|
if (!ph || !isFilled(ph)) return false;
|
|
247
312
|
wrap.classList.remove('is-empty');
|
|
248
313
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
249
|
-
if (id > 0) state.phState.set(id, '
|
|
314
|
+
if (id > 0) state.phState.set(id, 'shown');
|
|
250
315
|
return true;
|
|
251
316
|
}
|
|
252
317
|
|
|
@@ -257,7 +322,7 @@
|
|
|
257
322
|
}
|
|
258
323
|
}
|
|
259
324
|
|
|
260
|
-
// ── Pool
|
|
325
|
+
// ── Pool management ────────────────────────────────────────────────────────
|
|
261
326
|
|
|
262
327
|
function pickId(poolKey) {
|
|
263
328
|
const pool = state.pools[poolKey];
|
|
@@ -271,112 +336,10 @@
|
|
|
271
336
|
return null;
|
|
272
337
|
}
|
|
273
338
|
|
|
274
|
-
// ── Ezoic API layer ────────────────────────────────────────────────────────
|
|
275
|
-
//
|
|
276
|
-
// Correct Ezoic infinite scroll flow:
|
|
277
|
-
// First batch: ez.cmd.push(() => { ez.define(...ids); ez.enable(); })
|
|
278
|
-
// Next batches: ez.cmd.push(() => { ez.define(...ids); ez.displayMore(...ids); })
|
|
279
|
-
// Recycle: ez.cmd.push(() => { ez.destroyPlaceholders(id); })
|
|
280
|
-
// → wait → new DOM → ez.cmd.push(() => { ez.define(id); ez.displayMore(id); })
|
|
281
|
-
//
|
|
282
|
-
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
283
|
-
// Ezoic API on every single placeholder insertion.
|
|
284
|
-
|
|
285
|
-
function ezCmd(fn) {
|
|
286
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
287
|
-
const ez = window.ezstandalone;
|
|
288
|
-
if (Array.isArray(ez.cmd)) {
|
|
289
|
-
ez.cmd.push(fn);
|
|
290
|
-
} else {
|
|
291
|
-
try { fn(); } catch (_) {}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Queue a placeholder ID for the next batched showAds call.
|
|
297
|
-
*/
|
|
298
|
-
function ezEnqueue(id) {
|
|
299
|
-
if (isBlocked()) return;
|
|
300
|
-
state.ezBatch.add(id);
|
|
301
|
-
if (!state.ezFlushTimer) {
|
|
302
|
-
state.ezFlushTimer = setTimeout(ezFlush, TIMING.BATCH_FLUSH_MS);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Flush: call showAds() for all queued IDs.
|
|
308
|
-
*
|
|
309
|
-
* Per Ezoic docs (https://docs.ezoic.com/docs/ezoicads/dynamic-content/):
|
|
310
|
-
* - New placeholders: ez.showAds(id1, id2, ...)
|
|
311
|
-
* - After destroy+recreate: ez.showAds(id) again
|
|
312
|
-
* - No define(), no enable(), no displayMore()
|
|
313
|
-
*/
|
|
314
|
-
function ezFlush() {
|
|
315
|
-
state.ezFlushTimer = null;
|
|
316
|
-
if (isBlocked() || !state.ezBatch.size) return;
|
|
317
|
-
|
|
318
|
-
// Filter to only valid, connected, unfilled placeholders
|
|
319
|
-
const ids = [];
|
|
320
|
-
for (const id of state.ezBatch) {
|
|
321
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
322
|
-
if (!ph?.isConnected) { state.phState.delete(id); continue; }
|
|
323
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'displayed'); continue; }
|
|
324
|
-
ids.push(id);
|
|
325
|
-
}
|
|
326
|
-
state.ezBatch.clear();
|
|
327
|
-
if (!ids.length) return;
|
|
328
|
-
|
|
329
|
-
ezCmd(() => {
|
|
330
|
-
const ez = window.ezstandalone;
|
|
331
|
-
if (typeof ez?.showAds !== 'function') return;
|
|
332
|
-
|
|
333
|
-
try { ez.showAds(...ids); } catch (_) {}
|
|
334
|
-
|
|
335
|
-
// Mark as displayed and schedule fill checks
|
|
336
|
-
for (const id of ids) {
|
|
337
|
-
state.phState.set(id, 'displayed');
|
|
338
|
-
state.lastShow.set(id, now());
|
|
339
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
340
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
341
|
-
if (wrap) {
|
|
342
|
-
wrap.setAttribute(ATTR.SHOWN, String(now()));
|
|
343
|
-
scheduleUncollapseChecks(wrap);
|
|
344
|
-
}
|
|
345
|
-
scheduleEmptyCheck(id);
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function scheduleEmptyCheck(id) {
|
|
351
|
-
const showTs = now();
|
|
352
|
-
// Check at 30s, then again at 60s — very conservative to avoid
|
|
353
|
-
// collapsing slow-loading ads
|
|
354
|
-
for (const delay of [30_000, 60_000]) {
|
|
355
|
-
setTimeout(() => {
|
|
356
|
-
try {
|
|
357
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
358
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
359
|
-
if (!wrap || !ph?.isConnected) return;
|
|
360
|
-
// Don't collapse if a newer show happened
|
|
361
|
-
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
362
|
-
// If already collapsed or uncollapsed, skip
|
|
363
|
-
if (clearEmptyIfFilled(wrap)) return;
|
|
364
|
-
// Don't collapse if there's any GPT slot, even unfilled
|
|
365
|
-
// (GPT may still be processing the ad request)
|
|
366
|
-
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
367
|
-
// Don't collapse if there's any child with meaningful height
|
|
368
|
-
if (ph.offsetHeight > 10) return;
|
|
369
|
-
wrap.classList.add('is-empty');
|
|
370
|
-
} catch (_) {}
|
|
371
|
-
}, delay);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
339
|
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
376
340
|
//
|
|
377
|
-
// Per Ezoic docs:
|
|
378
|
-
//
|
|
379
|
-
// a fresh placeholder div, then call showAds(id) again.
|
|
341
|
+
// Per Ezoic docs: destroyPlaceholders(id) → remove old HTML →
|
|
342
|
+
// recreate fresh placeholder → showAds(id).
|
|
380
343
|
|
|
381
344
|
function recycleWrap(klass, targetEl, newKey) {
|
|
382
345
|
const ez = window.ezstandalone;
|
|
@@ -384,8 +347,7 @@
|
|
|
384
347
|
typeof ez?.showAds !== 'function') return null;
|
|
385
348
|
|
|
386
349
|
const vh = window.innerHeight || 800;
|
|
387
|
-
|
|
388
|
-
const threshold = -(6 * vh);
|
|
350
|
+
const threshold = -(3 * vh);
|
|
389
351
|
const t = now();
|
|
390
352
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
391
353
|
let bestFull = null, bestFullY = Infinity;
|
|
@@ -395,11 +357,13 @@
|
|
|
395
357
|
|
|
396
358
|
for (const wrap of wraps) {
|
|
397
359
|
try {
|
|
360
|
+
// Skip young wraps (ad might still be loading)
|
|
398
361
|
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
399
362
|
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
363
|
+
// Skip wraps with inflight showAds
|
|
400
364
|
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
401
|
-
|
|
402
|
-
|
|
365
|
+
if (wid > 0 && state.phState.get(wid) === 'show-queued') continue;
|
|
366
|
+
|
|
403
367
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
404
368
|
if (bottom > threshold) continue;
|
|
405
369
|
if (!isFilled(wrap)) {
|
|
@@ -424,16 +388,13 @@
|
|
|
424
388
|
if (ph) state.io?.unobserve(ph);
|
|
425
389
|
} catch (_) {}
|
|
426
390
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// Step 2: remove old HTML, create fresh placeholder at new position
|
|
431
|
-
// Step 3: showAds(id)
|
|
432
|
-
ezCmd(() => {
|
|
391
|
+
// Ezoic recycle: destroy → new DOM → showAds
|
|
392
|
+
const doRecycle = () => {
|
|
393
|
+
state.phState.set(id, 'destroyed');
|
|
433
394
|
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
434
395
|
|
|
435
396
|
setTimeout(() => {
|
|
436
|
-
// Recreate placeholder DOM
|
|
397
|
+
// Recreate fresh placeholder DOM at new position
|
|
437
398
|
mutate(() => {
|
|
438
399
|
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
439
400
|
best.setAttribute(ATTR.CREATED, String(now()));
|
|
@@ -452,14 +413,18 @@
|
|
|
452
413
|
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
453
414
|
state.wrapByKey.set(newKey, best);
|
|
454
415
|
|
|
455
|
-
//
|
|
416
|
+
// Re-show after DOM is settled
|
|
456
417
|
setTimeout(() => {
|
|
457
|
-
state.phState.set(id, 'new');
|
|
458
418
|
observePlaceholder(id);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
419
|
+
state.phState.set(id, 'new');
|
|
420
|
+
enqueueShow(id);
|
|
421
|
+
}, TIMING.RECYCLE_DELAY_MS);
|
|
422
|
+
}, TIMING.RECYCLE_DELAY_MS);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle();
|
|
427
|
+
} catch (_) {}
|
|
463
428
|
|
|
464
429
|
return { id, wrap: best };
|
|
465
430
|
}
|
|
@@ -474,6 +439,7 @@
|
|
|
474
439
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
475
440
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
476
441
|
w.style.cssText = 'width:100%;display:block';
|
|
442
|
+
|
|
477
443
|
const ph = document.createElement('div');
|
|
478
444
|
ph.id = `${PH_PREFIX}${id}`;
|
|
479
445
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -488,6 +454,7 @@
|
|
|
488
454
|
if (state.mountedIds.has(id)) return null;
|
|
489
455
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
490
456
|
if (existing?.isConnected) return null;
|
|
457
|
+
|
|
491
458
|
const w = makeWrap(id, klass, key);
|
|
492
459
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
493
460
|
state.mountedIds.add(id);
|
|
@@ -502,12 +469,16 @@
|
|
|
502
469
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
503
470
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
504
471
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
505
|
-
if (Number.isFinite(id)) {
|
|
472
|
+
if (Number.isFinite(id)) {
|
|
473
|
+
state.mountedIds.delete(id);
|
|
474
|
+
state.phState.delete(id);
|
|
475
|
+
}
|
|
506
476
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
507
477
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
508
478
|
for (const cls of w.classList) {
|
|
509
479
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
510
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
480
|
+
state.wrapsByClass.get(cls)?.delete(w);
|
|
481
|
+
break;
|
|
511
482
|
}
|
|
512
483
|
}
|
|
513
484
|
w.remove();
|
|
@@ -521,18 +492,22 @@
|
|
|
521
492
|
const cfg = KIND[klass];
|
|
522
493
|
const wraps = state.wrapsByClass.get(klass);
|
|
523
494
|
if (!wraps?.size) return;
|
|
495
|
+
|
|
524
496
|
const liveAnchors = new Set();
|
|
525
497
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
526
498
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
527
499
|
if (v) liveAnchors.add(v);
|
|
528
500
|
}
|
|
501
|
+
|
|
529
502
|
const t = now();
|
|
530
503
|
for (const w of wraps) {
|
|
531
504
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
532
505
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
533
506
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
534
507
|
const sid = key.slice(klass.length + 1);
|
|
535
|
-
if (!sid || !liveAnchors.has(sid))
|
|
508
|
+
if (!sid || !liveAnchors.has(sid)) {
|
|
509
|
+
mutate(() => dropWrap(w));
|
|
510
|
+
}
|
|
536
511
|
}
|
|
537
512
|
}
|
|
538
513
|
|
|
@@ -560,9 +535,11 @@
|
|
|
560
535
|
for (const el of items) {
|
|
561
536
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
562
537
|
if (!el?.isConnected) continue;
|
|
538
|
+
|
|
563
539
|
const ord = ordinal(klass, el);
|
|
564
540
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
565
541
|
if (adjacentWrap(el)) continue;
|
|
542
|
+
|
|
566
543
|
const key = anchorKey(klass, el);
|
|
567
544
|
if (findWrap(key)) continue;
|
|
568
545
|
|
|
@@ -571,8 +548,7 @@
|
|
|
571
548
|
const w = insertAfter(el, id, klass, key);
|
|
572
549
|
if (w) {
|
|
573
550
|
observePlaceholder(id);
|
|
574
|
-
|
|
575
|
-
ezEnqueue(id);
|
|
551
|
+
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
576
552
|
inserted++;
|
|
577
553
|
}
|
|
578
554
|
} else {
|
|
@@ -585,12 +561,6 @@
|
|
|
585
561
|
}
|
|
586
562
|
|
|
587
563
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
588
|
-
//
|
|
589
|
-
// The IO is used to eagerly observe placeholders so that when they enter
|
|
590
|
-
// the viewport margin, we can queue them for Ezoic. However, the actual
|
|
591
|
-
// Ezoic API calls (define/displayMore) happen in the batched flush.
|
|
592
|
-
// The IO callback is mainly useful for re-triggering after NodeBB
|
|
593
|
-
// virtualisation re-inserts posts.
|
|
594
564
|
|
|
595
565
|
function getIO() {
|
|
596
566
|
if (state.io) return state.io;
|
|
@@ -598,19 +568,9 @@
|
|
|
598
568
|
state.io = new IntersectionObserver(entries => {
|
|
599
569
|
for (const entry of entries) {
|
|
600
570
|
if (!entry.isIntersecting) continue;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (!id || id <= 0) continue;
|
|
605
|
-
const st = state.phState.get(id);
|
|
606
|
-
// Only enqueue if not yet processed by Ezoic
|
|
607
|
-
if (st === 'new') {
|
|
608
|
-
ezEnqueue(id);
|
|
609
|
-
} else if (st === 'displayed') {
|
|
610
|
-
// Already shown — check if the placeholder is actually filled.
|
|
611
|
-
// If not (Ezoic had no ad), don't re-trigger — it won't help.
|
|
612
|
-
// If yes, nothing to do.
|
|
613
|
-
}
|
|
571
|
+
if (entry.target instanceof Element) state.io?.unobserve(entry.target);
|
|
572
|
+
const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
|
|
573
|
+
if (id > 0) enqueueShow(id);
|
|
614
574
|
}
|
|
615
575
|
}, {
|
|
616
576
|
root: null,
|
|
@@ -623,16 +583,196 @@
|
|
|
623
583
|
|
|
624
584
|
function observePlaceholder(id) {
|
|
625
585
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
626
|
-
if (ph?.isConnected) {
|
|
586
|
+
if (ph?.isConnected) {
|
|
587
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
function enqueueShow(id) {
|
|
594
|
+
if (!id || isBlocked()) return;
|
|
595
|
+
const st = state.phState.get(id);
|
|
596
|
+
if (st === 'show-queued' || st === 'shown') return;
|
|
597
|
+
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
598
|
+
|
|
599
|
+
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
600
|
+
if (!state.pendingSet.has(id)) {
|
|
601
|
+
state.pending.push(id);
|
|
602
|
+
state.pendingSet.add(id);
|
|
603
|
+
state.phState.set(id, 'show-queued');
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
state.phState.set(id, 'show-queued');
|
|
608
|
+
startShow(id);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function drainQueue() {
|
|
612
|
+
if (isBlocked()) return;
|
|
613
|
+
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
614
|
+
const id = state.pending.shift();
|
|
615
|
+
state.pendingSet.delete(id);
|
|
616
|
+
startShow(id);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function startShow(id) {
|
|
621
|
+
if (!id || isBlocked()) return;
|
|
622
|
+
state.inflight++;
|
|
623
|
+
|
|
624
|
+
let released = false;
|
|
625
|
+
const release = () => {
|
|
626
|
+
if (released) return;
|
|
627
|
+
released = true;
|
|
628
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
629
|
+
drainQueue();
|
|
630
|
+
};
|
|
631
|
+
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
632
|
+
|
|
633
|
+
requestAnimationFrame(() => {
|
|
634
|
+
try {
|
|
635
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
636
|
+
|
|
637
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
638
|
+
if (!ph?.isConnected) {
|
|
639
|
+
state.phState.delete(id);
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
return release();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
645
|
+
state.phState.set(id, 'shown');
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
return release();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const t = now();
|
|
651
|
+
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
return release();
|
|
654
|
+
}
|
|
655
|
+
state.lastShow.set(id, t);
|
|
656
|
+
|
|
657
|
+
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
658
|
+
state.phState.set(id, 'shown');
|
|
659
|
+
|
|
660
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
661
|
+
const ez = window.ezstandalone;
|
|
662
|
+
|
|
663
|
+
const doShow = () => {
|
|
664
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
665
|
+
try { ez.showAds(id); } catch (_) {}
|
|
666
|
+
if (wrap) scheduleUncollapseChecks(wrap);
|
|
667
|
+
scheduleEmptyCheck(id, t);
|
|
668
|
+
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
672
|
+
} catch (_) {
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
release();
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
680
|
+
// Two-pass check: conservative to avoid collapsing slow-loading ads
|
|
681
|
+
for (const delay of [TIMING.EMPTY_CHECK_EARLY_MS, TIMING.EMPTY_CHECK_LATE_MS]) {
|
|
682
|
+
setTimeout(() => {
|
|
683
|
+
try {
|
|
684
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
685
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
686
|
+
if (!wrap || !ph?.isConnected) return;
|
|
687
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
688
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
689
|
+
// Don't collapse if a GPT slot exists (might still be loading)
|
|
690
|
+
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
691
|
+
// Don't collapse if placeholder has meaningful height
|
|
692
|
+
if (ph.offsetHeight > 10) return;
|
|
693
|
+
wrap.classList.add('is-empty');
|
|
694
|
+
} catch (_) {}
|
|
695
|
+
}, delay);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
700
|
+
//
|
|
701
|
+
// Intercepts ez.showAds() to batch calls and filter disconnected placeholders.
|
|
702
|
+
// IMPORTANT: no-arg showAds() calls (used by Ezoic for page transitions)
|
|
703
|
+
// are passed through unmodified.
|
|
704
|
+
|
|
705
|
+
function patchShowAds() {
|
|
706
|
+
const apply = () => {
|
|
707
|
+
try {
|
|
708
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
709
|
+
const ez = window.ezstandalone;
|
|
710
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
711
|
+
window.__nbbEzPatched = true;
|
|
712
|
+
|
|
713
|
+
const orig = ez.showAds.bind(ez);
|
|
714
|
+
const queue = new Set();
|
|
715
|
+
let flushTimer = null;
|
|
716
|
+
|
|
717
|
+
const flush = () => {
|
|
718
|
+
flushTimer = null;
|
|
719
|
+
if (isBlocked() || !queue.size) return;
|
|
720
|
+
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
721
|
+
queue.clear();
|
|
722
|
+
const valid = ids.filter(id => {
|
|
723
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
724
|
+
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
725
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
726
|
+
return true;
|
|
727
|
+
});
|
|
728
|
+
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
729
|
+
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
730
|
+
try { orig(...chunk); } catch (_) {
|
|
731
|
+
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
ez.showAds = function (...args) {
|
|
737
|
+
// No-arg call = Ezoic page refresh — pass through unmodified
|
|
738
|
+
if (args.length === 0) {
|
|
739
|
+
return orig();
|
|
740
|
+
}
|
|
741
|
+
if (isBlocked()) return;
|
|
742
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
743
|
+
for (const v of ids) {
|
|
744
|
+
const id = parseInt(v, 10);
|
|
745
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
746
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
747
|
+
if (!ph?.isConnected) continue;
|
|
748
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
|
|
749
|
+
state.phState.set(id, 'show-queued');
|
|
750
|
+
queue.add(id);
|
|
751
|
+
}
|
|
752
|
+
if (queue.size && !flushTimer) {
|
|
753
|
+
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
} catch (_) {}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
apply();
|
|
760
|
+
if (!window.__nbbEzPatched) {
|
|
761
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
762
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
763
|
+
}
|
|
627
764
|
}
|
|
628
765
|
|
|
629
766
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
630
767
|
|
|
631
768
|
async function runCore() {
|
|
632
769
|
if (isBlocked()) return 0;
|
|
770
|
+
try { gcDisconnectedWraps(); } catch (_) {}
|
|
771
|
+
|
|
633
772
|
const cfg = await fetchConfig();
|
|
634
773
|
if (!cfg || cfg.excluded) return 0;
|
|
635
774
|
initPools(cfg);
|
|
775
|
+
|
|
636
776
|
const kind = getKind();
|
|
637
777
|
if (kind === 'other') return 0;
|
|
638
778
|
|
|
@@ -643,16 +783,22 @@
|
|
|
643
783
|
};
|
|
644
784
|
|
|
645
785
|
if (kind === 'topic') {
|
|
646
|
-
return exec(
|
|
647
|
-
|
|
786
|
+
return exec(
|
|
787
|
+
'ezoic-ad-message', getPosts,
|
|
788
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
789
|
+
);
|
|
648
790
|
}
|
|
649
791
|
if (kind === 'categoryTopics') {
|
|
650
792
|
pruneOrphansBetween();
|
|
651
|
-
return exec(
|
|
652
|
-
|
|
793
|
+
return exec(
|
|
794
|
+
'ezoic-ad-between', getTopics,
|
|
795
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
796
|
+
);
|
|
653
797
|
}
|
|
654
|
-
return exec(
|
|
655
|
-
|
|
798
|
+
return exec(
|
|
799
|
+
'ezoic-ad-categories', getCategories,
|
|
800
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
801
|
+
);
|
|
656
802
|
}
|
|
657
803
|
|
|
658
804
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -681,30 +827,25 @@
|
|
|
681
827
|
state.burstCount = 0;
|
|
682
828
|
const step = () => {
|
|
683
829
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
684
|
-
state.burstActive = false;
|
|
830
|
+
state.burstActive = false;
|
|
831
|
+
return;
|
|
685
832
|
}
|
|
686
833
|
state.burstCount++;
|
|
687
834
|
scheduleRun(n => {
|
|
688
|
-
if (!n && !state.
|
|
835
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
689
836
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
690
837
|
});
|
|
691
838
|
};
|
|
692
839
|
step();
|
|
693
840
|
}
|
|
694
841
|
|
|
695
|
-
// ── Cleanup on navigation
|
|
696
|
-
//
|
|
697
|
-
// Only runs on actual page transitions (pageKey changes).
|
|
698
|
-
// Uses destroyAll() to properly clean up Ezoic state before removing DOM.
|
|
842
|
+
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
699
843
|
|
|
700
844
|
function cleanup() {
|
|
701
845
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
702
846
|
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
state.ezBatch.clear();
|
|
706
|
-
|
|
707
|
-
// Tell Ezoic to destroy all its placeholders BEFORE we remove DOM
|
|
847
|
+
// Tell Ezoic to destroy all placeholders BEFORE we remove DOM elements.
|
|
848
|
+
// This prevents GPT slotDestroyed events and Ezoic 400 errors.
|
|
708
849
|
try {
|
|
709
850
|
const ez = window.ezstandalone;
|
|
710
851
|
if (typeof ez?.destroyAll === 'function') {
|
|
@@ -715,26 +856,32 @@
|
|
|
715
856
|
mutate(() => {
|
|
716
857
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
717
858
|
});
|
|
718
|
-
state.cfg
|
|
719
|
-
state.poolsReady
|
|
720
|
-
state.pools
|
|
721
|
-
state.cursors
|
|
859
|
+
state.cfg = null;
|
|
860
|
+
state.poolsReady = false;
|
|
861
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
862
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
722
863
|
state.mountedIds.clear();
|
|
723
864
|
state.lastShow.clear();
|
|
724
865
|
state.wrapByKey.clear();
|
|
725
866
|
state.wrapsByClass.clear();
|
|
726
|
-
state.kind
|
|
867
|
+
state.kind = null;
|
|
727
868
|
state.phState.clear();
|
|
869
|
+
state.inflight = 0;
|
|
870
|
+
state.pending = [];
|
|
871
|
+
state.pendingSet.clear();
|
|
728
872
|
state.burstActive = false;
|
|
729
873
|
state.runQueued = false;
|
|
874
|
+
state.firstShown = false;
|
|
730
875
|
}
|
|
731
876
|
|
|
732
877
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
733
878
|
|
|
734
879
|
function ensureDomObserver() {
|
|
735
880
|
if (state.domObs) return;
|
|
881
|
+
|
|
736
882
|
state.domObs = new MutationObserver(muts => {
|
|
737
883
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
884
|
+
|
|
738
885
|
let needsBurst = false;
|
|
739
886
|
const kind = getKind();
|
|
740
887
|
const relevantSels =
|
|
@@ -745,6 +892,20 @@
|
|
|
745
892
|
|
|
746
893
|
for (const m of muts) {
|
|
747
894
|
if (m.type !== 'childList') continue;
|
|
895
|
+
|
|
896
|
+
// Free IDs from wraps removed by NodeBB virtualization
|
|
897
|
+
for (const node of m.removedNodes) {
|
|
898
|
+
if (!(node instanceof Element)) continue;
|
|
899
|
+
try {
|
|
900
|
+
if (node.classList?.contains(WRAP_CLASS)) {
|
|
901
|
+
dropWrap(node);
|
|
902
|
+
} else {
|
|
903
|
+
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
904
|
+
if (wraps?.length) { for (const w of wraps) dropWrap(w); }
|
|
905
|
+
}
|
|
906
|
+
} catch (_) {}
|
|
907
|
+
}
|
|
908
|
+
|
|
748
909
|
for (const node of m.addedNodes) {
|
|
749
910
|
if (!(node instanceof Element)) continue;
|
|
750
911
|
|
|
@@ -781,54 +942,88 @@
|
|
|
781
942
|
}
|
|
782
943
|
if (needsBurst) break;
|
|
783
944
|
}
|
|
945
|
+
|
|
784
946
|
if (needsBurst) requestBurst();
|
|
785
947
|
});
|
|
786
|
-
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
951
|
+
} catch (_) {}
|
|
787
952
|
}
|
|
788
953
|
|
|
789
|
-
// ── TCF / CMP Protection
|
|
954
|
+
// ── TCF / CMP Protection (3 layers) ────────────────────────────────────────
|
|
790
955
|
|
|
791
956
|
function ensureTcfLocator() {
|
|
792
957
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
958
|
+
|
|
793
959
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
960
|
+
|
|
794
961
|
const ensureInHead = () => {
|
|
795
962
|
let existing = document.getElementById(LOCATOR_ID);
|
|
796
963
|
if (existing) {
|
|
797
964
|
if (existing.parentElement !== document.head) {
|
|
798
965
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
799
966
|
}
|
|
800
|
-
return;
|
|
967
|
+
return existing;
|
|
801
968
|
}
|
|
802
969
|
const f = document.createElement('iframe');
|
|
803
|
-
f.style.display = 'none';
|
|
970
|
+
f.style.display = 'none';
|
|
971
|
+
f.id = f.name = LOCATOR_ID;
|
|
804
972
|
try { document.head.appendChild(f); } catch (_) {
|
|
805
973
|
(document.body || document.documentElement).appendChild(f);
|
|
806
974
|
}
|
|
975
|
+
return f;
|
|
807
976
|
};
|
|
977
|
+
|
|
808
978
|
ensureInHead();
|
|
979
|
+
|
|
809
980
|
if (!window.__nbbCmpGuarded) {
|
|
810
981
|
window.__nbbCmpGuarded = true;
|
|
811
982
|
if (typeof window.__tcfapi === 'function') {
|
|
812
|
-
const
|
|
813
|
-
window.__tcfapi = function (cmd,
|
|
814
|
-
try {
|
|
815
|
-
|
|
983
|
+
const origTcf = window.__tcfapi;
|
|
984
|
+
window.__tcfapi = function (cmd, version, cb, param) {
|
|
985
|
+
try {
|
|
986
|
+
return origTcf.call(this, cmd, version, function (...args) {
|
|
987
|
+
try { cb?.(...args); } catch (_) {}
|
|
988
|
+
}, param);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
if (e?.message?.includes('null')) {
|
|
991
|
+
ensureInHead();
|
|
992
|
+
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
816
995
|
};
|
|
817
996
|
}
|
|
818
997
|
if (typeof window.__cmp === 'function') {
|
|
819
|
-
const
|
|
820
|
-
window.__cmp = function (...
|
|
821
|
-
try { return
|
|
822
|
-
catch (e) {
|
|
998
|
+
const origCmp = window.__cmp;
|
|
999
|
+
window.__cmp = function (...args) {
|
|
1000
|
+
try { return origCmp.apply(this, args); }
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
if (e?.message?.includes('null')) {
|
|
1003
|
+
ensureInHead();
|
|
1004
|
+
try { return origCmp.apply(this, args); } catch (_) {}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
823
1007
|
};
|
|
824
1008
|
}
|
|
825
1009
|
}
|
|
1010
|
+
|
|
826
1011
|
if (!window.__nbbTcfObs) {
|
|
827
1012
|
window.__nbbTcfObs = new MutationObserver(() => {
|
|
828
1013
|
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
829
1014
|
});
|
|
830
|
-
try {
|
|
831
|
-
|
|
1015
|
+
try {
|
|
1016
|
+
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1017
|
+
childList: true, subtree: false,
|
|
1018
|
+
});
|
|
1019
|
+
} catch (_) {}
|
|
1020
|
+
try {
|
|
1021
|
+
if (document.head) {
|
|
1022
|
+
window.__nbbTcfObs.observe(document.head, {
|
|
1023
|
+
childList: true, subtree: false,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
} catch (_) {}
|
|
832
1027
|
}
|
|
833
1028
|
}
|
|
834
1029
|
|
|
@@ -837,11 +1032,20 @@
|
|
|
837
1032
|
function protectAriaHidden() {
|
|
838
1033
|
if (window.__nbbAriaObs) return;
|
|
839
1034
|
const remove = () => {
|
|
840
|
-
try {
|
|
1035
|
+
try {
|
|
1036
|
+
if (document.body.getAttribute('aria-hidden') === 'true') {
|
|
1037
|
+
document.body.removeAttribute('aria-hidden');
|
|
1038
|
+
}
|
|
1039
|
+
} catch (_) {}
|
|
841
1040
|
};
|
|
842
1041
|
remove();
|
|
843
1042
|
window.__nbbAriaObs = new MutationObserver(remove);
|
|
844
|
-
try {
|
|
1043
|
+
try {
|
|
1044
|
+
window.__nbbAriaObs.observe(document.body, {
|
|
1045
|
+
attributes: true,
|
|
1046
|
+
attributeFilter: ['aria-hidden'],
|
|
1047
|
+
});
|
|
1048
|
+
} catch (_) {}
|
|
845
1049
|
}
|
|
846
1050
|
|
|
847
1051
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
@@ -849,21 +1053,24 @@
|
|
|
849
1053
|
function muteConsole() {
|
|
850
1054
|
if (window.__nbbEzMuted) return;
|
|
851
1055
|
window.__nbbEzMuted = true;
|
|
1056
|
+
|
|
852
1057
|
const PREFIXES = [
|
|
853
1058
|
'[EzoicAds JS]: Placeholder Id',
|
|
1059
|
+
'No valid placeholders for loadMore',
|
|
854
1060
|
'cannot call refresh on the same page',
|
|
855
1061
|
'no placeholders are currently defined in Refresh',
|
|
856
1062
|
'Debugger iframe already exists',
|
|
857
1063
|
'[CMP] Error in custom getTCData',
|
|
858
1064
|
'vignette: no interstitial API',
|
|
1065
|
+
'Ezoic JS-Enable should only ever',
|
|
859
1066
|
];
|
|
860
1067
|
const PATTERNS = [
|
|
861
1068
|
`with id ${PH_PREFIX}`,
|
|
862
1069
|
'adsbygoogle.push() error: All',
|
|
863
1070
|
'has already been defined',
|
|
864
|
-
'No valid placeholders for loadMore',
|
|
865
1071
|
'bad response. Status',
|
|
866
1072
|
];
|
|
1073
|
+
|
|
867
1074
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
868
1075
|
const orig = console[method];
|
|
869
1076
|
if (typeof orig !== 'function') continue;
|
|
@@ -881,22 +1088,25 @@
|
|
|
881
1088
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
882
1089
|
|
|
883
1090
|
let _networkWarmed = false;
|
|
1091
|
+
|
|
884
1092
|
function warmNetwork() {
|
|
885
1093
|
if (_networkWarmed) return;
|
|
886
1094
|
_networkWarmed = true;
|
|
887
1095
|
const head = document.head;
|
|
888
1096
|
if (!head) return;
|
|
889
|
-
|
|
1097
|
+
const hints = [
|
|
890
1098
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
891
1099
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
892
1100
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
893
1101
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
894
1102
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
895
1103
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
896
|
-
]
|
|
1104
|
+
];
|
|
1105
|
+
for (const [rel, href, cors] of hints) {
|
|
897
1106
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
898
1107
|
const link = document.createElement('link');
|
|
899
|
-
link.rel = rel;
|
|
1108
|
+
link.rel = rel;
|
|
1109
|
+
link.href = href;
|
|
900
1110
|
if (cors) link.crossOrigin = 'anonymous';
|
|
901
1111
|
head.appendChild(link);
|
|
902
1112
|
}
|
|
@@ -907,35 +1117,55 @@
|
|
|
907
1117
|
function bindNodeBB() {
|
|
908
1118
|
const $ = window.jQuery;
|
|
909
1119
|
if (!$) return;
|
|
1120
|
+
|
|
910
1121
|
$(window).off('.nbbEzoic');
|
|
1122
|
+
|
|
1123
|
+
// Only cleanup on actual page change, not same-page pagination
|
|
911
1124
|
$(window).on('action:ajaxify.start.nbbEzoic', (ev, data) => {
|
|
912
|
-
// Only cleanup if navigating to a different page
|
|
913
|
-
// NodeBB fires ajaxify.start for pagination/sorting on the same page
|
|
914
1125
|
const targetUrl = data?.url || data?.tpl_url || '';
|
|
915
1126
|
const currentPath = location.pathname.replace(/^\//, '');
|
|
916
|
-
// If the URL is basically the same (ignoring query/hash), skip cleanup
|
|
917
1127
|
if (targetUrl && targetUrl.replace(/[?#].*$/, '') === currentPath.replace(/[?#].*$/, '')) {
|
|
918
|
-
return;
|
|
1128
|
+
return; // Same page — skip cleanup
|
|
919
1129
|
}
|
|
920
1130
|
cleanup();
|
|
921
1131
|
});
|
|
1132
|
+
|
|
922
1133
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
923
|
-
state.pageKey
|
|
924
|
-
state.kind
|
|
1134
|
+
state.pageKey = pageKey();
|
|
1135
|
+
state.kind = null;
|
|
925
1136
|
state.blockedUntil = 0;
|
|
926
|
-
|
|
927
|
-
|
|
1137
|
+
|
|
1138
|
+
muteConsole();
|
|
1139
|
+
ensureTcfLocator();
|
|
1140
|
+
protectAriaHidden();
|
|
1141
|
+
warmNetwork();
|
|
1142
|
+
patchShowAds();
|
|
1143
|
+
getIO();
|
|
1144
|
+
ensureDomObserver();
|
|
1145
|
+
requestBurst();
|
|
928
1146
|
});
|
|
1147
|
+
|
|
929
1148
|
const burstEvents = [
|
|
930
|
-
'action:ajaxify.contentLoaded',
|
|
931
|
-
'action:
|
|
1149
|
+
'action:ajaxify.contentLoaded',
|
|
1150
|
+
'action:posts.loaded',
|
|
1151
|
+
'action:topics.loaded',
|
|
1152
|
+
'action:categories.loaded',
|
|
1153
|
+
'action:category.loaded',
|
|
1154
|
+
'action:topic.loaded',
|
|
932
1155
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1156
|
+
|
|
933
1157
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1158
|
+
|
|
934
1159
|
try {
|
|
935
1160
|
require(['hooks'], hooks => {
|
|
936
1161
|
if (typeof hooks?.on !== 'function') return;
|
|
937
|
-
for (const ev of [
|
|
938
|
-
|
|
1162
|
+
for (const ev of [
|
|
1163
|
+
'action:ajaxify.end',
|
|
1164
|
+
'action:posts.loaded',
|
|
1165
|
+
'action:topics.loaded',
|
|
1166
|
+
'action:categories.loaded',
|
|
1167
|
+
'action:topic.loaded',
|
|
1168
|
+
]) {
|
|
939
1169
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
940
1170
|
}
|
|
941
1171
|
});
|
|
@@ -947,7 +1177,10 @@
|
|
|
947
1177
|
window.addEventListener('scroll', () => {
|
|
948
1178
|
if (ticking) return;
|
|
949
1179
|
ticking = true;
|
|
950
|
-
requestAnimationFrame(() => {
|
|
1180
|
+
requestAnimationFrame(() => {
|
|
1181
|
+
ticking = false;
|
|
1182
|
+
requestBurst();
|
|
1183
|
+
});
|
|
951
1184
|
}, { passive: true });
|
|
952
1185
|
}
|
|
953
1186
|
|
|
@@ -958,6 +1191,7 @@
|
|
|
958
1191
|
ensureTcfLocator();
|
|
959
1192
|
protectAriaHidden();
|
|
960
1193
|
warmNetwork();
|
|
1194
|
+
patchShowAds();
|
|
961
1195
|
getIO();
|
|
962
1196
|
ensureDomObserver();
|
|
963
1197
|
bindNodeBB();
|