nodebb-plugin-ezoic-infinite 1.5.68 → 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 +426 -472
- 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,93 +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
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// - Ezoic trying to act on ids that were removed from the DOM ("does not exist")
|
|
106
|
-
// - re-defining placeholders (we re-use the same node)
|
|
107
|
-
const POOL_ID = 'nodebb-ezoic-placeholder-pool';
|
|
108
|
-
function getPoolEl() {
|
|
109
|
-
let el = document.getElementById(POOL_ID);
|
|
110
|
-
if (el) return el;
|
|
111
|
-
el = document.createElement('div');
|
|
112
|
-
el.id = POOL_ID;
|
|
113
|
-
el.style.cssText = 'position:fixed;left:-99999px;top:-99999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;';
|
|
114
|
-
(document.body || document.documentElement).appendChild(el);
|
|
115
|
-
return el;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function isInPool(ph) {
|
|
119
|
-
try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function releaseWrapNode(wrap) {
|
|
123
|
-
try {
|
|
124
|
-
const ph = wrap && wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
125
|
-
if (ph) {
|
|
126
|
-
try { getPoolEl().appendChild(ph); } catch (e) {}
|
|
127
|
-
try { if (state.io) state.io.unobserve(ph); } catch (e) {}
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {}
|
|
130
|
-
try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
function markEmptyWrapper(id) {
|
|
135
|
-
try {
|
|
136
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
137
|
-
if (!ph || !ph.isConnected) return;
|
|
138
|
-
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
139
|
-
if (!wrap) return;
|
|
140
|
-
// If still empty after a delay, collapse it.
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
try {
|
|
143
|
-
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
144
|
-
if (!ph2 || !ph2.isConnected) return;
|
|
145
|
-
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
146
|
-
if (!w2) return;
|
|
147
|
-
// consider empty if only whitespace and no iframes/ins/img
|
|
148
|
-
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
149
|
-
if (!hasAd) w2.classList.add('is-empty');
|
|
150
|
-
} catch (e) {}
|
|
151
|
-
}, 3500);
|
|
152
|
-
} catch (e) {}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Production build: debug disabled
|
|
156
|
-
function dbg() {}
|
|
86
|
+
function now() { return Date.now(); }
|
|
87
|
+
function isBlocked() { return now() < blockedUntil; }
|
|
157
88
|
|
|
158
|
-
//
|
|
89
|
+
// ---------------- utils ----------------
|
|
159
90
|
|
|
160
91
|
function normalizeBool(v) {
|
|
161
92
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
@@ -194,19 +125,31 @@
|
|
|
194
125
|
return window.location.pathname;
|
|
195
126
|
}
|
|
196
127
|
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
201
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
128
|
+
function isMobile() {
|
|
129
|
+
try { return window.innerWidth < 768; } catch (e) { return false; }
|
|
130
|
+
}
|
|
202
131
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
206
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
207
|
-
return 'other';
|
|
132
|
+
function isBoosted() {
|
|
133
|
+
return now() < (state.scrollBoostUntil || 0);
|
|
208
134
|
}
|
|
209
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);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function withInternalDomChange(fn) {
|
|
147
|
+
state.internalDomChange++;
|
|
148
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------- DOM helpers ----------------
|
|
152
|
+
|
|
210
153
|
function getTopicItems() {
|
|
211
154
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
212
155
|
}
|
|
@@ -227,7 +170,59 @@
|
|
|
227
170
|
});
|
|
228
171
|
}
|
|
229
172
|
|
|
230
|
-
|
|
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 ----------------
|
|
231
226
|
|
|
232
227
|
const _warmLinksDone = new Set();
|
|
233
228
|
function warmUpNetwork() {
|
|
@@ -235,10 +230,18 @@
|
|
|
235
230
|
const head = document.head || document.getElementsByTagName('head')[0];
|
|
236
231
|
if (!head) return;
|
|
237
232
|
const links = [
|
|
233
|
+
// Ezoic
|
|
238
234
|
['preconnect', 'https://g.ezoic.net', true],
|
|
239
235
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
240
236
|
['preconnect', 'https://go.ezoic.net', true],
|
|
241
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],
|
|
242
245
|
];
|
|
243
246
|
for (const [rel, href, cors] of links) {
|
|
244
247
|
const key = `${rel}|${href}`;
|
|
@@ -253,7 +256,9 @@
|
|
|
253
256
|
} catch (e) {}
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
//
|
|
259
|
+
// ---------------- Ezoic bridge ----------------
|
|
260
|
+
|
|
261
|
+
// Patch showAds to silently skip ids not in DOM. This prevents console spam.
|
|
257
262
|
function patchShowAds() {
|
|
258
263
|
const applyPatch = () => {
|
|
259
264
|
try {
|
|
@@ -295,28 +300,7 @@
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
function kindKeyFromClass(kindClass) {
|
|
301
|
-
if (kindClass === 'ezoic-ad-message') return 'topic';
|
|
302
|
-
if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
|
|
303
|
-
if (kindClass === 'ezoic-ad-categories') return 'categories';
|
|
304
|
-
return 'topic';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function withInternalDomChange(fn) {
|
|
308
|
-
state.internalDomChange++;
|
|
309
|
-
try { fn(); } finally { state.internalDomChange--; }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function canRecycle(kind) {
|
|
313
|
-
const now = Date.now();
|
|
314
|
-
const last = state.lastRecycleAt[kind] || 0;
|
|
315
|
-
if (now - last < RECYCLE_COOLDOWN_MS) return false;
|
|
316
|
-
state.lastRecycleAt[kind] = now;
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
// ---------- config & pools ----------
|
|
303
|
+
// ---------------- config ----------------
|
|
320
304
|
|
|
321
305
|
async function fetchConfigOnce() {
|
|
322
306
|
if (state.cfg) return state.cfg;
|
|
@@ -332,89 +316,14 @@ function withInternalDomChange(fn) {
|
|
|
332
316
|
|
|
333
317
|
function initPools(cfg) {
|
|
334
318
|
if (!cfg) return;
|
|
335
|
-
if (state.allTopics.length
|
|
336
|
-
if (state.allPosts.length
|
|
337
|
-
if (state.allCategories.length
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ---------- insertion primitives ----------
|
|
341
|
-
|
|
342
|
-
function isAdjacentAd(target) {
|
|
343
|
-
if (!target) return false;
|
|
344
|
-
const next = target.nextElementSibling;
|
|
345
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
346
|
-
const prev = target.previousElementSibling;
|
|
347
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
function getWrapIdFromWrap(wrap) {
|
|
353
|
-
try {
|
|
354
|
-
const v = wrap.getAttribute('data-ezoic-wrapid');
|
|
355
|
-
if (v) return String(v);
|
|
356
|
-
const ph = wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
357
|
-
if (ph && ph.id) return ph.id.replace(PLACEHOLDER_PREFIX, '');
|
|
358
|
-
} catch (e) {}
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function safeDestroyById(id) {
|
|
363
|
-
// IMPORTANT:
|
|
364
|
-
// Do NOT call ez.destroyPlaceholders here.
|
|
365
|
-
// In NodeBB ajaxify/infinite-scroll flows, Ezoic can be mid-refresh.
|
|
366
|
-
// Destroy calls can create churn, reduce fill, and generate "does not exist" spam.
|
|
367
|
-
// We only remove our wrapper; Ezoic manages slot lifecycle.
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function pruneOrphanWraps(kindClass, items) {
|
|
372
|
-
if (!items || !items.length) return 0;
|
|
373
|
-
const itemSet = new Set(items);
|
|
374
|
-
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
375
|
-
let removed = 0;
|
|
376
|
-
|
|
377
|
-
wraps.forEach((wrap) => {
|
|
378
|
-
// NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
|
|
379
|
-
let ok = false;
|
|
380
|
-
let prev = wrap.previousElementSibling;
|
|
381
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
382
|
-
if (itemSet.has(prev)) { ok = true; break; }
|
|
383
|
-
prev = prev.previousElementSibling;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (!ok) {
|
|
387
|
-
withInternalDomChange(() => {
|
|
388
|
-
try {
|
|
389
|
-
// Do not destroy placeholders; move them back to the pool so
|
|
390
|
-
// Ezoic won't log "does not exist" and we can reuse them later.
|
|
391
|
-
releaseWrapNode(wrap);
|
|
392
|
-
} catch (e) {}
|
|
393
|
-
});
|
|
394
|
-
removed++;
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
399
|
-
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);
|
|
400
322
|
}
|
|
401
323
|
|
|
402
|
-
|
|
403
|
-
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
404
|
-
window.setTimeout(() => {
|
|
405
|
-
try {
|
|
406
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
407
|
-
if (!ph || !ph.isConnected) return;
|
|
408
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
409
|
-
if (!wrap) return;
|
|
410
|
-
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
411
|
-
if (hasContent) wrap.classList.remove('is-empty');
|
|
412
|
-
else wrap.classList.add('is-empty');
|
|
413
|
-
} catch (e) {}
|
|
414
|
-
}, 3500);
|
|
415
|
-
}
|
|
324
|
+
// ---------------- insertion primitives ----------------
|
|
416
325
|
|
|
417
|
-
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
326
|
+
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
418
327
|
const wrap = document.createElement('div');
|
|
419
328
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
420
329
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
@@ -431,10 +340,6 @@ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
|
431
340
|
return wrap;
|
|
432
341
|
}
|
|
433
342
|
|
|
434
|
-
function findWrap(kindClass, afterPos) {
|
|
435
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
343
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
439
344
|
if (!target || !target.insertAdjacentElement) return null;
|
|
440
345
|
if (findWrap(kindClass, afterPos)) return null;
|
|
@@ -444,24 +349,18 @@ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
|
444
349
|
|
|
445
350
|
insertingIds.add(id);
|
|
446
351
|
try {
|
|
447
|
-
// If a placeholder already exists (either in content or in our pool),
|
|
448
|
-
// do NOT create a new DOM node with the same id even temporarily.
|
|
449
352
|
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
450
353
|
target.insertAdjacentElement('afterend', wrap);
|
|
451
354
|
|
|
452
|
-
// If
|
|
453
|
-
// pre-create placeholders), move it into our wrapper instead of aborting.
|
|
454
|
-
// replaceChild moves the node atomically (no detach window).
|
|
355
|
+
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
455
356
|
if (existingPh) {
|
|
456
357
|
try {
|
|
457
358
|
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
458
|
-
// If we didn't create a placeholder, just append.
|
|
459
359
|
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
460
360
|
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
461
|
-
} catch (e) {
|
|
462
|
-
// Keep the new placeholder if replace fails.
|
|
463
|
-
}
|
|
361
|
+
} catch (e) {}
|
|
464
362
|
}
|
|
363
|
+
|
|
465
364
|
return wrap;
|
|
466
365
|
} finally {
|
|
467
366
|
insertingIds.delete(id);
|
|
@@ -472,138 +371,69 @@ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
|
472
371
|
const n = allIds.length;
|
|
473
372
|
if (!n) return null;
|
|
474
373
|
|
|
475
|
-
// Try at most n ids to find one that's not already in the DOM
|
|
476
374
|
for (let tries = 0; tries < n; tries++) {
|
|
477
375
|
const idx = state[cursorKey] % n;
|
|
478
376
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
479
|
-
|
|
480
377
|
const id = allIds[idx];
|
|
378
|
+
|
|
481
379
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
482
|
-
// If placeholder is currently mounted in the content flow, skip.
|
|
483
|
-
// If it's in our hidden pool, it's available for reuse.
|
|
484
380
|
if (ph && ph.isConnected && !isInPool(ph)) continue;
|
|
485
381
|
|
|
486
382
|
return id;
|
|
487
383
|
}
|
|
384
|
+
|
|
488
385
|
return null;
|
|
489
386
|
}
|
|
490
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;
|
|
491
393
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let victim = null;
|
|
499
|
-
for (const w of wraps) {
|
|
500
|
-
const r = w.getBoundingClientRect();
|
|
501
|
-
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;
|
|
502
400
|
}
|
|
503
|
-
// Otherwise remove the earliest one in the document
|
|
504
|
-
if (!victim) victim = wraps[0];
|
|
505
401
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function enqueueShow(id) {
|
|
514
|
-
if (!id || isBlocked()) return;
|
|
515
|
-
|
|
516
|
-
// Basic per-id throttle (prevents rapid re-requests when DOM churns)
|
|
517
|
-
const now = Date.now();
|
|
518
|
-
const last = state.lastShowById.get(id) || 0;
|
|
519
|
-
if (now - last < 900) return;
|
|
520
|
-
|
|
521
|
-
const max = getMaxInflight();
|
|
522
|
-
if (state.inflight >= max) {
|
|
523
|
-
if (!state.pendingSet.has(id)) {
|
|
524
|
-
state.pending.push(id);
|
|
525
|
-
state.pendingSet.add(id);
|
|
526
|
-
}
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
startShow(id);
|
|
530
|
-
}
|
|
402
|
+
if (!ok) {
|
|
403
|
+
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
404
|
+
removed++;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
531
407
|
|
|
532
|
-
|
|
533
|
-
if (isBlocked()) return;
|
|
534
|
-
const max = getMaxInflight();
|
|
535
|
-
while (state.inflight < max && state.pending.length) {
|
|
536
|
-
const id = state.pending.shift();
|
|
537
|
-
state.pendingSet.delete(id);
|
|
538
|
-
startShow(id);
|
|
408
|
+
return removed;
|
|
539
409
|
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function startShow(id) {
|
|
543
|
-
if (!id || isBlocked()) return;
|
|
544
|
-
|
|
545
|
-
state.inflight++;
|
|
546
|
-
let released = false;
|
|
547
|
-
const release = () => {
|
|
548
|
-
if (released) return;
|
|
549
|
-
released = true;
|
|
550
|
-
state.inflight = Math.max(0, state.inflight - 1);
|
|
551
|
-
drainQueue();
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const hardTimer = setTimeout(release, 6500);
|
|
555
|
-
|
|
556
|
-
requestAnimationFrame(() => {
|
|
557
|
-
try {
|
|
558
|
-
if (isBlocked()) return;
|
|
559
|
-
|
|
560
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
561
|
-
if (!ph || !ph.isConnected) return;
|
|
562
410
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// In ajaxify + infinite scroll flows, Ezoic can be in the middle of a refresh cycle.
|
|
574
|
-
// Calling destroy on active placeholders is a common source of:
|
|
575
|
-
// - "HTML element ... does not exist"
|
|
576
|
-
// - "Placeholder Id ... already been defined"
|
|
577
|
-
// Prefer a straight showAds; Ezoic will refresh as needed.
|
|
578
|
-
try { ez.showAds(id); } catch (e) {}
|
|
579
|
-
try { markEmptyWrapper(id); } catch (e) {}
|
|
580
|
-
|
|
581
|
-
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
if (Array.isArray(ez.cmd)) {
|
|
585
|
-
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
586
|
-
} else {
|
|
587
|
-
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++;
|
|
588
421
|
}
|
|
589
|
-
} finally {
|
|
590
|
-
// If we returned early, hardTimer will release.
|
|
591
422
|
}
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
423
|
+
return removed;
|
|
424
|
+
}
|
|
595
425
|
|
|
596
|
-
//
|
|
426
|
+
// ---------------- show (preload / fast fill) ----------------
|
|
597
427
|
|
|
598
428
|
function ensurePreloadObserver() {
|
|
599
429
|
const desiredMargin = getPreloadRootMargin();
|
|
600
430
|
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
601
431
|
|
|
602
|
-
// Rebuild IO if margin changed (e.g., scroll boost toggled)
|
|
603
432
|
if (state.io) {
|
|
604
433
|
try { state.io.disconnect(); } catch (e) {}
|
|
605
434
|
state.io = null;
|
|
606
435
|
}
|
|
436
|
+
|
|
607
437
|
try {
|
|
608
438
|
state.io = new IntersectionObserver((entries) => {
|
|
609
439
|
for (const ent of entries) {
|
|
@@ -616,29 +446,33 @@ function startShow(id) {
|
|
|
616
446
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
617
447
|
}
|
|
618
448
|
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
449
|
+
|
|
619
450
|
state.ioMargin = desiredMargin;
|
|
620
451
|
} catch (e) {
|
|
621
452
|
state.io = null;
|
|
622
453
|
state.ioMargin = null;
|
|
623
454
|
}
|
|
624
455
|
|
|
625
|
-
//
|
|
456
|
+
// Re-observe current placeholders
|
|
626
457
|
try {
|
|
627
458
|
if (state.io) {
|
|
628
459
|
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
629
460
|
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
630
461
|
}
|
|
631
462
|
} catch (e) {}
|
|
463
|
+
|
|
632
464
|
return state.io;
|
|
633
465
|
}
|
|
634
466
|
|
|
635
467
|
function observePlaceholder(id) {
|
|
636
468
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
637
469
|
if (!ph || !ph.isConnected) return;
|
|
470
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
471
|
+
|
|
638
472
|
const io = ensurePreloadObserver();
|
|
639
473
|
try { io && io.observe(ph); } catch (e) {}
|
|
640
474
|
|
|
641
|
-
// If already
|
|
475
|
+
// If already near viewport, fire immediately.
|
|
642
476
|
try {
|
|
643
477
|
const r = ph.getBoundingClientRect();
|
|
644
478
|
const screens = isBoosted() ? 5.0 : 3.0;
|
|
@@ -647,7 +481,104 @@ function startShow(id) {
|
|
|
647
481
|
} catch (e) {}
|
|
648
482
|
}
|
|
649
483
|
|
|
650
|
-
|
|
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 ----------------
|
|
651
582
|
|
|
652
583
|
function computeTargets(count, interval, showFirst) {
|
|
653
584
|
const out = [];
|
|
@@ -656,6 +587,7 @@ function startShow(id) {
|
|
|
656
587
|
for (let i = 1; i <= count; i++) {
|
|
657
588
|
if (i % interval === 0) out.push(i);
|
|
658
589
|
}
|
|
590
|
+
// unique + sorted
|
|
659
591
|
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
660
592
|
}
|
|
661
593
|
|
|
@@ -674,43 +606,87 @@ function startShow(id) {
|
|
|
674
606
|
if (isAdjacentAd(el)) continue;
|
|
675
607
|
if (findWrap(kindClass, afterPos)) continue;
|
|
676
608
|
|
|
677
|
-
|
|
678
|
-
if (!id)
|
|
679
|
-
|
|
680
|
-
// Guard against tight observer loops.
|
|
681
|
-
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
682
|
-
dbg('recycle-skip-cooldown', kindClass);
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
let recycled = false;
|
|
686
|
-
withInternalDomChange(() => {
|
|
687
|
-
recycled = removeOneOldWrap(kindClass);
|
|
688
|
-
});
|
|
689
|
-
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
690
|
-
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
609
|
+
const id = pickIdFromAll(allIds, cursorKey);
|
|
610
|
+
if (!id) break;
|
|
611
|
+
|
|
693
612
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
694
|
-
if (!wrap)
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
613
|
+
if (!wrap) continue;
|
|
697
614
|
|
|
698
615
|
observePlaceholder(id);
|
|
699
|
-
inserted
|
|
616
|
+
inserted++;
|
|
700
617
|
}
|
|
701
618
|
|
|
702
619
|
return inserted;
|
|
703
620
|
}
|
|
704
621
|
|
|
705
|
-
async function
|
|
706
|
-
if (
|
|
622
|
+
async function runCore() {
|
|
623
|
+
if (isBlocked()) return 0;
|
|
624
|
+
|
|
625
|
+
patchShowAds();
|
|
626
|
+
|
|
707
627
|
const cfg = await fetchConfigOnce();
|
|
708
|
-
if (!cfg
|
|
709
|
-
|
|
628
|
+
if (!cfg || cfg.excluded) return 0;
|
|
629
|
+
initPools(cfg);
|
|
630
|
+
|
|
631
|
+
const kind = getKind();
|
|
632
|
+
let inserted = 0;
|
|
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;
|
|
710
683
|
|
|
684
|
+
const cfg = await fetchConfigOnce();
|
|
685
|
+
if (!cfg || cfg.excluded) return;
|
|
711
686
|
initPools(cfg);
|
|
712
687
|
|
|
713
688
|
const kind = getKind();
|
|
689
|
+
|
|
714
690
|
let items = [];
|
|
715
691
|
let allIds = [];
|
|
716
692
|
let cursorKey = '';
|
|
@@ -742,9 +718,8 @@ function startShow(id) {
|
|
|
742
718
|
if (!items.length) return;
|
|
743
719
|
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
744
720
|
|
|
745
|
-
// Insert after the very first item (above-the-fold)
|
|
746
721
|
const afterPos = 1;
|
|
747
|
-
const el = items[
|
|
722
|
+
const el = items[0];
|
|
748
723
|
if (!el || !el.isConnected) return;
|
|
749
724
|
if (isAdjacentAd(el)) return;
|
|
750
725
|
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
@@ -753,119 +728,76 @@ function startShow(id) {
|
|
|
753
728
|
if (!id) return;
|
|
754
729
|
|
|
755
730
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
756
|
-
if (!wrap)
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
731
|
+
if (!wrap) return;
|
|
759
732
|
|
|
760
733
|
state.heroDoneForPage = true;
|
|
761
734
|
observePlaceholder(id);
|
|
762
735
|
}
|
|
763
736
|
|
|
764
|
-
|
|
765
|
-
if (isBlocked()) { dbg('blocked'); return; }
|
|
766
|
-
let insertedThisRun = 0;
|
|
767
|
-
|
|
768
|
-
patchShowAds();
|
|
737
|
+
// ---------------- scheduler ----------------
|
|
769
738
|
|
|
770
|
-
|
|
771
|
-
if (!cfg) { dbg('no-config'); return; }
|
|
772
|
-
if (cfg.excluded) { dbg('excluded'); return; }
|
|
773
|
-
initPools(cfg);
|
|
774
|
-
|
|
775
|
-
const kind = getKind();
|
|
776
|
-
|
|
777
|
-
if (kind === 'topic') {
|
|
778
|
-
if (normalizeBool(cfg.enableMessageAds)) {
|
|
779
|
-
const __items = getPostContainers();
|
|
780
|
-
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
781
|
-
insertedThisRun += injectBetween(
|
|
782
|
-
'ezoic-ad-message',
|
|
783
|
-
__items,
|
|
784
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
785
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
786
|
-
state.allPosts,
|
|
787
|
-
'curPosts'
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
} else if (kind === 'categoryTopics') {
|
|
791
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
792
|
-
const __items = getTopicItems();
|
|
793
|
-
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
794
|
-
insertedThisRun += injectBetween(
|
|
795
|
-
'ezoic-ad-between',
|
|
796
|
-
__items,
|
|
797
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
798
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
799
|
-
state.allTopics,
|
|
800
|
-
'curTopics'
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
} else if (kind === 'categories') {
|
|
804
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
805
|
-
const __items = getCategoryItems();
|
|
806
|
-
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
807
|
-
insertedThisRun += injectBetween(
|
|
808
|
-
'ezoic-ad-categories',
|
|
809
|
-
__items,
|
|
810
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
811
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
812
|
-
state.allCategories,
|
|
813
|
-
'curCategories'
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function scheduleRun(delayMs = 0) {
|
|
820
|
-
// schedule a single run (coalesced)
|
|
739
|
+
function scheduleRun(delayMs = 0, cb) {
|
|
821
740
|
if (state.runQueued) return;
|
|
822
741
|
state.runQueued = true;
|
|
823
|
-
|
|
742
|
+
|
|
743
|
+
const run = async () => {
|
|
824
744
|
state.runQueued = false;
|
|
825
745
|
const pk = getPageKey();
|
|
826
746
|
if (state.pageKey && pk !== state.pageKey) return;
|
|
827
|
-
|
|
747
|
+
let inserted = 0;
|
|
748
|
+
try { inserted = await runCore(); } catch (e) { inserted = 0; }
|
|
749
|
+
try { cb && cb(inserted); } catch (e) {}
|
|
828
750
|
};
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
751
|
+
|
|
752
|
+
const doRun = () => requestAnimationFrame(run);
|
|
753
|
+
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
754
|
+
else doRun();
|
|
834
755
|
}
|
|
835
756
|
|
|
836
|
-
function
|
|
837
|
-
|
|
838
|
-
|
|
757
|
+
function requestBurst() {
|
|
758
|
+
if (isBlocked()) return;
|
|
759
|
+
|
|
760
|
+
const t = now();
|
|
761
|
+
if (t - state.lastBurstReqTs < 120) return;
|
|
762
|
+
state.lastBurstReqTs = t;
|
|
763
|
+
|
|
839
764
|
const pk = getPageKey();
|
|
840
765
|
state.pageKey = pk;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
+
});
|
|
851
787
|
};
|
|
852
|
-
burst();
|
|
853
|
-
}
|
|
854
788
|
|
|
789
|
+
step();
|
|
790
|
+
}
|
|
855
791
|
|
|
856
|
-
//
|
|
792
|
+
// ---------------- lifecycle ----------------
|
|
857
793
|
|
|
858
794
|
function cleanup() {
|
|
859
|
-
blockedUntil =
|
|
795
|
+
blockedUntil = now() + 1200;
|
|
860
796
|
|
|
861
|
-
// remove all wrappers
|
|
862
797
|
try {
|
|
863
|
-
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) =>
|
|
864
|
-
releaseWrapNode(el);
|
|
865
|
-
});
|
|
798
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => releaseWrapNode(el));
|
|
866
799
|
} catch (e) {}
|
|
867
800
|
|
|
868
|
-
// reset state
|
|
869
801
|
state.cfg = null;
|
|
870
802
|
state.allTopics = [];
|
|
871
803
|
state.allPosts = [];
|
|
@@ -874,21 +806,48 @@ function startShow(id) {
|
|
|
874
806
|
state.curPosts = 0;
|
|
875
807
|
state.curCategories = 0;
|
|
876
808
|
state.lastShowById.clear();
|
|
809
|
+
|
|
877
810
|
state.inflight = 0;
|
|
878
811
|
state.pending = [];
|
|
879
|
-
|
|
880
|
-
|
|
812
|
+
state.pendingSet.clear();
|
|
813
|
+
|
|
881
814
|
state.heroDoneForPage = false;
|
|
882
815
|
|
|
883
|
-
// 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;
|
|
884
839
|
}
|
|
885
840
|
|
|
886
841
|
function ensureDomObserver() {
|
|
887
842
|
if (state.domObs) return;
|
|
888
|
-
|
|
843
|
+
|
|
844
|
+
state.domObs = new MutationObserver((mutations) => {
|
|
889
845
|
if (state.internalDomChange > 0) return;
|
|
890
|
-
if (
|
|
846
|
+
if (isBlocked()) return;
|
|
847
|
+
if (!shouldReactToMutations(mutations)) return;
|
|
848
|
+
requestBurst();
|
|
891
849
|
});
|
|
850
|
+
|
|
892
851
|
try {
|
|
893
852
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
894
853
|
} catch (e) {}
|
|
@@ -912,56 +871,52 @@ function startShow(id) {
|
|
|
912
871
|
ensurePreloadObserver();
|
|
913
872
|
ensureDomObserver();
|
|
914
873
|
|
|
915
|
-
// Ultra-fast above-the-fold first
|
|
916
874
|
insertHeroAdEarly().catch(() => {});
|
|
917
|
-
|
|
918
|
-
// Then normal insertion
|
|
919
|
-
scheduleBurst();
|
|
875
|
+
requestBurst();
|
|
920
876
|
});
|
|
921
877
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
);
|
|
927
885
|
}
|
|
928
886
|
|
|
929
887
|
function bindScroll() {
|
|
930
888
|
let ticking = false;
|
|
931
889
|
window.addEventListener('scroll', () => {
|
|
932
|
-
//
|
|
890
|
+
// Fast-scroll boost
|
|
933
891
|
try {
|
|
934
|
-
const
|
|
892
|
+
const t = now();
|
|
935
893
|
const y = window.scrollY || window.pageYOffset || 0;
|
|
936
894
|
if (state.lastScrollTs) {
|
|
937
|
-
const dt =
|
|
895
|
+
const dt = t - state.lastScrollTs;
|
|
938
896
|
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
939
897
|
if (dt > 0) {
|
|
940
|
-
const speed = dy / dt;
|
|
898
|
+
const speed = dy / dt;
|
|
941
899
|
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
942
900
|
const wasBoosted = isBoosted();
|
|
943
|
-
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0,
|
|
944
|
-
if (!wasBoosted)
|
|
945
|
-
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
946
|
-
ensurePreloadObserver();
|
|
947
|
-
}
|
|
901
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, t + BOOST_DURATION_MS);
|
|
902
|
+
if (!wasBoosted) ensurePreloadObserver();
|
|
948
903
|
}
|
|
949
904
|
}
|
|
950
905
|
}
|
|
951
906
|
state.lastScrollY = y;
|
|
952
|
-
state.lastScrollTs =
|
|
907
|
+
state.lastScrollTs = t;
|
|
953
908
|
} catch (e) {}
|
|
954
909
|
|
|
955
910
|
if (ticking) return;
|
|
956
911
|
ticking = true;
|
|
957
|
-
|
|
912
|
+
requestAnimationFrame(() => {
|
|
958
913
|
ticking = false;
|
|
959
|
-
|
|
914
|
+
requestBurst();
|
|
960
915
|
});
|
|
961
916
|
}, { passive: true });
|
|
962
917
|
}
|
|
963
918
|
|
|
964
|
-
//
|
|
919
|
+
// ---------------- boot ----------------
|
|
965
920
|
|
|
966
921
|
state.pageKey = getPageKey();
|
|
967
922
|
warmUpNetwork();
|
|
@@ -972,8 +927,7 @@ function startShow(id) {
|
|
|
972
927
|
bindNodeBB();
|
|
973
928
|
bindScroll();
|
|
974
929
|
|
|
975
|
-
// First paint: try hero + run
|
|
976
930
|
blockedUntil = 0;
|
|
977
931
|
insertHeroAdEarly().catch(() => {});
|
|
978
|
-
|
|
932
|
+
requestBurst();
|
|
979
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.)
|