nodebb-plugin-ezoic-infinite 1.8.38 → 1.8.39
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 +308 -535
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v3.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Proper Ezoic infinite scroll API flow:
|
|
5
|
+
*
|
|
6
|
+
* FIRST BATCH: ez.define(ids...) → ez.enable()
|
|
7
|
+
* NEXT BATCHES: ez.define(ids...) → ez.displayMore(ids...)
|
|
8
|
+
* RECYCLE: ez.destroyPlaceholders(id) → new placeholder DOM
|
|
9
|
+
* → ez.define(id) → ez.displayMore(id)
|
|
10
|
+
*
|
|
11
|
+
* phState machine per placeholder ID:
|
|
12
|
+
* new → defined → displayed → [destroyed → new] (recycle loop)
|
|
11
13
|
*/
|
|
12
14
|
(function nbbEzoicInfinite() {
|
|
13
15
|
'use strict';
|
|
@@ -30,16 +32,13 @@
|
|
|
30
32
|
SHOW_THROTTLE_MS: 900,
|
|
31
33
|
BURST_COOLDOWN_MS: 200,
|
|
32
34
|
BLOCK_DURATION_MS: 1_500,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RECYCLE_DELAY_MS: 450,
|
|
35
|
+
BATCH_FLUSH_MS: 120,
|
|
36
|
+
RECYCLE_DESTROY_MS: 300,
|
|
37
|
+
RECYCLE_DEFINE_MS: 300,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
const LIMITS = {
|
|
40
41
|
MAX_INSERTS_RUN: 6,
|
|
41
|
-
MAX_INFLIGHT: 4,
|
|
42
|
-
BATCH_SIZE: 3,
|
|
43
42
|
MAX_BURST_STEPS: 8,
|
|
44
43
|
BURST_WINDOW_MS: 2_000,
|
|
45
44
|
};
|
|
@@ -63,30 +62,25 @@
|
|
|
63
62
|
|
|
64
63
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
65
64
|
|
|
65
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
66
|
+
|
|
66
67
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
67
68
|
|
|
68
69
|
const now = () => Date.now();
|
|
69
70
|
const isMobile = () => window.innerWidth < 768;
|
|
70
71
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
71
72
|
|
|
72
|
-
function isFilled(node)
|
|
73
|
-
return node?.querySelector?.(FILL_SEL) != null;
|
|
74
|
-
}
|
|
75
|
-
|
|
73
|
+
function isFilled(node) { return node?.querySelector?.(FILL_SEL) != null; }
|
|
76
74
|
function isPlaceholderUsed(ph) {
|
|
77
75
|
if (!ph?.isConnected) return false;
|
|
78
76
|
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
function parseIds(raw) {
|
|
82
|
-
const out = [];
|
|
83
|
-
const seen = new Set();
|
|
80
|
+
const out = [], seen = new Set();
|
|
84
81
|
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
85
82
|
const n = parseInt(line, 10);
|
|
86
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
87
|
-
seen.add(n);
|
|
88
|
-
out.push(n);
|
|
89
|
-
}
|
|
83
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
90
84
|
}
|
|
91
85
|
return out;
|
|
92
86
|
}
|
|
@@ -94,37 +88,30 @@
|
|
|
94
88
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
95
89
|
|
|
96
90
|
const state = {
|
|
97
|
-
pageKey:
|
|
98
|
-
kind: null,
|
|
99
|
-
cfg: null,
|
|
91
|
+
pageKey: null, kind: null, cfg: null,
|
|
100
92
|
|
|
101
93
|
poolsReady: false,
|
|
102
|
-
pools:
|
|
103
|
-
cursors:
|
|
104
|
-
|
|
105
|
-
mountedIds: new Set(),
|
|
106
|
-
phState: new Map(),
|
|
107
|
-
lastShow: new Map(),
|
|
94
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
95
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
108
96
|
|
|
97
|
+
mountedIds: new Set(),
|
|
98
|
+
// phState: 'new' | 'defined' | 'displayed' | 'destroyed'
|
|
99
|
+
phState: new Map(),
|
|
100
|
+
lastShow: new Map(),
|
|
109
101
|
wrapByKey: new Map(),
|
|
110
102
|
wrapsByClass: new Map(),
|
|
111
103
|
|
|
112
|
-
io:
|
|
113
|
-
|
|
104
|
+
io: null, domObs: null,
|
|
105
|
+
mutGuard: 0, blockedUntil: 0,
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
// Ezoic batch queue: ids waiting for define+displayMore
|
|
108
|
+
ezBatch: new Set(),
|
|
109
|
+
ezFlushTimer: null,
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
runQueued: false,
|
|
123
|
-
burstActive: false,
|
|
124
|
-
burstDeadline: 0,
|
|
125
|
-
burstCount: 0,
|
|
126
|
-
lastBurstTs: 0,
|
|
127
|
-
firstShown: false,
|
|
111
|
+
// Lifecycle
|
|
112
|
+
enableCalled: false, // ez.enable() called once per page
|
|
113
|
+
runQueued: false,
|
|
114
|
+
burstActive: false, burstDeadline: 0, burstCount: 0, lastBurstTs: 0,
|
|
128
115
|
};
|
|
129
116
|
|
|
130
117
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -140,10 +127,7 @@
|
|
|
140
127
|
if (state.cfg) return state.cfg;
|
|
141
128
|
try {
|
|
142
129
|
const inline = window.__nbbEzoicCfg;
|
|
143
|
-
if (inline && typeof inline === 'object') {
|
|
144
|
-
state.cfg = inline;
|
|
145
|
-
return state.cfg;
|
|
146
|
-
}
|
|
130
|
+
if (inline && typeof inline === 'object') { state.cfg = inline; return state.cfg; }
|
|
147
131
|
} catch (_) {}
|
|
148
132
|
try {
|
|
149
133
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
@@ -182,15 +166,12 @@
|
|
|
182
166
|
return 'other';
|
|
183
167
|
}
|
|
184
168
|
|
|
185
|
-
function getKind() {
|
|
186
|
-
return state.kind || (state.kind = detectKind());
|
|
187
|
-
}
|
|
169
|
+
function getKind() { return state.kind || (state.kind = detectKind()); }
|
|
188
170
|
|
|
189
171
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
190
172
|
|
|
191
173
|
function getPosts() {
|
|
192
|
-
const all = document.querySelectorAll(SEL.post);
|
|
193
|
-
const out = [];
|
|
174
|
+
const all = document.querySelectorAll(SEL.post), out = [];
|
|
194
175
|
for (let i = 0; i < all.length; i++) {
|
|
195
176
|
const el = all[i];
|
|
196
177
|
if (!el.isConnected) continue;
|
|
@@ -202,7 +183,6 @@
|
|
|
202
183
|
}
|
|
203
184
|
return out;
|
|
204
185
|
}
|
|
205
|
-
|
|
206
186
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
207
187
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
208
188
|
|
|
@@ -210,15 +190,10 @@
|
|
|
210
190
|
|
|
211
191
|
function stableId(klass, el) {
|
|
212
192
|
const attr = KIND[klass]?.anchorAttr;
|
|
213
|
-
if (attr) {
|
|
214
|
-
const v = el.getAttribute(attr);
|
|
215
|
-
if (v != null && v !== '') return v;
|
|
216
|
-
}
|
|
193
|
+
if (attr) { const v = el.getAttribute(attr); if (v != null && v !== '') return v; }
|
|
217
194
|
const children = el.parentElement?.children;
|
|
218
195
|
if (!children) return 'i0';
|
|
219
|
-
for (let i = 0; i < children.length; i++) {
|
|
220
|
-
if (children[i] === el) return `i${i}`;
|
|
221
|
-
}
|
|
196
|
+
for (let i = 0; i < children.length; i++) { if (children[i] === el) return `i${i}`; }
|
|
222
197
|
return 'i0';
|
|
223
198
|
}
|
|
224
199
|
|
|
@@ -230,9 +205,9 @@
|
|
|
230
205
|
}
|
|
231
206
|
|
|
232
207
|
function getWrapSet(klass) {
|
|
233
|
-
let
|
|
234
|
-
if (!
|
|
235
|
-
return
|
|
208
|
+
let s = state.wrapsByClass.get(klass);
|
|
209
|
+
if (!s) { s = new Set(); state.wrapsByClass.set(klass, s); }
|
|
210
|
+
return s;
|
|
236
211
|
}
|
|
237
212
|
|
|
238
213
|
// ── Wrap lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -241,28 +216,19 @@
|
|
|
241
216
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
242
217
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
243
218
|
if (!key) return false;
|
|
244
|
-
|
|
245
219
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
246
|
-
|
|
247
220
|
const colonIdx = key.indexOf(':');
|
|
248
|
-
const klass
|
|
249
|
-
const anchorId = key.slice(colonIdx + 1);
|
|
221
|
+
const klass = key.slice(0, colonIdx), anchorId = key.slice(colonIdx + 1);
|
|
250
222
|
const cfg = KIND[klass];
|
|
251
223
|
if (!cfg) return false;
|
|
252
|
-
|
|
253
224
|
const parent = wrap.parentElement;
|
|
254
225
|
if (parent) {
|
|
255
226
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
256
227
|
for (const sib of parent.children) {
|
|
257
|
-
if (sib !== wrap) {
|
|
258
|
-
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
259
|
-
}
|
|
228
|
+
if (sib !== wrap) { try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {} }
|
|
260
229
|
}
|
|
261
230
|
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
265
|
-
} catch (_) { return false; }
|
|
231
|
+
try { return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true; } catch (_) { return false; }
|
|
266
232
|
}
|
|
267
233
|
|
|
268
234
|
function adjacentWrap(el) {
|
|
@@ -277,20 +243,18 @@
|
|
|
277
243
|
if (!ph || !isFilled(ph)) return false;
|
|
278
244
|
wrap.classList.remove('is-empty');
|
|
279
245
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
280
|
-
if (id > 0) state.phState.set(id, '
|
|
246
|
+
if (id > 0) state.phState.set(id, 'displayed');
|
|
281
247
|
return true;
|
|
282
248
|
}
|
|
283
249
|
|
|
284
250
|
function scheduleUncollapseChecks(wrap) {
|
|
285
251
|
if (!wrap) return;
|
|
286
252
|
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
287
|
-
setTimeout(() => {
|
|
288
|
-
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
289
|
-
}, ms);
|
|
253
|
+
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
290
254
|
}
|
|
291
255
|
}
|
|
292
256
|
|
|
293
|
-
// ── Pool
|
|
257
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
294
258
|
|
|
295
259
|
function pickId(poolKey) {
|
|
296
260
|
const pool = state.pools[poolKey];
|
|
@@ -304,17 +268,140 @@
|
|
|
304
268
|
return null;
|
|
305
269
|
}
|
|
306
270
|
|
|
307
|
-
// ──
|
|
271
|
+
// ── Ezoic API layer ────────────────────────────────────────────────────────
|
|
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); })
|
|
308
278
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// and wraps got recycled before ads had time to load
|
|
312
|
-
// - Never recycle a wrap whose showAds is still inflight (show-queued state)
|
|
313
|
-
// - Clear ins.adsbygoogle children before re-creating placeholder to avoid
|
|
314
|
-
// "All ins elements already have ads" error
|
|
315
|
-
// - Skip wraps that were created less than 5s ago (give ads time to fill)
|
|
279
|
+
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
280
|
+
// Ezoic API on every single placeholder insertion.
|
|
316
281
|
|
|
317
|
-
|
|
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
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Destroy a placeholder in Ezoic (for recycling).
|
|
370
|
+
* Returns a Promise that resolves after the destroy delay.
|
|
371
|
+
*/
|
|
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.
|
|
318
405
|
|
|
319
406
|
function recycleWrap(klass, targetEl, newKey) {
|
|
320
407
|
const ez = window.ezstandalone;
|
|
@@ -323,7 +410,6 @@
|
|
|
323
410
|
typeof ez?.displayMore !== 'function') return null;
|
|
324
411
|
|
|
325
412
|
const vh = window.innerHeight || 800;
|
|
326
|
-
// Conservative threshold: 3× viewport height above the top of the screen
|
|
327
413
|
const threshold = -(3 * vh);
|
|
328
414
|
const t = now();
|
|
329
415
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
@@ -334,17 +420,14 @@
|
|
|
334
420
|
|
|
335
421
|
for (const wrap of wraps) {
|
|
336
422
|
try {
|
|
337
|
-
// Don't recycle wraps that are too young (ads might still be loading)
|
|
338
423
|
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
339
424
|
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
340
|
-
|
|
341
|
-
// Don't recycle wraps with inflight showAds
|
|
342
425
|
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
343
|
-
|
|
344
|
-
|
|
426
|
+
const st = state.phState.get(wid);
|
|
427
|
+
// Don't recycle placeholders that are still being processed
|
|
428
|
+
if (st === 'new' || st === 'defined') continue;
|
|
345
429
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
346
430
|
if (bottom > threshold) continue;
|
|
347
|
-
|
|
348
431
|
if (!isFilled(wrap)) {
|
|
349
432
|
if (bottom < bestEmptyY) { bestEmptyY = bottom; bestEmpty = wrap; }
|
|
350
433
|
} else {
|
|
@@ -367,43 +450,40 @@
|
|
|
367
450
|
if (ph) state.io?.unobserve(ph);
|
|
368
451
|
} catch (_) {}
|
|
369
452
|
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
373
|
-
best.setAttribute(ATTR.CREATED, String(now()));
|
|
374
|
-
best.setAttribute(ATTR.SHOWN, '0');
|
|
375
|
-
best.classList.remove('is-empty');
|
|
376
|
-
// Clear ALL children including ins.adsbygoogle to avoid
|
|
377
|
-
// "All ins elements already have ads" error on re-show
|
|
378
|
-
best.replaceChildren();
|
|
379
|
-
|
|
380
|
-
const fresh = document.createElement('div');
|
|
381
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
382
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
383
|
-
fresh.style.minHeight = '1px';
|
|
384
|
-
best.appendChild(fresh);
|
|
385
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
386
|
-
});
|
|
453
|
+
// Step 1: Destroy in Ezoic, then recreate DOM and re-define
|
|
454
|
+
state.phState.set(id, 'destroyed');
|
|
387
455
|
|
|
388
|
-
|
|
389
|
-
|
|
456
|
+
ezCmd(() => {
|
|
457
|
+
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
390
458
|
|
|
391
|
-
// Ezoic destroy → re-define → re-show sequence
|
|
392
|
-
const doDestroy = () => {
|
|
393
|
-
state.phState.set(id, 'destroyed');
|
|
394
|
-
try { ez.destroyPlaceholders(id); } catch (_) {
|
|
395
|
-
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
396
|
-
}
|
|
397
459
|
setTimeout(() => {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
460
|
+
// Step 2: Recreate placeholder DOM at new position
|
|
461
|
+
mutate(() => {
|
|
462
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
463
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
464
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
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
|
+
});
|
|
403
475
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
476
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
477
|
+
state.wrapByKey.set(newKey, best);
|
|
478
|
+
|
|
479
|
+
// Step 3: Re-define + displayMore via the batch queue
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
state.phState.set(id, 'new');
|
|
482
|
+
observePlaceholder(id);
|
|
483
|
+
ezEnqueue(id);
|
|
484
|
+
}, TIMING.RECYCLE_DEFINE_MS);
|
|
485
|
+
}, TIMING.RECYCLE_DESTROY_MS);
|
|
486
|
+
});
|
|
407
487
|
|
|
408
488
|
return { id, wrap: best };
|
|
409
489
|
}
|
|
@@ -418,7 +498,6 @@
|
|
|
418
498
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
419
499
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
420
500
|
w.style.cssText = 'width:100%;display:block';
|
|
421
|
-
|
|
422
501
|
const ph = document.createElement('div');
|
|
423
502
|
ph.id = `${PH_PREFIX}${id}`;
|
|
424
503
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -433,7 +512,6 @@
|
|
|
433
512
|
if (state.mountedIds.has(id)) return null;
|
|
434
513
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
435
514
|
if (existing?.isConnected) return null;
|
|
436
|
-
|
|
437
515
|
const w = makeWrap(id, klass, key);
|
|
438
516
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
439
517
|
state.mountedIds.add(id);
|
|
@@ -447,20 +525,13 @@
|
|
|
447
525
|
try {
|
|
448
526
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
449
527
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
450
|
-
|
|
451
528
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
452
|
-
if (Number.isFinite(id)) {
|
|
453
|
-
state.mountedIds.delete(id);
|
|
454
|
-
state.phState.delete(id);
|
|
455
|
-
}
|
|
456
|
-
|
|
529
|
+
if (Number.isFinite(id)) { state.mountedIds.delete(id); state.phState.delete(id); }
|
|
457
530
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
458
531
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
459
|
-
|
|
460
532
|
for (const cls of w.classList) {
|
|
461
533
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
462
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
463
|
-
break;
|
|
534
|
+
state.wrapsByClass.get(cls)?.delete(w); break;
|
|
464
535
|
}
|
|
465
536
|
}
|
|
466
537
|
w.remove();
|
|
@@ -474,22 +545,18 @@
|
|
|
474
545
|
const cfg = KIND[klass];
|
|
475
546
|
const wraps = state.wrapsByClass.get(klass);
|
|
476
547
|
if (!wraps?.size) return;
|
|
477
|
-
|
|
478
548
|
const liveAnchors = new Set();
|
|
479
549
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
480
550
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
481
551
|
if (v) liveAnchors.add(v);
|
|
482
552
|
}
|
|
483
|
-
|
|
484
553
|
const t = now();
|
|
485
554
|
for (const w of wraps) {
|
|
486
555
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
487
556
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
488
557
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
489
558
|
const sid = key.slice(klass.length + 1);
|
|
490
|
-
if (!sid || !liveAnchors.has(sid))
|
|
491
|
-
mutate(() => dropWrap(w));
|
|
492
|
-
}
|
|
559
|
+
if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
493
560
|
}
|
|
494
561
|
}
|
|
495
562
|
|
|
@@ -517,11 +584,9 @@
|
|
|
517
584
|
for (const el of items) {
|
|
518
585
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
519
586
|
if (!el?.isConnected) continue;
|
|
520
|
-
|
|
521
587
|
const ord = ordinal(klass, el);
|
|
522
588
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
523
589
|
if (adjacentWrap(el)) continue;
|
|
524
|
-
|
|
525
590
|
const key = anchorKey(klass, el);
|
|
526
591
|
if (findWrap(key)) continue;
|
|
527
592
|
|
|
@@ -530,7 +595,8 @@
|
|
|
530
595
|
const w = insertAfter(el, id, klass, key);
|
|
531
596
|
if (w) {
|
|
532
597
|
observePlaceholder(id);
|
|
533
|
-
|
|
598
|
+
// Queue for batched define+enable/displayMore
|
|
599
|
+
ezEnqueue(id);
|
|
534
600
|
inserted++;
|
|
535
601
|
}
|
|
536
602
|
} else {
|
|
@@ -543,6 +609,12 @@
|
|
|
543
609
|
}
|
|
544
610
|
|
|
545
611
|
// ── 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.
|
|
546
618
|
|
|
547
619
|
function getIO() {
|
|
548
620
|
if (state.io) return state.io;
|
|
@@ -550,12 +622,19 @@
|
|
|
550
622
|
state.io = new IntersectionObserver(entries => {
|
|
551
623
|
for (const entry of entries) {
|
|
552
624
|
if (!entry.isIntersecting) continue;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
if
|
|
625
|
+
const target = entry.target;
|
|
626
|
+
if (target instanceof Element) state.io?.unobserve(target);
|
|
627
|
+
const id = parseInt(target.getAttribute('data-ezoic-id'), 10);
|
|
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
|
+
}
|
|
559
638
|
}
|
|
560
639
|
}, {
|
|
561
640
|
root: null,
|
|
@@ -568,227 +647,19 @@
|
|
|
568
647
|
|
|
569
648
|
function observePlaceholder(id) {
|
|
570
649
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
571
|
-
if (ph?.isConnected) {
|
|
572
|
-
try { getIO()?.observe(ph); } catch (_) {}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
577
|
-
|
|
578
|
-
function enqueueShow(id) {
|
|
579
|
-
if (!id || isBlocked()) return;
|
|
580
|
-
const st = state.phState.get(id);
|
|
581
|
-
if (st === 'show-queued' || st === 'shown') return;
|
|
582
|
-
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
583
|
-
|
|
584
|
-
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
585
|
-
if (!state.pendingSet.has(id)) {
|
|
586
|
-
state.pending.push(id);
|
|
587
|
-
state.pendingSet.add(id);
|
|
588
|
-
state.phState.set(id, 'show-queued');
|
|
589
|
-
}
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
state.phState.set(id, 'show-queued');
|
|
593
|
-
startShow(id);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function drainQueue() {
|
|
597
|
-
if (isBlocked()) return;
|
|
598
|
-
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
599
|
-
const id = state.pending.shift();
|
|
600
|
-
state.pendingSet.delete(id);
|
|
601
|
-
startShow(id);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function startShow(id) {
|
|
606
|
-
if (!id || isBlocked()) return;
|
|
607
|
-
state.inflight++;
|
|
608
|
-
|
|
609
|
-
let released = false;
|
|
610
|
-
const release = () => {
|
|
611
|
-
if (released) return;
|
|
612
|
-
released = true;
|
|
613
|
-
state.inflight = Math.max(0, state.inflight - 1);
|
|
614
|
-
drainQueue();
|
|
615
|
-
};
|
|
616
|
-
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
617
|
-
|
|
618
|
-
requestAnimationFrame(() => {
|
|
619
|
-
try {
|
|
620
|
-
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
621
|
-
|
|
622
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
623
|
-
if (!ph?.isConnected) {
|
|
624
|
-
state.phState.delete(id);
|
|
625
|
-
clearTimeout(timer);
|
|
626
|
-
return release();
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
630
|
-
state.phState.set(id, 'shown');
|
|
631
|
-
clearTimeout(timer);
|
|
632
|
-
return release();
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const t = now();
|
|
636
|
-
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
637
|
-
clearTimeout(timer);
|
|
638
|
-
return release();
|
|
639
|
-
}
|
|
640
|
-
state.lastShow.set(id, t);
|
|
641
|
-
|
|
642
|
-
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
643
|
-
state.phState.set(id, 'shown');
|
|
644
|
-
|
|
645
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
646
|
-
const ez = window.ezstandalone;
|
|
647
|
-
|
|
648
|
-
const doShow = () => {
|
|
649
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
650
|
-
try { ez.showAds(id); } catch (_) {}
|
|
651
|
-
if (wrap) scheduleUncollapseChecks(wrap);
|
|
652
|
-
scheduleEmptyCheck(id, t);
|
|
653
|
-
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
657
|
-
} catch (_) {
|
|
658
|
-
clearTimeout(timer);
|
|
659
|
-
release();
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function scheduleEmptyCheck(id, showTs) {
|
|
665
|
-
setTimeout(() => {
|
|
666
|
-
try {
|
|
667
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
668
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
669
|
-
if (!wrap || !ph?.isConnected) return;
|
|
670
|
-
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
671
|
-
if (clearEmptyIfFilled(wrap)) return;
|
|
672
|
-
wrap.classList.add('is-empty');
|
|
673
|
-
} catch (_) {}
|
|
674
|
-
}, TIMING.EMPTY_CHECK_MS);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
678
|
-
|
|
679
|
-
function patchShowAds() {
|
|
680
|
-
const apply = () => {
|
|
681
|
-
try {
|
|
682
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
683
|
-
const ez = window.ezstandalone;
|
|
684
|
-
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
685
|
-
window.__nbbEzPatched = true;
|
|
686
|
-
|
|
687
|
-
const orig = ez.showAds.bind(ez);
|
|
688
|
-
const queue = new Set();
|
|
689
|
-
let flushTimer = null;
|
|
690
|
-
|
|
691
|
-
const flush = () => {
|
|
692
|
-
flushTimer = null;
|
|
693
|
-
if (isBlocked() || !queue.size) return;
|
|
694
|
-
|
|
695
|
-
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
696
|
-
queue.clear();
|
|
697
|
-
|
|
698
|
-
const valid = ids.filter(id => {
|
|
699
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
700
|
-
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
701
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
702
|
-
return true;
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
706
|
-
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
707
|
-
try { orig(...chunk); } catch (_) {
|
|
708
|
-
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
};
|
|
712
|
-
|
|
713
|
-
ez.showAds = function (...args) {
|
|
714
|
-
if (isBlocked()) return;
|
|
715
|
-
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
716
|
-
for (const v of ids) {
|
|
717
|
-
const id = parseInt(v, 10);
|
|
718
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
719
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
720
|
-
if (!ph?.isConnected) continue;
|
|
721
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
|
|
722
|
-
state.phState.set(id, 'show-queued');
|
|
723
|
-
queue.add(id);
|
|
724
|
-
}
|
|
725
|
-
if (queue.size && !flushTimer) {
|
|
726
|
-
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
} catch (_) {}
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
apply();
|
|
733
|
-
if (!window.__nbbEzPatched) {
|
|
734
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
735
|
-
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// ── Re-observe on scroll-up ────────────────────────────────────────────────
|
|
740
|
-
//
|
|
741
|
-
// NodeBB virtualizes posts: removes them from DOM when off-viewport, then
|
|
742
|
-
// re-inserts them when scrolling back up. Our wraps stay in the DOM but
|
|
743
|
-
// the placeholder was already unobserved by IO (in v2.0) or the phState
|
|
744
|
-
// was 'shown'. We need to check if wraps that are back in viewport have
|
|
745
|
-
// unfilled placeholders and re-trigger show for them.
|
|
746
|
-
|
|
747
|
-
function reobserveVisibleWraps() {
|
|
748
|
-
const vh = window.innerHeight || 800;
|
|
749
|
-
for (const [key, wrap] of state.wrapByKey) {
|
|
750
|
-
if (!wrap.isConnected) continue;
|
|
751
|
-
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
752
|
-
if (!id || id <= 0) continue;
|
|
753
|
-
|
|
754
|
-
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
755
|
-
if (!ph?.isConnected) continue;
|
|
756
|
-
|
|
757
|
-
// Already filled — nothing to do
|
|
758
|
-
if (isFilled(ph) || isPlaceholderUsed(ph)) continue;
|
|
759
|
-
|
|
760
|
-
// Check if the wrap is in or near the viewport
|
|
761
|
-
const rect = wrap.getBoundingClientRect();
|
|
762
|
-
if (rect.bottom < -vh || rect.top > 2 * vh) continue;
|
|
763
|
-
|
|
764
|
-
// This wrap is visible-ish but unfilled — re-trigger
|
|
765
|
-
const st = state.phState.get(id);
|
|
766
|
-
if (st === 'shown' || st === 'show-queued') {
|
|
767
|
-
// Reset state so enqueueShow accepts it again
|
|
768
|
-
state.phState.set(id, 'new');
|
|
769
|
-
state.lastShow.delete(id);
|
|
770
|
-
}
|
|
771
|
-
// Re-observe in IO (safe to call multiple times)
|
|
772
|
-
try { getIO()?.observe(ph); } catch (_) {}
|
|
773
|
-
enqueueShow(id);
|
|
774
|
-
}
|
|
650
|
+
if (ph?.isConnected) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
775
651
|
}
|
|
776
652
|
|
|
777
653
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
778
654
|
|
|
779
655
|
async function runCore() {
|
|
780
656
|
if (isBlocked()) return 0;
|
|
781
|
-
|
|
782
657
|
const cfg = await fetchConfig();
|
|
783
658
|
if (!cfg || cfg.excluded) return 0;
|
|
784
659
|
initPools(cfg);
|
|
785
|
-
|
|
786
660
|
const kind = getKind();
|
|
787
661
|
if (kind === 'other') return 0;
|
|
788
662
|
|
|
789
|
-
// Re-observe wraps that came back into viewport (scroll-up fix)
|
|
790
|
-
reobserveVisibleWraps();
|
|
791
|
-
|
|
792
663
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
793
664
|
if (!normBool(cfgEnable)) return 0;
|
|
794
665
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
@@ -796,24 +667,16 @@
|
|
|
796
667
|
};
|
|
797
668
|
|
|
798
669
|
if (kind === 'topic') {
|
|
799
|
-
return exec(
|
|
800
|
-
|
|
801
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
802
|
-
);
|
|
670
|
+
return exec('ezoic-ad-message', getPosts,
|
|
671
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
803
672
|
}
|
|
804
|
-
|
|
805
673
|
if (kind === 'categoryTopics') {
|
|
806
674
|
pruneOrphansBetween();
|
|
807
|
-
return exec(
|
|
808
|
-
|
|
809
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
810
|
-
);
|
|
675
|
+
return exec('ezoic-ad-between', getTopics,
|
|
676
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
811
677
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
'ezoic-ad-categories', getCategories,
|
|
815
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
816
|
-
);
|
|
678
|
+
return exec('ezoic-ad-categories', getCategories,
|
|
679
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
817
680
|
}
|
|
818
681
|
|
|
819
682
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -837,62 +700,56 @@
|
|
|
837
700
|
state.lastBurstTs = t;
|
|
838
701
|
state.pageKey = pageKey();
|
|
839
702
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
840
|
-
|
|
841
703
|
if (state.burstActive) return;
|
|
842
704
|
state.burstActive = true;
|
|
843
705
|
state.burstCount = 0;
|
|
844
|
-
|
|
845
706
|
const step = () => {
|
|
846
707
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
847
|
-
state.burstActive = false;
|
|
848
|
-
return;
|
|
708
|
+
state.burstActive = false; return;
|
|
849
709
|
}
|
|
850
710
|
state.burstCount++;
|
|
851
711
|
scheduleRun(n => {
|
|
852
|
-
if (!n && !state.
|
|
712
|
+
if (!n && !state.ezBatch.size) { state.burstActive = false; return; }
|
|
853
713
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
854
714
|
});
|
|
855
715
|
};
|
|
856
716
|
step();
|
|
857
717
|
}
|
|
858
718
|
|
|
859
|
-
// ── Cleanup
|
|
719
|
+
// ── Cleanup ────────────────────────────────────────────────────────────────
|
|
860
720
|
|
|
861
721
|
function cleanup() {
|
|
862
722
|
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
|
+
|
|
863
728
|
mutate(() => {
|
|
864
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
865
|
-
dropWrap(w);
|
|
866
|
-
}
|
|
729
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
867
730
|
});
|
|
868
|
-
state.cfg
|
|
869
|
-
state.poolsReady
|
|
870
|
-
state.pools
|
|
871
|
-
state.cursors
|
|
731
|
+
state.cfg = null;
|
|
732
|
+
state.poolsReady = false;
|
|
733
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
734
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
872
735
|
state.mountedIds.clear();
|
|
873
736
|
state.lastShow.clear();
|
|
874
737
|
state.wrapByKey.clear();
|
|
875
738
|
state.wrapsByClass.clear();
|
|
876
|
-
state.kind
|
|
739
|
+
state.kind = null;
|
|
877
740
|
state.phState.clear();
|
|
878
|
-
state.
|
|
879
|
-
state.pending = [];
|
|
880
|
-
state.pendingSet.clear();
|
|
741
|
+
state.enableCalled = false;
|
|
881
742
|
state.burstActive = false;
|
|
882
743
|
state.runQueued = false;
|
|
883
|
-
state.firstShown = false;
|
|
884
744
|
}
|
|
885
745
|
|
|
886
746
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
887
747
|
|
|
888
748
|
function ensureDomObserver() {
|
|
889
749
|
if (state.domObs) return;
|
|
890
|
-
|
|
891
750
|
state.domObs = new MutationObserver(muts => {
|
|
892
751
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
893
|
-
|
|
894
752
|
let needsBurst = false;
|
|
895
|
-
|
|
896
753
|
const kind = getKind();
|
|
897
754
|
const relevantSels =
|
|
898
755
|
kind === 'topic' ? [SEL.post] :
|
|
@@ -905,7 +762,7 @@
|
|
|
905
762
|
for (const node of m.addedNodes) {
|
|
906
763
|
if (!(node instanceof Element)) continue;
|
|
907
764
|
|
|
908
|
-
//
|
|
765
|
+
// Ad fill detection
|
|
909
766
|
try {
|
|
910
767
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
911
768
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -914,140 +771,91 @@
|
|
|
914
771
|
}
|
|
915
772
|
} catch (_) {}
|
|
916
773
|
|
|
917
|
-
//
|
|
774
|
+
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
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
|
|
918
788
|
if (!needsBurst) {
|
|
919
789
|
for (const sel of relevantSels) {
|
|
920
790
|
try {
|
|
921
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
922
|
-
needsBurst = true;
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
791
|
+
if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
|
|
925
792
|
} catch (_) {}
|
|
926
793
|
}
|
|
927
794
|
}
|
|
928
795
|
}
|
|
929
796
|
if (needsBurst) break;
|
|
930
797
|
}
|
|
931
|
-
|
|
932
798
|
if (needsBurst) requestBurst();
|
|
933
799
|
});
|
|
934
|
-
|
|
935
|
-
try {
|
|
936
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
937
|
-
} catch (_) {}
|
|
800
|
+
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
938
801
|
}
|
|
939
802
|
|
|
940
803
|
// ── TCF / CMP Protection ───────────────────────────────────────────────────
|
|
941
|
-
//
|
|
942
|
-
// 3 layers:
|
|
943
|
-
// 1. PROTECT: locator iframe in <head> (ajaxify never touches <head>)
|
|
944
|
-
// 2. GUARD: wrap __tcfapi/__cmp to catch null contentWindow errors
|
|
945
|
-
// 3. RESTORE: MutationObserver for immediate re-creation
|
|
946
804
|
|
|
947
805
|
function ensureTcfLocator() {
|
|
948
806
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
949
|
-
|
|
950
807
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
951
|
-
|
|
952
808
|
const ensureInHead = () => {
|
|
953
809
|
let existing = document.getElementById(LOCATOR_ID);
|
|
954
810
|
if (existing) {
|
|
955
811
|
if (existing.parentElement !== document.head) {
|
|
956
812
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
957
813
|
}
|
|
958
|
-
return
|
|
814
|
+
return;
|
|
959
815
|
}
|
|
960
816
|
const f = document.createElement('iframe');
|
|
961
|
-
f.style.display = 'none';
|
|
962
|
-
f.id = f.name = LOCATOR_ID;
|
|
817
|
+
f.style.display = 'none'; f.id = f.name = LOCATOR_ID;
|
|
963
818
|
try { document.head.appendChild(f); } catch (_) {
|
|
964
819
|
(document.body || document.documentElement).appendChild(f);
|
|
965
820
|
}
|
|
966
|
-
return f;
|
|
967
821
|
};
|
|
968
|
-
|
|
969
822
|
ensureInHead();
|
|
970
|
-
|
|
971
|
-
// Guard CMP API calls
|
|
972
823
|
if (!window.__nbbCmpGuarded) {
|
|
973
824
|
window.__nbbCmpGuarded = true;
|
|
974
|
-
|
|
975
825
|
if (typeof window.__tcfapi === 'function') {
|
|
976
|
-
const
|
|
977
|
-
window.__tcfapi = function (cmd,
|
|
978
|
-
try {
|
|
979
|
-
|
|
980
|
-
try { cb?.(...args); } catch (_) {}
|
|
981
|
-
}, param);
|
|
982
|
-
} catch (e) {
|
|
983
|
-
if (e?.message?.includes('null')) {
|
|
984
|
-
ensureInHead();
|
|
985
|
-
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
826
|
+
const orig = window.__tcfapi;
|
|
827
|
+
window.__tcfapi = function (cmd, ver, cb, param) {
|
|
828
|
+
try { return orig.call(this, cmd, ver, function (...a) { try { cb?.(...a); } catch (_) {} }, param); }
|
|
829
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.call(this, cmd, ver, cb, param); } catch (_) {} } }
|
|
988
830
|
};
|
|
989
831
|
}
|
|
990
|
-
|
|
991
832
|
if (typeof window.__cmp === 'function') {
|
|
992
|
-
const
|
|
993
|
-
window.__cmp = function (...
|
|
994
|
-
try {
|
|
995
|
-
|
|
996
|
-
} catch (e) {
|
|
997
|
-
if (e?.message?.includes('null')) {
|
|
998
|
-
ensureInHead();
|
|
999
|
-
try { return origCmp.apply(this, args); } catch (_) {}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
833
|
+
const orig = window.__cmp;
|
|
834
|
+
window.__cmp = function (...a) {
|
|
835
|
+
try { return orig.apply(this, a); }
|
|
836
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.apply(this, a); } catch (_) {} } }
|
|
1002
837
|
};
|
|
1003
838
|
}
|
|
1004
839
|
}
|
|
1005
|
-
|
|
1006
840
|
if (!window.__nbbTcfObs) {
|
|
1007
841
|
window.__nbbTcfObs = new MutationObserver(() => {
|
|
1008
|
-
if (document.getElementById(LOCATOR_ID))
|
|
1009
|
-
ensureInHead();
|
|
842
|
+
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
1010
843
|
});
|
|
1011
|
-
try {
|
|
1012
|
-
|
|
1013
|
-
childList: true, subtree: false,
|
|
1014
|
-
});
|
|
1015
|
-
} catch (_) {}
|
|
1016
|
-
try {
|
|
1017
|
-
if (document.head) {
|
|
1018
|
-
window.__nbbTcfObs.observe(document.head, {
|
|
1019
|
-
childList: true, subtree: false,
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
} catch (_) {}
|
|
844
|
+
try { window.__nbbTcfObs.observe(document.body || document.documentElement, { childList: true, subtree: false }); } catch (_) {}
|
|
845
|
+
try { if (document.head) window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false }); } catch (_) {}
|
|
1023
846
|
}
|
|
1024
847
|
}
|
|
1025
848
|
|
|
1026
849
|
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
1027
|
-
//
|
|
1028
|
-
// The CMP modal sets aria-hidden="true" on <body> when opening, and may not
|
|
1029
|
-
// remove it after ajaxify navigation. A simple removeAttribute in ajaxify.end
|
|
1030
|
-
// isn't enough because the CMP can re-set it asynchronously.
|
|
1031
|
-
//
|
|
1032
|
-
// Use a MutationObserver on <body> attributes to remove it immediately.
|
|
1033
850
|
|
|
1034
851
|
function protectAriaHidden() {
|
|
1035
852
|
if (window.__nbbAriaObs) return;
|
|
1036
853
|
const remove = () => {
|
|
1037
|
-
try {
|
|
1038
|
-
if (document.body.getAttribute('aria-hidden') === 'true') {
|
|
1039
|
-
document.body.removeAttribute('aria-hidden');
|
|
1040
|
-
}
|
|
1041
|
-
} catch (_) {}
|
|
854
|
+
try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1042
855
|
};
|
|
1043
856
|
remove();
|
|
1044
857
|
window.__nbbAriaObs = new MutationObserver(remove);
|
|
1045
|
-
try {
|
|
1046
|
-
window.__nbbAriaObs.observe(document.body, {
|
|
1047
|
-
attributes: true,
|
|
1048
|
-
attributeFilter: ['aria-hidden'],
|
|
1049
|
-
});
|
|
1050
|
-
} catch (_) {}
|
|
858
|
+
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
1051
859
|
}
|
|
1052
860
|
|
|
1053
861
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
@@ -1055,10 +863,8 @@
|
|
|
1055
863
|
function muteConsole() {
|
|
1056
864
|
if (window.__nbbEzMuted) return;
|
|
1057
865
|
window.__nbbEzMuted = true;
|
|
1058
|
-
|
|
1059
866
|
const PREFIXES = [
|
|
1060
867
|
'[EzoicAds JS]: Placeholder Id',
|
|
1061
|
-
'No valid placeholders for loadMore',
|
|
1062
868
|
'cannot call refresh on the same page',
|
|
1063
869
|
'no placeholders are currently defined in Refresh',
|
|
1064
870
|
'Debugger iframe already exists',
|
|
@@ -1068,20 +874,18 @@
|
|
|
1068
874
|
const PATTERNS = [
|
|
1069
875
|
`with id ${PH_PREFIX}`,
|
|
1070
876
|
'adsbygoogle.push() error: All',
|
|
877
|
+
'has already been defined',
|
|
878
|
+
'No valid placeholders for loadMore',
|
|
879
|
+
'bad response. Status',
|
|
1071
880
|
];
|
|
1072
|
-
|
|
1073
881
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
1074
882
|
const orig = console[method];
|
|
1075
883
|
if (typeof orig !== 'function') continue;
|
|
1076
884
|
console[method] = function (...args) {
|
|
1077
885
|
if (typeof args[0] === 'string') {
|
|
1078
886
|
const msg = args[0];
|
|
1079
|
-
for (const
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
for (const pat of PATTERNS) {
|
|
1083
|
-
if (msg.includes(pat)) return;
|
|
1084
|
-
}
|
|
887
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
888
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
1085
889
|
}
|
|
1086
890
|
return orig.apply(console, args);
|
|
1087
891
|
};
|
|
@@ -1091,28 +895,22 @@
|
|
|
1091
895
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1092
896
|
|
|
1093
897
|
let _networkWarmed = false;
|
|
1094
|
-
|
|
1095
898
|
function warmNetwork() {
|
|
1096
899
|
if (_networkWarmed) return;
|
|
1097
900
|
_networkWarmed = true;
|
|
1098
|
-
|
|
1099
901
|
const head = document.head;
|
|
1100
902
|
if (!head) return;
|
|
1101
|
-
|
|
1102
|
-
const hints = [
|
|
903
|
+
for (const [rel, href, cors] of [
|
|
1103
904
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
1104
905
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
1105
906
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
1106
907
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
1107
908
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1108
909
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1109
|
-
]
|
|
1110
|
-
|
|
1111
|
-
for (const [rel, href, cors] of hints) {
|
|
910
|
+
]) {
|
|
1112
911
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1113
912
|
const link = document.createElement('link');
|
|
1114
|
-
link.rel = rel;
|
|
1115
|
-
link.href = href;
|
|
913
|
+
link.rel = rel; link.href = href;
|
|
1116
914
|
if (cors) link.crossOrigin = 'anonymous';
|
|
1117
915
|
head.appendChild(link);
|
|
1118
916
|
}
|
|
@@ -1123,46 +921,25 @@
|
|
|
1123
921
|
function bindNodeBB() {
|
|
1124
922
|
const $ = window.jQuery;
|
|
1125
923
|
if (!$) return;
|
|
1126
|
-
|
|
1127
924
|
$(window).off('.nbbEzoic');
|
|
1128
925
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1129
|
-
|
|
1130
926
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1131
|
-
state.pageKey
|
|
1132
|
-
state.kind
|
|
927
|
+
state.pageKey = pageKey();
|
|
928
|
+
state.kind = null;
|
|
1133
929
|
state.blockedUntil = 0;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
ensureTcfLocator();
|
|
1137
|
-
protectAriaHidden();
|
|
1138
|
-
warmNetwork();
|
|
1139
|
-
patchShowAds();
|
|
1140
|
-
getIO();
|
|
1141
|
-
ensureDomObserver();
|
|
1142
|
-
requestBurst();
|
|
930
|
+
muteConsole(); ensureTcfLocator(); protectAriaHidden(); warmNetwork();
|
|
931
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
1143
932
|
});
|
|
1144
|
-
|
|
1145
933
|
const burstEvents = [
|
|
1146
|
-
'action:ajaxify.contentLoaded',
|
|
1147
|
-
'action:
|
|
1148
|
-
'action:topics.loaded',
|
|
1149
|
-
'action:categories.loaded',
|
|
1150
|
-
'action:category.loaded',
|
|
1151
|
-
'action:topic.loaded',
|
|
934
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
935
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
1152
936
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1153
|
-
|
|
1154
937
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1155
|
-
|
|
1156
938
|
try {
|
|
1157
939
|
require(['hooks'], hooks => {
|
|
1158
940
|
if (typeof hooks?.on !== 'function') return;
|
|
1159
|
-
for (const ev of [
|
|
1160
|
-
|
|
1161
|
-
'action:posts.loaded',
|
|
1162
|
-
'action:topics.loaded',
|
|
1163
|
-
'action:categories.loaded',
|
|
1164
|
-
'action:topic.loaded',
|
|
1165
|
-
]) {
|
|
941
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
942
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
1166
943
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
1167
944
|
}
|
|
1168
945
|
});
|
|
@@ -1174,10 +951,7 @@
|
|
|
1174
951
|
window.addEventListener('scroll', () => {
|
|
1175
952
|
if (ticking) return;
|
|
1176
953
|
ticking = true;
|
|
1177
|
-
requestAnimationFrame(() => {
|
|
1178
|
-
ticking = false;
|
|
1179
|
-
requestBurst();
|
|
1180
|
-
});
|
|
954
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1181
955
|
}, { passive: true });
|
|
1182
956
|
}
|
|
1183
957
|
|
|
@@ -1188,7 +962,6 @@
|
|
|
1188
962
|
ensureTcfLocator();
|
|
1189
963
|
protectAriaHidden();
|
|
1190
964
|
warmNetwork();
|
|
1191
|
-
patchShowAds();
|
|
1192
965
|
getIO();
|
|
1193
966
|
ensureDomObserver();
|
|
1194
967
|
bindNodeBB();
|