nodebb-plugin-ezoic-infinite 1.8.60 → 1.8.61
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 +38 -16
- package/package.json +1 -1
- package/public/client.js +370 -208
package/library.js
CHANGED
|
@@ -87,8 +87,11 @@ async function isUserExcluded(uid, excludedGroups) {
|
|
|
87
87
|
const value = (userGroups[0] || []).some(g => excludedSet.has(g.name));
|
|
88
88
|
|
|
89
89
|
_excludeCache.set(key, { value, at: Date.now() });
|
|
90
|
+
|
|
91
|
+
// Limit cache size to prevent unbounded growth
|
|
90
92
|
if (_excludeCache.size > 1000) {
|
|
91
|
-
_excludeCache.
|
|
93
|
+
const oldest = _excludeCache.keys().next().value;
|
|
94
|
+
_excludeCache.delete(oldest);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
return value;
|
|
@@ -136,24 +139,15 @@ const HEAD_PRECONNECTS = [
|
|
|
136
139
|
].join('\n');
|
|
137
140
|
|
|
138
141
|
const EZOIC_SCRIPTS = [
|
|
139
|
-
HEAD_PRECONNECTS,
|
|
140
|
-
|
|
141
|
-
// CMP en premier
|
|
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
|
-
|
|
145
|
-
// Standalone Ezoic
|
|
146
|
-
'<script async src="https://www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
147
|
-
|
|
148
|
-
// cmd queue (OK en inline)
|
|
149
144
|
'<script>',
|
|
145
|
+
'window._ezaq = window._ezaq || {};',
|
|
150
146
|
'window.ezstandalone = window.ezstandalone || {};',
|
|
151
147
|
'ezstandalone.cmd = ezstandalone.cmd || [];',
|
|
152
148
|
'</script>',
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'<script src="https://ezoicanalytics.com/analytics.js"></script>',
|
|
156
|
-
].join('\n') + '\n';
|
|
149
|
+
'<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
|
|
150
|
+
].join('\n');
|
|
157
151
|
|
|
158
152
|
// ── Hooks ────────────────────────────────────────────────────────────────────
|
|
159
153
|
|
|
@@ -171,15 +165,43 @@ plugin.addAdminNavigation = async (header) => {
|
|
|
171
165
|
return header;
|
|
172
166
|
};
|
|
173
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Inject Ezoic scripts into <head> via templateData.customHTML.
|
|
170
|
+
* NodeBB v4/Harmony: header.tpl has {{customHTML}} in <head>.
|
|
171
|
+
*/
|
|
174
172
|
plugin.injectEzoicHead = async (data) => {
|
|
175
173
|
try {
|
|
176
174
|
const settings = await getSettings();
|
|
177
175
|
const uid = data.req?.uid ?? 0;
|
|
178
176
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
179
|
-
|
|
180
|
-
|
|
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 {
|
|
195
|
+
const cfg = buildClientConfig(settings, false);
|
|
196
|
+
data.templateData.customHTML =
|
|
197
|
+
HEAD_PRECONNECTS + '\n' +
|
|
198
|
+
EZOIC_SCRIPTS + '\n' +
|
|
199
|
+
serializeInlineConfig(cfg) +
|
|
200
|
+
(data.templateData.customHTML || '');
|
|
181
201
|
}
|
|
182
|
-
} catch (
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('[ezoic-infinite] injectEzoicHead error:', err.message);
|
|
204
|
+
}
|
|
183
205
|
return data;
|
|
184
206
|
};
|
|
185
207
|
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js v2.
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - Ezoic API: showAds() + destroyPlaceholders() per official docs
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
4
|
+
* Based on working v2-fixed with targeted fixes:
|
|
5
|
+
* - Ezoic API: showAds() + destroyPlaceholders() only (per official docs)
|
|
6
|
+
* - cleanup(): destroyAll() before DOM removal, only on real page change
|
|
7
|
+
* - Recycling: conservative threshold (-3vh), age/inflight guards
|
|
8
|
+
* - aria-hidden: MutationObserver protection
|
|
9
|
+
* - Empty check: more conservative timing and GPT slot awareness
|
|
10
10
|
*/
|
|
11
11
|
(function nbbEzoicInfinite() {
|
|
12
12
|
'use strict';
|
|
@@ -24,25 +24,28 @@
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const TIMING = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
MIN_PRUNE_AGE_MS:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RECYCLE_DELAY_MS: 450,
|
|
27
|
+
EMPTY_CHECK_EARLY_MS: 30_000,
|
|
28
|
+
EMPTY_CHECK_LATE_MS: 60_000,
|
|
29
|
+
MIN_PRUNE_AGE_MS: 8_000,
|
|
30
|
+
SHOW_THROTTLE_MS: 900,
|
|
31
|
+
BURST_COOLDOWN_MS: 200,
|
|
32
|
+
BLOCK_DURATION_MS: 1_500,
|
|
33
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
34
|
+
SHOW_RELEASE_MS: 700,
|
|
35
|
+
RECYCLE_DELAY_MS: 450,
|
|
37
36
|
};
|
|
38
37
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const LIMITS = {
|
|
39
|
+
MAX_INSERTS_RUN: 6,
|
|
40
|
+
MAX_INFLIGHT: 4,
|
|
41
|
+
MAX_BURST_STEPS: 8,
|
|
42
|
+
BURST_WINDOW_MS: 2_000,
|
|
43
|
+
};
|
|
43
44
|
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const IO_MARGIN = {
|
|
46
|
+
DESKTOP: '2500px 0px 2500px 0px',
|
|
47
|
+
MOBILE: '3500px 0px 3500px 0px',
|
|
48
|
+
};
|
|
46
49
|
|
|
47
50
|
const SEL = {
|
|
48
51
|
post: '[component="post"][data-pid]',
|
|
@@ -57,26 +60,39 @@
|
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
63
|
+
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
60
64
|
|
|
61
65
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
62
66
|
|
|
63
67
|
const now = () => Date.now();
|
|
64
68
|
const isMobile = () => window.innerWidth < 768;
|
|
65
69
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
66
|
-
|
|
70
|
+
|
|
71
|
+
function isFilled(node) {
|
|
72
|
+
return node?.querySelector?.(FILL_SEL) != null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPlaceholderUsed(ph) {
|
|
76
|
+
if (!ph?.isConnected) return false;
|
|
77
|
+
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
78
|
+
}
|
|
67
79
|
|
|
68
80
|
function parseIds(raw) {
|
|
69
|
-
const out = []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
const out = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
84
|
+
const n = parseInt(line, 10);
|
|
85
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
86
|
+
seen.add(n);
|
|
87
|
+
out.push(n);
|
|
88
|
+
}
|
|
73
89
|
}
|
|
74
90
|
return out;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
78
94
|
|
|
79
|
-
const
|
|
95
|
+
const state = {
|
|
80
96
|
pageKey: null,
|
|
81
97
|
kind: null,
|
|
82
98
|
cfg: null,
|
|
@@ -85,17 +101,18 @@
|
|
|
85
101
|
pools: { topics: [], posts: [], categories: [] },
|
|
86
102
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
87
103
|
|
|
88
|
-
mountedIds:
|
|
89
|
-
|
|
104
|
+
mountedIds: new Set(),
|
|
105
|
+
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
106
|
+
lastShow: new Map(), // id → timestamp
|
|
90
107
|
|
|
91
|
-
wrapByKey: new Map(),
|
|
92
|
-
wrapsByClass: new Map(),
|
|
108
|
+
wrapByKey: new Map(),
|
|
109
|
+
wrapsByClass: new Map(),
|
|
93
110
|
|
|
94
111
|
io: null,
|
|
95
112
|
domObs: null,
|
|
96
113
|
|
|
97
|
-
mutGuard:
|
|
98
|
-
blockedUntil:
|
|
114
|
+
mutGuard: 0,
|
|
115
|
+
blockedUntil: 0,
|
|
99
116
|
|
|
100
117
|
inflight: 0,
|
|
101
118
|
pending: [],
|
|
@@ -106,36 +123,40 @@
|
|
|
106
123
|
burstDeadline: 0,
|
|
107
124
|
burstCount: 0,
|
|
108
125
|
lastBurstTs: 0,
|
|
126
|
+
firstShown: false,
|
|
109
127
|
};
|
|
110
128
|
|
|
111
|
-
const isBlocked = () => now() <
|
|
129
|
+
const isBlocked = () => now() < state.blockedUntil;
|
|
112
130
|
|
|
113
131
|
function mutate(fn) {
|
|
114
|
-
|
|
115
|
-
try { fn(); } finally {
|
|
132
|
+
state.mutGuard++;
|
|
133
|
+
try { fn(); } finally { state.mutGuard--; }
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
119
137
|
|
|
120
138
|
async function fetchConfig() {
|
|
121
|
-
if (
|
|
139
|
+
if (state.cfg) return state.cfg;
|
|
122
140
|
try {
|
|
123
141
|
const inline = window.__nbbEzoicCfg;
|
|
124
|
-
if (inline && typeof inline === 'object') {
|
|
142
|
+
if (inline && typeof inline === 'object') {
|
|
143
|
+
state.cfg = inline;
|
|
144
|
+
return state.cfg;
|
|
145
|
+
}
|
|
125
146
|
} catch (_) {}
|
|
126
147
|
try {
|
|
127
148
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
128
|
-
if (r.ok)
|
|
149
|
+
if (r.ok) state.cfg = await r.json();
|
|
129
150
|
} catch (_) {}
|
|
130
|
-
return
|
|
151
|
+
return state.cfg;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
function initPools(cfg) {
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
if (state.poolsReady) return;
|
|
156
|
+
state.pools.topics = parseIds(cfg.placeholderIds);
|
|
157
|
+
state.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
158
|
+
state.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
159
|
+
state.poolsReady = true;
|
|
139
160
|
}
|
|
140
161
|
|
|
141
162
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
@@ -161,7 +182,7 @@
|
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
function getKind() {
|
|
164
|
-
return
|
|
185
|
+
return state.kind || (state.kind = detectKind());
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
@@ -181,8 +202,8 @@
|
|
|
181
202
|
return out;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
206
|
+
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
186
207
|
|
|
187
208
|
// ── Anchor keys & wrap registry ────────────────────────────────────────────
|
|
188
209
|
|
|
@@ -203,45 +224,60 @@
|
|
|
203
224
|
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
204
225
|
|
|
205
226
|
function findWrap(key) {
|
|
206
|
-
const w =
|
|
227
|
+
const w = state.wrapByKey.get(key);
|
|
207
228
|
return w?.isConnected ? w : null;
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
function getWrapSet(klass) {
|
|
211
|
-
let set =
|
|
212
|
-
if (!set) { set = new Set();
|
|
232
|
+
let set = state.wrapsByClass.get(klass);
|
|
233
|
+
if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
|
|
213
234
|
return set;
|
|
214
235
|
}
|
|
215
236
|
|
|
216
|
-
// ── GC disconnected wraps
|
|
217
|
-
// NodeBB virtualizes posts off-viewport. The MutationObserver catches most
|
|
218
|
-
// removals, but this is a safety net for edge cases.
|
|
237
|
+
// ── GC disconnected wraps (NodeBB virtualization) ──────────────────────────
|
|
219
238
|
|
|
220
239
|
function gcDisconnectedWraps() {
|
|
221
|
-
for (const [key, w] of
|
|
222
|
-
if (!w?.isConnected)
|
|
240
|
+
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
241
|
+
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
223
242
|
}
|
|
224
|
-
for (const [klass, set] of
|
|
225
|
-
for (const w of set) {
|
|
243
|
+
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
244
|
+
for (const w of Array.from(set)) {
|
|
226
245
|
if (w?.isConnected) continue;
|
|
227
246
|
set.delete(w);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
247
|
+
try {
|
|
248
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
249
|
+
if (Number.isFinite(id)) {
|
|
250
|
+
state.mountedIds.delete(id);
|
|
251
|
+
state.phState.delete(id);
|
|
252
|
+
state.lastShow.delete(id);
|
|
253
|
+
}
|
|
254
|
+
} catch (_) {}
|
|
233
255
|
}
|
|
234
|
-
if (!set.size)
|
|
256
|
+
if (!set.size) state.wrapsByClass.delete(klass);
|
|
235
257
|
}
|
|
258
|
+
try {
|
|
259
|
+
const live = new Set();
|
|
260
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
261
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
262
|
+
if (id > 0) live.add(id);
|
|
263
|
+
}
|
|
264
|
+
for (const id of Array.from(state.mountedIds)) {
|
|
265
|
+
if (!live.has(id)) {
|
|
266
|
+
state.mountedIds.delete(id);
|
|
267
|
+
state.phState.delete(id);
|
|
268
|
+
state.lastShow.delete(id);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (_) {}
|
|
236
272
|
}
|
|
237
273
|
|
|
238
|
-
// ── Wrap lifecycle
|
|
274
|
+
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
239
275
|
|
|
240
276
|
function wrapIsLive(wrap) {
|
|
241
277
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
242
278
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
243
279
|
if (!key) return false;
|
|
244
|
-
if (
|
|
280
|
+
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
245
281
|
const colonIdx = key.indexOf(':');
|
|
246
282
|
const klass = key.slice(0, colonIdx);
|
|
247
283
|
const anchorId = key.slice(colonIdx + 1);
|
|
@@ -272,12 +308,14 @@
|
|
|
272
308
|
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
273
309
|
if (!ph || !isFilled(ph)) return false;
|
|
274
310
|
wrap.classList.remove('is-empty');
|
|
311
|
+
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
312
|
+
if (id > 0) state.phState.set(id, 'shown');
|
|
275
313
|
return true;
|
|
276
314
|
}
|
|
277
315
|
|
|
278
316
|
function scheduleUncollapseChecks(wrap) {
|
|
279
317
|
if (!wrap) return;
|
|
280
|
-
for (const ms of [500, 3000,
|
|
318
|
+
for (const ms of [500, 1500, 3000, 7000, 15000]) {
|
|
281
319
|
setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
|
|
282
320
|
}
|
|
283
321
|
}
|
|
@@ -285,19 +323,21 @@
|
|
|
285
323
|
// ── Pool management ────────────────────────────────────────────────────────
|
|
286
324
|
|
|
287
325
|
function pickId(poolKey) {
|
|
288
|
-
const pool =
|
|
326
|
+
const pool = state.pools[poolKey];
|
|
289
327
|
if (!pool.length) return null;
|
|
290
328
|
for (let t = 0; t < pool.length; t++) {
|
|
291
|
-
const idx =
|
|
292
|
-
|
|
329
|
+
const idx = state.cursors[poolKey] % pool.length;
|
|
330
|
+
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % pool.length;
|
|
293
331
|
const id = pool[idx];
|
|
294
|
-
if (!
|
|
332
|
+
if (!state.mountedIds.has(id)) return id;
|
|
295
333
|
}
|
|
296
334
|
return null;
|
|
297
335
|
}
|
|
298
336
|
|
|
299
337
|
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
300
|
-
//
|
|
338
|
+
//
|
|
339
|
+
// Per Ezoic docs: destroyPlaceholders(id) → remove old HTML →
|
|
340
|
+
// recreate fresh placeholder → showAds(id).
|
|
301
341
|
|
|
302
342
|
function recycleWrap(klass, targetEl, newKey) {
|
|
303
343
|
const ez = window.ezstandalone;
|
|
@@ -310,13 +350,18 @@
|
|
|
310
350
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
311
351
|
let bestFull = null, bestFullY = Infinity;
|
|
312
352
|
|
|
313
|
-
const wraps =
|
|
353
|
+
const wraps = state.wrapsByClass.get(klass);
|
|
314
354
|
if (!wraps) return null;
|
|
315
355
|
|
|
316
356
|
for (const wrap of wraps) {
|
|
317
357
|
try {
|
|
358
|
+
// Skip young wraps (ad might still be loading)
|
|
318
359
|
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
319
|
-
if (t - created <
|
|
360
|
+
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
361
|
+
// Skip wraps with inflight showAds
|
|
362
|
+
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
363
|
+
if (wid > 0 && state.phState.get(wid) === 'show-queued') continue;
|
|
364
|
+
|
|
320
365
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
321
366
|
if (bottom > threshold) continue;
|
|
322
367
|
if (!isFilled(wrap)) {
|
|
@@ -332,19 +377,22 @@
|
|
|
332
377
|
|
|
333
378
|
const id = parseInt(best.getAttribute(ATTR.WRAPID), 10);
|
|
334
379
|
if (!Number.isFinite(id)) return null;
|
|
380
|
+
|
|
335
381
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
336
382
|
|
|
337
383
|
// Unobserve before moving
|
|
338
384
|
try {
|
|
339
385
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
340
|
-
if (ph)
|
|
386
|
+
if (ph) state.io?.unobserve(ph);
|
|
341
387
|
} catch (_) {}
|
|
342
388
|
|
|
343
|
-
// Ezoic recycle: destroy →
|
|
389
|
+
// Ezoic recycle: destroy → new DOM → showAds
|
|
344
390
|
const doRecycle = () => {
|
|
391
|
+
state.phState.set(id, 'destroyed');
|
|
345
392
|
try { ez.destroyPlaceholders(id); } catch (_) {}
|
|
346
393
|
|
|
347
394
|
setTimeout(() => {
|
|
395
|
+
// Recreate fresh placeholder DOM at new position
|
|
348
396
|
mutate(() => {
|
|
349
397
|
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
350
398
|
best.setAttribute(ATTR.CREATED, String(now()));
|
|
@@ -355,21 +403,27 @@
|
|
|
355
403
|
const fresh = document.createElement('div');
|
|
356
404
|
fresh.id = `${PH_PREFIX}${id}`;
|
|
357
405
|
fresh.setAttribute('data-ezoic-id', String(id));
|
|
406
|
+
fresh.style.minHeight = '1px';
|
|
358
407
|
best.appendChild(fresh);
|
|
359
408
|
targetEl.insertAdjacentElement('afterend', best);
|
|
360
409
|
});
|
|
361
410
|
|
|
362
|
-
if (oldKey &&
|
|
363
|
-
|
|
411
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
412
|
+
state.wrapByKey.set(newKey, best);
|
|
364
413
|
|
|
414
|
+
// Re-show after DOM is settled
|
|
365
415
|
setTimeout(() => {
|
|
366
|
-
|
|
416
|
+
observePlaceholder(id);
|
|
417
|
+
state.phState.set(id, 'new');
|
|
367
418
|
enqueueShow(id);
|
|
368
419
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
369
420
|
}, TIMING.RECYCLE_DELAY_MS);
|
|
370
421
|
};
|
|
371
422
|
|
|
372
|
-
try {
|
|
423
|
+
try {
|
|
424
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle();
|
|
425
|
+
} catch (_) {}
|
|
426
|
+
|
|
373
427
|
return { id, wrap: best };
|
|
374
428
|
}
|
|
375
429
|
|
|
@@ -383,9 +437,11 @@
|
|
|
383
437
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
384
438
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
385
439
|
w.style.cssText = 'width:100%;display:block';
|
|
440
|
+
|
|
386
441
|
const ph = document.createElement('div');
|
|
387
442
|
ph.id = `${PH_PREFIX}${id}`;
|
|
388
443
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
444
|
+
ph.style.minHeight = '1px';
|
|
389
445
|
w.appendChild(ph);
|
|
390
446
|
return w;
|
|
391
447
|
}
|
|
@@ -393,12 +449,15 @@
|
|
|
393
449
|
function insertAfter(el, id, klass, key) {
|
|
394
450
|
if (!el?.insertAdjacentElement) return null;
|
|
395
451
|
if (findWrap(key)) return null;
|
|
396
|
-
if (
|
|
397
|
-
|
|
452
|
+
if (state.mountedIds.has(id)) return null;
|
|
453
|
+
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
454
|
+
if (existing?.isConnected) return null;
|
|
455
|
+
|
|
398
456
|
const w = makeWrap(id, klass, key);
|
|
399
457
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
400
|
-
|
|
401
|
-
|
|
458
|
+
state.mountedIds.add(id);
|
|
459
|
+
state.phState.set(id, 'new');
|
|
460
|
+
state.wrapByKey.set(key, w);
|
|
402
461
|
getWrapSet(klass).add(w);
|
|
403
462
|
return w;
|
|
404
463
|
}
|
|
@@ -406,17 +465,17 @@
|
|
|
406
465
|
function dropWrap(w) {
|
|
407
466
|
try {
|
|
408
467
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
409
|
-
if (ph instanceof Element)
|
|
468
|
+
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
410
469
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
411
470
|
if (Number.isFinite(id)) {
|
|
412
|
-
|
|
413
|
-
|
|
471
|
+
state.mountedIds.delete(id);
|
|
472
|
+
state.phState.delete(id);
|
|
414
473
|
}
|
|
415
474
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
416
|
-
if (key &&
|
|
475
|
+
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
417
476
|
for (const cls of w.classList) {
|
|
418
477
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
419
|
-
|
|
478
|
+
state.wrapsByClass.get(cls)?.delete(w);
|
|
420
479
|
break;
|
|
421
480
|
}
|
|
422
481
|
}
|
|
@@ -429,7 +488,7 @@
|
|
|
429
488
|
function pruneOrphansBetween() {
|
|
430
489
|
const klass = 'ezoic-ad-between';
|
|
431
490
|
const cfg = KIND[klass];
|
|
432
|
-
const wraps =
|
|
491
|
+
const wraps = state.wrapsByClass.get(klass);
|
|
433
492
|
if (!wraps?.size) return;
|
|
434
493
|
|
|
435
494
|
const liveAnchors = new Set();
|
|
@@ -437,13 +496,16 @@
|
|
|
437
496
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
438
497
|
if (v) liveAnchors.add(v);
|
|
439
498
|
}
|
|
499
|
+
|
|
440
500
|
const t = now();
|
|
441
501
|
for (const w of wraps) {
|
|
442
502
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
443
503
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
444
504
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
445
505
|
const sid = key.slice(klass.length + 1);
|
|
446
|
-
if (!sid || !liveAnchors.has(sid))
|
|
506
|
+
if (!sid || !liveAnchors.has(sid)) {
|
|
507
|
+
mutate(() => dropWrap(w));
|
|
508
|
+
}
|
|
447
509
|
}
|
|
448
510
|
}
|
|
449
511
|
|
|
@@ -467,18 +529,26 @@
|
|
|
467
529
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
468
530
|
if (!items.length) return 0;
|
|
469
531
|
let inserted = 0;
|
|
532
|
+
|
|
470
533
|
for (const el of items) {
|
|
471
|
-
if (inserted >= MAX_INSERTS_RUN) break;
|
|
534
|
+
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
472
535
|
if (!el?.isConnected) continue;
|
|
536
|
+
|
|
473
537
|
const ord = ordinal(klass, el);
|
|
474
538
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
475
539
|
if (adjacentWrap(el)) continue;
|
|
540
|
+
|
|
476
541
|
const key = anchorKey(klass, el);
|
|
477
542
|
if (findWrap(key)) continue;
|
|
543
|
+
|
|
478
544
|
const id = pickId(poolKey);
|
|
479
545
|
if (id) {
|
|
480
546
|
const w = insertAfter(el, id, klass, key);
|
|
481
|
-
if (w) {
|
|
547
|
+
if (w) {
|
|
548
|
+
observePlaceholder(id);
|
|
549
|
+
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
550
|
+
inserted++;
|
|
551
|
+
}
|
|
482
552
|
} else {
|
|
483
553
|
const recycled = recycleWrap(klass, el, key);
|
|
484
554
|
if (!recycled) break;
|
|
@@ -491,58 +561,69 @@
|
|
|
491
561
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
492
562
|
|
|
493
563
|
function getIO() {
|
|
494
|
-
if (
|
|
564
|
+
if (state.io) return state.io;
|
|
495
565
|
try {
|
|
496
|
-
|
|
497
|
-
for (const
|
|
498
|
-
if (!
|
|
499
|
-
if (
|
|
500
|
-
const id = parseInt(
|
|
566
|
+
state.io = new IntersectionObserver(entries => {
|
|
567
|
+
for (const entry of entries) {
|
|
568
|
+
if (!entry.isIntersecting) continue;
|
|
569
|
+
if (entry.target instanceof Element) state.io?.unobserve(entry.target);
|
|
570
|
+
const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
|
|
501
571
|
if (id > 0) enqueueShow(id);
|
|
502
572
|
}
|
|
503
573
|
}, {
|
|
504
574
|
root: null,
|
|
505
|
-
rootMargin: isMobile() ?
|
|
575
|
+
rootMargin: isMobile() ? IO_MARGIN.MOBILE : IO_MARGIN.DESKTOP,
|
|
506
576
|
threshold: 0,
|
|
507
577
|
});
|
|
508
|
-
} catch (_) {
|
|
509
|
-
return
|
|
578
|
+
} catch (_) { state.io = null; }
|
|
579
|
+
return state.io;
|
|
510
580
|
}
|
|
511
581
|
|
|
512
|
-
function
|
|
582
|
+
function observePlaceholder(id) {
|
|
513
583
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
514
|
-
if (ph?.isConnected)
|
|
584
|
+
if (ph?.isConnected) {
|
|
585
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
586
|
+
}
|
|
515
587
|
}
|
|
516
588
|
|
|
517
589
|
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
518
590
|
|
|
519
591
|
function enqueueShow(id) {
|
|
520
592
|
if (!id || isBlocked()) return;
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
593
|
+
const st = state.phState.get(id);
|
|
594
|
+
if (st === 'show-queued' || st === 'shown') return;
|
|
595
|
+
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
596
|
+
|
|
597
|
+
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
598
|
+
if (!state.pendingSet.has(id)) {
|
|
599
|
+
state.pending.push(id);
|
|
600
|
+
state.pendingSet.add(id);
|
|
601
|
+
state.phState.set(id, 'show-queued');
|
|
602
|
+
}
|
|
524
603
|
return;
|
|
525
604
|
}
|
|
605
|
+
state.phState.set(id, 'show-queued');
|
|
526
606
|
startShow(id);
|
|
527
607
|
}
|
|
528
608
|
|
|
529
609
|
function drainQueue() {
|
|
530
610
|
if (isBlocked()) return;
|
|
531
|
-
while (
|
|
532
|
-
const id =
|
|
533
|
-
|
|
611
|
+
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
612
|
+
const id = state.pending.shift();
|
|
613
|
+
state.pendingSet.delete(id);
|
|
534
614
|
startShow(id);
|
|
535
615
|
}
|
|
536
616
|
}
|
|
537
617
|
|
|
538
618
|
function startShow(id) {
|
|
539
619
|
if (!id || isBlocked()) return;
|
|
540
|
-
|
|
541
|
-
|
|
620
|
+
state.inflight++;
|
|
621
|
+
|
|
622
|
+
let released = false;
|
|
542
623
|
const release = () => {
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
624
|
+
if (released) return;
|
|
625
|
+
released = true;
|
|
626
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
546
627
|
drainQueue();
|
|
547
628
|
};
|
|
548
629
|
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
@@ -550,31 +631,52 @@
|
|
|
550
631
|
requestAnimationFrame(() => {
|
|
551
632
|
try {
|
|
552
633
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
634
|
+
|
|
553
635
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
554
|
-
if (!ph?.isConnected
|
|
636
|
+
if (!ph?.isConnected) {
|
|
637
|
+
state.phState.delete(id);
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
return release();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
643
|
+
state.phState.set(id, 'shown');
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
return release();
|
|
646
|
+
}
|
|
555
647
|
|
|
556
648
|
const t = now();
|
|
557
|
-
if (t - (
|
|
558
|
-
|
|
649
|
+
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
return release();
|
|
652
|
+
}
|
|
653
|
+
state.lastShow.set(id, t);
|
|
559
654
|
|
|
560
|
-
|
|
561
|
-
|
|
655
|
+
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
656
|
+
state.phState.set(id, 'shown');
|
|
562
657
|
|
|
563
658
|
window.ezstandalone = window.ezstandalone || {};
|
|
564
659
|
const ez = window.ezstandalone;
|
|
660
|
+
|
|
565
661
|
const doShow = () => {
|
|
662
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
566
663
|
try { ez.showAds(id); } catch (_) {}
|
|
567
664
|
if (wrap) scheduleUncollapseChecks(wrap);
|
|
568
665
|
scheduleEmptyCheck(id, t);
|
|
569
666
|
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
570
667
|
};
|
|
668
|
+
|
|
571
669
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
572
|
-
} catch (_) {
|
|
670
|
+
} catch (_) {
|
|
671
|
+
clearTimeout(timer);
|
|
672
|
+
release();
|
|
673
|
+
}
|
|
573
674
|
});
|
|
574
675
|
}
|
|
575
676
|
|
|
576
677
|
function scheduleEmptyCheck(id, showTs) {
|
|
577
|
-
|
|
678
|
+
// Two-pass check: conservative to avoid collapsing slow-loading ads
|
|
679
|
+
for (const delay of [TIMING.EMPTY_CHECK_EARLY_MS, TIMING.EMPTY_CHECK_LATE_MS]) {
|
|
578
680
|
setTimeout(() => {
|
|
579
681
|
try {
|
|
580
682
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
@@ -582,8 +684,9 @@
|
|
|
582
684
|
if (!wrap || !ph?.isConnected) return;
|
|
583
685
|
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
584
686
|
if (clearEmptyIfFilled(wrap)) return;
|
|
585
|
-
// Don't collapse if GPT slot exists (still loading)
|
|
687
|
+
// Don't collapse if a GPT slot exists (might still be loading)
|
|
586
688
|
if (ph.querySelector('[id^="div-gpt-ad"]')) return;
|
|
689
|
+
// Don't collapse if placeholder has meaningful height
|
|
587
690
|
if (ph.offsetHeight > 10) return;
|
|
588
691
|
wrap.classList.add('is-empty');
|
|
589
692
|
} catch (_) {}
|
|
@@ -592,7 +695,10 @@
|
|
|
592
695
|
}
|
|
593
696
|
|
|
594
697
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
595
|
-
//
|
|
698
|
+
//
|
|
699
|
+
// Intercepts ez.showAds() to filter disconnected placeholders and
|
|
700
|
+
// block calls during navigation. Matches v50 behavior: individual calls,
|
|
701
|
+
// no batching.
|
|
596
702
|
|
|
597
703
|
function patchShowAds() {
|
|
598
704
|
const apply = () => {
|
|
@@ -601,8 +707,10 @@
|
|
|
601
707
|
const ez = window.ezstandalone;
|
|
602
708
|
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
603
709
|
window.__nbbEzPatched = true;
|
|
710
|
+
|
|
604
711
|
const orig = ez.showAds.bind(ez);
|
|
605
712
|
ez.showAds = function (...args) {
|
|
713
|
+
// No-arg call = Ezoic internal page refresh — pass through
|
|
606
714
|
if (args.length === 0) return orig();
|
|
607
715
|
if (isBlocked()) return;
|
|
608
716
|
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
@@ -617,6 +725,7 @@
|
|
|
617
725
|
};
|
|
618
726
|
} catch (_) {}
|
|
619
727
|
};
|
|
728
|
+
|
|
620
729
|
apply();
|
|
621
730
|
if (!window.__nbbEzPatched) {
|
|
622
731
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -628,7 +737,6 @@
|
|
|
628
737
|
|
|
629
738
|
async function runCore() {
|
|
630
739
|
if (isBlocked()) return 0;
|
|
631
|
-
patchShowAds();
|
|
632
740
|
try { gcDisconnectedWraps(); } catch (_) {}
|
|
633
741
|
|
|
634
742
|
const cfg = await fetchConfig();
|
|
@@ -645,26 +753,32 @@
|
|
|
645
753
|
};
|
|
646
754
|
|
|
647
755
|
if (kind === 'topic') {
|
|
648
|
-
return exec(
|
|
649
|
-
|
|
756
|
+
return exec(
|
|
757
|
+
'ezoic-ad-message', getPosts,
|
|
758
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
759
|
+
);
|
|
650
760
|
}
|
|
651
761
|
if (kind === 'categoryTopics') {
|
|
652
762
|
pruneOrphansBetween();
|
|
653
|
-
return exec(
|
|
654
|
-
|
|
763
|
+
return exec(
|
|
764
|
+
'ezoic-ad-between', getTopics,
|
|
765
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
766
|
+
);
|
|
655
767
|
}
|
|
656
|
-
return exec(
|
|
657
|
-
|
|
768
|
+
return exec(
|
|
769
|
+
'ezoic-ad-categories', getCategories,
|
|
770
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
771
|
+
);
|
|
658
772
|
}
|
|
659
773
|
|
|
660
774
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
661
775
|
|
|
662
776
|
function scheduleRun(cb) {
|
|
663
|
-
if (
|
|
664
|
-
|
|
777
|
+
if (state.runQueued) return;
|
|
778
|
+
state.runQueued = true;
|
|
665
779
|
requestAnimationFrame(async () => {
|
|
666
|
-
|
|
667
|
-
if (
|
|
780
|
+
state.runQueued = false;
|
|
781
|
+
if (state.pageKey && pageKey() !== state.pageKey) return;
|
|
668
782
|
let n = 0;
|
|
669
783
|
try { n = await runCore(); } catch (_) {}
|
|
670
784
|
try { cb?.(n); } catch (_) {}
|
|
@@ -674,20 +788,21 @@
|
|
|
674
788
|
function requestBurst() {
|
|
675
789
|
if (isBlocked()) return;
|
|
676
790
|
const t = now();
|
|
677
|
-
if (t -
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
791
|
+
if (t - state.lastBurstTs < TIMING.BURST_COOLDOWN_MS) return;
|
|
792
|
+
state.lastBurstTs = t;
|
|
793
|
+
state.pageKey = pageKey();
|
|
794
|
+
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
795
|
+
if (state.burstActive) return;
|
|
796
|
+
state.burstActive = true;
|
|
797
|
+
state.burstCount = 0;
|
|
684
798
|
const step = () => {
|
|
685
|
-
if (pageKey() !==
|
|
686
|
-
|
|
799
|
+
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
800
|
+
state.burstActive = false;
|
|
801
|
+
return;
|
|
687
802
|
}
|
|
688
|
-
|
|
803
|
+
state.burstCount++;
|
|
689
804
|
scheduleRun(n => {
|
|
690
|
-
if (!n && !
|
|
805
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
691
806
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
692
807
|
});
|
|
693
808
|
};
|
|
@@ -697,32 +812,37 @@
|
|
|
697
812
|
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
698
813
|
|
|
699
814
|
function cleanup() {
|
|
700
|
-
|
|
815
|
+
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
816
|
+
|
|
701
817
|
mutate(() => {
|
|
702
818
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
|
|
703
819
|
});
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
820
|
+
state.cfg = null;
|
|
821
|
+
state.poolsReady = false;
|
|
822
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
823
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
824
|
+
state.mountedIds.clear();
|
|
825
|
+
state.lastShow.clear();
|
|
826
|
+
state.wrapByKey.clear();
|
|
827
|
+
state.wrapsByClass.clear();
|
|
828
|
+
state.kind = null;
|
|
829
|
+
state.phState.clear();
|
|
830
|
+
state.inflight = 0;
|
|
831
|
+
state.pending = [];
|
|
832
|
+
state.pendingSet.clear();
|
|
833
|
+
state.burstActive = false;
|
|
834
|
+
state.runQueued = false;
|
|
835
|
+
state.firstShown = false;
|
|
718
836
|
}
|
|
719
837
|
|
|
720
838
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
721
839
|
|
|
722
840
|
function ensureDomObserver() {
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
841
|
+
if (state.domObs) return;
|
|
842
|
+
|
|
843
|
+
state.domObs = new MutationObserver(muts => {
|
|
844
|
+
if (state.mutGuard > 0 || isBlocked()) return;
|
|
845
|
+
|
|
726
846
|
let needsBurst = false;
|
|
727
847
|
const kind = getKind();
|
|
728
848
|
const relevantSels =
|
|
@@ -730,8 +850,10 @@
|
|
|
730
850
|
kind === 'categoryTopics' ? [SEL.topic] :
|
|
731
851
|
kind === 'categories' ? [SEL.category] :
|
|
732
852
|
[SEL.post, SEL.topic, SEL.category];
|
|
853
|
+
|
|
733
854
|
for (const m of muts) {
|
|
734
855
|
if (m.type !== 'childList') continue;
|
|
856
|
+
|
|
735
857
|
// Free IDs from wraps removed by NodeBB virtualization
|
|
736
858
|
for (const node of m.removedNodes) {
|
|
737
859
|
if (!(node instanceof Element)) continue;
|
|
@@ -740,13 +862,15 @@
|
|
|
740
862
|
dropWrap(node);
|
|
741
863
|
} else {
|
|
742
864
|
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
743
|
-
if (wraps?.length) for (const w of wraps) dropWrap(w);
|
|
865
|
+
if (wraps?.length) { for (const w of wraps) dropWrap(w); }
|
|
744
866
|
}
|
|
745
867
|
} catch (_) {}
|
|
746
868
|
}
|
|
869
|
+
|
|
747
870
|
for (const node of m.addedNodes) {
|
|
748
871
|
if (!(node instanceof Element)) continue;
|
|
749
|
-
|
|
872
|
+
|
|
873
|
+
// Ad fill detection
|
|
750
874
|
try {
|
|
751
875
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
752
876
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -754,19 +878,20 @@
|
|
|
754
878
|
if (wrap) clearEmptyIfFilled(wrap);
|
|
755
879
|
}
|
|
756
880
|
} catch (_) {}
|
|
881
|
+
|
|
757
882
|
// Re-observe wraps re-inserted by NodeBB virtualization
|
|
758
883
|
try {
|
|
759
|
-
const
|
|
884
|
+
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
760
885
|
? [node]
|
|
761
886
|
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
762
|
-
for (const wrap of
|
|
887
|
+
for (const wrap of wraps) {
|
|
763
888
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
764
|
-
if (id
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
889
|
+
if (!id || id <= 0) continue;
|
|
890
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
891
|
+
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
768
892
|
}
|
|
769
893
|
} catch (_) {}
|
|
894
|
+
|
|
770
895
|
// New content detection
|
|
771
896
|
if (!needsBurst) {
|
|
772
897
|
for (const sel of relevantSels) {
|
|
@@ -778,23 +903,29 @@
|
|
|
778
903
|
}
|
|
779
904
|
if (needsBurst) break;
|
|
780
905
|
}
|
|
906
|
+
|
|
781
907
|
if (needsBurst) requestBurst();
|
|
782
908
|
});
|
|
783
|
-
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
912
|
+
} catch (_) {}
|
|
784
913
|
}
|
|
785
914
|
|
|
786
|
-
// ── TCF / CMP Protection
|
|
915
|
+
// ── TCF / CMP Protection (3 layers) ────────────────────────────────────────
|
|
787
916
|
|
|
788
917
|
function ensureTcfLocator() {
|
|
789
918
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
919
|
+
|
|
790
920
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
921
|
+
|
|
791
922
|
const ensureInHead = () => {
|
|
792
923
|
let existing = document.getElementById(LOCATOR_ID);
|
|
793
924
|
if (existing) {
|
|
794
925
|
if (existing.parentElement !== document.head) {
|
|
795
926
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
796
927
|
}
|
|
797
|
-
return;
|
|
928
|
+
return existing;
|
|
798
929
|
}
|
|
799
930
|
const f = document.createElement('iframe');
|
|
800
931
|
f.style.display = 'none';
|
|
@@ -802,7 +933,9 @@
|
|
|
802
933
|
try { document.head.appendChild(f); } catch (_) {
|
|
803
934
|
(document.body || document.documentElement).appendChild(f);
|
|
804
935
|
}
|
|
936
|
+
return f;
|
|
805
937
|
};
|
|
938
|
+
|
|
806
939
|
ensureInHead();
|
|
807
940
|
|
|
808
941
|
if (!window.__nbbCmpGuarded) {
|
|
@@ -844,8 +977,12 @@
|
|
|
844
977
|
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
845
978
|
childList: true, subtree: false,
|
|
846
979
|
});
|
|
980
|
+
} catch (_) {}
|
|
981
|
+
try {
|
|
847
982
|
if (document.head) {
|
|
848
|
-
window.__nbbTcfObs.observe(document.head, {
|
|
983
|
+
window.__nbbTcfObs.observe(document.head, {
|
|
984
|
+
childList: true, subtree: false,
|
|
985
|
+
});
|
|
849
986
|
}
|
|
850
987
|
} catch (_) {}
|
|
851
988
|
}
|
|
@@ -866,7 +1003,8 @@
|
|
|
866
1003
|
window.__nbbAriaObs = new MutationObserver(remove);
|
|
867
1004
|
try {
|
|
868
1005
|
window.__nbbAriaObs.observe(document.body, {
|
|
869
|
-
attributes: true,
|
|
1006
|
+
attributes: true,
|
|
1007
|
+
attributeFilter: ['aria-hidden'],
|
|
870
1008
|
});
|
|
871
1009
|
} catch (_) {}
|
|
872
1010
|
}
|
|
@@ -876,7 +1014,8 @@
|
|
|
876
1014
|
function muteConsole() {
|
|
877
1015
|
if (window.__nbbEzMuted) return;
|
|
878
1016
|
window.__nbbEzMuted = true;
|
|
879
|
-
|
|
1017
|
+
|
|
1018
|
+
const PREFIXES = [
|
|
880
1019
|
'[EzoicAds JS]: Placeholder Id',
|
|
881
1020
|
'No valid placeholders for loadMore',
|
|
882
1021
|
'cannot call refresh on the same page',
|
|
@@ -885,22 +1024,22 @@
|
|
|
885
1024
|
'[CMP] Error in custom getTCData',
|
|
886
1025
|
'vignette: no interstitial API',
|
|
887
1026
|
'Ezoic JS-Enable should only ever',
|
|
888
|
-
|
|
889
|
-
|
|
1027
|
+
];
|
|
1028
|
+
const PATTERNS = [
|
|
890
1029
|
`with id ${PH_PREFIX}`,
|
|
891
1030
|
'adsbygoogle.push() error: All',
|
|
892
1031
|
'has already been defined',
|
|
893
1032
|
'bad response. Status',
|
|
894
|
-
'slotDestroyed event',
|
|
895
|
-
'identity bridging',
|
|
896
1033
|
];
|
|
1034
|
+
|
|
897
1035
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
898
1036
|
const orig = console[method];
|
|
899
1037
|
if (typeof orig !== 'function') continue;
|
|
900
1038
|
console[method] = function (...args) {
|
|
901
1039
|
if (typeof args[0] === 'string') {
|
|
902
1040
|
const msg = args[0];
|
|
903
|
-
for (const p of
|
|
1041
|
+
for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
|
|
1042
|
+
for (const p of PATTERNS) { if (msg.includes(p)) return; }
|
|
904
1043
|
}
|
|
905
1044
|
return orig.apply(console, args);
|
|
906
1045
|
};
|
|
@@ -909,25 +1048,28 @@
|
|
|
909
1048
|
|
|
910
1049
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
911
1050
|
|
|
912
|
-
|
|
1051
|
+
let _networkWarmed = false;
|
|
1052
|
+
|
|
913
1053
|
function warmNetwork() {
|
|
1054
|
+
if (_networkWarmed) return;
|
|
1055
|
+
_networkWarmed = true;
|
|
914
1056
|
const head = document.head;
|
|
915
1057
|
if (!head) return;
|
|
916
|
-
|
|
1058
|
+
const hints = [
|
|
917
1059
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
918
1060
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
919
1061
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
920
1062
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
921
1063
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
922
1064
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
923
|
-
]
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (cors)
|
|
930
|
-
head.appendChild(
|
|
1065
|
+
];
|
|
1066
|
+
for (const [rel, href, cors] of hints) {
|
|
1067
|
+
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
1068
|
+
const link = document.createElement('link');
|
|
1069
|
+
link.rel = rel;
|
|
1070
|
+
link.href = href;
|
|
1071
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
1072
|
+
head.appendChild(link);
|
|
931
1073
|
}
|
|
932
1074
|
}
|
|
933
1075
|
|
|
@@ -936,30 +1078,47 @@
|
|
|
936
1078
|
function bindNodeBB() {
|
|
937
1079
|
const $ = window.jQuery;
|
|
938
1080
|
if (!$) return;
|
|
1081
|
+
|
|
939
1082
|
$(window).off('.nbbEzoic');
|
|
1083
|
+
|
|
1084
|
+
// Cleanup on every navigation, same as v50
|
|
940
1085
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1086
|
+
|
|
941
1087
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1088
|
+
state.pageKey = pageKey();
|
|
1089
|
+
state.kind = null;
|
|
1090
|
+
state.blockedUntil = 0;
|
|
1091
|
+
|
|
1092
|
+
muteConsole();
|
|
1093
|
+
ensureTcfLocator();
|
|
1094
|
+
protectAriaHidden();
|
|
1095
|
+
warmNetwork();
|
|
1096
|
+
patchShowAds();
|
|
1097
|
+
getIO();
|
|
1098
|
+
ensureDomObserver();
|
|
947
1099
|
requestBurst();
|
|
948
1100
|
});
|
|
949
1101
|
|
|
950
1102
|
const burstEvents = [
|
|
951
|
-
'action:ajaxify.contentLoaded',
|
|
952
|
-
'action:
|
|
953
|
-
'action:
|
|
1103
|
+
'action:ajaxify.contentLoaded',
|
|
1104
|
+
'action:posts.loaded',
|
|
1105
|
+
'action:topics.loaded',
|
|
1106
|
+
'action:categories.loaded',
|
|
1107
|
+
'action:category.loaded',
|
|
1108
|
+
'action:topic.loaded',
|
|
954
1109
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1110
|
+
|
|
955
1111
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
956
1112
|
|
|
957
1113
|
try {
|
|
958
1114
|
require(['hooks'], hooks => {
|
|
959
1115
|
if (typeof hooks?.on !== 'function') return;
|
|
960
1116
|
for (const ev of [
|
|
961
|
-
'action:ajaxify.end',
|
|
962
|
-
'action:
|
|
1117
|
+
'action:ajaxify.end',
|
|
1118
|
+
'action:posts.loaded',
|
|
1119
|
+
'action:topics.loaded',
|
|
1120
|
+
'action:categories.loaded',
|
|
1121
|
+
'action:topic.loaded',
|
|
963
1122
|
]) {
|
|
964
1123
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
965
1124
|
}
|
|
@@ -972,13 +1131,16 @@
|
|
|
972
1131
|
window.addEventListener('scroll', () => {
|
|
973
1132
|
if (ticking) return;
|
|
974
1133
|
ticking = true;
|
|
975
|
-
requestAnimationFrame(() => {
|
|
1134
|
+
requestAnimationFrame(() => {
|
|
1135
|
+
ticking = false;
|
|
1136
|
+
requestBurst();
|
|
1137
|
+
});
|
|
976
1138
|
}, { passive: true });
|
|
977
1139
|
}
|
|
978
1140
|
|
|
979
1141
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
980
1142
|
|
|
981
|
-
|
|
1143
|
+
state.pageKey = pageKey();
|
|
982
1144
|
muteConsole();
|
|
983
1145
|
ensureTcfLocator();
|
|
984
1146
|
protectAriaHidden();
|
|
@@ -988,7 +1150,7 @@
|
|
|
988
1150
|
ensureDomObserver();
|
|
989
1151
|
bindNodeBB();
|
|
990
1152
|
bindScroll();
|
|
991
|
-
|
|
1153
|
+
state.blockedUntil = 0;
|
|
992
1154
|
requestBurst();
|
|
993
1155
|
|
|
994
1156
|
})();
|