nodebb-plugin-ezoic-infinite 1.5.67 → 1.5.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/client.js +437 -460
- package/public/style.css +23 -11
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -5,63 +5,41 @@
|
|
|
5
5
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
6
6
|
|
|
7
7
|
// IMPORTANT: must NOT collide with Ezoic's own markup (they use `.ezoic-ad`).
|
|
8
|
-
// If we reuse that class, cleanup/pruning can delete real ads and cause
|
|
9
|
-
// "placeholder does not exist" spam + broken 1/X insertion.
|
|
10
8
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
11
9
|
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
10
|
+
const POOL_ID = 'nodebb-ezoic-placeholder-pool';
|
|
12
11
|
|
|
13
|
-
//
|
|
12
|
+
// Smoothness caps
|
|
14
13
|
const MAX_INSERTS_PER_RUN = 3;
|
|
15
14
|
|
|
16
|
-
// Preload
|
|
15
|
+
// Preload margins
|
|
17
16
|
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
18
17
|
const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
|
|
19
|
-
|
|
20
|
-
// When the user scrolls very fast, temporarily preload more aggressively.
|
|
21
|
-
// This helps ensure ads are already in-flight before the user reaches them.
|
|
22
18
|
const PRELOAD_MARGIN_DESKTOP_BOOST = '4200px 0px 4200px 0px';
|
|
23
19
|
const PRELOAD_MARGIN_MOBILE_BOOST = '2500px 0px 2500px 0px';
|
|
20
|
+
|
|
24
21
|
const BOOST_DURATION_MS = 2500;
|
|
25
22
|
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
26
23
|
|
|
27
24
|
const MAX_INFLIGHT_DESKTOP = 4;
|
|
28
25
|
const MAX_INFLIGHT_MOBILE = 3;
|
|
29
26
|
|
|
30
|
-
function isBoosted() {
|
|
31
|
-
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isMobile() {
|
|
35
|
-
try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getPreloadRootMargin() {
|
|
39
|
-
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
40
|
-
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getMaxInflight() {
|
|
44
|
-
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
45
|
-
return base + (isBoosted() ? 1 : 0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
27
|
const SELECTORS = {
|
|
49
28
|
topicItem: 'li[component="category/topic"]',
|
|
50
29
|
postItem: '[component="post"][data-pid]',
|
|
51
30
|
categoryItem: 'li[component="categories/category"]',
|
|
52
31
|
};
|
|
53
32
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
33
|
+
// Production build: debug disabled
|
|
34
|
+
function dbg() {}
|
|
35
|
+
|
|
36
|
+
// ---------------- state ----------------
|
|
59
37
|
|
|
60
38
|
const state = {
|
|
61
39
|
pageKey: null,
|
|
62
40
|
cfg: null,
|
|
63
41
|
|
|
64
|
-
//
|
|
42
|
+
// pools (full lists) + cursors
|
|
65
43
|
allTopics: [],
|
|
66
44
|
allPosts: [],
|
|
67
45
|
allCategories: [],
|
|
@@ -69,63 +47,46 @@
|
|
|
69
47
|
curPosts: 0,
|
|
70
48
|
curCategories: 0,
|
|
71
49
|
|
|
72
|
-
//
|
|
50
|
+
// per-id throttle
|
|
73
51
|
lastShowById: new Map(),
|
|
74
|
-
internalDomChange: 0,
|
|
75
|
-
lastRecycleAt: { topic: 0, categoryTopics: 0, categories: 0 },
|
|
76
|
-
|
|
77
|
-
// track placeholders that have been shown at least once in this pageview
|
|
78
|
-
usedOnce: new Set(),
|
|
79
52
|
|
|
80
53
|
// observers / schedulers
|
|
81
54
|
domObs: null,
|
|
82
55
|
io: null,
|
|
83
|
-
|
|
56
|
+
ioMargin: null,
|
|
57
|
+
|
|
58
|
+
// internal mutations guard
|
|
59
|
+
internalDomChange: 0,
|
|
84
60
|
|
|
85
61
|
// preloading budget
|
|
86
62
|
inflight: 0,
|
|
87
63
|
pending: [],
|
|
88
64
|
pendingSet: new Set(),
|
|
89
65
|
|
|
90
|
-
//
|
|
66
|
+
// scroll boost
|
|
91
67
|
scrollBoostUntil: 0,
|
|
92
68
|
lastScrollY: 0,
|
|
93
69
|
lastScrollTs: 0,
|
|
94
|
-
ioMargin: null,
|
|
95
70
|
|
|
96
|
-
// hero
|
|
71
|
+
// hero
|
|
97
72
|
heroDoneForPage: false,
|
|
98
|
-
|
|
73
|
+
|
|
74
|
+
// run scheduler
|
|
75
|
+
runQueued: false,
|
|
76
|
+
burstActive: false,
|
|
77
|
+
burstDeadline: 0,
|
|
78
|
+
burstCount: 0,
|
|
79
|
+
lastBurstReqTs: 0,
|
|
99
80
|
};
|
|
100
81
|
|
|
82
|
+
// Soft block during navigation / heavy DOM churn
|
|
83
|
+
let blockedUntil = 0;
|
|
101
84
|
const insertingIds = new Set();
|
|
102
85
|
|
|
86
|
+
function now() { return Date.now(); }
|
|
87
|
+
function isBlocked() { return now() < blockedUntil; }
|
|
103
88
|
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
107
|
-
if (!ph || !ph.isConnected) return;
|
|
108
|
-
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
109
|
-
if (!wrap) return;
|
|
110
|
-
// If still empty after a delay, collapse it.
|
|
111
|
-
setTimeout(() => {
|
|
112
|
-
try {
|
|
113
|
-
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
114
|
-
if (!ph2 || !ph2.isConnected) return;
|
|
115
|
-
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
116
|
-
if (!w2) return;
|
|
117
|
-
// consider empty if only whitespace and no iframes/ins/img
|
|
118
|
-
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
119
|
-
if (!hasAd) w2.classList.add('is-empty');
|
|
120
|
-
} catch (e) {}
|
|
121
|
-
}, 3500);
|
|
122
|
-
} catch (e) {}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Production build: debug disabled
|
|
126
|
-
function dbg() {}
|
|
127
|
-
|
|
128
|
-
// ---------- small utils ----------
|
|
89
|
+
// ---------------- utils ----------------
|
|
129
90
|
|
|
130
91
|
function normalizeBool(v) {
|
|
131
92
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
@@ -164,19 +125,31 @@
|
|
|
164
125
|
return window.location.pathname;
|
|
165
126
|
}
|
|
166
127
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
171
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
128
|
+
function isMobile() {
|
|
129
|
+
try { return window.innerWidth < 768; } catch (e) { return false; }
|
|
130
|
+
}
|
|
172
131
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
132
|
+
function isBoosted() {
|
|
133
|
+
return now() < (state.scrollBoostUntil || 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getPreloadRootMargin() {
|
|
137
|
+
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
138
|
+
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getMaxInflight() {
|
|
142
|
+
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
143
|
+
return base + (isBoosted() ? 1 : 0);
|
|
178
144
|
}
|
|
179
145
|
|
|
146
|
+
function withInternalDomChange(fn) {
|
|
147
|
+
state.internalDomChange++;
|
|
148
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------- DOM helpers ----------------
|
|
152
|
+
|
|
180
153
|
function getTopicItems() {
|
|
181
154
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
182
155
|
}
|
|
@@ -197,7 +170,59 @@
|
|
|
197
170
|
});
|
|
198
171
|
}
|
|
199
172
|
|
|
200
|
-
|
|
173
|
+
function getKind() {
|
|
174
|
+
const p = window.location.pathname || '';
|
|
175
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
176
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
177
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
178
|
+
|
|
179
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
180
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
181
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
182
|
+
return 'other';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isAdjacentAd(target) {
|
|
186
|
+
if (!target) return false;
|
|
187
|
+
const next = target.nextElementSibling;
|
|
188
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
189
|
+
const prev = target.previousElementSibling;
|
|
190
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findWrap(kindClass, afterPos) {
|
|
195
|
+
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------- placeholder pool ----------------
|
|
199
|
+
|
|
200
|
+
function getPoolEl() {
|
|
201
|
+
let el = document.getElementById(POOL_ID);
|
|
202
|
+
if (el) return el;
|
|
203
|
+
el = document.createElement('div');
|
|
204
|
+
el.id = POOL_ID;
|
|
205
|
+
el.style.cssText = 'position:fixed;left:-99999px;top:-99999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;';
|
|
206
|
+
(document.body || document.documentElement).appendChild(el);
|
|
207
|
+
return el;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isInPool(ph) {
|
|
211
|
+
try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function releaseWrapNode(wrap) {
|
|
215
|
+
try {
|
|
216
|
+
const ph = wrap && wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
217
|
+
if (ph) {
|
|
218
|
+
try { getPoolEl().appendChild(ph); } catch (e) {}
|
|
219
|
+
try { state.io && state.io.unobserve(ph); } catch (e) {}
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------- network warmup ----------------
|
|
201
226
|
|
|
202
227
|
const _warmLinksDone = new Set();
|
|
203
228
|
function warmUpNetwork() {
|
|
@@ -205,10 +230,18 @@
|
|
|
205
230
|
const head = document.head || document.getElementsByTagName('head')[0];
|
|
206
231
|
if (!head) return;
|
|
207
232
|
const links = [
|
|
233
|
+
// Ezoic
|
|
208
234
|
['preconnect', 'https://g.ezoic.net', true],
|
|
209
235
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
210
236
|
['preconnect', 'https://go.ezoic.net', true],
|
|
211
237
|
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
238
|
+
// Google ads
|
|
239
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
240
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
241
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
242
|
+
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
243
|
+
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
244
|
+
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
212
245
|
];
|
|
213
246
|
for (const [rel, href, cors] of links) {
|
|
214
247
|
const key = `${rel}|${href}`;
|
|
@@ -223,7 +256,9 @@
|
|
|
223
256
|
} catch (e) {}
|
|
224
257
|
}
|
|
225
258
|
|
|
226
|
-
//
|
|
259
|
+
// ---------------- Ezoic bridge ----------------
|
|
260
|
+
|
|
261
|
+
// Patch showAds to silently skip ids not in DOM. This prevents console spam.
|
|
227
262
|
function patchShowAds() {
|
|
228
263
|
const applyPatch = () => {
|
|
229
264
|
try {
|
|
@@ -265,28 +300,7 @@
|
|
|
265
300
|
}
|
|
266
301
|
}
|
|
267
302
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
function kindKeyFromClass(kindClass) {
|
|
271
|
-
if (kindClass === 'ezoic-ad-message') return 'topic';
|
|
272
|
-
if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
|
|
273
|
-
if (kindClass === 'ezoic-ad-categories') return 'categories';
|
|
274
|
-
return 'topic';
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function withInternalDomChange(fn) {
|
|
278
|
-
state.internalDomChange++;
|
|
279
|
-
try { fn(); } finally { state.internalDomChange--; }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function canRecycle(kind) {
|
|
283
|
-
const now = Date.now();
|
|
284
|
-
const last = state.lastRecycleAt[kind] || 0;
|
|
285
|
-
if (now - last < RECYCLE_COOLDOWN_MS) return false;
|
|
286
|
-
state.lastRecycleAt[kind] = now;
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
// ---------- config & pools ----------
|
|
303
|
+
// ---------------- config ----------------
|
|
290
304
|
|
|
291
305
|
async function fetchConfigOnce() {
|
|
292
306
|
if (state.cfg) return state.cfg;
|
|
@@ -302,107 +316,30 @@ function withInternalDomChange(fn) {
|
|
|
302
316
|
|
|
303
317
|
function initPools(cfg) {
|
|
304
318
|
if (!cfg) return;
|
|
305
|
-
if (state.allTopics.length
|
|
306
|
-
if (state.allPosts.length
|
|
307
|
-
if (state.allCategories.length
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ---------- insertion primitives ----------
|
|
311
|
-
|
|
312
|
-
function isAdjacentAd(target) {
|
|
313
|
-
if (!target) return false;
|
|
314
|
-
const next = target.nextElementSibling;
|
|
315
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
316
|
-
const prev = target.previousElementSibling;
|
|
317
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
function getWrapIdFromWrap(wrap) {
|
|
323
|
-
try {
|
|
324
|
-
const v = wrap.getAttribute('data-ezoic-wrapid');
|
|
325
|
-
if (v) return String(v);
|
|
326
|
-
const ph = wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
327
|
-
if (ph && ph.id) return ph.id.replace(PLACEHOLDER_PREFIX, '');
|
|
328
|
-
} catch (e) {}
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function safeDestroyById(id) {
|
|
333
|
-
// IMPORTANT:
|
|
334
|
-
// Do NOT call ez.destroyPlaceholders here.
|
|
335
|
-
// In NodeBB ajaxify/infinite-scroll flows, Ezoic can be mid-refresh.
|
|
336
|
-
// Destroy calls can create churn, reduce fill, and generate "does not exist" spam.
|
|
337
|
-
// We only remove our wrapper; Ezoic manages slot lifecycle.
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function pruneOrphanWraps(kindClass, items) {
|
|
342
|
-
if (!items || !items.length) return 0;
|
|
343
|
-
const itemSet = new Set(items);
|
|
344
|
-
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
345
|
-
let removed = 0;
|
|
346
|
-
|
|
347
|
-
wraps.forEach((wrap) => {
|
|
348
|
-
// NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
|
|
349
|
-
let ok = false;
|
|
350
|
-
let prev = wrap.previousElementSibling;
|
|
351
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
352
|
-
if (itemSet.has(prev)) { ok = true; break; }
|
|
353
|
-
prev = prev.previousElementSibling;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (!ok) {
|
|
357
|
-
const id = getWrapIdFromWrap(wrap);
|
|
358
|
-
withInternalDomChange(() => {
|
|
359
|
-
try {
|
|
360
|
-
if (id) safeDestroyById(id);
|
|
361
|
-
wrap.remove();
|
|
362
|
-
} catch (e) {}
|
|
363
|
-
});
|
|
364
|
-
removed++;
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
369
|
-
return removed;
|
|
319
|
+
if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
|
|
320
|
+
if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
321
|
+
if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
370
322
|
}
|
|
371
323
|
|
|
372
|
-
|
|
373
|
-
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
374
|
-
window.setTimeout(() => {
|
|
375
|
-
try {
|
|
376
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
377
|
-
if (!ph || !ph.isConnected) return;
|
|
378
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
379
|
-
if (!wrap) return;
|
|
380
|
-
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
381
|
-
if (hasContent) wrap.classList.remove('is-empty');
|
|
382
|
-
else wrap.classList.add('is-empty');
|
|
383
|
-
} catch (e) {}
|
|
384
|
-
}, 3500);
|
|
385
|
-
}
|
|
324
|
+
// ---------------- insertion primitives ----------------
|
|
386
325
|
|
|
387
|
-
function buildWrap(id, kindClass, afterPos) {
|
|
326
|
+
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
388
327
|
const wrap = document.createElement('div');
|
|
389
328
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
390
329
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
391
330
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
392
331
|
wrap.style.width = '100%';
|
|
393
332
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
333
|
+
if (createPlaceholder) {
|
|
334
|
+
const ph = document.createElement('div');
|
|
335
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
336
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
337
|
+
wrap.appendChild(ph);
|
|
338
|
+
}
|
|
398
339
|
|
|
399
340
|
return wrap;
|
|
400
341
|
}
|
|
401
342
|
|
|
402
|
-
function findWrap(kindClass, afterPos) {
|
|
403
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
343
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
407
344
|
if (!target || !target.insertAdjacentElement) return null;
|
|
408
345
|
if (findWrap(kindClass, afterPos)) return null;
|
|
@@ -412,20 +349,18 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
412
349
|
|
|
413
350
|
insertingIds.add(id);
|
|
414
351
|
try {
|
|
415
|
-
const wrap = buildWrap(id, kindClass, afterPos);
|
|
352
|
+
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
416
353
|
target.insertAdjacentElement('afterend', wrap);
|
|
417
354
|
|
|
418
|
-
// If
|
|
419
|
-
|
|
420
|
-
// replaceChild moves the node atomically (no detach window).
|
|
421
|
-
if (existingPh && existingPh !== wrap.firstElementChild) {
|
|
355
|
+
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
356
|
+
if (existingPh) {
|
|
422
357
|
try {
|
|
423
358
|
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
424
|
-
wrap.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
359
|
+
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
360
|
+
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
361
|
+
} catch (e) {}
|
|
428
362
|
}
|
|
363
|
+
|
|
429
364
|
return wrap;
|
|
430
365
|
} finally {
|
|
431
366
|
insertingIds.delete(id);
|
|
@@ -436,142 +371,69 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
436
371
|
const n = allIds.length;
|
|
437
372
|
if (!n) return null;
|
|
438
373
|
|
|
439
|
-
// Try at most n ids to find one that's not already in the DOM
|
|
440
374
|
for (let tries = 0; tries < n; tries++) {
|
|
441
375
|
const idx = state[cursorKey] % n;
|
|
442
376
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
443
|
-
|
|
444
377
|
const id = allIds[idx];
|
|
378
|
+
|
|
445
379
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
446
|
-
if (ph && ph.isConnected) continue;
|
|
380
|
+
if (ph && ph.isConnected && !isInPool(ph)) continue;
|
|
447
381
|
|
|
448
382
|
return id;
|
|
449
383
|
}
|
|
384
|
+
|
|
450
385
|
return null;
|
|
451
386
|
}
|
|
452
387
|
|
|
388
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
389
|
+
if (!items || !items.length) return 0;
|
|
390
|
+
const itemSet = new Set(items);
|
|
391
|
+
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
392
|
+
let removed = 0;
|
|
453
393
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
let victim = null;
|
|
461
|
-
for (const w of wraps) {
|
|
462
|
-
const r = w.getBoundingClientRect();
|
|
463
|
-
if (r.bottom < -2000) { victim = w; break; }
|
|
394
|
+
wraps.forEach((wrap) => {
|
|
395
|
+
let ok = false;
|
|
396
|
+
let prev = wrap.previousElementSibling;
|
|
397
|
+
for (let i = 0; i < 3 && prev; i++) {
|
|
398
|
+
if (itemSet.has(prev)) { ok = true; break; }
|
|
399
|
+
prev = prev.previousElementSibling;
|
|
464
400
|
}
|
|
465
|
-
// Otherwise remove the earliest one in the document
|
|
466
|
-
if (!victim) victim = wraps[0];
|
|
467
|
-
|
|
468
|
-
// Unobserve placeholder if still observed
|
|
469
|
-
try {
|
|
470
|
-
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
471
|
-
if (ph && state.io) state.io.unobserve(ph);
|
|
472
|
-
} catch (e) {}
|
|
473
|
-
|
|
474
|
-
victim.remove();
|
|
475
|
-
return true;
|
|
476
|
-
} catch (e) {
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
401
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const last = state.lastShowById.get(id) || 0;
|
|
487
|
-
if (now - last < 900) return;
|
|
488
|
-
|
|
489
|
-
const max = getMaxInflight();
|
|
490
|
-
if (state.inflight >= max) {
|
|
491
|
-
if (!state.pendingSet.has(id)) {
|
|
492
|
-
state.pending.push(id);
|
|
493
|
-
state.pendingSet.add(id);
|
|
494
|
-
}
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
startShow(id);
|
|
498
|
-
}
|
|
402
|
+
if (!ok) {
|
|
403
|
+
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
404
|
+
removed++;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
499
407
|
|
|
500
|
-
|
|
501
|
-
if (isBlocked()) return;
|
|
502
|
-
const max = getMaxInflight();
|
|
503
|
-
while (state.inflight < max && state.pending.length) {
|
|
504
|
-
const id = state.pending.shift();
|
|
505
|
-
state.pendingSet.delete(id);
|
|
506
|
-
startShow(id);
|
|
408
|
+
return removed;
|
|
507
409
|
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function startShow(id) {
|
|
511
|
-
if (!id || isBlocked()) return;
|
|
512
410
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
requestAnimationFrame(() => {
|
|
525
|
-
try {
|
|
526
|
-
if (isBlocked()) return;
|
|
527
|
-
|
|
528
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
529
|
-
if (!ph || !ph.isConnected) return;
|
|
530
|
-
|
|
531
|
-
const now2 = Date.now();
|
|
532
|
-
const last2 = state.lastShowById.get(id) || 0;
|
|
533
|
-
if (now2 - last2 < 900) return;
|
|
534
|
-
state.lastShowById.set(id, now2);
|
|
535
|
-
|
|
536
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
537
|
-
const ez = window.ezstandalone;
|
|
538
|
-
|
|
539
|
-
const doShow = () => {
|
|
540
|
-
// Do NOT call destroyPlaceholders here.
|
|
541
|
-
// In ajaxify + infinite scroll flows, Ezoic can be in the middle of a refresh cycle.
|
|
542
|
-
// Calling destroy on active placeholders is a common source of:
|
|
543
|
-
// - "HTML element ... does not exist"
|
|
544
|
-
// - "Placeholder Id ... already been defined"
|
|
545
|
-
// Prefer a straight showAds; Ezoic will refresh as needed.
|
|
546
|
-
try { ez.showAds(id); } catch (e) {}
|
|
547
|
-
try { markEmptyWrapper(id); } catch (e) {}
|
|
548
|
-
|
|
549
|
-
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
if (Array.isArray(ez.cmd)) {
|
|
553
|
-
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
554
|
-
} else {
|
|
555
|
-
doShow();
|
|
411
|
+
function decluster(kindClass) {
|
|
412
|
+
// Remove consecutive wraps (keep the first)
|
|
413
|
+
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
414
|
+
if (wraps.length < 2) return 0;
|
|
415
|
+
let removed = 0;
|
|
416
|
+
for (const w of wraps) {
|
|
417
|
+
const prev = w.previousElementSibling;
|
|
418
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
419
|
+
withInternalDomChange(() => releaseWrapNode(w));
|
|
420
|
+
removed++;
|
|
556
421
|
}
|
|
557
|
-
} finally {
|
|
558
|
-
// If we returned early, hardTimer will release.
|
|
559
422
|
}
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
423
|
+
return removed;
|
|
424
|
+
}
|
|
563
425
|
|
|
564
|
-
//
|
|
426
|
+
// ---------------- show (preload / fast fill) ----------------
|
|
565
427
|
|
|
566
428
|
function ensurePreloadObserver() {
|
|
567
429
|
const desiredMargin = getPreloadRootMargin();
|
|
568
430
|
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
569
431
|
|
|
570
|
-
// Rebuild IO if margin changed (e.g., scroll boost toggled)
|
|
571
432
|
if (state.io) {
|
|
572
433
|
try { state.io.disconnect(); } catch (e) {}
|
|
573
434
|
state.io = null;
|
|
574
435
|
}
|
|
436
|
+
|
|
575
437
|
try {
|
|
576
438
|
state.io = new IntersectionObserver((entries) => {
|
|
577
439
|
for (const ent of entries) {
|
|
@@ -584,29 +446,33 @@ function startShow(id) {
|
|
|
584
446
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
585
447
|
}
|
|
586
448
|
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
449
|
+
|
|
587
450
|
state.ioMargin = desiredMargin;
|
|
588
451
|
} catch (e) {
|
|
589
452
|
state.io = null;
|
|
590
453
|
state.ioMargin = null;
|
|
591
454
|
}
|
|
592
455
|
|
|
593
|
-
//
|
|
456
|
+
// Re-observe current placeholders
|
|
594
457
|
try {
|
|
595
458
|
if (state.io) {
|
|
596
459
|
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
597
460
|
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
598
461
|
}
|
|
599
462
|
} catch (e) {}
|
|
463
|
+
|
|
600
464
|
return state.io;
|
|
601
465
|
}
|
|
602
466
|
|
|
603
467
|
function observePlaceholder(id) {
|
|
604
468
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
605
469
|
if (!ph || !ph.isConnected) return;
|
|
470
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
471
|
+
|
|
606
472
|
const io = ensurePreloadObserver();
|
|
607
473
|
try { io && io.observe(ph); } catch (e) {}
|
|
608
474
|
|
|
609
|
-
// If already
|
|
475
|
+
// If already near viewport, fire immediately.
|
|
610
476
|
try {
|
|
611
477
|
const r = ph.getBoundingClientRect();
|
|
612
478
|
const screens = isBoosted() ? 5.0 : 3.0;
|
|
@@ -615,7 +481,104 @@ function startShow(id) {
|
|
|
615
481
|
} catch (e) {}
|
|
616
482
|
}
|
|
617
483
|
|
|
618
|
-
|
|
484
|
+
function enqueueShow(id) {
|
|
485
|
+
if (!id || isBlocked()) return;
|
|
486
|
+
|
|
487
|
+
// per-id throttle
|
|
488
|
+
const t = now();
|
|
489
|
+
const last = state.lastShowById.get(id) || 0;
|
|
490
|
+
if (t - last < 900) return;
|
|
491
|
+
|
|
492
|
+
const max = getMaxInflight();
|
|
493
|
+
if (state.inflight >= max) {
|
|
494
|
+
if (!state.pendingSet.has(id)) {
|
|
495
|
+
state.pending.push(id);
|
|
496
|
+
state.pendingSet.add(id);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
startShow(id);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function drainQueue() {
|
|
505
|
+
if (isBlocked()) return;
|
|
506
|
+
const max = getMaxInflight();
|
|
507
|
+
while (state.inflight < max && state.pending.length) {
|
|
508
|
+
const id = state.pending.shift();
|
|
509
|
+
state.pendingSet.delete(id);
|
|
510
|
+
startShow(id);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function markEmptyWrapper(id) {
|
|
515
|
+
// If still empty after delay, mark empty for CSS (1px)
|
|
516
|
+
try {
|
|
517
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
518
|
+
if (!ph || !ph.isConnected) return;
|
|
519
|
+
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
520
|
+
if (!wrap) return;
|
|
521
|
+
|
|
522
|
+
setTimeout(() => {
|
|
523
|
+
try {
|
|
524
|
+
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
525
|
+
if (!ph2 || !ph2.isConnected) return;
|
|
526
|
+
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
527
|
+
if (!w2) return;
|
|
528
|
+
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
529
|
+
if (!hasAd) w2.classList.add('is-empty');
|
|
530
|
+
} catch (e) {}
|
|
531
|
+
}, 3500);
|
|
532
|
+
} catch (e) {}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function startShow(id) {
|
|
536
|
+
if (!id || isBlocked()) return;
|
|
537
|
+
|
|
538
|
+
state.inflight++;
|
|
539
|
+
let released = false;
|
|
540
|
+
const release = () => {
|
|
541
|
+
if (released) return;
|
|
542
|
+
released = true;
|
|
543
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
544
|
+
drainQueue();
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const hardTimer = setTimeout(release, 6500);
|
|
548
|
+
|
|
549
|
+
requestAnimationFrame(() => {
|
|
550
|
+
try {
|
|
551
|
+
if (isBlocked()) return;
|
|
552
|
+
|
|
553
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
554
|
+
if (!ph || !ph.isConnected) return;
|
|
555
|
+
|
|
556
|
+
const t = now();
|
|
557
|
+
const last = state.lastShowById.get(id) || 0;
|
|
558
|
+
if (t - last < 900) return;
|
|
559
|
+
state.lastShowById.set(id, t);
|
|
560
|
+
|
|
561
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
562
|
+
const ez = window.ezstandalone;
|
|
563
|
+
|
|
564
|
+
const doShow = () => {
|
|
565
|
+
try { ez.showAds(id); } catch (e) {}
|
|
566
|
+
try { markEmptyWrapper(id); } catch (e) {}
|
|
567
|
+
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
if (Array.isArray(ez.cmd)) {
|
|
571
|
+
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
572
|
+
} else {
|
|
573
|
+
doShow();
|
|
574
|
+
}
|
|
575
|
+
} finally {
|
|
576
|
+
// hardTimer releases on early return
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ---------------- core injection ----------------
|
|
619
582
|
|
|
620
583
|
function computeTargets(count, interval, showFirst) {
|
|
621
584
|
const out = [];
|
|
@@ -624,16 +587,8 @@ function startShow(id) {
|
|
|
624
587
|
for (let i = 1; i <= count; i++) {
|
|
625
588
|
if (i % interval === 0) out.push(i);
|
|
626
589
|
}
|
|
590
|
+
// unique + sorted
|
|
627
591
|
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
628
|
-
// If we inserted the maximum batch, likely there are more targets.
|
|
629
|
-
// Schedule a follow-up pass (bounded via scheduleRun coalescing).
|
|
630
|
-
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
631
|
-
if (insertedThisRun >= maxInserts) {
|
|
632
|
-
scheduleRun(120);
|
|
633
|
-
scheduleRun(420);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
592
|
}
|
|
638
593
|
|
|
639
594
|
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
@@ -651,43 +606,87 @@ function startShow(id) {
|
|
|
651
606
|
if (isAdjacentAd(el)) continue;
|
|
652
607
|
if (findWrap(kindClass, afterPos)) continue;
|
|
653
608
|
|
|
654
|
-
|
|
655
|
-
if (!id)
|
|
656
|
-
|
|
657
|
-
// Guard against tight observer loops.
|
|
658
|
-
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
659
|
-
dbg('recycle-skip-cooldown', kindClass);
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
let recycled = false;
|
|
663
|
-
withInternalDomChange(() => {
|
|
664
|
-
recycled = removeOneOldWrap(kindClass);
|
|
665
|
-
});
|
|
666
|
-
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
667
|
-
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
609
|
+
const id = pickIdFromAll(allIds, cursorKey);
|
|
610
|
+
if (!id) break;
|
|
611
|
+
|
|
670
612
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
671
|
-
if (!wrap)
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
613
|
+
if (!wrap) continue;
|
|
674
614
|
|
|
675
615
|
observePlaceholder(id);
|
|
676
|
-
inserted
|
|
616
|
+
inserted++;
|
|
677
617
|
}
|
|
678
618
|
|
|
679
619
|
return inserted;
|
|
680
620
|
}
|
|
681
621
|
|
|
682
|
-
async function
|
|
683
|
-
if (
|
|
622
|
+
async function runCore() {
|
|
623
|
+
if (isBlocked()) return 0;
|
|
624
|
+
|
|
625
|
+
patchShowAds();
|
|
626
|
+
|
|
684
627
|
const cfg = await fetchConfigOnce();
|
|
685
|
-
if (!cfg
|
|
686
|
-
|
|
628
|
+
if (!cfg || cfg.excluded) return 0;
|
|
629
|
+
initPools(cfg);
|
|
630
|
+
|
|
631
|
+
const kind = getKind();
|
|
632
|
+
let inserted = 0;
|
|
687
633
|
|
|
634
|
+
if (kind === 'topic') {
|
|
635
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
636
|
+
const items = getPostContainers();
|
|
637
|
+
pruneOrphanWraps('ezoic-ad-message', items);
|
|
638
|
+
inserted += injectBetween(
|
|
639
|
+
'ezoic-ad-message',
|
|
640
|
+
items,
|
|
641
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
642
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
643
|
+
state.allPosts,
|
|
644
|
+
'curPosts'
|
|
645
|
+
);
|
|
646
|
+
decluster('ezoic-ad-message');
|
|
647
|
+
}
|
|
648
|
+
} else if (kind === 'categoryTopics') {
|
|
649
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
650
|
+
const items = getTopicItems();
|
|
651
|
+
pruneOrphanWraps('ezoic-ad-between', items);
|
|
652
|
+
inserted += injectBetween(
|
|
653
|
+
'ezoic-ad-between',
|
|
654
|
+
items,
|
|
655
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
656
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
657
|
+
state.allTopics,
|
|
658
|
+
'curTopics'
|
|
659
|
+
);
|
|
660
|
+
decluster('ezoic-ad-between');
|
|
661
|
+
}
|
|
662
|
+
} else if (kind === 'categories') {
|
|
663
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
664
|
+
const items = getCategoryItems();
|
|
665
|
+
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
666
|
+
inserted += injectBetween(
|
|
667
|
+
'ezoic-ad-categories',
|
|
668
|
+
items,
|
|
669
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
670
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
671
|
+
state.allCategories,
|
|
672
|
+
'curCategories'
|
|
673
|
+
);
|
|
674
|
+
decluster('ezoic-ad-categories');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return inserted;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function insertHeroAdEarly() {
|
|
682
|
+
if (state.heroDoneForPage || isBlocked()) return;
|
|
683
|
+
|
|
684
|
+
const cfg = await fetchConfigOnce();
|
|
685
|
+
if (!cfg || cfg.excluded) return;
|
|
688
686
|
initPools(cfg);
|
|
689
687
|
|
|
690
688
|
const kind = getKind();
|
|
689
|
+
|
|
691
690
|
let items = [];
|
|
692
691
|
let allIds = [];
|
|
693
692
|
let cursorKey = '';
|
|
@@ -719,9 +718,8 @@ function startShow(id) {
|
|
|
719
718
|
if (!items.length) return;
|
|
720
719
|
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
721
720
|
|
|
722
|
-
// Insert after the very first item (above-the-fold)
|
|
723
721
|
const afterPos = 1;
|
|
724
|
-
const el = items[
|
|
722
|
+
const el = items[0];
|
|
725
723
|
if (!el || !el.isConnected) return;
|
|
726
724
|
if (isAdjacentAd(el)) return;
|
|
727
725
|
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
@@ -730,119 +728,76 @@ function startShow(id) {
|
|
|
730
728
|
if (!id) return;
|
|
731
729
|
|
|
732
730
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
733
|
-
if (!wrap)
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
731
|
+
if (!wrap) return;
|
|
736
732
|
|
|
737
733
|
state.heroDoneForPage = true;
|
|
738
734
|
observePlaceholder(id);
|
|
739
735
|
}
|
|
740
736
|
|
|
741
|
-
|
|
742
|
-
if (isBlocked()) { dbg('blocked'); return; }
|
|
743
|
-
let insertedThisRun = 0;
|
|
744
|
-
|
|
745
|
-
patchShowAds();
|
|
746
|
-
|
|
747
|
-
const cfg = await fetchConfigOnce();
|
|
748
|
-
if (!cfg) { dbg('no-config'); return; }
|
|
749
|
-
if (cfg.excluded) { dbg('excluded'); return; }
|
|
750
|
-
initPools(cfg);
|
|
751
|
-
|
|
752
|
-
const kind = getKind();
|
|
753
|
-
|
|
754
|
-
if (kind === 'topic') {
|
|
755
|
-
if (normalizeBool(cfg.enableMessageAds)) {
|
|
756
|
-
const __items = getPostContainers();
|
|
757
|
-
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
758
|
-
insertedThisRun += injectBetween(
|
|
759
|
-
'ezoic-ad-message',
|
|
760
|
-
__items,
|
|
761
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
762
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
763
|
-
state.allPosts,
|
|
764
|
-
'curPosts'
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
} else if (kind === 'categoryTopics') {
|
|
768
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
769
|
-
const __items = getTopicItems();
|
|
770
|
-
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
771
|
-
insertedThisRun += injectBetween(
|
|
772
|
-
'ezoic-ad-between',
|
|
773
|
-
__items,
|
|
774
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
775
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
776
|
-
state.allTopics,
|
|
777
|
-
'curTopics'
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
} else if (kind === 'categories') {
|
|
781
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
782
|
-
const __items = getCategoryItems();
|
|
783
|
-
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
784
|
-
insertedThisRun += injectBetween(
|
|
785
|
-
'ezoic-ad-categories',
|
|
786
|
-
__items,
|
|
787
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
788
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
789
|
-
state.allCategories,
|
|
790
|
-
'curCategories'
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
737
|
+
// ---------------- scheduler ----------------
|
|
795
738
|
|
|
796
|
-
function scheduleRun(delayMs = 0) {
|
|
797
|
-
// schedule a single run (coalesced)
|
|
739
|
+
function scheduleRun(delayMs = 0, cb) {
|
|
798
740
|
if (state.runQueued) return;
|
|
799
741
|
state.runQueued = true;
|
|
800
|
-
|
|
742
|
+
|
|
743
|
+
const run = async () => {
|
|
801
744
|
state.runQueued = false;
|
|
802
745
|
const pk = getPageKey();
|
|
803
746
|
if (state.pageKey && pk !== state.pageKey) return;
|
|
804
|
-
|
|
747
|
+
let inserted = 0;
|
|
748
|
+
try { inserted = await runCore(); } catch (e) { inserted = 0; }
|
|
749
|
+
try { cb && cb(inserted); } catch (e) {}
|
|
805
750
|
};
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
751
|
+
|
|
752
|
+
const doRun = () => requestAnimationFrame(run);
|
|
753
|
+
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
754
|
+
else doRun();
|
|
811
755
|
}
|
|
812
756
|
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
757
|
+
function requestBurst() {
|
|
758
|
+
if (isBlocked()) return;
|
|
759
|
+
|
|
760
|
+
const t = now();
|
|
761
|
+
if (t - state.lastBurstReqTs < 120) return;
|
|
762
|
+
state.lastBurstReqTs = t;
|
|
763
|
+
|
|
816
764
|
const pk = getPageKey();
|
|
817
765
|
state.pageKey = pk;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
766
|
+
|
|
767
|
+
state.burstDeadline = t + 1800;
|
|
768
|
+
if (state.burstActive) return;
|
|
769
|
+
|
|
770
|
+
state.burstActive = true;
|
|
771
|
+
state.burstCount = 0;
|
|
772
|
+
|
|
773
|
+
const step = () => {
|
|
774
|
+
if (getPageKey() !== pk) { state.burstActive = false; return; }
|
|
775
|
+
if (isBlocked()) { state.burstActive = false; return; }
|
|
776
|
+
if (now() > state.burstDeadline) { state.burstActive = false; return; }
|
|
777
|
+
if (state.burstCount >= 8) { state.burstActive = false; return; }
|
|
778
|
+
|
|
779
|
+
state.burstCount++;
|
|
780
|
+
scheduleRun(0, (inserted) => {
|
|
781
|
+
// Continue while we are still inserting or we have pending shows.
|
|
782
|
+
const hasWork = inserted > 0 || state.pending.length > 0;
|
|
783
|
+
if (!hasWork) { state.burstActive = false; return; }
|
|
784
|
+
// Short delay keeps UI smooth while catching late DOM waves.
|
|
785
|
+
setTimeout(step, inserted > 0 ? 120 : 220);
|
|
786
|
+
});
|
|
828
787
|
};
|
|
829
|
-
burst();
|
|
830
|
-
}
|
|
831
788
|
|
|
789
|
+
step();
|
|
790
|
+
}
|
|
832
791
|
|
|
833
|
-
//
|
|
792
|
+
// ---------------- lifecycle ----------------
|
|
834
793
|
|
|
835
794
|
function cleanup() {
|
|
836
|
-
blockedUntil =
|
|
795
|
+
blockedUntil = now() + 1200;
|
|
837
796
|
|
|
838
|
-
// remove all wrappers
|
|
839
797
|
try {
|
|
840
|
-
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) =>
|
|
841
|
-
try { el.remove(); } catch (e) {}
|
|
842
|
-
});
|
|
798
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => releaseWrapNode(el));
|
|
843
799
|
} catch (e) {}
|
|
844
800
|
|
|
845
|
-
// reset state
|
|
846
801
|
state.cfg = null;
|
|
847
802
|
state.allTopics = [];
|
|
848
803
|
state.allPosts = [];
|
|
@@ -851,21 +806,48 @@ function startShow(id) {
|
|
|
851
806
|
state.curPosts = 0;
|
|
852
807
|
state.curCategories = 0;
|
|
853
808
|
state.lastShowById.clear();
|
|
809
|
+
|
|
854
810
|
state.inflight = 0;
|
|
855
811
|
state.pending = [];
|
|
856
|
-
|
|
857
|
-
|
|
812
|
+
state.pendingSet.clear();
|
|
813
|
+
|
|
858
814
|
state.heroDoneForPage = false;
|
|
859
815
|
|
|
860
|
-
// keep observers alive
|
|
816
|
+
// keep observers alive
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function shouldReactToMutations(mutations) {
|
|
820
|
+
// Fast filter: only react if relevant nodes were added/removed.
|
|
821
|
+
for (const m of mutations) {
|
|
822
|
+
if (!m.addedNodes || m.addedNodes.length === 0) continue;
|
|
823
|
+
for (const n of m.addedNodes) {
|
|
824
|
+
if (!n || n.nodeType !== 1) continue;
|
|
825
|
+
const el = /** @type {Element} */ (n);
|
|
826
|
+
if (
|
|
827
|
+
el.matches?.(SELECTORS.postItem) ||
|
|
828
|
+
el.matches?.(SELECTORS.topicItem) ||
|
|
829
|
+
el.matches?.(SELECTORS.categoryItem) ||
|
|
830
|
+
el.querySelector?.(SELECTORS.postItem) ||
|
|
831
|
+
el.querySelector?.(SELECTORS.topicItem) ||
|
|
832
|
+
el.querySelector?.(SELECTORS.categoryItem)
|
|
833
|
+
) {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
861
839
|
}
|
|
862
840
|
|
|
863
841
|
function ensureDomObserver() {
|
|
864
842
|
if (state.domObs) return;
|
|
865
|
-
|
|
843
|
+
|
|
844
|
+
state.domObs = new MutationObserver((mutations) => {
|
|
866
845
|
if (state.internalDomChange > 0) return;
|
|
867
|
-
if (
|
|
846
|
+
if (isBlocked()) return;
|
|
847
|
+
if (!shouldReactToMutations(mutations)) return;
|
|
848
|
+
requestBurst();
|
|
868
849
|
});
|
|
850
|
+
|
|
869
851
|
try {
|
|
870
852
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
871
853
|
} catch (e) {}
|
|
@@ -889,56 +871,52 @@ function startShow(id) {
|
|
|
889
871
|
ensurePreloadObserver();
|
|
890
872
|
ensureDomObserver();
|
|
891
873
|
|
|
892
|
-
// Ultra-fast above-the-fold first
|
|
893
874
|
insertHeroAdEarly().catch(() => {});
|
|
894
|
-
|
|
895
|
-
// Then normal insertion
|
|
896
|
-
scheduleBurst();
|
|
875
|
+
requestBurst();
|
|
897
876
|
});
|
|
898
877
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
878
|
+
$(window).on(
|
|
879
|
+
'action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite',
|
|
880
|
+
() => {
|
|
881
|
+
if (isBlocked()) return;
|
|
882
|
+
requestBurst();
|
|
883
|
+
}
|
|
884
|
+
);
|
|
904
885
|
}
|
|
905
886
|
|
|
906
887
|
function bindScroll() {
|
|
907
888
|
let ticking = false;
|
|
908
889
|
window.addEventListener('scroll', () => {
|
|
909
|
-
//
|
|
890
|
+
// Fast-scroll boost
|
|
910
891
|
try {
|
|
911
|
-
const
|
|
892
|
+
const t = now();
|
|
912
893
|
const y = window.scrollY || window.pageYOffset || 0;
|
|
913
894
|
if (state.lastScrollTs) {
|
|
914
|
-
const dt =
|
|
895
|
+
const dt = t - state.lastScrollTs;
|
|
915
896
|
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
916
897
|
if (dt > 0) {
|
|
917
|
-
const speed = dy / dt;
|
|
898
|
+
const speed = dy / dt;
|
|
918
899
|
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
919
900
|
const wasBoosted = isBoosted();
|
|
920
|
-
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0,
|
|
921
|
-
if (!wasBoosted)
|
|
922
|
-
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
923
|
-
ensurePreloadObserver();
|
|
924
|
-
}
|
|
901
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, t + BOOST_DURATION_MS);
|
|
902
|
+
if (!wasBoosted) ensurePreloadObserver();
|
|
925
903
|
}
|
|
926
904
|
}
|
|
927
905
|
}
|
|
928
906
|
state.lastScrollY = y;
|
|
929
|
-
state.lastScrollTs =
|
|
907
|
+
state.lastScrollTs = t;
|
|
930
908
|
} catch (e) {}
|
|
931
909
|
|
|
932
910
|
if (ticking) return;
|
|
933
911
|
ticking = true;
|
|
934
|
-
|
|
912
|
+
requestAnimationFrame(() => {
|
|
935
913
|
ticking = false;
|
|
936
|
-
|
|
914
|
+
requestBurst();
|
|
937
915
|
});
|
|
938
916
|
}, { passive: true });
|
|
939
917
|
}
|
|
940
918
|
|
|
941
|
-
//
|
|
919
|
+
// ---------------- boot ----------------
|
|
942
920
|
|
|
943
921
|
state.pageKey = getPageKey();
|
|
944
922
|
warmUpNetwork();
|
|
@@ -949,8 +927,7 @@ function startShow(id) {
|
|
|
949
927
|
bindNodeBB();
|
|
950
928
|
bindScroll();
|
|
951
929
|
|
|
952
|
-
// First paint: try hero + run
|
|
953
930
|
blockedUntil = 0;
|
|
954
931
|
insertHeroAdEarly().catch(() => {});
|
|
955
|
-
|
|
932
|
+
requestBurst();
|
|
956
933
|
})();
|
package/public/style.css
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
.nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
|
|
14
14
|
margin: 0 !important;
|
|
15
15
|
padding: 0 !important;
|
|
16
|
-
|
|
16
|
+
/* Keep the placeholder measurable (IO) but visually negligible */
|
|
17
|
+
min-height: 1px;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/* If Ezoic wraps inside our wrapper, keep it tight */
|
|
@@ -23,25 +24,36 @@
|
|
|
23
24
|
padding: 0 !important;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/* Remove the classic "gap under iframe" (baseline/inline-block) */
|
|
28
|
+
.nodebb-ezoic-wrap,
|
|
29
|
+
.nodebb-ezoic-wrap * {
|
|
30
|
+
line-height: 0 !important;
|
|
31
|
+
font-size: 0 !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.nodebb-ezoic-wrap iframe,
|
|
35
|
+
.nodebb-ezoic-wrap div[id$="__container__"] iframe {
|
|
36
|
+
display: block !important;
|
|
37
|
+
vertical-align: top !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.nodebb-ezoic-wrap div[id$="__container__"] {
|
|
41
|
+
display: block !important;
|
|
42
|
+
line-height: 0 !important;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
|
|
27
46
|
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
28
47
|
.nodebb-ezoic-wrap.is-empty {
|
|
29
48
|
display: block !important;
|
|
30
49
|
margin: 0 !important;
|
|
31
50
|
padding: 0 !important;
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
/* Don't fully collapse (can prevent fill / triggers "unused"), keep it at 1px */
|
|
52
|
+
height: 1px !important;
|
|
53
|
+
min-height: 1px !important;
|
|
34
54
|
overflow: hidden !important;
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
.nodebb-ezoic-wrap {
|
|
38
|
-
min-height: 0 !important;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
|
|
42
|
-
min-height: 0 !important;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
57
|
/*
|
|
46
58
|
Optional: also neutralize spacing on native Ezoic `.ezoic-ad` blocks.
|
|
47
59
|
(Keeps your previous "CSS very good" behavior.)
|