nodebb-plugin-ezoic-infinite 1.8.38 → 1.8.40
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 +318 -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,29 @@
|
|
|
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
|
+
runQueued: false,
|
|
113
|
+
burstActive: false, burstDeadline: 0, burstCount: 0, lastBurstTs: 0,
|
|
128
114
|
};
|
|
129
115
|
|
|
130
116
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -140,10 +126,7 @@
|
|
|
140
126
|
if (state.cfg) return state.cfg;
|
|
141
127
|
try {
|
|
142
128
|
const inline = window.__nbbEzoicCfg;
|
|
143
|
-
if (inline && typeof inline === 'object') {
|
|
144
|
-
state.cfg = inline;
|
|
145
|
-
return state.cfg;
|
|
146
|
-
}
|
|
129
|
+
if (inline && typeof inline === 'object') { state.cfg = inline; return state.cfg; }
|
|
147
130
|
} catch (_) {}
|
|
148
131
|
try {
|
|
149
132
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
@@ -182,15 +165,12 @@
|
|
|
182
165
|
return 'other';
|
|
183
166
|
}
|
|
184
167
|
|
|
185
|
-
function getKind() {
|
|
186
|
-
return state.kind || (state.kind = detectKind());
|
|
187
|
-
}
|
|
168
|
+
function getKind() { return state.kind || (state.kind = detectKind()); }
|
|
188
169
|
|
|
189
170
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
190
171
|
|
|
191
172
|
function getPosts() {
|
|
192
|
-
const all = document.querySelectorAll(SEL.post);
|
|
193
|
-
const out = [];
|
|
173
|
+
const all = document.querySelectorAll(SEL.post), out = [];
|
|
194
174
|
for (let i = 0; i < all.length; i++) {
|
|
195
175
|
const el = all[i];
|
|
196
176
|
if (!el.isConnected) continue;
|
|
@@ -202,7 +182,6 @@
|
|
|
202
182
|
}
|
|
203
183
|
return out;
|
|
204
184
|
}
|
|
205
|
-
|
|
206
185
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
207
186
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
208
187
|
|
|
@@ -210,15 +189,10 @@
|
|
|
210
189
|
|
|
211
190
|
function stableId(klass, el) {
|
|
212
191
|
const attr = KIND[klass]?.anchorAttr;
|
|
213
|
-
if (attr) {
|
|
214
|
-
const v = el.getAttribute(attr);
|
|
215
|
-
if (v != null && v !== '') return v;
|
|
216
|
-
}
|
|
192
|
+
if (attr) { const v = el.getAttribute(attr); if (v != null && v !== '') return v; }
|
|
217
193
|
const children = el.parentElement?.children;
|
|
218
194
|
if (!children) return 'i0';
|
|
219
|
-
for (let i = 0; i < children.length; i++) {
|
|
220
|
-
if (children[i] === el) return `i${i}`;
|
|
221
|
-
}
|
|
195
|
+
for (let i = 0; i < children.length; i++) { if (children[i] === el) return `i${i}`; }
|
|
222
196
|
return 'i0';
|
|
223
197
|
}
|
|
224
198
|
|
|
@@ -230,9 +204,9 @@
|
|
|
230
204
|
}
|
|
231
205
|
|
|
232
206
|
function getWrapSet(klass) {
|
|
233
|
-
let
|
|
234
|
-
if (!
|
|
235
|
-
return
|
|
207
|
+
let s = state.wrapsByClass.get(klass);
|
|
208
|
+
if (!s) { s = new Set(); state.wrapsByClass.set(klass, s); }
|
|
209
|
+
return s;
|
|
236
210
|
}
|
|
237
211
|
|
|
238
212
|
// ── Wrap lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -241,28 +215,19 @@
|
|
|
241
215
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
242
216
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
243
217
|
if (!key) return false;
|
|
244
|
-
|
|
245
218
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
246
|
-
|
|
247
219
|
const colonIdx = key.indexOf(':');
|
|
248
|
-
const klass
|
|
249
|
-
const anchorId = key.slice(colonIdx + 1);
|
|
220
|
+
const klass = key.slice(0, colonIdx), anchorId = key.slice(colonIdx + 1);
|
|
250
221
|
const cfg = KIND[klass];
|
|
251
222
|
if (!cfg) return false;
|
|
252
|
-
|
|
253
223
|
const parent = wrap.parentElement;
|
|
254
224
|
if (parent) {
|
|
255
225
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
256
226
|
for (const sib of parent.children) {
|
|
257
|
-
if (sib !== wrap) {
|
|
258
|
-
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
259
|
-
}
|
|
227
|
+
if (sib !== wrap) { try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {} }
|
|
260
228
|
}
|
|
261
229
|
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
265
|
-
} catch (_) { return false; }
|
|
230
|
+
try { return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true; } catch (_) { return false; }
|
|
266
231
|
}
|
|
267
232
|
|
|
268
233
|
function adjacentWrap(el) {
|
|
@@ -277,20 +242,18 @@
|
|
|
277
242
|
if (!ph || !isFilled(ph)) return false;
|
|
278
243
|
wrap.classList.remove('is-empty');
|
|
279
244
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
280
|
-
if (id > 0) state.phState.set(id, '
|
|
245
|
+
if (id > 0) state.phState.set(id, 'displayed');
|
|
281
246
|
return true;
|
|
282
247
|
}
|
|
283
248
|
|
|
284
249
|
function scheduleUncollapseChecks(wrap) {
|
|
285
250
|
if (!wrap) return;
|
|
286
251
|
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
287
|
-
setTimeout(() => {
|
|
288
|
-
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
289
|
-
}, ms);
|
|
252
|
+
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
290
253
|
}
|
|
291
254
|
}
|
|
292
255
|
|
|
293
|
-
// ── Pool
|
|
256
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
294
257
|
|
|
295
258
|
function pickId(poolKey) {
|
|
296
259
|
const pool = state.pools[poolKey];
|
|
@@ -304,17 +267,152 @@
|
|
|
304
267
|
return null;
|
|
305
268
|
}
|
|
306
269
|
|
|
307
|
-
// ──
|
|
270
|
+
// ── Ezoic API layer ────────────────────────────────────────────────────────
|
|
271
|
+
//
|
|
272
|
+
// Correct Ezoic infinite scroll flow:
|
|
273
|
+
// First batch: ez.cmd.push(() => { ez.define(...ids); ez.enable(); })
|
|
274
|
+
// Next batches: ez.cmd.push(() => { ez.define(...ids); ez.displayMore(...ids); })
|
|
275
|
+
// Recycle: ez.cmd.push(() => { ez.destroyPlaceholders(id); })
|
|
276
|
+
// → wait → new DOM → ez.cmd.push(() => { ez.define(id); ez.displayMore(id); })
|
|
308
277
|
//
|
|
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)
|
|
278
|
+
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
279
|
+
// Ezoic API on every single placeholder insertion.
|
|
316
280
|
|
|
317
|
-
|
|
281
|
+
function ezCmd(fn) {
|
|
282
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
283
|
+
const ez = window.ezstandalone;
|
|
284
|
+
if (Array.isArray(ez.cmd)) {
|
|
285
|
+
ez.cmd.push(fn);
|
|
286
|
+
} else {
|
|
287
|
+
try { fn(); } catch (_) {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Queue a placeholder ID for the next batched define+displayMore call.
|
|
293
|
+
*/
|
|
294
|
+
function ezEnqueue(id) {
|
|
295
|
+
if (isBlocked()) return;
|
|
296
|
+
state.ezBatch.add(id);
|
|
297
|
+
if (!state.ezFlushTimer) {
|
|
298
|
+
state.ezFlushTimer = setTimeout(ezFlush, TIMING.BATCH_FLUSH_MS);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Flush: define new IDs with Ezoic, then call displayMore.
|
|
304
|
+
*
|
|
305
|
+
* IMPORTANT: We NEVER call ez.enable() — Ezoic's sa.min.js calls it
|
|
306
|
+
* automatically on page load. Calling it again causes
|
|
307
|
+
* "Enable should only ever be called once" error.
|
|
308
|
+
*
|
|
309
|
+
* We also check ez.getSelectedPlaceholders() or equivalent to skip
|
|
310
|
+
* IDs that Ezoic already knows about (defined during initial enable).
|
|
311
|
+
*/
|
|
312
|
+
function ezFlush() {
|
|
313
|
+
state.ezFlushTimer = null;
|
|
314
|
+
if (isBlocked() || !state.ezBatch.size) return;
|
|
315
|
+
|
|
316
|
+
// Filter to only valid, connected placeholders
|
|
317
|
+
const ids = [];
|
|
318
|
+
for (const id of state.ezBatch) {
|
|
319
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
320
|
+
if (!ph?.isConnected) { state.phState.delete(id); continue; }
|
|
321
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'displayed'); continue; }
|
|
322
|
+
ids.push(id);
|
|
323
|
+
}
|
|
324
|
+
state.ezBatch.clear();
|
|
325
|
+
if (!ids.length) return;
|
|
326
|
+
|
|
327
|
+
ezCmd(() => {
|
|
328
|
+
const ez = window.ezstandalone;
|
|
329
|
+
if (!ez) return;
|
|
330
|
+
|
|
331
|
+
// Check which IDs Ezoic already knows about to avoid "already defined"
|
|
332
|
+
const alreadyDefined = new Set();
|
|
333
|
+
try {
|
|
334
|
+
// ez.allPlaceholders is an array of IDs Ezoic has already seen
|
|
335
|
+
if (Array.isArray(ez.allPlaceholders)) {
|
|
336
|
+
for (const p of ez.allPlaceholders) alreadyDefined.add(Number(p));
|
|
337
|
+
}
|
|
338
|
+
} catch (_) {}
|
|
339
|
+
|
|
340
|
+
const toDefine = ids.filter(id => !alreadyDefined.has(id));
|
|
341
|
+
// All IDs get displayMore, but only new ones get define
|
|
342
|
+
const toDisplay = ids;
|
|
343
|
+
|
|
344
|
+
// define() only for truly new placeholders
|
|
345
|
+
if (toDefine.length) {
|
|
346
|
+
try {
|
|
347
|
+
if (typeof ez.define === 'function') {
|
|
348
|
+
ez.define(...toDefine);
|
|
349
|
+
}
|
|
350
|
+
} catch (_) {}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const id of ids) state.phState.set(id, 'defined');
|
|
354
|
+
|
|
355
|
+
// displayMore() for all IDs — this is the infinite scroll API
|
|
356
|
+
if (toDisplay.length) {
|
|
357
|
+
try {
|
|
358
|
+
if (typeof ez.displayMore === 'function') {
|
|
359
|
+
ez.displayMore(...toDisplay);
|
|
360
|
+
}
|
|
361
|
+
} catch (_) {}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Mark as displayed and schedule fill checks
|
|
365
|
+
for (const id of ids) {
|
|
366
|
+
state.phState.set(id, 'displayed');
|
|
367
|
+
state.lastShow.set(id, now());
|
|
368
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
369
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
370
|
+
if (wrap) {
|
|
371
|
+
wrap.setAttribute(ATTR.SHOWN, String(now()));
|
|
372
|
+
scheduleUncollapseChecks(wrap);
|
|
373
|
+
}
|
|
374
|
+
scheduleEmptyCheck(id);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Destroy a placeholder in Ezoic (for recycling).
|
|
381
|
+
* Returns a Promise that resolves after the destroy delay.
|
|
382
|
+
*/
|
|
383
|
+
function ezDestroy(id) {
|
|
384
|
+
return new Promise(resolve => {
|
|
385
|
+
ezCmd(() => {
|
|
386
|
+
state.phState.set(id, 'destroyed');
|
|
387
|
+
const ez = window.ezstandalone;
|
|
388
|
+
try {
|
|
389
|
+
if (typeof ez?.destroyPlaceholders === 'function') {
|
|
390
|
+
ez.destroyPlaceholders(id);
|
|
391
|
+
}
|
|
392
|
+
} catch (_) {}
|
|
393
|
+
setTimeout(resolve, TIMING.RECYCLE_DESTROY_MS);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function scheduleEmptyCheck(id) {
|
|
399
|
+
const showTs = now();
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
try {
|
|
402
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
403
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
404
|
+
if (!wrap || !ph?.isConnected) return;
|
|
405
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
406
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
407
|
+
wrap.classList.add('is-empty');
|
|
408
|
+
} catch (_) {}
|
|
409
|
+
}, TIMING.EMPTY_CHECK_MS);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
413
|
+
//
|
|
414
|
+
// When pool is exhausted, find the farthest wrap above viewport,
|
|
415
|
+
// destroy its Ezoic placeholder, recreate the DOM, then define+displayMore.
|
|
318
416
|
|
|
319
417
|
function recycleWrap(klass, targetEl, newKey) {
|
|
320
418
|
const ez = window.ezstandalone;
|
|
@@ -323,7 +421,6 @@
|
|
|
323
421
|
typeof ez?.displayMore !== 'function') return null;
|
|
324
422
|
|
|
325
423
|
const vh = window.innerHeight || 800;
|
|
326
|
-
// Conservative threshold: 3× viewport height above the top of the screen
|
|
327
424
|
const threshold = -(3 * vh);
|
|
328
425
|
const t = now();
|
|
329
426
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
@@ -334,17 +431,14 @@
|
|
|
334
431
|
|
|
335
432
|
for (const wrap of wraps) {
|
|
336
433
|
try {
|
|
337
|
-
// Don't recycle wraps that are too young (ads might still be loading)
|
|
338
434
|
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
339
435
|
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
340
|
-
|
|
341
|
-
// Don't recycle wraps with inflight showAds
|
|
342
436
|
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
343
|
-
|
|
344
|
-
|
|
437
|
+
const st = state.phState.get(wid);
|
|
438
|
+
// Don't recycle placeholders that are still being processed
|
|
439
|
+
if (st === 'new' || st === 'defined') continue;
|
|
345
440
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
346
441
|
if (bottom > threshold) continue;
|
|
347
|
-
|
|
348
442
|
if (!isFilled(wrap)) {
|
|
349
443
|
if (bottom < bestEmptyY) { bestEmptyY = bottom; bestEmpty = wrap; }
|
|
350
444
|
} else {
|
|
@@ -367,43 +461,40 @@
|
|
|
367
461
|
if (ph) state.io?.unobserve(ph);
|
|
368
462
|
} catch (_) {}
|
|
369
463
|
|
|
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
|
-
});
|
|
464
|
+
// Step 1: Destroy in Ezoic, then recreate DOM and re-define
|
|
465
|
+
state.phState.set(id, 'destroyed');
|
|
387
466
|
|
|
388
|
-
|
|
389
|
-
|
|
467
|
+
ezCmd(() => {
|
|
468
|
+
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
390
469
|
|
|
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
470
|
setTimeout(() => {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
471
|
+
// Step 2: Recreate placeholder DOM at new position
|
|
472
|
+
mutate(() => {
|
|
473
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
474
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
475
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
476
|
+
best.classList.remove('is-empty');
|
|
477
|
+
best.replaceChildren();
|
|
478
|
+
|
|
479
|
+
const fresh = document.createElement('div');
|
|
480
|
+
fresh.id = `${PH_PREFIX}${id}`;
|
|
481
|
+
fresh.setAttribute('data-ezoic-id', String(id));
|
|
482
|
+
fresh.style.minHeight = '1px';
|
|
483
|
+
best.appendChild(fresh);
|
|
484
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
485
|
+
});
|
|
403
486
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
487
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
488
|
+
state.wrapByKey.set(newKey, best);
|
|
489
|
+
|
|
490
|
+
// Step 3: Re-define + displayMore via the batch queue
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
state.phState.set(id, 'new');
|
|
493
|
+
observePlaceholder(id);
|
|
494
|
+
ezEnqueue(id);
|
|
495
|
+
}, TIMING.RECYCLE_DEFINE_MS);
|
|
496
|
+
}, TIMING.RECYCLE_DESTROY_MS);
|
|
497
|
+
});
|
|
407
498
|
|
|
408
499
|
return { id, wrap: best };
|
|
409
500
|
}
|
|
@@ -418,7 +509,6 @@
|
|
|
418
509
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
419
510
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
420
511
|
w.style.cssText = 'width:100%;display:block';
|
|
421
|
-
|
|
422
512
|
const ph = document.createElement('div');
|
|
423
513
|
ph.id = `${PH_PREFIX}${id}`;
|
|
424
514
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -433,7 +523,6 @@
|
|
|
433
523
|
if (state.mountedIds.has(id)) return null;
|
|
434
524
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
435
525
|
if (existing?.isConnected) return null;
|
|
436
|
-
|
|
437
526
|
const w = makeWrap(id, klass, key);
|
|
438
527
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
439
528
|
state.mountedIds.add(id);
|
|
@@ -447,20 +536,13 @@
|
|
|
447
536
|
try {
|
|
448
537
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
449
538
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
450
|
-
|
|
451
539
|
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
|
-
|
|
540
|
+
if (Number.isFinite(id)) { state.mountedIds.delete(id); state.phState.delete(id); }
|
|
457
541
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
458
542
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
459
|
-
|
|
460
543
|
for (const cls of w.classList) {
|
|
461
544
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
462
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
463
|
-
break;
|
|
545
|
+
state.wrapsByClass.get(cls)?.delete(w); break;
|
|
464
546
|
}
|
|
465
547
|
}
|
|
466
548
|
w.remove();
|
|
@@ -474,22 +556,18 @@
|
|
|
474
556
|
const cfg = KIND[klass];
|
|
475
557
|
const wraps = state.wrapsByClass.get(klass);
|
|
476
558
|
if (!wraps?.size) return;
|
|
477
|
-
|
|
478
559
|
const liveAnchors = new Set();
|
|
479
560
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
480
561
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
481
562
|
if (v) liveAnchors.add(v);
|
|
482
563
|
}
|
|
483
|
-
|
|
484
564
|
const t = now();
|
|
485
565
|
for (const w of wraps) {
|
|
486
566
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
487
567
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
488
568
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
489
569
|
const sid = key.slice(klass.length + 1);
|
|
490
|
-
if (!sid || !liveAnchors.has(sid))
|
|
491
|
-
mutate(() => dropWrap(w));
|
|
492
|
-
}
|
|
570
|
+
if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
493
571
|
}
|
|
494
572
|
}
|
|
495
573
|
|
|
@@ -517,11 +595,9 @@
|
|
|
517
595
|
for (const el of items) {
|
|
518
596
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
519
597
|
if (!el?.isConnected) continue;
|
|
520
|
-
|
|
521
598
|
const ord = ordinal(klass, el);
|
|
522
599
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
523
600
|
if (adjacentWrap(el)) continue;
|
|
524
|
-
|
|
525
601
|
const key = anchorKey(klass, el);
|
|
526
602
|
if (findWrap(key)) continue;
|
|
527
603
|
|
|
@@ -530,7 +606,8 @@
|
|
|
530
606
|
const w = insertAfter(el, id, klass, key);
|
|
531
607
|
if (w) {
|
|
532
608
|
observePlaceholder(id);
|
|
533
|
-
|
|
609
|
+
// Queue for batched define+enable/displayMore
|
|
610
|
+
ezEnqueue(id);
|
|
534
611
|
inserted++;
|
|
535
612
|
}
|
|
536
613
|
} else {
|
|
@@ -543,6 +620,12 @@
|
|
|
543
620
|
}
|
|
544
621
|
|
|
545
622
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
623
|
+
//
|
|
624
|
+
// The IO is used to eagerly observe placeholders so that when they enter
|
|
625
|
+
// the viewport margin, we can queue them for Ezoic. However, the actual
|
|
626
|
+
// Ezoic API calls (define/displayMore) happen in the batched flush.
|
|
627
|
+
// The IO callback is mainly useful for re-triggering after NodeBB
|
|
628
|
+
// virtualisation re-inserts posts.
|
|
546
629
|
|
|
547
630
|
function getIO() {
|
|
548
631
|
if (state.io) return state.io;
|
|
@@ -550,12 +633,19 @@
|
|
|
550
633
|
state.io = new IntersectionObserver(entries => {
|
|
551
634
|
for (const entry of entries) {
|
|
552
635
|
if (!entry.isIntersecting) continue;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
if
|
|
636
|
+
const target = entry.target;
|
|
637
|
+
if (target instanceof Element) state.io?.unobserve(target);
|
|
638
|
+
const id = parseInt(target.getAttribute('data-ezoic-id'), 10);
|
|
639
|
+
if (!id || id <= 0) continue;
|
|
640
|
+
const st = state.phState.get(id);
|
|
641
|
+
// Only enqueue if not yet processed by Ezoic
|
|
642
|
+
if (st === 'new') {
|
|
643
|
+
ezEnqueue(id);
|
|
644
|
+
} else if (st === 'displayed') {
|
|
645
|
+
// Already shown — check if the placeholder is actually filled.
|
|
646
|
+
// If not (Ezoic had no ad), don't re-trigger — it won't help.
|
|
647
|
+
// If yes, nothing to do.
|
|
648
|
+
}
|
|
559
649
|
}
|
|
560
650
|
}, {
|
|
561
651
|
root: null,
|
|
@@ -568,227 +658,19 @@
|
|
|
568
658
|
|
|
569
659
|
function observePlaceholder(id) {
|
|
570
660
|
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
|
-
}
|
|
661
|
+
if (ph?.isConnected) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
775
662
|
}
|
|
776
663
|
|
|
777
664
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
778
665
|
|
|
779
666
|
async function runCore() {
|
|
780
667
|
if (isBlocked()) return 0;
|
|
781
|
-
|
|
782
668
|
const cfg = await fetchConfig();
|
|
783
669
|
if (!cfg || cfg.excluded) return 0;
|
|
784
670
|
initPools(cfg);
|
|
785
|
-
|
|
786
671
|
const kind = getKind();
|
|
787
672
|
if (kind === 'other') return 0;
|
|
788
673
|
|
|
789
|
-
// Re-observe wraps that came back into viewport (scroll-up fix)
|
|
790
|
-
reobserveVisibleWraps();
|
|
791
|
-
|
|
792
674
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
793
675
|
if (!normBool(cfgEnable)) return 0;
|
|
794
676
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
@@ -796,24 +678,16 @@
|
|
|
796
678
|
};
|
|
797
679
|
|
|
798
680
|
if (kind === 'topic') {
|
|
799
|
-
return exec(
|
|
800
|
-
|
|
801
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
802
|
-
);
|
|
681
|
+
return exec('ezoic-ad-message', getPosts,
|
|
682
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
803
683
|
}
|
|
804
|
-
|
|
805
684
|
if (kind === 'categoryTopics') {
|
|
806
685
|
pruneOrphansBetween();
|
|
807
|
-
return exec(
|
|
808
|
-
|
|
809
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
810
|
-
);
|
|
686
|
+
return exec('ezoic-ad-between', getTopics,
|
|
687
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
811
688
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
'ezoic-ad-categories', getCategories,
|
|
815
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
816
|
-
);
|
|
689
|
+
return exec('ezoic-ad-categories', getCategories,
|
|
690
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
817
691
|
}
|
|
818
692
|
|
|
819
693
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -837,62 +711,55 @@
|
|
|
837
711
|
state.lastBurstTs = t;
|
|
838
712
|
state.pageKey = pageKey();
|
|
839
713
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
840
|
-
|
|
841
714
|
if (state.burstActive) return;
|
|
842
715
|
state.burstActive = true;
|
|
843
716
|
state.burstCount = 0;
|
|
844
|
-
|
|
845
717
|
const step = () => {
|
|
846
718
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
847
|
-
state.burstActive = false;
|
|
848
|
-
return;
|
|
719
|
+
state.burstActive = false; return;
|
|
849
720
|
}
|
|
850
721
|
state.burstCount++;
|
|
851
722
|
scheduleRun(n => {
|
|
852
|
-
if (!n && !state.
|
|
723
|
+
if (!n && !state.ezBatch.size) { state.burstActive = false; return; }
|
|
853
724
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
854
725
|
});
|
|
855
726
|
};
|
|
856
727
|
step();
|
|
857
728
|
}
|
|
858
729
|
|
|
859
|
-
// ── Cleanup
|
|
730
|
+
// ── Cleanup ────────────────────────────────────────────────────────────────
|
|
860
731
|
|
|
861
732
|
function cleanup() {
|
|
862
733
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
734
|
+
|
|
735
|
+
// Cancel pending Ezoic batch
|
|
736
|
+
if (state.ezFlushTimer) { clearTimeout(state.ezFlushTimer); state.ezFlushTimer = null; }
|
|
737
|
+
state.ezBatch.clear();
|
|
738
|
+
|
|
863
739
|
mutate(() => {
|
|
864
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
865
|
-
dropWrap(w);
|
|
866
|
-
}
|
|
740
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
867
741
|
});
|
|
868
|
-
state.cfg
|
|
869
|
-
state.poolsReady
|
|
870
|
-
state.pools
|
|
871
|
-
state.cursors
|
|
742
|
+
state.cfg = null;
|
|
743
|
+
state.poolsReady = false;
|
|
744
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
745
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
872
746
|
state.mountedIds.clear();
|
|
873
747
|
state.lastShow.clear();
|
|
874
748
|
state.wrapByKey.clear();
|
|
875
749
|
state.wrapsByClass.clear();
|
|
876
|
-
state.kind
|
|
750
|
+
state.kind = null;
|
|
877
751
|
state.phState.clear();
|
|
878
|
-
state.inflight = 0;
|
|
879
|
-
state.pending = [];
|
|
880
|
-
state.pendingSet.clear();
|
|
881
752
|
state.burstActive = false;
|
|
882
753
|
state.runQueued = false;
|
|
883
|
-
state.firstShown = false;
|
|
884
754
|
}
|
|
885
755
|
|
|
886
756
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
887
757
|
|
|
888
758
|
function ensureDomObserver() {
|
|
889
759
|
if (state.domObs) return;
|
|
890
|
-
|
|
891
760
|
state.domObs = new MutationObserver(muts => {
|
|
892
761
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
893
|
-
|
|
894
762
|
let needsBurst = false;
|
|
895
|
-
|
|
896
763
|
const kind = getKind();
|
|
897
764
|
const relevantSels =
|
|
898
765
|
kind === 'topic' ? [SEL.post] :
|
|
@@ -905,7 +772,7 @@
|
|
|
905
772
|
for (const node of m.addedNodes) {
|
|
906
773
|
if (!(node instanceof Element)) continue;
|
|
907
774
|
|
|
908
|
-
//
|
|
775
|
+
// Ad fill detection
|
|
909
776
|
try {
|
|
910
777
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
911
778
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -914,140 +781,91 @@
|
|
|
914
781
|
}
|
|
915
782
|
} catch (_) {}
|
|
916
783
|
|
|
917
|
-
//
|
|
784
|
+
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
785
|
+
try {
|
|
786
|
+
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
787
|
+
? [node]
|
|
788
|
+
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
789
|
+
for (const wrap of wraps) {
|
|
790
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
791
|
+
if (!id || id <= 0) continue;
|
|
792
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
793
|
+
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
794
|
+
}
|
|
795
|
+
} catch (_) {}
|
|
796
|
+
|
|
797
|
+
// New content detection
|
|
918
798
|
if (!needsBurst) {
|
|
919
799
|
for (const sel of relevantSels) {
|
|
920
800
|
try {
|
|
921
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
922
|
-
needsBurst = true;
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
801
|
+
if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
|
|
925
802
|
} catch (_) {}
|
|
926
803
|
}
|
|
927
804
|
}
|
|
928
805
|
}
|
|
929
806
|
if (needsBurst) break;
|
|
930
807
|
}
|
|
931
|
-
|
|
932
808
|
if (needsBurst) requestBurst();
|
|
933
809
|
});
|
|
934
|
-
|
|
935
|
-
try {
|
|
936
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
937
|
-
} catch (_) {}
|
|
810
|
+
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
938
811
|
}
|
|
939
812
|
|
|
940
813
|
// ── 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
814
|
|
|
947
815
|
function ensureTcfLocator() {
|
|
948
816
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
949
|
-
|
|
950
817
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
951
|
-
|
|
952
818
|
const ensureInHead = () => {
|
|
953
819
|
let existing = document.getElementById(LOCATOR_ID);
|
|
954
820
|
if (existing) {
|
|
955
821
|
if (existing.parentElement !== document.head) {
|
|
956
822
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
957
823
|
}
|
|
958
|
-
return
|
|
824
|
+
return;
|
|
959
825
|
}
|
|
960
826
|
const f = document.createElement('iframe');
|
|
961
|
-
f.style.display = 'none';
|
|
962
|
-
f.id = f.name = LOCATOR_ID;
|
|
827
|
+
f.style.display = 'none'; f.id = f.name = LOCATOR_ID;
|
|
963
828
|
try { document.head.appendChild(f); } catch (_) {
|
|
964
829
|
(document.body || document.documentElement).appendChild(f);
|
|
965
830
|
}
|
|
966
|
-
return f;
|
|
967
831
|
};
|
|
968
|
-
|
|
969
832
|
ensureInHead();
|
|
970
|
-
|
|
971
|
-
// Guard CMP API calls
|
|
972
833
|
if (!window.__nbbCmpGuarded) {
|
|
973
834
|
window.__nbbCmpGuarded = true;
|
|
974
|
-
|
|
975
835
|
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
|
-
}
|
|
836
|
+
const orig = window.__tcfapi;
|
|
837
|
+
window.__tcfapi = function (cmd, ver, cb, param) {
|
|
838
|
+
try { return orig.call(this, cmd, ver, function (...a) { try { cb?.(...a); } catch (_) {} }, param); }
|
|
839
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.call(this, cmd, ver, cb, param); } catch (_) {} } }
|
|
988
840
|
};
|
|
989
841
|
}
|
|
990
|
-
|
|
991
842
|
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
|
-
}
|
|
843
|
+
const orig = window.__cmp;
|
|
844
|
+
window.__cmp = function (...a) {
|
|
845
|
+
try { return orig.apply(this, a); }
|
|
846
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.apply(this, a); } catch (_) {} } }
|
|
1002
847
|
};
|
|
1003
848
|
}
|
|
1004
849
|
}
|
|
1005
|
-
|
|
1006
850
|
if (!window.__nbbTcfObs) {
|
|
1007
851
|
window.__nbbTcfObs = new MutationObserver(() => {
|
|
1008
|
-
if (document.getElementById(LOCATOR_ID))
|
|
1009
|
-
ensureInHead();
|
|
852
|
+
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
1010
853
|
});
|
|
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 (_) {}
|
|
854
|
+
try { window.__nbbTcfObs.observe(document.body || document.documentElement, { childList: true, subtree: false }); } catch (_) {}
|
|
855
|
+
try { if (document.head) window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false }); } catch (_) {}
|
|
1023
856
|
}
|
|
1024
857
|
}
|
|
1025
858
|
|
|
1026
859
|
// ── 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
860
|
|
|
1034
861
|
function protectAriaHidden() {
|
|
1035
862
|
if (window.__nbbAriaObs) return;
|
|
1036
863
|
const remove = () => {
|
|
1037
|
-
try {
|
|
1038
|
-
if (document.body.getAttribute('aria-hidden') === 'true') {
|
|
1039
|
-
document.body.removeAttribute('aria-hidden');
|
|
1040
|
-
}
|
|
1041
|
-
} catch (_) {}
|
|
864
|
+
try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1042
865
|
};
|
|
1043
866
|
remove();
|
|
1044
867
|
window.__nbbAriaObs = new MutationObserver(remove);
|
|
1045
|
-
try {
|
|
1046
|
-
window.__nbbAriaObs.observe(document.body, {
|
|
1047
|
-
attributes: true,
|
|
1048
|
-
attributeFilter: ['aria-hidden'],
|
|
1049
|
-
});
|
|
1050
|
-
} catch (_) {}
|
|
868
|
+
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
1051
869
|
}
|
|
1052
870
|
|
|
1053
871
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
@@ -1055,10 +873,8 @@
|
|
|
1055
873
|
function muteConsole() {
|
|
1056
874
|
if (window.__nbbEzMuted) return;
|
|
1057
875
|
window.__nbbEzMuted = true;
|
|
1058
|
-
|
|
1059
876
|
const PREFIXES = [
|
|
1060
877
|
'[EzoicAds JS]: Placeholder Id',
|
|
1061
|
-
'No valid placeholders for loadMore',
|
|
1062
878
|
'cannot call refresh on the same page',
|
|
1063
879
|
'no placeholders are currently defined in Refresh',
|
|
1064
880
|
'Debugger iframe already exists',
|
|
@@ -1068,20 +884,18 @@
|
|
|
1068
884
|
const PATTERNS = [
|
|
1069
885
|
`with id ${PH_PREFIX}`,
|
|
1070
886
|
'adsbygoogle.push() error: All',
|
|
887
|
+
'has already been defined',
|
|
888
|
+
'No valid placeholders for loadMore',
|
|
889
|
+
'bad response. Status',
|
|
1071
890
|
];
|
|
1072
|
-
|
|
1073
891
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
1074
892
|
const orig = console[method];
|
|
1075
893
|
if (typeof orig !== 'function') continue;
|
|
1076
894
|
console[method] = function (...args) {
|
|
1077
895
|
if (typeof args[0] === 'string') {
|
|
1078
896
|
const msg = args[0];
|
|
1079
|
-
for (const
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
for (const pat of PATTERNS) {
|
|
1083
|
-
if (msg.includes(pat)) return;
|
|
1084
|
-
}
|
|
897
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
898
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
1085
899
|
}
|
|
1086
900
|
return orig.apply(console, args);
|
|
1087
901
|
};
|
|
@@ -1091,28 +905,22 @@
|
|
|
1091
905
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1092
906
|
|
|
1093
907
|
let _networkWarmed = false;
|
|
1094
|
-
|
|
1095
908
|
function warmNetwork() {
|
|
1096
909
|
if (_networkWarmed) return;
|
|
1097
910
|
_networkWarmed = true;
|
|
1098
|
-
|
|
1099
911
|
const head = document.head;
|
|
1100
912
|
if (!head) return;
|
|
1101
|
-
|
|
1102
|
-
const hints = [
|
|
913
|
+
for (const [rel, href, cors] of [
|
|
1103
914
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
1104
915
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
1105
916
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
1106
917
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
1107
918
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1108
919
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1109
|
-
]
|
|
1110
|
-
|
|
1111
|
-
for (const [rel, href, cors] of hints) {
|
|
920
|
+
]) {
|
|
1112
921
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1113
922
|
const link = document.createElement('link');
|
|
1114
|
-
link.rel = rel;
|
|
1115
|
-
link.href = href;
|
|
923
|
+
link.rel = rel; link.href = href;
|
|
1116
924
|
if (cors) link.crossOrigin = 'anonymous';
|
|
1117
925
|
head.appendChild(link);
|
|
1118
926
|
}
|
|
@@ -1123,46 +931,25 @@
|
|
|
1123
931
|
function bindNodeBB() {
|
|
1124
932
|
const $ = window.jQuery;
|
|
1125
933
|
if (!$) return;
|
|
1126
|
-
|
|
1127
934
|
$(window).off('.nbbEzoic');
|
|
1128
935
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1129
|
-
|
|
1130
936
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1131
|
-
state.pageKey
|
|
1132
|
-
state.kind
|
|
937
|
+
state.pageKey = pageKey();
|
|
938
|
+
state.kind = null;
|
|
1133
939
|
state.blockedUntil = 0;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
ensureTcfLocator();
|
|
1137
|
-
protectAriaHidden();
|
|
1138
|
-
warmNetwork();
|
|
1139
|
-
patchShowAds();
|
|
1140
|
-
getIO();
|
|
1141
|
-
ensureDomObserver();
|
|
1142
|
-
requestBurst();
|
|
940
|
+
muteConsole(); ensureTcfLocator(); protectAriaHidden(); warmNetwork();
|
|
941
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
1143
942
|
});
|
|
1144
|
-
|
|
1145
943
|
const burstEvents = [
|
|
1146
|
-
'action:ajaxify.contentLoaded',
|
|
1147
|
-
'action:
|
|
1148
|
-
'action:topics.loaded',
|
|
1149
|
-
'action:categories.loaded',
|
|
1150
|
-
'action:category.loaded',
|
|
1151
|
-
'action:topic.loaded',
|
|
944
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
945
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
1152
946
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1153
|
-
|
|
1154
947
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1155
|
-
|
|
1156
948
|
try {
|
|
1157
949
|
require(['hooks'], hooks => {
|
|
1158
950
|
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
|
-
]) {
|
|
951
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
952
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
1166
953
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
1167
954
|
}
|
|
1168
955
|
});
|
|
@@ -1174,10 +961,7 @@
|
|
|
1174
961
|
window.addEventListener('scroll', () => {
|
|
1175
962
|
if (ticking) return;
|
|
1176
963
|
ticking = true;
|
|
1177
|
-
requestAnimationFrame(() => {
|
|
1178
|
-
ticking = false;
|
|
1179
|
-
requestBurst();
|
|
1180
|
-
});
|
|
964
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1181
965
|
}, { passive: true });
|
|
1182
966
|
}
|
|
1183
967
|
|
|
@@ -1188,7 +972,6 @@
|
|
|
1188
972
|
ensureTcfLocator();
|
|
1189
973
|
protectAriaHidden();
|
|
1190
974
|
warmNetwork();
|
|
1191
|
-
patchShowAds();
|
|
1192
975
|
getIO();
|
|
1193
976
|
ensureDomObserver();
|
|
1194
977
|
bindNodeBB();
|