nodebb-plugin-ezoic-infinite 1.5.47 → 1.5.49
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 +132 -433
- package/public/style.css +29 -23
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -14,66 +14,32 @@
|
|
|
14
14
|
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
15
15
|
const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
|
|
16
16
|
|
|
17
|
+
// When the user scrolls very fast, temporarily preload more aggressively.
|
|
18
|
+
// This helps ensure ads are already in-flight before the user reaches them.
|
|
19
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '4200px 0px 4200px 0px';
|
|
20
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '2500px 0px 2500px 0px';
|
|
21
|
+
const BOOST_DURATION_MS = 2500;
|
|
22
|
+
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
23
|
+
|
|
17
24
|
const MAX_INFLIGHT_DESKTOP = 4;
|
|
18
25
|
const MAX_INFLIGHT_MOBILE = 3;
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
maxInflightDesktop: MAX_INFLIGHT_DESKTOP,
|
|
24
|
-
maxInflightMobile: MAX_INFLIGHT_MOBILE,
|
|
25
|
-
maxInsertsPerRun: MAX_INSERTS_PER_RUN,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function getPerfProfile() {
|
|
29
|
-
// Cache result for this pageview; recomputed on navigation via cleanup()
|
|
30
|
-
if (state.perfProfile) return state.perfProfile;
|
|
31
|
-
const p = {
|
|
32
|
-
maxInflightDesktop: PERF_DEFAULTS.maxInflightDesktop,
|
|
33
|
-
maxInflightMobile: PERF_DEFAULTS.maxInflightMobile,
|
|
34
|
-
maxInsertsPerRun: PERF_DEFAULTS.maxInsertsPerRun,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const mem = typeof navigator !== 'undefined' ? navigator.deviceMemory : undefined; // GB
|
|
39
|
-
const cores = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined;
|
|
40
|
-
|
|
41
|
-
// Conservative tuning for low-end devices
|
|
42
|
-
if (typeof mem === 'number' && mem > 0 && mem <= 2) {
|
|
43
|
-
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
44
|
-
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
45
|
-
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
46
|
-
}
|
|
47
|
-
if (typeof cores === 'number' && cores > 0 && cores <= 4) {
|
|
48
|
-
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
49
|
-
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
50
|
-
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const conn = typeof navigator !== 'undefined' ? navigator.connection : undefined;
|
|
54
|
-
const eff = conn && conn.effectiveType ? String(conn.effectiveType).toLowerCase() : '';
|
|
55
|
-
// Slow connections: don't ramp concurrency too high (keeps page responsive).
|
|
56
|
-
if (eff.includes('2g')) {
|
|
57
|
-
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
58
|
-
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
59
|
-
}
|
|
60
|
-
} catch (e) {}
|
|
61
|
-
|
|
62
|
-
state.perfProfile = p;
|
|
63
|
-
return p;
|
|
64
|
-
}
|
|
27
|
+
function isBoosted() {
|
|
28
|
+
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
29
|
+
}
|
|
65
30
|
|
|
66
31
|
function isMobile() {
|
|
67
32
|
try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
|
|
68
33
|
}
|
|
69
34
|
|
|
70
35
|
function getPreloadRootMargin() {
|
|
71
|
-
|
|
36
|
+
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
37
|
+
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
72
38
|
}
|
|
73
39
|
|
|
74
40
|
function getMaxInflight() {
|
|
75
|
-
const
|
|
76
|
-
return
|
|
41
|
+
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
42
|
+
return base + (isBoosted() ? 1 : 0);
|
|
77
43
|
}
|
|
78
44
|
|
|
79
45
|
const SELECTORS = {
|
|
@@ -82,31 +48,6 @@ function getPerfProfile() {
|
|
|
82
48
|
categoryItem: 'li[component="categories/category"]',
|
|
83
49
|
};
|
|
84
50
|
|
|
85
|
-
const RELEVANT_MATCHERS = [
|
|
86
|
-
SELECTORS.postItem,
|
|
87
|
-
SELECTORS.topicItem,
|
|
88
|
-
SELECTORS.categoryItem,
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
function mutationHasRelevantAddedNodes(mutations) {
|
|
92
|
-
try {
|
|
93
|
-
for (const m of mutations) {
|
|
94
|
-
if (!m || !m.addedNodes || !m.addedNodes.length) continue;
|
|
95
|
-
for (const n of m.addedNodes) {
|
|
96
|
-
if (!n || n.nodeType !== 1) continue;
|
|
97
|
-
const el = /** @type {Element} */ (n);
|
|
98
|
-
for (const sel of RELEVANT_MATCHERS) {
|
|
99
|
-
if (el.matches && el.matches(sel)) return true;
|
|
100
|
-
if (el.querySelector && el.querySelector(sel)) return true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch (e) {}
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
51
|
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
111
52
|
let blockedUntil = 0;
|
|
112
53
|
function isBlocked() {
|
|
@@ -117,7 +58,6 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
117
58
|
pageKey: null,
|
|
118
59
|
cfg: null,
|
|
119
60
|
|
|
120
|
-
navSeq: 0,
|
|
121
61
|
// Full lists (never consumed) + cursors for round-robin reuse
|
|
122
62
|
allTopics: [],
|
|
123
63
|
allPosts: [],
|
|
@@ -136,8 +76,6 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
136
76
|
|
|
137
77
|
// observers / schedulers
|
|
138
78
|
domObs: null,
|
|
139
|
-
tightenObs: null,
|
|
140
|
-
fillObs: null,
|
|
141
79
|
io: null,
|
|
142
80
|
runQueued: false,
|
|
143
81
|
|
|
@@ -145,6 +83,11 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
145
83
|
inflight: 0,
|
|
146
84
|
pending: [],
|
|
147
85
|
pendingSet: new Set(),
|
|
86
|
+
|
|
87
|
+
// fast scroll boosting
|
|
88
|
+
scrollBoostUntil: 0,
|
|
89
|
+
lastScrollY: 0,
|
|
90
|
+
lastScrollTs: 0,
|
|
148
91
|
ioMargin: null,
|
|
149
92
|
|
|
150
93
|
// hero)
|
|
@@ -152,6 +95,29 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
152
95
|
};
|
|
153
96
|
|
|
154
97
|
const insertingIds = new Set();
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function markEmptyWrapper(id) {
|
|
101
|
+
try {
|
|
102
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
103
|
+
if (!ph || !ph.isConnected) return;
|
|
104
|
+
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
105
|
+
if (!wrap) return;
|
|
106
|
+
// If still empty after a delay, collapse it.
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
try {
|
|
109
|
+
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
110
|
+
if (!ph2 || !ph2.isConnected) return;
|
|
111
|
+
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
112
|
+
if (!w2) return;
|
|
113
|
+
// consider empty if only whitespace and no iframes/ins/img
|
|
114
|
+
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
115
|
+
if (!hasAd) w2.classList.add('is-empty');
|
|
116
|
+
} catch (e) {}
|
|
117
|
+
}, 3500);
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
|
|
155
121
|
// Production build: debug disabled
|
|
156
122
|
function dbg() {}
|
|
157
123
|
|
|
@@ -266,7 +232,7 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
266
232
|
const orig = ez.showAds;
|
|
267
233
|
|
|
268
234
|
ez.showAds = function (...args) {
|
|
269
|
-
if (isBlocked())
|
|
235
|
+
if (isBlocked()) return;
|
|
270
236
|
|
|
271
237
|
let ids = [];
|
|
272
238
|
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
@@ -295,124 +261,6 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
295
261
|
}
|
|
296
262
|
}
|
|
297
263
|
|
|
298
|
-
// ---------- Ezoic min-height tightening (lightweight) ----------
|
|
299
|
-
// Some Ezoic placements reserve a large min-height (e.g. 400px) even when
|
|
300
|
-
// the rendered iframe/container is smaller (often 250px). That creates a
|
|
301
|
-
// visible empty gap and can make creatives appear to "slide" inside the slot.
|
|
302
|
-
// We correct ONLY those cases, without scroll listeners or full-page rescans.
|
|
303
|
-
|
|
304
|
-
function getRenderedAdHeight(adSpan) {
|
|
305
|
-
try {
|
|
306
|
-
const c = adSpan.querySelector('div[id$="__container__"]');
|
|
307
|
-
if (c && c.offsetHeight) return c.offsetHeight;
|
|
308
|
-
const f = adSpan.querySelector('iframe');
|
|
309
|
-
if (!f) return 0;
|
|
310
|
-
const attr = parseInt(f.getAttribute('height') || '', 10);
|
|
311
|
-
if (Number.isFinite(attr) && attr > 0) return attr;
|
|
312
|
-
if (f.offsetHeight) return f.offsetHeight;
|
|
313
|
-
} catch (e) {}
|
|
314
|
-
return 0;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function tightenMinHeight(adSpan) {
|
|
318
|
-
try {
|
|
319
|
-
if (!adSpan || adSpan.nodeType !== 1) return;
|
|
320
|
-
if (adSpan.tagName !== 'SPAN') return;
|
|
321
|
-
if (!adSpan.classList || !adSpan.classList.contains('ezoic-ad')) return;
|
|
322
|
-
|
|
323
|
-
// Some Ezoic templates apply sticky/fixed positioning inside the ad slot
|
|
324
|
-
// (e.g. .ezads-sticky-intradiv) which can make the creative appear to
|
|
325
|
-
// "slide" within an oversized container. Neutralize it inside the slot.
|
|
326
|
-
try {
|
|
327
|
-
const sticky = adSpan.querySelectorAll('.ezads-sticky-intradiv');
|
|
328
|
-
sticky.forEach((el) => {
|
|
329
|
-
el.style.setProperty('position', 'static', 'important');
|
|
330
|
-
el.style.setProperty('top', 'auto', 'important');
|
|
331
|
-
el.style.setProperty('bottom', 'auto', 'important');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Safety net: any descendant that ends up sticky/fixed via inline style
|
|
335
|
-
// (rare, but causes "floating" creatives).
|
|
336
|
-
const positioned = adSpan.querySelectorAll('[style*="position: sticky"], [style*="position:sticky"], [style*="position: fixed"], [style*="position:fixed"]');
|
|
337
|
-
positioned.forEach((el) => {
|
|
338
|
-
el.style.setProperty('position', 'static', 'important');
|
|
339
|
-
el.style.setProperty('top', 'auto', 'important');
|
|
340
|
-
el.style.setProperty('bottom', 'auto', 'important');
|
|
341
|
-
});
|
|
342
|
-
} catch (e) {}
|
|
343
|
-
|
|
344
|
-
const mhStr = adSpan.style && adSpan.style.minHeight ? String(adSpan.style.minHeight) : '';
|
|
345
|
-
const mh = mhStr ? parseInt(mhStr, 10) : 0;
|
|
346
|
-
if (!mh || mh < 350) return; // only fix the "400px"-style reservations
|
|
347
|
-
|
|
348
|
-
const h = getRenderedAdHeight(adSpan);
|
|
349
|
-
if (!h || h <= 0) return;
|
|
350
|
-
if (h >= mh) return;
|
|
351
|
-
|
|
352
|
-
adSpan.style.setProperty('min-height', `${h}px`, 'important');
|
|
353
|
-
} catch (e) {}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function closestEzoicAdSpan(node) {
|
|
357
|
-
try {
|
|
358
|
-
if (!node || node.nodeType !== 1) return null;
|
|
359
|
-
const el = /** @type {Element} */ (node);
|
|
360
|
-
if (el.tagName === 'SPAN' && el.classList && el.classList.contains('ezoic-ad')) return el;
|
|
361
|
-
if (el.closest) return el.closest('span.ezoic-ad');
|
|
362
|
-
} catch (e) {}
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function ensureTightenObserver() {
|
|
367
|
-
if (state.tightenObs) return;
|
|
368
|
-
|
|
369
|
-
let raf = 0;
|
|
370
|
-
const pending = new Set();
|
|
371
|
-
const schedule = (adSpan) => {
|
|
372
|
-
if (!adSpan) return;
|
|
373
|
-
pending.add(adSpan);
|
|
374
|
-
if (raf) return;
|
|
375
|
-
raf = requestAnimationFrame(() => {
|
|
376
|
-
raf = 0;
|
|
377
|
-
for (const el of pending) tightenMinHeight(el);
|
|
378
|
-
pending.clear();
|
|
379
|
-
});
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
state.tightenObs = new MutationObserver((mutations) => {
|
|
383
|
-
try {
|
|
384
|
-
for (const m of mutations) {
|
|
385
|
-
if (m.type === 'attributes') {
|
|
386
|
-
const ad = closestEzoicAdSpan(m.target);
|
|
387
|
-
if (ad) schedule(ad);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
391
|
-
for (const n of m.addedNodes) {
|
|
392
|
-
const ad = closestEzoicAdSpan(n);
|
|
393
|
-
if (ad) schedule(ad);
|
|
394
|
-
if (n && n.nodeType === 1 && n.querySelectorAll) {
|
|
395
|
-
n.querySelectorAll('span.ezoic-ad').forEach(schedule);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
} catch (e) {}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
state.tightenObs.observe(document.documentElement, {
|
|
404
|
-
subtree: true,
|
|
405
|
-
childList: true,
|
|
406
|
-
attributes: true,
|
|
407
|
-
attributeFilter: ['style', 'class', 'data-load-complete', 'height'],
|
|
408
|
-
});
|
|
409
|
-
} catch (e) {}
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
document.querySelectorAll('span.ezoic-ad[style*="min-height"]').forEach(tightenMinHeight);
|
|
413
|
-
} catch (e) {}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
264
|
const RECYCLE_COOLDOWN_MS = 1500;
|
|
417
265
|
|
|
418
266
|
function kindKeyFromClass(kindClass) {
|
|
@@ -486,33 +334,7 @@ function withInternalDomChange(fn) {
|
|
|
486
334
|
} catch (e) {}
|
|
487
335
|
}
|
|
488
336
|
|
|
489
|
-
|
|
490
|
-
function findWrapById(id) {
|
|
491
|
-
try {
|
|
492
|
-
return document.querySelector(`.${WRAP_CLASS}[data-ezoic-wrapid="${id}"]`);
|
|
493
|
-
} catch (e) {}
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function armPlaceholder(wrap, id) {
|
|
498
|
-
try {
|
|
499
|
-
if (!wrap || !wrap.isConnected) return null;
|
|
500
|
-
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
501
|
-
|
|
502
|
-
// If the id is already present somewhere, do not reassign it.
|
|
503
|
-
const existing = document.getElementById(domId);
|
|
504
|
-
if (existing && existing.isConnected) return existing;
|
|
505
|
-
|
|
506
|
-
const ph = wrap.querySelector('[data-ezoic-ph="1"]');
|
|
507
|
-
if (!ph) return null;
|
|
508
|
-
|
|
509
|
-
if (!ph.id) ph.id = domId;
|
|
510
|
-
return ph;
|
|
511
|
-
} catch (e) {}
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function pruneOrphanWraps(kindClass, items) {
|
|
337
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
516
338
|
if (!items || !items.length) return 0;
|
|
517
339
|
const itemSet = new Set(items);
|
|
518
340
|
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
@@ -542,120 +364,37 @@ function pruneOrphanWraps(kindClass, items) {
|
|
|
542
364
|
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
543
365
|
return removed;
|
|
544
366
|
}
|
|
367
|
+
|
|
368
|
+
function refreshEmptyState(id) {
|
|
369
|
+
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
370
|
+
window.setTimeout(() => {
|
|
371
|
+
try {
|
|
372
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
373
|
+
if (!ph || !ph.isConnected) return;
|
|
374
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
375
|
+
if (!wrap) return;
|
|
376
|
+
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
377
|
+
if (hasContent) wrap.classList.remove('is-empty');
|
|
378
|
+
else wrap.classList.add('is-empty');
|
|
379
|
+
} catch (e) {}
|
|
380
|
+
}, 3500);
|
|
381
|
+
}
|
|
382
|
+
|
|
545
383
|
function buildWrap(id, kindClass, afterPos) {
|
|
546
384
|
const wrap = document.createElement('div');
|
|
547
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}
|
|
385
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
548
386
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
549
387
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
550
388
|
wrap.style.width = '100%';
|
|
551
389
|
|
|
552
390
|
const ph = document.createElement('div');
|
|
553
|
-
|
|
554
|
-
// This avoids Ezoic defining placeholders too early during DOM churn/infinite scroll.
|
|
391
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
555
392
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
556
|
-
ph.setAttribute('data-ezoic-ph', '1');
|
|
557
393
|
wrap.appendChild(ph);
|
|
558
394
|
|
|
559
395
|
return wrap;
|
|
560
396
|
}
|
|
561
397
|
|
|
562
|
-
// ---------- Fill detection & collapse handling (lightweight) ----------
|
|
563
|
-
// If ad fill is slow, showing a big empty slot is visually jarring. We keep
|
|
564
|
-
// our injected wrapper collapsed (ez-pending) until a creative is present,
|
|
565
|
-
// then mark it ez-ready.
|
|
566
|
-
|
|
567
|
-
function wrapHasFilledCreative(wrap) {
|
|
568
|
-
try {
|
|
569
|
-
if (!wrap || !wrap.isConnected) return false;
|
|
570
|
-
// Safeframe container (most common)
|
|
571
|
-
const c = wrap.querySelector('div[id$="__container__"]');
|
|
572
|
-
if (c && c.offsetHeight > 10) return true;
|
|
573
|
-
// Any iframe with non-trivial height
|
|
574
|
-
const f = wrap.querySelector('iframe');
|
|
575
|
-
if (!f) return false;
|
|
576
|
-
if (f.getAttribute('data-load-complete') === 'true') return true;
|
|
577
|
-
if (f.offsetHeight > 10) return true;
|
|
578
|
-
return false;
|
|
579
|
-
} catch (e) {}
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function markWrapFilledIfNeeded(wrap) {
|
|
584
|
-
try {
|
|
585
|
-
if (!wrap || !wrap.isConnected) return;
|
|
586
|
-
if (!wrap.classList || !wrap.classList.contains(WRAP_CLASS)) return;
|
|
587
|
-
// Only our injected wrappers are DIVs with data-ezoic-wrapid.
|
|
588
|
-
if (wrap.tagName !== 'DIV') return;
|
|
589
|
-
if (!wrap.getAttribute('data-ezoic-wrapid')) return;
|
|
590
|
-
|
|
591
|
-
if (wrapHasFilledCreative(wrap)) {
|
|
592
|
-
wrap.classList.remove('ez-pending');
|
|
593
|
-
wrap.classList.add('ez-ready');
|
|
594
|
-
}
|
|
595
|
-
} catch (e) {}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function ensureFillObserver() {
|
|
599
|
-
if (state.fillObs) return;
|
|
600
|
-
|
|
601
|
-
let raf = 0;
|
|
602
|
-
const pending = new Set();
|
|
603
|
-
const schedule = (wrap) => {
|
|
604
|
-
if (!wrap) return;
|
|
605
|
-
pending.add(wrap);
|
|
606
|
-
if (raf) return;
|
|
607
|
-
raf = requestAnimationFrame(() => {
|
|
608
|
-
raf = 0;
|
|
609
|
-
for (const w of pending) markWrapFilledIfNeeded(w);
|
|
610
|
-
pending.clear();
|
|
611
|
-
});
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const closestWrap = (node) => {
|
|
615
|
-
try {
|
|
616
|
-
if (!node || node.nodeType !== 1) return null;
|
|
617
|
-
const el = /** @type {Element} */ (node);
|
|
618
|
-
if (el.tagName === 'DIV' && el.classList && el.classList.contains(WRAP_CLASS) && el.getAttribute('data-ezoic-wrapid')) return el;
|
|
619
|
-
if (el.closest) return el.closest(`div.${WRAP_CLASS}[data-ezoic-wrapid]`);
|
|
620
|
-
} catch (e) {}
|
|
621
|
-
return null;
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
state.fillObs = new MutationObserver((mutations) => {
|
|
625
|
-
try {
|
|
626
|
-
for (const m of mutations) {
|
|
627
|
-
if (m.type === 'attributes') {
|
|
628
|
-
const w = closestWrap(m.target);
|
|
629
|
-
if (w) schedule(w);
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
633
|
-
for (const n of m.addedNodes) {
|
|
634
|
-
const w = closestWrap(n);
|
|
635
|
-
if (w) schedule(w);
|
|
636
|
-
if (n && n.nodeType === 1 && n.querySelectorAll) {
|
|
637
|
-
n.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(schedule);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
} catch (e) {}
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
try {
|
|
645
|
-
state.fillObs.observe(document.documentElement, {
|
|
646
|
-
subtree: true,
|
|
647
|
-
childList: true,
|
|
648
|
-
attributes: true,
|
|
649
|
-
attributeFilter: ['style', 'class', 'data-load-complete', 'height', 'src'],
|
|
650
|
-
});
|
|
651
|
-
} catch (e) {}
|
|
652
|
-
|
|
653
|
-
// Kick once for already-present wraps
|
|
654
|
-
try {
|
|
655
|
-
document.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(markWrapFilledIfNeeded);
|
|
656
|
-
} catch (e) {}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
398
|
function findWrap(kindClass, afterPos) {
|
|
660
399
|
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
661
400
|
}
|
|
@@ -665,8 +404,8 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
665
404
|
if (findWrap(kindClass, afterPos)) return null;
|
|
666
405
|
if (insertingIds.has(id)) return null;
|
|
667
406
|
|
|
668
|
-
|
|
669
|
-
if (
|
|
407
|
+
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
408
|
+
if (existingPh && existingPh.isConnected) return null;
|
|
670
409
|
|
|
671
410
|
insertingIds.add(id);
|
|
672
411
|
try {
|
|
@@ -688,8 +427,8 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
688
427
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
689
428
|
|
|
690
429
|
const id = allIds[idx];
|
|
691
|
-
|
|
692
|
-
if (
|
|
430
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
431
|
+
if (ph && ph.isConnected) continue;
|
|
693
432
|
|
|
694
433
|
return id;
|
|
695
434
|
}
|
|
@@ -711,20 +450,13 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
711
450
|
// Otherwise remove the earliest one in the document
|
|
712
451
|
if (!victim) victim = wraps[0];
|
|
713
452
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const t = victim;
|
|
720
|
-
if (t && state.io) state.io.unobserve(t);
|
|
721
|
-
} catch (e) {}
|
|
722
|
-
|
|
723
|
-
try { if (id) safeDestroyById(id); } catch (e) {}
|
|
724
|
-
|
|
725
|
-
try { victim.remove(); } catch (e) {}
|
|
726
|
-
});
|
|
453
|
+
// Unobserve placeholder if still observed
|
|
454
|
+
try {
|
|
455
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
456
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
457
|
+
} catch (e) {}
|
|
727
458
|
|
|
459
|
+
victim.remove();
|
|
728
460
|
return true;
|
|
729
461
|
} catch (e) {
|
|
730
462
|
return false;
|
|
@@ -763,7 +495,6 @@ function drainQueue() {
|
|
|
763
495
|
function startShow(id) {
|
|
764
496
|
if (!id || isBlocked()) return;
|
|
765
497
|
|
|
766
|
-
const seq = state.navSeq;
|
|
767
498
|
state.inflight++;
|
|
768
499
|
let released = false;
|
|
769
500
|
const release = () => {
|
|
@@ -779,50 +510,27 @@ function startShow(id) {
|
|
|
779
510
|
try {
|
|
780
511
|
if (isBlocked()) return;
|
|
781
512
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const wrap = findWrapById(id);
|
|
785
|
-
if (wrap && wrap.isConnected) {
|
|
786
|
-
try { armPlaceholder(wrap, id); } catch (e) {}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
790
|
-
const ph = document.getElementById(domId);
|
|
791
|
-
if (!ph || !ph.isConnected) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
|
|
513
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
514
|
+
if (!ph || !ph.isConnected) return;
|
|
792
515
|
|
|
793
516
|
const now2 = Date.now();
|
|
794
517
|
const last2 = state.lastShowById.get(id) || 0;
|
|
795
|
-
if (now2 - last2 < 900)
|
|
518
|
+
if (now2 - last2 < 900) return;
|
|
796
519
|
state.lastShowById.set(id, now2);
|
|
797
520
|
|
|
798
521
|
window.ezstandalone = window.ezstandalone || {};
|
|
799
522
|
const ez = window.ezstandalone;
|
|
800
523
|
|
|
801
524
|
const doShow = () => {
|
|
802
|
-
// Re-check right before showing: the placeholder can disappear between
|
|
803
|
-
// scheduling and execution (ajaxify/infinite scroll DOM churn).
|
|
804
|
-
if (seq !== state.navSeq) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
|
|
805
|
-
const phNow = document.getElementById(domId);
|
|
806
|
-
if (!phNow || !phNow.isConnected) {
|
|
807
|
-
try { clearTimeout(hardTimer); } catch (e) {}
|
|
808
|
-
release();
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
525
|
try {
|
|
813
|
-
if (state.usedOnce && state.usedOnce.has(id)) {
|
|
814
|
-
|
|
526
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
527
|
+
try { ez.destroyPlaceholders(id); } catch (e) {}
|
|
815
528
|
}
|
|
816
529
|
} catch (e) {}
|
|
817
530
|
|
|
818
|
-
|
|
819
|
-
setTimeout(() => {
|
|
820
|
-
if (seq !== state.navSeq) { return; }
|
|
821
|
-
const phFinal = document.getElementById(domId);
|
|
822
|
-
if (!phFinal || !phFinal.isConnected) { return; }
|
|
823
|
-
try { ez.showAds(id); } catch (e) {}
|
|
824
|
-
}, 0);
|
|
531
|
+
try { ez.showAds(id); } catch (e) {}
|
|
825
532
|
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
533
|
+
try { markEmptyWrapper(id); } catch (e) {}
|
|
826
534
|
|
|
827
535
|
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
828
536
|
};
|
|
@@ -857,12 +565,9 @@ function startShow(id) {
|
|
|
857
565
|
const el = ent.target;
|
|
858
566
|
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
859
567
|
|
|
860
|
-
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-
|
|
568
|
+
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
861
569
|
const id = parseInt(idAttr, 10);
|
|
862
|
-
if (Number.isFinite(id) && id > 0)
|
|
863
|
-
armPlaceholder(el, id);
|
|
864
|
-
enqueueShow(id);
|
|
865
|
-
}
|
|
570
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
866
571
|
}
|
|
867
572
|
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
868
573
|
state.ioMargin = desiredMargin;
|
|
@@ -874,7 +579,7 @@ function startShow(id) {
|
|
|
874
579
|
// If we rebuilt the observer, re-observe existing placeholders so we don't miss them.
|
|
875
580
|
try {
|
|
876
581
|
if (state.io) {
|
|
877
|
-
const nodes = document.querySelectorAll(
|
|
582
|
+
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
878
583
|
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
879
584
|
}
|
|
880
585
|
} catch (e) {}
|
|
@@ -882,20 +587,17 @@ function startShow(id) {
|
|
|
882
587
|
}
|
|
883
588
|
|
|
884
589
|
function observePlaceholder(id) {
|
|
885
|
-
const
|
|
886
|
-
if (!
|
|
590
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
591
|
+
if (!ph || !ph.isConnected) return;
|
|
887
592
|
const io = ensurePreloadObserver();
|
|
888
|
-
try { io && io.observe(
|
|
593
|
+
try { io && io.observe(ph); } catch (e) {}
|
|
889
594
|
|
|
890
|
-
// If already
|
|
595
|
+
// If already above fold, fire immediately
|
|
891
596
|
try {
|
|
892
|
-
const r =
|
|
893
|
-
const screens = 3.0;
|
|
894
|
-
const
|
|
895
|
-
if (r.top <
|
|
896
|
-
armPlaceholder(wrap, id);
|
|
897
|
-
enqueueShow(id);
|
|
898
|
-
}
|
|
597
|
+
const r = ph.getBoundingClientRect();
|
|
598
|
+
const screens = isBoosted() ? 5.0 : 3.0;
|
|
599
|
+
const minBottom = isBoosted() ? -1500 : -800;
|
|
600
|
+
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
899
601
|
} catch (e) {}
|
|
900
602
|
}
|
|
901
603
|
|
|
@@ -916,8 +618,7 @@ function startShow(id) {
|
|
|
916
618
|
|
|
917
619
|
const targets = computeTargets(items.length, interval, showFirst);
|
|
918
620
|
let inserted = 0;
|
|
919
|
-
const
|
|
920
|
-
const maxInserts = perf.maxInsertsPerRun;
|
|
621
|
+
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
921
622
|
|
|
922
623
|
for (const afterPos of targets) {
|
|
923
624
|
if (inserted >= maxInserts) break;
|
|
@@ -1084,27 +785,13 @@ function startShow(id) {
|
|
|
1084
785
|
function cleanup() {
|
|
1085
786
|
blockedUntil = Date.now() + 1200;
|
|
1086
787
|
|
|
1087
|
-
// invalidate any queued showAds from previous view
|
|
1088
|
-
state.navSeq++;
|
|
1089
|
-
state.inflight = 0;
|
|
1090
|
-
state.pending = [];
|
|
1091
|
-
state.pendingSet = new Set();
|
|
1092
|
-
|
|
1093
788
|
// remove all wrappers
|
|
1094
789
|
try {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
try { el.remove(); } catch (e) {}
|
|
1098
|
-
});
|
|
790
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
791
|
+
try { el.remove(); } catch (e) {}
|
|
1099
792
|
});
|
|
1100
793
|
} catch (e) {}
|
|
1101
794
|
|
|
1102
|
-
// reset perf profile cache
|
|
1103
|
-
state.perfProfile = null;
|
|
1104
|
-
|
|
1105
|
-
// tighten observer is global; keep it across ajaxify navigation but ensure it exists
|
|
1106
|
-
// (do not disconnect here to avoid missing late style rewrites during transitions)
|
|
1107
|
-
|
|
1108
795
|
// reset state
|
|
1109
796
|
state.cfg = null;
|
|
1110
797
|
state.allTopics = [];
|
|
@@ -1125,11 +812,9 @@ function startShow(id) {
|
|
|
1125
812
|
|
|
1126
813
|
function ensureDomObserver() {
|
|
1127
814
|
if (state.domObs) return;
|
|
1128
|
-
state.domObs = new MutationObserver((
|
|
815
|
+
state.domObs = new MutationObserver(() => {
|
|
1129
816
|
if (state.internalDomChange > 0) return;
|
|
1130
|
-
if (isBlocked())
|
|
1131
|
-
// Only rescan when NodeBB actually added posts/topics/categories.
|
|
1132
|
-
if (mutationHasRelevantAddedNodes(mutations)) scheduleRun();
|
|
817
|
+
if (!isBlocked()) scheduleRun();
|
|
1133
818
|
});
|
|
1134
819
|
try {
|
|
1135
820
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -1151,8 +836,6 @@ function startShow(id) {
|
|
|
1151
836
|
|
|
1152
837
|
warmUpNetwork();
|
|
1153
838
|
patchShowAds();
|
|
1154
|
-
ensureTightenObserver();
|
|
1155
|
-
ensureFillObserver();
|
|
1156
839
|
ensurePreloadObserver();
|
|
1157
840
|
ensureDomObserver();
|
|
1158
841
|
|
|
@@ -1170,35 +853,51 @@ function startShow(id) {
|
|
|
1170
853
|
});
|
|
1171
854
|
}
|
|
1172
855
|
|
|
856
|
+
function bindScroll() {
|
|
857
|
+
let ticking = false;
|
|
858
|
+
window.addEventListener('scroll', () => {
|
|
859
|
+
// Detect very fast scrolling and temporarily boost preload/parallelism.
|
|
860
|
+
try {
|
|
861
|
+
const now = Date.now();
|
|
862
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
863
|
+
if (state.lastScrollTs) {
|
|
864
|
+
const dt = now - state.lastScrollTs;
|
|
865
|
+
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
866
|
+
if (dt > 0) {
|
|
867
|
+
const speed = dy / dt; // px/ms
|
|
868
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
869
|
+
const wasBoosted = isBoosted();
|
|
870
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, now + BOOST_DURATION_MS);
|
|
871
|
+
if (!wasBoosted) {
|
|
872
|
+
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
873
|
+
ensurePreloadObserver();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
state.lastScrollY = y;
|
|
879
|
+
state.lastScrollTs = now;
|
|
880
|
+
} catch (e) {}
|
|
881
|
+
|
|
882
|
+
if (ticking) return;
|
|
883
|
+
ticking = true;
|
|
884
|
+
window.requestAnimationFrame(() => {
|
|
885
|
+
ticking = false;
|
|
886
|
+
if (!isBlocked()) scheduleRun();
|
|
887
|
+
});
|
|
888
|
+
}, { passive: true });
|
|
889
|
+
}
|
|
890
|
+
|
|
1173
891
|
// ---------- boot ----------
|
|
1174
892
|
|
|
1175
893
|
state.pageKey = getPageKey();
|
|
1176
894
|
warmUpNetwork();
|
|
1177
895
|
patchShowAds();
|
|
1178
|
-
ensureTightenObserver();
|
|
1179
|
-
ensureFillObserver();
|
|
1180
896
|
ensurePreloadObserver();
|
|
1181
897
|
ensureDomObserver();
|
|
1182
|
-
bindNodeBB();
|
|
1183
898
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
// This is throttled and only triggers near the bottom to keep CPU usage minimal.
|
|
1187
|
-
state.lastScrollKick = 0;
|
|
1188
|
-
window.addEventListener('scroll', () => {
|
|
1189
|
-
const now = Date.now();
|
|
1190
|
-
if (now - state.lastScrollKick < 250) return;
|
|
1191
|
-
state.lastScrollKick = now;
|
|
1192
|
-
|
|
1193
|
-
// Only kick when user is approaching the end of currently rendered content
|
|
1194
|
-
const doc = document.documentElement;
|
|
1195
|
-
const scrollTop = window.pageYOffset || doc.scrollTop || 0;
|
|
1196
|
-
const viewportH = window.innerHeight || doc.clientHeight || 0;
|
|
1197
|
-
const fullH = Math.max(doc.scrollHeight, document.body ? document.body.scrollHeight : 0);
|
|
1198
|
-
if (scrollTop + viewportH > fullH - 2000) {
|
|
1199
|
-
if (!isBlocked()) scheduleRun();
|
|
1200
|
-
}
|
|
1201
|
-
}, { passive: true });
|
|
899
|
+
bindNodeBB();
|
|
900
|
+
bindScroll();
|
|
1202
901
|
|
|
1203
902
|
// First paint: try hero + run
|
|
1204
903
|
blockedUntil = 0;
|
package/public/style.css
CHANGED
|
@@ -1,34 +1,40 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Spacing (margins/padding) is intentionally NOT forced here, because it can be
|
|
3
|
-
configured inside Ezoic and may vary by placement/device.
|
|
4
|
-
*/
|
|
5
|
-
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
6
2
|
.ezoic-ad {
|
|
7
3
|
display: block;
|
|
8
4
|
width: 100%;
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
margin: 0 !important;
|
|
6
|
+
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
div.ezoic-ad.ez-pending {
|
|
19
|
-
height: 1px !important;
|
|
20
|
-
min-height: 1px !important;
|
|
21
|
-
overflow: hidden !important;
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
11
|
+
margin: 0 !important;
|
|
12
|
+
padding: 0 !important;
|
|
13
|
+
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
+
.ezoic-ad span.ezoic-ad,
|
|
18
|
+
.ezoic-ad .ezoic-ad {
|
|
19
|
+
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
|
|
24
|
+
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
+
.ezoic-ad.is-empty {
|
|
32
26
|
display: block !important;
|
|
33
|
-
|
|
27
|
+
margin: 0 !important;
|
|
28
|
+
padding: 0 !important;
|
|
29
|
+
height: 0 !important;
|
|
30
|
+
min-height: 0 !important;
|
|
31
|
+
overflow: hidden !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ezoic-ad {
|
|
35
|
+
min-height: 0 !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
39
|
+
min-height: 0 !important;
|
|
34
40
|
}
|