nodebb-plugin-ezoic-infinite 1.8.42 → 1.8.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library.js +20 -3
- package/package.json +1 -1
- package/public/client.js +323 -508
package/library.js
CHANGED
|
@@ -141,11 +141,12 @@ const HEAD_PRECONNECTS = [
|
|
|
141
141
|
const EZOIC_SCRIPTS = [
|
|
142
142
|
'<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>',
|
|
143
143
|
'<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>',
|
|
144
|
-
'<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
145
144
|
'<script>',
|
|
145
|
+
'window._ezaq = window._ezaq || {};',
|
|
146
146
|
'window.ezstandalone = window.ezstandalone || {};',
|
|
147
147
|
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
148
148
|
'</script>',
|
|
149
|
+
'<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
149
150
|
].join('\n');
|
|
150
151
|
|
|
151
152
|
// ── Hooks ────────────────────────────────────────────────────────────────────
|
|
@@ -173,7 +174,24 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
173
174
|
const settings = await getSettings();
|
|
174
175
|
const uid = data.req?.uid ?? 0;
|
|
175
176
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
176
|
-
|
|
177
|
+
|
|
178
|
+
if (excluded) {
|
|
179
|
+
// Even for excluded users, inject a minimal stub so that any script
|
|
180
|
+
// referencing ezstandalone doesn't throw a ReferenceError, plus
|
|
181
|
+
// the config with excluded=true so client.js bails early.
|
|
182
|
+
const cfg = buildClientConfig(settings, true);
|
|
183
|
+
const stub = [
|
|
184
|
+
'<script>',
|
|
185
|
+
'window._ezaq = window._ezaq || {};',
|
|
186
|
+
'window.ezstandalone = window.ezstandalone || {};',
|
|
187
|
+
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
188
|
+
'</script>',
|
|
189
|
+
].join('\n');
|
|
190
|
+
data.templateData.customHTML =
|
|
191
|
+
stub + '\n' +
|
|
192
|
+
serializeInlineConfig(cfg) +
|
|
193
|
+
(data.templateData.customHTML || '');
|
|
194
|
+
} else {
|
|
177
195
|
const cfg = buildClientConfig(settings, false);
|
|
178
196
|
data.templateData.customHTML =
|
|
179
197
|
HEAD_PRECONNECTS + '\n' +
|
|
@@ -182,7 +200,6 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
182
200
|
(data.templateData.customHTML || '');
|
|
183
201
|
}
|
|
184
202
|
} catch (err) {
|
|
185
|
-
// Log but don't break rendering
|
|
186
203
|
console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
|
|
187
204
|
}
|
|
188
205
|
return data;
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v3.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Ezoic API usage per official docs:
|
|
5
|
+
* https://docs.ezoic.com/docs/ezoicads/dynamic-content/
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* NEW PLACEHOLDERS: ez.cmd.push(() => ez.showAds(id1, id2, ...))
|
|
8
|
+
* RECYCLE (reuse ID): ez.cmd.push(() => ez.destroyPlaceholders(id))
|
|
9
|
+
* → remove old HTML → create fresh placeholder
|
|
10
|
+
* → ez.cmd.push(() => ez.showAds(id))
|
|
11
|
+
*
|
|
12
|
+
* phState per placeholder ID:
|
|
13
|
+
* new → displayed → [destroyed → new] (recycle loop)
|
|
13
14
|
*/
|
|
14
15
|
(function nbbEzoicInfinite() {
|
|
15
16
|
'use strict';
|
|
@@ -19,7 +20,6 @@
|
|
|
19
20
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
20
21
|
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
22
|
|
|
22
|
-
// Data attributes
|
|
23
23
|
const ATTR = {
|
|
24
24
|
ANCHOR: 'data-ezoic-anchor',
|
|
25
25
|
WRAPID: 'data-ezoic-wrapid',
|
|
@@ -27,25 +27,19 @@
|
|
|
27
27
|
SHOWN: 'data-ezoic-shown',
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// Timing
|
|
31
30
|
const TIMING = {
|
|
32
31
|
EMPTY_CHECK_MS: 20_000,
|
|
33
32
|
MIN_PRUNE_AGE_MS: 8_000,
|
|
34
33
|
SHOW_THROTTLE_MS: 900,
|
|
35
34
|
BURST_COOLDOWN_MS: 200,
|
|
36
35
|
BLOCK_DURATION_MS: 1_500,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
RECYCLE_DELAY_MS: 450,
|
|
41
|
-
TCF_DEBOUNCE_MS: 500,
|
|
36
|
+
BATCH_FLUSH_MS: 120,
|
|
37
|
+
RECYCLE_DESTROY_MS: 300,
|
|
38
|
+
RECYCLE_SHOW_MS: 300,
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
// Limits
|
|
45
41
|
const LIMITS = {
|
|
46
42
|
MAX_INSERTS_RUN: 6,
|
|
47
|
-
MAX_INFLIGHT: 4,
|
|
48
|
-
BATCH_SIZE: 3,
|
|
49
43
|
MAX_BURST_STEPS: 8,
|
|
50
44
|
BURST_WINDOW_MS: 2_000,
|
|
51
45
|
};
|
|
@@ -55,47 +49,39 @@
|
|
|
55
49
|
MOBILE: '3500px 0px 3500px 0px',
|
|
56
50
|
};
|
|
57
51
|
|
|
58
|
-
// Selectors
|
|
59
52
|
const SEL = {
|
|
60
53
|
post: '[component="post"][data-pid]',
|
|
61
54
|
topic: 'li[component="category/topic"]',
|
|
62
55
|
category: 'li[component="categories/category"]',
|
|
63
56
|
};
|
|
64
57
|
|
|
65
|
-
// Kind configuration table — single source of truth per ad type
|
|
66
58
|
const KIND = {
|
|
67
59
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
68
60
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
69
61
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
70
62
|
};
|
|
71
63
|
|
|
72
|
-
// Selector for detecting filled ad slots
|
|
73
64
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
74
65
|
|
|
66
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
67
|
+
|
|
75
68
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
76
69
|
|
|
77
70
|
const now = () => Date.now();
|
|
78
71
|
const isMobile = () => window.innerWidth < 768;
|
|
79
72
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
80
73
|
|
|
81
|
-
function isFilled(node)
|
|
82
|
-
return node?.querySelector?.(FILL_SEL) != null;
|
|
83
|
-
}
|
|
84
|
-
|
|
74
|
+
function isFilled(node) { return node?.querySelector?.(FILL_SEL) != null; }
|
|
85
75
|
function isPlaceholderUsed(ph) {
|
|
86
76
|
if (!ph?.isConnected) return false;
|
|
87
77
|
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
88
78
|
}
|
|
89
79
|
|
|
90
80
|
function parseIds(raw) {
|
|
91
|
-
const out = [];
|
|
92
|
-
const seen = new Set();
|
|
81
|
+
const out = [], seen = new Set();
|
|
93
82
|
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
94
83
|
const n = parseInt(line, 10);
|
|
95
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
96
|
-
seen.add(n);
|
|
97
|
-
out.push(n);
|
|
98
|
-
}
|
|
84
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
99
85
|
}
|
|
100
86
|
return out;
|
|
101
87
|
}
|
|
@@ -103,45 +89,29 @@
|
|
|
103
89
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
104
90
|
|
|
105
91
|
const state = {
|
|
106
|
-
|
|
107
|
-
pageKey: null,
|
|
108
|
-
kind: null,
|
|
109
|
-
cfg: null,
|
|
92
|
+
pageKey: null, kind: null, cfg: null,
|
|
110
93
|
|
|
111
|
-
// Pools
|
|
112
94
|
poolsReady: false,
|
|
113
|
-
pools:
|
|
114
|
-
cursors:
|
|
115
|
-
|
|
116
|
-
// Mounted placeholders
|
|
117
|
-
mountedIds: new Set(),
|
|
118
|
-
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
119
|
-
lastShow: new Map(), // id → timestamp
|
|
120
|
-
|
|
121
|
-
// Wrap registry
|
|
122
|
-
wrapByKey: new Map(), // anchorKey → wrap element
|
|
123
|
-
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
95
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
96
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
124
97
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
98
|
+
mountedIds: new Set(),
|
|
99
|
+
// phState: 'new' | 'displayed' | 'destroyed'
|
|
100
|
+
phState: new Map(),
|
|
101
|
+
lastShow: new Map(),
|
|
102
|
+
wrapByKey: new Map(),
|
|
103
|
+
wrapsByClass: new Map(),
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
mutGuard:
|
|
131
|
-
blockedUntil: 0,
|
|
105
|
+
io: null, domObs: null,
|
|
106
|
+
mutGuard: 0, blockedUntil: 0,
|
|
132
107
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
pendingSet: new Set(),
|
|
108
|
+
// Ezoic batch queue: ids waiting for define+displayMore
|
|
109
|
+
ezBatch: new Set(),
|
|
110
|
+
ezFlushTimer: null,
|
|
137
111
|
|
|
138
|
-
//
|
|
112
|
+
// Lifecycle
|
|
139
113
|
runQueued: false,
|
|
140
|
-
burstActive: false,
|
|
141
|
-
burstDeadline: 0,
|
|
142
|
-
burstCount: 0,
|
|
143
|
-
lastBurstTs: 0,
|
|
144
|
-
firstShown: false,
|
|
114
|
+
burstActive: false, burstDeadline: 0, burstCount: 0, lastBurstTs: 0,
|
|
145
115
|
};
|
|
146
116
|
|
|
147
117
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -155,15 +125,10 @@
|
|
|
155
125
|
|
|
156
126
|
async function fetchConfig() {
|
|
157
127
|
if (state.cfg) return state.cfg;
|
|
158
|
-
// Prefer inline config injected by server (zero latency)
|
|
159
128
|
try {
|
|
160
129
|
const inline = window.__nbbEzoicCfg;
|
|
161
|
-
if (inline && typeof inline === 'object') {
|
|
162
|
-
state.cfg = inline;
|
|
163
|
-
return state.cfg;
|
|
164
|
-
}
|
|
130
|
+
if (inline && typeof inline === 'object') { state.cfg = inline; return state.cfg; }
|
|
165
131
|
} catch (_) {}
|
|
166
|
-
// Fallback to API
|
|
167
132
|
try {
|
|
168
133
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
169
134
|
if (r.ok) state.cfg = await r.json();
|
|
@@ -195,27 +160,22 @@
|
|
|
195
160
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
196
161
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
197
162
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
198
|
-
// DOM fallback
|
|
199
163
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
200
164
|
if (document.querySelector(SEL.post)) return 'topic';
|
|
201
165
|
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
202
166
|
return 'other';
|
|
203
167
|
}
|
|
204
168
|
|
|
205
|
-
function getKind() {
|
|
206
|
-
return state.kind || (state.kind = detectKind());
|
|
207
|
-
}
|
|
169
|
+
function getKind() { return state.kind || (state.kind = detectKind()); }
|
|
208
170
|
|
|
209
171
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
210
172
|
|
|
211
173
|
function getPosts() {
|
|
212
|
-
const all = document.querySelectorAll(SEL.post);
|
|
213
|
-
const out = [];
|
|
174
|
+
const all = document.querySelectorAll(SEL.post), out = [];
|
|
214
175
|
for (let i = 0; i < all.length; i++) {
|
|
215
176
|
const el = all[i];
|
|
216
177
|
if (!el.isConnected) continue;
|
|
217
178
|
if (!el.querySelector('[component="post/content"]')) continue;
|
|
218
|
-
// Skip nested quotes / parent posts
|
|
219
179
|
const parent = el.parentElement?.closest(SEL.post);
|
|
220
180
|
if (parent && parent !== el) continue;
|
|
221
181
|
if (el.getAttribute('component') === 'post/parent') continue;
|
|
@@ -223,7 +183,6 @@
|
|
|
223
183
|
}
|
|
224
184
|
return out;
|
|
225
185
|
}
|
|
226
|
-
|
|
227
186
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
228
187
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
229
188
|
|
|
@@ -231,16 +190,10 @@
|
|
|
231
190
|
|
|
232
191
|
function stableId(klass, el) {
|
|
233
192
|
const attr = KIND[klass]?.anchorAttr;
|
|
234
|
-
if (attr) {
|
|
235
|
-
const v = el.getAttribute(attr);
|
|
236
|
-
if (v != null && v !== '') return v;
|
|
237
|
-
}
|
|
238
|
-
// Positional fallback
|
|
193
|
+
if (attr) { const v = el.getAttribute(attr); if (v != null && v !== '') return v; }
|
|
239
194
|
const children = el.parentElement?.children;
|
|
240
195
|
if (!children) return 'i0';
|
|
241
|
-
for (let i = 0; i < children.length; i++) {
|
|
242
|
-
if (children[i] === el) return `i${i}`;
|
|
243
|
-
}
|
|
196
|
+
for (let i = 0; i < children.length; i++) { if (children[i] === el) return `i${i}`; }
|
|
244
197
|
return 'i0';
|
|
245
198
|
}
|
|
246
199
|
|
|
@@ -252,47 +205,30 @@
|
|
|
252
205
|
}
|
|
253
206
|
|
|
254
207
|
function getWrapSet(klass) {
|
|
255
|
-
let
|
|
256
|
-
if (!
|
|
257
|
-
return
|
|
208
|
+
let s = state.wrapsByClass.get(klass);
|
|
209
|
+
if (!s) { s = new Set(); state.wrapsByClass.set(klass, s); }
|
|
210
|
+
return s;
|
|
258
211
|
}
|
|
259
212
|
|
|
260
|
-
// ── Wrap lifecycle
|
|
213
|
+
// ── Wrap lifecycle ─────────────────────────────────────────────────────────
|
|
261
214
|
|
|
262
|
-
/**
|
|
263
|
-
* Check if a wrap element still has its corresponding anchor in the DOM.
|
|
264
|
-
* Uses O(1) registry lookup first, then sibling scan, then global querySelector.
|
|
265
|
-
*/
|
|
266
215
|
function wrapIsLive(wrap) {
|
|
267
216
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
268
217
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
269
218
|
if (!key) return false;
|
|
270
|
-
|
|
271
|
-
// Fast path: registry match
|
|
272
219
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
273
|
-
|
|
274
|
-
// Parse key
|
|
275
220
|
const colonIdx = key.indexOf(':');
|
|
276
|
-
const klass
|
|
277
|
-
const anchorId = key.slice(colonIdx + 1);
|
|
221
|
+
const klass = key.slice(0, colonIdx), anchorId = key.slice(colonIdx + 1);
|
|
278
222
|
const cfg = KIND[klass];
|
|
279
223
|
if (!cfg) return false;
|
|
280
|
-
|
|
281
|
-
// Sibling scan (cheap for adjacent anchors)
|
|
282
224
|
const parent = wrap.parentElement;
|
|
283
225
|
if (parent) {
|
|
284
226
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
285
227
|
for (const sib of parent.children) {
|
|
286
|
-
if (sib !== wrap) {
|
|
287
|
-
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
288
|
-
}
|
|
228
|
+
if (sib !== wrap) { try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {} }
|
|
289
229
|
}
|
|
290
230
|
}
|
|
291
|
-
|
|
292
|
-
// Global fallback (expensive, rare)
|
|
293
|
-
try {
|
|
294
|
-
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
295
|
-
} catch (_) { return false; }
|
|
231
|
+
try { return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true; } catch (_) { return false; }
|
|
296
232
|
}
|
|
297
233
|
|
|
298
234
|
function adjacentWrap(el) {
|
|
@@ -307,21 +243,18 @@
|
|
|
307
243
|
if (!ph || !isFilled(ph)) return false;
|
|
308
244
|
wrap.classList.remove('is-empty');
|
|
309
245
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
310
|
-
if (id > 0) state.phState.set(id, '
|
|
246
|
+
if (id > 0) state.phState.set(id, 'displayed');
|
|
311
247
|
return true;
|
|
312
248
|
}
|
|
313
249
|
|
|
314
250
|
function scheduleUncollapseChecks(wrap) {
|
|
315
251
|
if (!wrap) return;
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
setTimeout(() => {
|
|
319
|
-
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
320
|
-
}, ms);
|
|
252
|
+
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
253
|
+
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
321
254
|
}
|
|
322
255
|
}
|
|
323
256
|
|
|
324
|
-
// ── Pool
|
|
257
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
325
258
|
|
|
326
259
|
function pickId(poolKey) {
|
|
327
260
|
const pool = state.pools[poolKey];
|
|
@@ -335,20 +268,110 @@
|
|
|
335
268
|
return null;
|
|
336
269
|
}
|
|
337
270
|
|
|
338
|
-
// ──
|
|
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); })
|
|
278
|
+
//
|
|
279
|
+
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
280
|
+
// Ezoic API on every single placeholder insertion.
|
|
281
|
+
|
|
282
|
+
function ezCmd(fn) {
|
|
283
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
284
|
+
const ez = window.ezstandalone;
|
|
285
|
+
if (Array.isArray(ez.cmd)) {
|
|
286
|
+
ez.cmd.push(fn);
|
|
287
|
+
} else {
|
|
288
|
+
try { fn(); } catch (_) {}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
339
291
|
|
|
340
292
|
/**
|
|
341
|
-
*
|
|
342
|
-
* Sequence: destroy → delay → re-observe → enqueueShow
|
|
293
|
+
* Queue a placeholder ID for the next batched showAds call.
|
|
343
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: call showAds() for all queued IDs.
|
|
305
|
+
*
|
|
306
|
+
* Per Ezoic docs (https://docs.ezoic.com/docs/ezoicads/dynamic-content/):
|
|
307
|
+
* - New placeholders: ez.showAds(id1, id2, ...)
|
|
308
|
+
* - After destroy+recreate: ez.showAds(id) again
|
|
309
|
+
* - No define(), no enable(), no displayMore()
|
|
310
|
+
*/
|
|
311
|
+
function ezFlush() {
|
|
312
|
+
state.ezFlushTimer = null;
|
|
313
|
+
if (isBlocked() || !state.ezBatch.size) return;
|
|
314
|
+
|
|
315
|
+
// Filter to only valid, connected, unfilled placeholders
|
|
316
|
+
const ids = [];
|
|
317
|
+
for (const id of state.ezBatch) {
|
|
318
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
319
|
+
if (!ph?.isConnected) { state.phState.delete(id); continue; }
|
|
320
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'displayed'); continue; }
|
|
321
|
+
ids.push(id);
|
|
322
|
+
}
|
|
323
|
+
state.ezBatch.clear();
|
|
324
|
+
if (!ids.length) return;
|
|
325
|
+
|
|
326
|
+
ezCmd(() => {
|
|
327
|
+
const ez = window.ezstandalone;
|
|
328
|
+
if (typeof ez?.showAds !== 'function') return;
|
|
329
|
+
|
|
330
|
+
try { ez.showAds(...ids); } catch (_) {}
|
|
331
|
+
|
|
332
|
+
// Mark as displayed and schedule fill checks
|
|
333
|
+
for (const id of ids) {
|
|
334
|
+
state.phState.set(id, 'displayed');
|
|
335
|
+
state.lastShow.set(id, now());
|
|
336
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
337
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
338
|
+
if (wrap) {
|
|
339
|
+
wrap.setAttribute(ATTR.SHOWN, String(now()));
|
|
340
|
+
scheduleUncollapseChecks(wrap);
|
|
341
|
+
}
|
|
342
|
+
scheduleEmptyCheck(id);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function scheduleEmptyCheck(id) {
|
|
348
|
+
const showTs = now();
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
try {
|
|
351
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
352
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
353
|
+
if (!wrap || !ph?.isConnected) return;
|
|
354
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
355
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
356
|
+
wrap.classList.add('is-empty');
|
|
357
|
+
} catch (_) {}
|
|
358
|
+
}, TIMING.EMPTY_CHECK_MS);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
362
|
+
//
|
|
363
|
+
// Per Ezoic docs: when reusing placeholder IDs in infinite scroll,
|
|
364
|
+
// call destroyPlaceholders(id) first, remove the old HTML, recreate
|
|
365
|
+
// a fresh placeholder div, then call showAds(id) again.
|
|
366
|
+
|
|
344
367
|
function recycleWrap(klass, targetEl, newKey) {
|
|
345
368
|
const ez = window.ezstandalone;
|
|
346
369
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
347
|
-
typeof ez?.
|
|
348
|
-
typeof ez?.displayMore !== 'function') return null;
|
|
370
|
+
typeof ez?.showAds !== 'function') return null;
|
|
349
371
|
|
|
350
372
|
const vh = window.innerHeight || 800;
|
|
351
|
-
const threshold = -vh;
|
|
373
|
+
const threshold = -(3 * vh);
|
|
374
|
+
const t = now();
|
|
352
375
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
353
376
|
let bestFull = null, bestFullY = Infinity;
|
|
354
377
|
|
|
@@ -357,6 +380,11 @@
|
|
|
357
380
|
|
|
358
381
|
for (const wrap of wraps) {
|
|
359
382
|
try {
|
|
383
|
+
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
384
|
+
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
385
|
+
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
386
|
+
const st = state.phState.get(wid);
|
|
387
|
+
if (st === 'new') continue; // not yet shown, don't recycle
|
|
360
388
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
361
389
|
if (bottom > threshold) continue;
|
|
362
390
|
if (!isFilled(wrap)) {
|
|
@@ -375,48 +403,48 @@
|
|
|
375
403
|
|
|
376
404
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
377
405
|
|
|
378
|
-
// Unobserve before moving
|
|
406
|
+
// Unobserve before moving
|
|
379
407
|
try {
|
|
380
408
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
381
409
|
if (ph) state.io?.unobserve(ph);
|
|
382
410
|
} catch (_) {}
|
|
383
411
|
|
|
384
|
-
|
|
385
|
-
mutate(() => {
|
|
386
|
-
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
387
|
-
best.setAttribute(ATTR.CREATED, String(now()));
|
|
388
|
-
best.setAttribute(ATTR.SHOWN, '0');
|
|
389
|
-
best.classList.remove('is-empty');
|
|
390
|
-
best.replaceChildren();
|
|
391
|
-
|
|
392
|
-
const fresh = document.createElement('div');
|
|
393
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
394
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
395
|
-
fresh.style.minHeight = '1px';
|
|
396
|
-
best.appendChild(fresh);
|
|
397
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
398
|
-
});
|
|
412
|
+
state.phState.set(id, 'destroyed');
|
|
399
413
|
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
414
|
+
// Step 1: destroyPlaceholders(id)
|
|
415
|
+
// Step 2: remove old HTML, create fresh placeholder at new position
|
|
416
|
+
// Step 3: showAds(id)
|
|
417
|
+
ezCmd(() => {
|
|
418
|
+
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
403
419
|
|
|
404
|
-
// Ezoic recycle sequence
|
|
405
|
-
const doDestroy = () => {
|
|
406
|
-
state.phState.set(id, 'destroyed');
|
|
407
|
-
try { ez.destroyPlaceholders(id); } catch (_) {
|
|
408
|
-
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
409
|
-
}
|
|
410
420
|
setTimeout(() => {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
421
|
+
// Recreate placeholder DOM
|
|
422
|
+
mutate(() => {
|
|
423
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
424
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
425
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
426
|
+
best.classList.remove('is-empty');
|
|
427
|
+
best.replaceChildren();
|
|
428
|
+
|
|
429
|
+
const fresh = document.createElement('div');
|
|
430
|
+
fresh.id = `${PH_PREFIX}${id}`;
|
|
431
|
+
fresh.setAttribute('data-ezoic-id', String(id));
|
|
432
|
+
fresh.style.minHeight = '1px';
|
|
433
|
+
best.appendChild(fresh);
|
|
434
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
438
|
+
state.wrapByKey.set(newKey, best);
|
|
439
|
+
|
|
440
|
+
// showAds again after DOM is ready
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
state.phState.set(id, 'new');
|
|
443
|
+
observePlaceholder(id);
|
|
444
|
+
ezEnqueue(id);
|
|
445
|
+
}, TIMING.RECYCLE_SHOW_MS);
|
|
446
|
+
}, TIMING.RECYCLE_DESTROY_MS);
|
|
447
|
+
});
|
|
420
448
|
|
|
421
449
|
return { id, wrap: best };
|
|
422
450
|
}
|
|
@@ -431,7 +459,6 @@
|
|
|
431
459
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
432
460
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
433
461
|
w.style.cssText = 'width:100%;display:block';
|
|
434
|
-
|
|
435
462
|
const ph = document.createElement('div');
|
|
436
463
|
ph.id = `${PH_PREFIX}${id}`;
|
|
437
464
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -444,10 +471,8 @@
|
|
|
444
471
|
if (!el?.insertAdjacentElement) return null;
|
|
445
472
|
if (findWrap(key)) return null;
|
|
446
473
|
if (state.mountedIds.has(id)) return null;
|
|
447
|
-
// Ensure no duplicate DOM element with same placeholder ID
|
|
448
474
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
449
475
|
if (existing?.isConnected) return null;
|
|
450
|
-
|
|
451
476
|
const w = makeWrap(id, klass, key);
|
|
452
477
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
453
478
|
state.mountedIds.add(id);
|
|
@@ -461,21 +486,13 @@
|
|
|
461
486
|
try {
|
|
462
487
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
463
488
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
464
|
-
|
|
465
489
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
466
|
-
if (Number.isFinite(id)) {
|
|
467
|
-
state.mountedIds.delete(id);
|
|
468
|
-
state.phState.delete(id);
|
|
469
|
-
}
|
|
470
|
-
|
|
490
|
+
if (Number.isFinite(id)) { state.mountedIds.delete(id); state.phState.delete(id); }
|
|
471
491
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
472
492
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
473
|
-
|
|
474
|
-
// Find the kind class to unregister
|
|
475
493
|
for (const cls of w.classList) {
|
|
476
494
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
477
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
478
|
-
break;
|
|
495
|
+
state.wrapsByClass.get(cls)?.delete(w); break;
|
|
479
496
|
}
|
|
480
497
|
}
|
|
481
498
|
w.remove();
|
|
@@ -483,33 +500,24 @@
|
|
|
483
500
|
}
|
|
484
501
|
|
|
485
502
|
// ── Prune (category topic lists only) ──────────────────────────────────────
|
|
486
|
-
//
|
|
487
|
-
// Safe for category topic lists: NodeBB does NOT virtualize topics in categories.
|
|
488
|
-
// NOT safe for posts: NodeBB virtualizes posts off-viewport.
|
|
489
503
|
|
|
490
504
|
function pruneOrphansBetween() {
|
|
491
505
|
const klass = 'ezoic-ad-between';
|
|
492
506
|
const cfg = KIND[klass];
|
|
493
507
|
const wraps = state.wrapsByClass.get(klass);
|
|
494
508
|
if (!wraps?.size) return;
|
|
495
|
-
|
|
496
|
-
// Build set of live anchor IDs
|
|
497
509
|
const liveAnchors = new Set();
|
|
498
510
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
499
511
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
500
512
|
if (v) liveAnchors.add(v);
|
|
501
513
|
}
|
|
502
|
-
|
|
503
514
|
const t = now();
|
|
504
515
|
for (const w of wraps) {
|
|
505
516
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
506
517
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
507
|
-
|
|
508
518
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
509
519
|
const sid = key.slice(klass.length + 1);
|
|
510
|
-
if (!sid || !liveAnchors.has(sid))
|
|
511
|
-
mutate(() => dropWrap(w));
|
|
512
|
-
}
|
|
520
|
+
if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
513
521
|
}
|
|
514
522
|
}
|
|
515
523
|
|
|
@@ -521,7 +529,6 @@
|
|
|
521
529
|
const v = el.getAttribute(attr);
|
|
522
530
|
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
523
531
|
}
|
|
524
|
-
// Positional fallback
|
|
525
532
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
526
533
|
let i = 0;
|
|
527
534
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -538,11 +545,9 @@
|
|
|
538
545
|
for (const el of items) {
|
|
539
546
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
540
547
|
if (!el?.isConnected) continue;
|
|
541
|
-
|
|
542
548
|
const ord = ordinal(klass, el);
|
|
543
549
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
544
550
|
if (adjacentWrap(el)) continue;
|
|
545
|
-
|
|
546
551
|
const key = anchorKey(klass, el);
|
|
547
552
|
if (findWrap(key)) continue;
|
|
548
553
|
|
|
@@ -551,12 +556,13 @@
|
|
|
551
556
|
const w = insertAfter(el, id, klass, key);
|
|
552
557
|
if (w) {
|
|
553
558
|
observePlaceholder(id);
|
|
554
|
-
|
|
559
|
+
// Queue for batched define+enable/displayMore
|
|
560
|
+
ezEnqueue(id);
|
|
555
561
|
inserted++;
|
|
556
562
|
}
|
|
557
563
|
} else {
|
|
558
564
|
const recycled = recycleWrap(klass, el, key);
|
|
559
|
-
if (!recycled) break;
|
|
565
|
+
if (!recycled) break;
|
|
560
566
|
inserted++;
|
|
561
567
|
}
|
|
562
568
|
}
|
|
@@ -564,6 +570,12 @@
|
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
573
|
+
//
|
|
574
|
+
// The IO is used to eagerly observe placeholders so that when they enter
|
|
575
|
+
// the viewport margin, we can queue them for Ezoic. However, the actual
|
|
576
|
+
// Ezoic API calls (define/displayMore) happen in the batched flush.
|
|
577
|
+
// The IO callback is mainly useful for re-triggering after NodeBB
|
|
578
|
+
// virtualisation re-inserts posts.
|
|
567
579
|
|
|
568
580
|
function getIO() {
|
|
569
581
|
if (state.io) return state.io;
|
|
@@ -571,9 +583,19 @@
|
|
|
571
583
|
state.io = new IntersectionObserver(entries => {
|
|
572
584
|
for (const entry of entries) {
|
|
573
585
|
if (!entry.isIntersecting) continue;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
586
|
+
const target = entry.target;
|
|
587
|
+
if (target instanceof Element) state.io?.unobserve(target);
|
|
588
|
+
const id = parseInt(target.getAttribute('data-ezoic-id'), 10);
|
|
589
|
+
if (!id || id <= 0) continue;
|
|
590
|
+
const st = state.phState.get(id);
|
|
591
|
+
// Only enqueue if not yet processed by Ezoic
|
|
592
|
+
if (st === 'new') {
|
|
593
|
+
ezEnqueue(id);
|
|
594
|
+
} else if (st === 'displayed') {
|
|
595
|
+
// Already shown — check if the placeholder is actually filled.
|
|
596
|
+
// If not (Ezoic had no ad), don't re-trigger — it won't help.
|
|
597
|
+
// If yes, nothing to do.
|
|
598
|
+
}
|
|
577
599
|
}
|
|
578
600
|
}, {
|
|
579
601
|
root: null,
|
|
@@ -586,189 +608,16 @@
|
|
|
586
608
|
|
|
587
609
|
function observePlaceholder(id) {
|
|
588
610
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
589
|
-
if (ph?.isConnected) {
|
|
590
|
-
try { getIO()?.observe(ph); } catch (_) {}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
595
|
-
|
|
596
|
-
function enqueueShow(id) {
|
|
597
|
-
if (!id || isBlocked()) return;
|
|
598
|
-
const st = state.phState.get(id);
|
|
599
|
-
if (st === 'show-queued' || st === 'shown') return;
|
|
600
|
-
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
601
|
-
|
|
602
|
-
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
603
|
-
if (!state.pendingSet.has(id)) {
|
|
604
|
-
state.pending.push(id);
|
|
605
|
-
state.pendingSet.add(id);
|
|
606
|
-
state.phState.set(id, 'show-queued');
|
|
607
|
-
}
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
state.phState.set(id, 'show-queued');
|
|
611
|
-
startShow(id);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function drainQueue() {
|
|
615
|
-
if (isBlocked()) return;
|
|
616
|
-
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
617
|
-
const id = state.pending.shift();
|
|
618
|
-
state.pendingSet.delete(id);
|
|
619
|
-
startShow(id);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function startShow(id) {
|
|
624
|
-
if (!id || isBlocked()) return;
|
|
625
|
-
state.inflight++;
|
|
626
|
-
|
|
627
|
-
let released = false;
|
|
628
|
-
const release = () => {
|
|
629
|
-
if (released) return;
|
|
630
|
-
released = true;
|
|
631
|
-
state.inflight = Math.max(0, state.inflight - 1);
|
|
632
|
-
drainQueue();
|
|
633
|
-
};
|
|
634
|
-
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
635
|
-
|
|
636
|
-
requestAnimationFrame(() => {
|
|
637
|
-
try {
|
|
638
|
-
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
639
|
-
|
|
640
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
641
|
-
if (!ph?.isConnected) {
|
|
642
|
-
state.phState.delete(id);
|
|
643
|
-
clearTimeout(timer);
|
|
644
|
-
return release();
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
648
|
-
state.phState.set(id, 'shown');
|
|
649
|
-
clearTimeout(timer);
|
|
650
|
-
return release();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const t = now();
|
|
654
|
-
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
655
|
-
clearTimeout(timer);
|
|
656
|
-
return release();
|
|
657
|
-
}
|
|
658
|
-
state.lastShow.set(id, t);
|
|
659
|
-
|
|
660
|
-
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
661
|
-
state.phState.set(id, 'shown');
|
|
662
|
-
|
|
663
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
664
|
-
const ez = window.ezstandalone;
|
|
665
|
-
|
|
666
|
-
const doShow = () => {
|
|
667
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
668
|
-
try { ez.showAds(id); } catch (_) {}
|
|
669
|
-
if (wrap) scheduleUncollapseChecks(wrap);
|
|
670
|
-
scheduleEmptyCheck(id, t);
|
|
671
|
-
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
675
|
-
} catch (_) {
|
|
676
|
-
clearTimeout(timer);
|
|
677
|
-
release();
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function scheduleEmptyCheck(id, showTs) {
|
|
683
|
-
setTimeout(() => {
|
|
684
|
-
try {
|
|
685
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
686
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
687
|
-
if (!wrap || !ph?.isConnected) return;
|
|
688
|
-
// Skip if a newer show happened since
|
|
689
|
-
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
690
|
-
if (clearEmptyIfFilled(wrap)) return;
|
|
691
|
-
wrap.classList.add('is-empty');
|
|
692
|
-
} catch (_) {}
|
|
693
|
-
}, TIMING.EMPTY_CHECK_MS);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
697
|
-
//
|
|
698
|
-
// Intercepts ez.showAds() to:
|
|
699
|
-
// - block calls during navigation transitions
|
|
700
|
-
// - filter out disconnected placeholders
|
|
701
|
-
// - batch calls for efficiency
|
|
702
|
-
|
|
703
|
-
function patchShowAds() {
|
|
704
|
-
const apply = () => {
|
|
705
|
-
try {
|
|
706
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
707
|
-
const ez = window.ezstandalone;
|
|
708
|
-
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
709
|
-
window.__nbbEzPatched = true;
|
|
710
|
-
|
|
711
|
-
const orig = ez.showAds.bind(ez);
|
|
712
|
-
const queue = new Set();
|
|
713
|
-
let flushTimer = null;
|
|
714
|
-
|
|
715
|
-
const flush = () => {
|
|
716
|
-
flushTimer = null;
|
|
717
|
-
if (isBlocked() || !queue.size) return;
|
|
718
|
-
|
|
719
|
-
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
720
|
-
queue.clear();
|
|
721
|
-
|
|
722
|
-
const valid = ids.filter(id => {
|
|
723
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
724
|
-
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
725
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
726
|
-
return true;
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
730
|
-
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
731
|
-
try { orig(...chunk); } catch (_) {
|
|
732
|
-
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
ez.showAds = function (...args) {
|
|
738
|
-
if (isBlocked()) return;
|
|
739
|
-
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
740
|
-
for (const v of ids) {
|
|
741
|
-
const id = parseInt(v, 10);
|
|
742
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
743
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
744
|
-
if (!ph?.isConnected) continue;
|
|
745
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
|
|
746
|
-
state.phState.set(id, 'show-queued');
|
|
747
|
-
queue.add(id);
|
|
748
|
-
}
|
|
749
|
-
if (queue.size && !flushTimer) {
|
|
750
|
-
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
} catch (_) {}
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
apply();
|
|
757
|
-
if (!window.__nbbEzPatched) {
|
|
758
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
759
|
-
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
760
|
-
}
|
|
611
|
+
if (ph?.isConnected) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
761
612
|
}
|
|
762
613
|
|
|
763
614
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
764
615
|
|
|
765
616
|
async function runCore() {
|
|
766
617
|
if (isBlocked()) return 0;
|
|
767
|
-
|
|
768
618
|
const cfg = await fetchConfig();
|
|
769
619
|
if (!cfg || cfg.excluded) return 0;
|
|
770
620
|
initPools(cfg);
|
|
771
|
-
|
|
772
621
|
const kind = getKind();
|
|
773
622
|
if (kind === 'other') return 0;
|
|
774
623
|
|
|
@@ -779,24 +628,16 @@
|
|
|
779
628
|
};
|
|
780
629
|
|
|
781
630
|
if (kind === 'topic') {
|
|
782
|
-
return exec(
|
|
783
|
-
|
|
784
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
785
|
-
);
|
|
631
|
+
return exec('ezoic-ad-message', getPosts,
|
|
632
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
786
633
|
}
|
|
787
|
-
|
|
788
634
|
if (kind === 'categoryTopics') {
|
|
789
635
|
pruneOrphansBetween();
|
|
790
|
-
return exec(
|
|
791
|
-
|
|
792
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
793
|
-
);
|
|
636
|
+
return exec('ezoic-ad-between', getTopics,
|
|
637
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
794
638
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
'ezoic-ad-categories', getCategories,
|
|
798
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
799
|
-
);
|
|
639
|
+
return exec('ezoic-ad-categories', getCategories,
|
|
640
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
800
641
|
}
|
|
801
642
|
|
|
802
643
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -820,78 +661,68 @@
|
|
|
820
661
|
state.lastBurstTs = t;
|
|
821
662
|
state.pageKey = pageKey();
|
|
822
663
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
823
|
-
|
|
824
664
|
if (state.burstActive) return;
|
|
825
665
|
state.burstActive = true;
|
|
826
666
|
state.burstCount = 0;
|
|
827
|
-
|
|
828
667
|
const step = () => {
|
|
829
668
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
830
|
-
state.burstActive = false;
|
|
831
|
-
return;
|
|
669
|
+
state.burstActive = false; return;
|
|
832
670
|
}
|
|
833
671
|
state.burstCount++;
|
|
834
672
|
scheduleRun(n => {
|
|
835
|
-
if (!n && !state.
|
|
673
|
+
if (!n && !state.ezBatch.size) { state.burstActive = false; return; }
|
|
836
674
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
837
675
|
});
|
|
838
676
|
};
|
|
839
677
|
step();
|
|
840
678
|
}
|
|
841
679
|
|
|
842
|
-
// ── Cleanup
|
|
680
|
+
// ── Cleanup ────────────────────────────────────────────────────────────────
|
|
843
681
|
|
|
844
682
|
function cleanup() {
|
|
845
683
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
684
|
+
|
|
685
|
+
// Cancel pending Ezoic batch
|
|
686
|
+
if (state.ezFlushTimer) { clearTimeout(state.ezFlushTimer); state.ezFlushTimer = null; }
|
|
687
|
+
state.ezBatch.clear();
|
|
688
|
+
|
|
846
689
|
mutate(() => {
|
|
847
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
848
|
-
dropWrap(w);
|
|
849
|
-
}
|
|
690
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
850
691
|
});
|
|
851
|
-
state.cfg
|
|
852
|
-
state.poolsReady
|
|
853
|
-
state.pools
|
|
854
|
-
state.cursors
|
|
692
|
+
state.cfg = null;
|
|
693
|
+
state.poolsReady = false;
|
|
694
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
695
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
855
696
|
state.mountedIds.clear();
|
|
856
697
|
state.lastShow.clear();
|
|
857
698
|
state.wrapByKey.clear();
|
|
858
699
|
state.wrapsByClass.clear();
|
|
859
|
-
state.kind
|
|
700
|
+
state.kind = null;
|
|
860
701
|
state.phState.clear();
|
|
861
|
-
state.inflight = 0;
|
|
862
|
-
state.pending = [];
|
|
863
|
-
state.pendingSet.clear();
|
|
864
702
|
state.burstActive = false;
|
|
865
703
|
state.runQueued = false;
|
|
866
|
-
state.firstShown = false;
|
|
867
704
|
}
|
|
868
705
|
|
|
869
706
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
870
|
-
//
|
|
871
|
-
// Scoped to detect: (1) ad fill events in wraps, (2) new content items
|
|
872
707
|
|
|
873
708
|
function ensureDomObserver() {
|
|
874
709
|
if (state.domObs) return;
|
|
875
|
-
|
|
876
710
|
state.domObs = new MutationObserver(muts => {
|
|
877
711
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
878
|
-
|
|
879
712
|
let needsBurst = false;
|
|
880
|
-
|
|
881
|
-
// Determine relevant selectors for current page kind
|
|
882
713
|
const kind = getKind();
|
|
883
714
|
const relevantSels =
|
|
884
|
-
kind === 'topic'
|
|
885
|
-
kind === 'categoryTopics'? [SEL.topic] :
|
|
886
|
-
kind === 'categories'
|
|
887
|
-
|
|
715
|
+
kind === 'topic' ? [SEL.post] :
|
|
716
|
+
kind === 'categoryTopics' ? [SEL.topic] :
|
|
717
|
+
kind === 'categories' ? [SEL.category] :
|
|
718
|
+
[SEL.post, SEL.topic, SEL.category];
|
|
888
719
|
|
|
889
720
|
for (const m of muts) {
|
|
890
721
|
if (m.type !== 'childList') continue;
|
|
891
722
|
for (const node of m.addedNodes) {
|
|
892
723
|
if (!(node instanceof Element)) continue;
|
|
893
724
|
|
|
894
|
-
//
|
|
725
|
+
// Ad fill detection
|
|
895
726
|
try {
|
|
896
727
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
897
728
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -900,104 +731,121 @@
|
|
|
900
731
|
}
|
|
901
732
|
} catch (_) {}
|
|
902
733
|
|
|
903
|
-
//
|
|
734
|
+
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
735
|
+
try {
|
|
736
|
+
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
737
|
+
? [node]
|
|
738
|
+
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
739
|
+
for (const wrap of wraps) {
|
|
740
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
741
|
+
if (!id || id <= 0) continue;
|
|
742
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
743
|
+
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
744
|
+
}
|
|
745
|
+
} catch (_) {}
|
|
746
|
+
|
|
747
|
+
// New content detection
|
|
904
748
|
if (!needsBurst) {
|
|
905
749
|
for (const sel of relevantSels) {
|
|
906
750
|
try {
|
|
907
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
908
|
-
needsBurst = true;
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
751
|
+
if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break; }
|
|
911
752
|
} catch (_) {}
|
|
912
753
|
}
|
|
913
754
|
}
|
|
914
755
|
}
|
|
915
756
|
if (needsBurst) break;
|
|
916
757
|
}
|
|
917
|
-
|
|
918
758
|
if (needsBurst) requestBurst();
|
|
919
759
|
});
|
|
920
|
-
|
|
921
|
-
try {
|
|
922
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
923
|
-
} catch (_) {}
|
|
760
|
+
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
924
761
|
}
|
|
925
762
|
|
|
926
|
-
// ── TCF
|
|
927
|
-
//
|
|
928
|
-
// FIX: The CMP iframe locator (__tcfapiLocator) must exist for the CMP to
|
|
929
|
-
// communicate via postMessage. NodeBB's ajaxify can remove it during navigation.
|
|
930
|
-
//
|
|
931
|
-
// Previous approach: MutationObserver that re-injects on EVERY mutation → race
|
|
932
|
-
// conditions with CMP, causing "Cannot read properties of null (reading 'postMessage')"
|
|
933
|
-
// and "Cannot set properties of null (setting 'addtlConsent')".
|
|
934
|
-
//
|
|
935
|
-
// New approach: Debounced check — only re-inject after DOM stabilizes (500ms).
|
|
936
|
-
// The CMP scripts handle their own initialization; we just ensure the locator
|
|
937
|
-
// iframe exists when needed.
|
|
938
|
-
|
|
939
|
-
let _tcfDebounce = null;
|
|
763
|
+
// ── TCF / CMP Protection ───────────────────────────────────────────────────
|
|
940
764
|
|
|
941
765
|
function ensureTcfLocator() {
|
|
942
|
-
// Only needed if CMP APIs are present
|
|
943
766
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
767
|
+
const LOCATOR_ID = '__tcfapiLocator';
|
|
768
|
+
const ensureInHead = () => {
|
|
769
|
+
let existing = document.getElementById(LOCATOR_ID);
|
|
770
|
+
if (existing) {
|
|
771
|
+
if (existing.parentElement !== document.head) {
|
|
772
|
+
try { document.head.appendChild(existing); } catch (_) {}
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
947
776
|
const f = document.createElement('iframe');
|
|
948
|
-
f.style.display = 'none';
|
|
949
|
-
|
|
950
|
-
|
|
777
|
+
f.style.display = 'none'; f.id = f.name = LOCATOR_ID;
|
|
778
|
+
try { document.head.appendChild(f); } catch (_) {
|
|
779
|
+
(document.body || document.documentElement).appendChild(f);
|
|
780
|
+
}
|
|
951
781
|
};
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
782
|
+
ensureInHead();
|
|
783
|
+
if (!window.__nbbCmpGuarded) {
|
|
784
|
+
window.__nbbCmpGuarded = true;
|
|
785
|
+
if (typeof window.__tcfapi === 'function') {
|
|
786
|
+
const orig = window.__tcfapi;
|
|
787
|
+
window.__tcfapi = function (cmd, ver, cb, param) {
|
|
788
|
+
try { return orig.call(this, cmd, ver, function (...a) { try { cb?.(...a); } catch (_) {} }, param); }
|
|
789
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.call(this, cmd, ver, cb, param); } catch (_) {} } }
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (typeof window.__cmp === 'function') {
|
|
793
|
+
const orig = window.__cmp;
|
|
794
|
+
window.__cmp = function (...a) {
|
|
795
|
+
try { return orig.apply(this, a); }
|
|
796
|
+
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.apply(this, a); } catch (_) {} } }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
956
800
|
if (!window.__nbbTcfObs) {
|
|
957
801
|
window.__nbbTcfObs = new MutationObserver(() => {
|
|
958
|
-
|
|
959
|
-
if (document.getElementById('__tcfapiLocator')) return;
|
|
960
|
-
// Debounce: wait for DOM to stabilize before re-injecting
|
|
961
|
-
clearTimeout(_tcfDebounce);
|
|
962
|
-
_tcfDebounce = setTimeout(inject, TIMING.TCF_DEBOUNCE_MS);
|
|
963
|
-
});
|
|
964
|
-
// Observe only direct children of body (not deep subtree)
|
|
965
|
-
// since the locator iframe is a direct child
|
|
966
|
-
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
967
|
-
childList: true,
|
|
968
|
-
subtree: false,
|
|
802
|
+
if (!document.getElementById(LOCATOR_ID)) ensureInHead();
|
|
969
803
|
});
|
|
804
|
+
try { window.__nbbTcfObs.observe(document.body || document.documentElement, { childList: true, subtree: false }); } catch (_) {}
|
|
805
|
+
try { if (document.head) window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false }); } catch (_) {}
|
|
970
806
|
}
|
|
971
807
|
}
|
|
972
808
|
|
|
809
|
+
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
function protectAriaHidden() {
|
|
812
|
+
if (window.__nbbAriaObs) return;
|
|
813
|
+
const remove = () => {
|
|
814
|
+
try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
815
|
+
};
|
|
816
|
+
remove();
|
|
817
|
+
window.__nbbAriaObs = new MutationObserver(remove);
|
|
818
|
+
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
819
|
+
}
|
|
820
|
+
|
|
973
821
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
974
|
-
//
|
|
975
|
-
// Mute noisy Ezoic warnings that are expected in infinite scroll context.
|
|
976
|
-
// Uses startsWith checks instead of includes for performance.
|
|
977
822
|
|
|
978
823
|
function muteConsole() {
|
|
979
824
|
if (window.__nbbEzMuted) return;
|
|
980
825
|
window.__nbbEzMuted = true;
|
|
981
|
-
|
|
982
826
|
const PREFIXES = [
|
|
983
827
|
'[EzoicAds JS]: Placeholder Id',
|
|
984
|
-
'No valid placeholders for loadMore',
|
|
985
828
|
'cannot call refresh on the same page',
|
|
986
829
|
'no placeholders are currently defined in Refresh',
|
|
987
830
|
'Debugger iframe already exists',
|
|
831
|
+
'[CMP] Error in custom getTCData',
|
|
832
|
+
'vignette: no interstitial API',
|
|
833
|
+
];
|
|
834
|
+
const PATTERNS = [
|
|
835
|
+
`with id ${PH_PREFIX}`,
|
|
836
|
+
'adsbygoogle.push() error: All',
|
|
837
|
+
'has already been defined',
|
|
838
|
+
'No valid placeholders for loadMore',
|
|
839
|
+
'bad response. Status',
|
|
988
840
|
];
|
|
989
|
-
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
990
|
-
|
|
991
841
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
992
842
|
const orig = console[method];
|
|
993
843
|
if (typeof orig !== 'function') continue;
|
|
994
844
|
console[method] = function (...args) {
|
|
995
845
|
if (typeof args[0] === 'string') {
|
|
996
846
|
const msg = args[0];
|
|
997
|
-
for (const
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
if (msg.includes(PH_PATTERN)) return;
|
|
847
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
848
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
1001
849
|
}
|
|
1002
850
|
return orig.apply(console, args);
|
|
1003
851
|
};
|
|
@@ -1005,31 +853,24 @@
|
|
|
1005
853
|
}
|
|
1006
854
|
|
|
1007
855
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1008
|
-
// Run once per session — preconnect hints are in <head> via server-side injection
|
|
1009
856
|
|
|
1010
857
|
let _networkWarmed = false;
|
|
1011
|
-
|
|
1012
858
|
function warmNetwork() {
|
|
1013
859
|
if (_networkWarmed) return;
|
|
1014
860
|
_networkWarmed = true;
|
|
1015
|
-
|
|
1016
861
|
const head = document.head;
|
|
1017
862
|
if (!head) return;
|
|
1018
|
-
|
|
1019
|
-
const hints = [
|
|
863
|
+
for (const [rel, href, cors] of [
|
|
1020
864
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
1021
865
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
1022
866
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
1023
867
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
1024
868
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1025
869
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1026
|
-
]
|
|
1027
|
-
|
|
1028
|
-
for (const [rel, href, cors] of hints) {
|
|
870
|
+
]) {
|
|
1029
871
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1030
872
|
const link = document.createElement('link');
|
|
1031
|
-
link.rel = rel;
|
|
1032
|
-
link.href = href;
|
|
873
|
+
link.rel = rel; link.href = href;
|
|
1033
874
|
if (cors) link.crossOrigin = 'anonymous';
|
|
1034
875
|
head.appendChild(link);
|
|
1035
876
|
}
|
|
@@ -1040,48 +881,25 @@
|
|
|
1040
881
|
function bindNodeBB() {
|
|
1041
882
|
const $ = window.jQuery;
|
|
1042
883
|
if (!$) return;
|
|
1043
|
-
|
|
1044
884
|
$(window).off('.nbbEzoic');
|
|
1045
|
-
|
|
1046
885
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1047
|
-
|
|
1048
886
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1049
|
-
state.pageKey
|
|
1050
|
-
state.kind
|
|
887
|
+
state.pageKey = pageKey();
|
|
888
|
+
state.kind = null;
|
|
1051
889
|
state.blockedUntil = 0;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
ensureTcfLocator();
|
|
1055
|
-
warmNetwork();
|
|
1056
|
-
patchShowAds();
|
|
1057
|
-
getIO();
|
|
1058
|
-
ensureDomObserver();
|
|
1059
|
-
requestBurst();
|
|
890
|
+
muteConsole(); ensureTcfLocator(); protectAriaHidden(); warmNetwork();
|
|
891
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
1060
892
|
});
|
|
1061
|
-
|
|
1062
|
-
// Content-loaded events trigger burst
|
|
1063
893
|
const burstEvents = [
|
|
1064
|
-
'action:ajaxify.contentLoaded',
|
|
1065
|
-
'action:
|
|
1066
|
-
'action:topics.loaded',
|
|
1067
|
-
'action:categories.loaded',
|
|
1068
|
-
'action:category.loaded',
|
|
1069
|
-
'action:topic.loaded',
|
|
894
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
895
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
1070
896
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1071
|
-
|
|
1072
897
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1073
|
-
|
|
1074
|
-
// Also bind via NodeBB hooks module (for compatibility)
|
|
1075
898
|
try {
|
|
1076
899
|
require(['hooks'], hooks => {
|
|
1077
900
|
if (typeof hooks?.on !== 'function') return;
|
|
1078
|
-
for (const ev of [
|
|
1079
|
-
|
|
1080
|
-
'action:posts.loaded',
|
|
1081
|
-
'action:topics.loaded',
|
|
1082
|
-
'action:categories.loaded',
|
|
1083
|
-
'action:topic.loaded',
|
|
1084
|
-
]) {
|
|
901
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
902
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
1085
903
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
1086
904
|
}
|
|
1087
905
|
});
|
|
@@ -1093,10 +911,7 @@
|
|
|
1093
911
|
window.addEventListener('scroll', () => {
|
|
1094
912
|
if (ticking) return;
|
|
1095
913
|
ticking = true;
|
|
1096
|
-
requestAnimationFrame(() => {
|
|
1097
|
-
ticking = false;
|
|
1098
|
-
requestBurst();
|
|
1099
|
-
});
|
|
914
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1100
915
|
}, { passive: true });
|
|
1101
916
|
}
|
|
1102
917
|
|
|
@@ -1105,8 +920,8 @@
|
|
|
1105
920
|
state.pageKey = pageKey();
|
|
1106
921
|
muteConsole();
|
|
1107
922
|
ensureTcfLocator();
|
|
923
|
+
protectAriaHidden();
|
|
1108
924
|
warmNetwork();
|
|
1109
|
-
patchShowAds();
|
|
1110
925
|
getIO();
|
|
1111
926
|
ensureDomObserver();
|
|
1112
927
|
bindNodeBB();
|