nodebb-plugin-ezoic-infinite 1.5.33 → 1.5.35
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 +152 -71
- package/public/style.css +5 -36
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -24,7 +24,53 @@
|
|
|
24
24
|
const MAX_INFLIGHT_DESKTOP = 4;
|
|
25
25
|
const MAX_INFLIGHT_MOBILE = 3;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
// Adaptive performance profile (device/network aware)
|
|
29
|
+
const PERF_DEFAULTS = Object.freeze({
|
|
30
|
+
maxInflightDesktop: MAX_INFLIGHT_DESKTOP,
|
|
31
|
+
maxInflightMobile: MAX_INFLIGHT_MOBILE,
|
|
32
|
+
maxInsertsPerRun: MAX_INSERTS_PER_RUN,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function getPerfProfile() {
|
|
36
|
+
// Cache result for this pageview; recomputed on navigation via cleanup()
|
|
37
|
+
if (state.perfProfile) return state.perfProfile;
|
|
38
|
+
const p = {
|
|
39
|
+
maxInflightDesktop: PERF_DEFAULTS.maxInflightDesktop,
|
|
40
|
+
maxInflightMobile: PERF_DEFAULTS.maxInflightMobile,
|
|
41
|
+
maxInsertsPerRun: PERF_DEFAULTS.maxInsertsPerRun,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const mem = typeof navigator !== 'undefined' ? navigator.deviceMemory : undefined; // GB
|
|
46
|
+
const cores = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined;
|
|
47
|
+
|
|
48
|
+
// Conservative tuning for low-end devices
|
|
49
|
+
if (typeof mem === 'number' && mem > 0 && mem <= 2) {
|
|
50
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
51
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
52
|
+
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
53
|
+
}
|
|
54
|
+
if (typeof cores === 'number' && cores > 0 && cores <= 4) {
|
|
55
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
56
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
57
|
+
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const conn = typeof navigator !== 'undefined' ? navigator.connection : undefined;
|
|
61
|
+
const eff = conn && conn.effectiveType ? String(conn.effectiveType).toLowerCase() : '';
|
|
62
|
+
// Slow connections: don't ramp concurrency too high (keeps page responsive).
|
|
63
|
+
if (eff.includes('2g')) {
|
|
64
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
65
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
|
|
69
|
+
state.perfProfile = p;
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isBoosted() {
|
|
28
74
|
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
29
75
|
}
|
|
30
76
|
|
|
@@ -38,7 +84,8 @@
|
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
function getMaxInflight() {
|
|
41
|
-
const
|
|
87
|
+
const perf = getPerfProfile();
|
|
88
|
+
const base = isMobile() ? perf.maxInflightMobile : perf.maxInflightDesktop;
|
|
42
89
|
return base + (isBoosted() ? 1 : 0);
|
|
43
90
|
}
|
|
44
91
|
|
|
@@ -48,6 +95,31 @@
|
|
|
48
95
|
categoryItem: 'li[component="categories/category"]',
|
|
49
96
|
};
|
|
50
97
|
|
|
98
|
+
const RELEVANT_MATCHERS = [
|
|
99
|
+
SELECTORS.postItem,
|
|
100
|
+
SELECTORS.topicItem,
|
|
101
|
+
SELECTORS.categoryItem,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
function mutationHasRelevantAddedNodes(mutations) {
|
|
105
|
+
try {
|
|
106
|
+
for (const m of mutations) {
|
|
107
|
+
if (!m || !m.addedNodes || !m.addedNodes.length) continue;
|
|
108
|
+
for (const n of m.addedNodes) {
|
|
109
|
+
if (!n || n.nodeType !== 1) continue;
|
|
110
|
+
const el = /** @type {Element} */ (n);
|
|
111
|
+
for (const sel of RELEVANT_MATCHERS) {
|
|
112
|
+
if (el.matches && el.matches(sel)) return true;
|
|
113
|
+
if (el.querySelector && el.querySelector(sel)) return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
51
123
|
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
52
124
|
let blockedUntil = 0;
|
|
53
125
|
function isBlocked() {
|
|
@@ -95,29 +167,6 @@
|
|
|
95
167
|
};
|
|
96
168
|
|
|
97
169
|
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
|
-
|
|
121
170
|
// Production build: debug disabled
|
|
122
171
|
function dbg() {}
|
|
123
172
|
|
|
@@ -334,7 +383,33 @@ function withInternalDomChange(fn) {
|
|
|
334
383
|
} catch (e) {}
|
|
335
384
|
}
|
|
336
385
|
|
|
337
|
-
|
|
386
|
+
|
|
387
|
+
function findWrapById(id) {
|
|
388
|
+
try {
|
|
389
|
+
return document.querySelector(`.${WRAP_CLASS}[data-ezoic-wrapid="${id}"]`);
|
|
390
|
+
} catch (e) {}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function armPlaceholder(wrap, id) {
|
|
395
|
+
try {
|
|
396
|
+
if (!wrap || !wrap.isConnected) return null;
|
|
397
|
+
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
398
|
+
|
|
399
|
+
// If the id is already present somewhere, do not reassign it.
|
|
400
|
+
const existing = document.getElementById(domId);
|
|
401
|
+
if (existing && existing.isConnected) return existing;
|
|
402
|
+
|
|
403
|
+
const ph = wrap.querySelector('[data-ezoic-ph="1"]');
|
|
404
|
+
if (!ph) return null;
|
|
405
|
+
|
|
406
|
+
if (!ph.id) ph.id = domId;
|
|
407
|
+
return ph;
|
|
408
|
+
} catch (e) {}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
338
413
|
if (!items || !items.length) return 0;
|
|
339
414
|
const itemSet = new Set(items);
|
|
340
415
|
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
@@ -364,22 +439,6 @@ function withInternalDomChange(fn) {
|
|
|
364
439
|
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
365
440
|
return removed;
|
|
366
441
|
}
|
|
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
|
-
|
|
383
442
|
function buildWrap(id, kindClass, afterPos) {
|
|
384
443
|
const wrap = document.createElement('div');
|
|
385
444
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
@@ -388,8 +447,10 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
388
447
|
wrap.style.width = '100%';
|
|
389
448
|
|
|
390
449
|
const ph = document.createElement('div');
|
|
391
|
-
|
|
450
|
+
// Do not assign the Ezoic placeholder id until we actually want to load the ad.
|
|
451
|
+
// This avoids Ezoic defining placeholders too early during DOM churn/infinite scroll.
|
|
392
452
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
453
|
+
ph.setAttribute('data-ezoic-ph', '1');
|
|
393
454
|
wrap.appendChild(ph);
|
|
394
455
|
|
|
395
456
|
return wrap;
|
|
@@ -404,8 +465,8 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
404
465
|
if (findWrap(kindClass, afterPos)) return null;
|
|
405
466
|
if (insertingIds.has(id)) return null;
|
|
406
467
|
|
|
407
|
-
|
|
408
|
-
if (
|
|
468
|
+
const existingWrap = findWrapById(id);
|
|
469
|
+
if (existingWrap && existingWrap.isConnected) return null;
|
|
409
470
|
|
|
410
471
|
insertingIds.add(id);
|
|
411
472
|
try {
|
|
@@ -427,8 +488,8 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
427
488
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
428
489
|
|
|
429
490
|
const id = allIds[idx];
|
|
430
|
-
|
|
431
|
-
if (
|
|
491
|
+
const w = findWrapById(id);
|
|
492
|
+
if (w && w.isConnected) continue;
|
|
432
493
|
|
|
433
494
|
return id;
|
|
434
495
|
}
|
|
@@ -450,13 +511,20 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
450
511
|
// Otherwise remove the earliest one in the document
|
|
451
512
|
if (!victim) victim = wraps[0];
|
|
452
513
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
514
|
+
const id = getWrapIdFromWrap(victim);
|
|
515
|
+
|
|
516
|
+
withInternalDomChange(() => {
|
|
517
|
+
// Unobserve placeholder if still observed
|
|
518
|
+
try {
|
|
519
|
+
const t = victim;
|
|
520
|
+
if (t && state.io) state.io.unobserve(t);
|
|
521
|
+
} catch (e) {}
|
|
522
|
+
|
|
523
|
+
try { if (id) safeDestroyById(id); } catch (e) {}
|
|
524
|
+
|
|
525
|
+
try { victim.remove(); } catch (e) {}
|
|
526
|
+
});
|
|
458
527
|
|
|
459
|
-
victim.remove();
|
|
460
528
|
return true;
|
|
461
529
|
} catch (e) {
|
|
462
530
|
return false;
|
|
@@ -523,14 +591,13 @@ function startShow(id) {
|
|
|
523
591
|
|
|
524
592
|
const doShow = () => {
|
|
525
593
|
try {
|
|
526
|
-
if (state.usedOnce && state.usedOnce.has(id)
|
|
527
|
-
|
|
594
|
+
if (state.usedOnce && state.usedOnce.has(id)) {
|
|
595
|
+
safeDestroyById(id);
|
|
528
596
|
}
|
|
529
597
|
} catch (e) {}
|
|
530
598
|
|
|
531
599
|
try { ez.showAds(id); } catch (e) {}
|
|
532
600
|
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
533
|
-
try { markEmptyWrapper(id); } catch (e) {}
|
|
534
601
|
|
|
535
602
|
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
536
603
|
};
|
|
@@ -565,9 +632,12 @@ function startShow(id) {
|
|
|
565
632
|
const el = ent.target;
|
|
566
633
|
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
567
634
|
|
|
568
|
-
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-
|
|
635
|
+
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-wrapid');
|
|
569
636
|
const id = parseInt(idAttr, 10);
|
|
570
|
-
if (Number.isFinite(id) && id > 0)
|
|
637
|
+
if (Number.isFinite(id) && id > 0) {
|
|
638
|
+
armPlaceholder(el, id);
|
|
639
|
+
enqueueShow(id);
|
|
640
|
+
}
|
|
571
641
|
}
|
|
572
642
|
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
573
643
|
state.ioMargin = desiredMargin;
|
|
@@ -579,7 +649,7 @@ function startShow(id) {
|
|
|
579
649
|
// If we rebuilt the observer, re-observe existing placeholders so we don't miss them.
|
|
580
650
|
try {
|
|
581
651
|
if (state.io) {
|
|
582
|
-
const nodes = document.querySelectorAll(
|
|
652
|
+
const nodes = document.querySelectorAll(`.${WRAP_CLASS}[data-ezoic-wrapid]`);
|
|
583
653
|
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
584
654
|
}
|
|
585
655
|
} catch (e) {}
|
|
@@ -587,17 +657,20 @@ function startShow(id) {
|
|
|
587
657
|
}
|
|
588
658
|
|
|
589
659
|
function observePlaceholder(id) {
|
|
590
|
-
const
|
|
591
|
-
if (!
|
|
660
|
+
const wrap = findWrapById(id);
|
|
661
|
+
if (!wrap || !wrap.isConnected) return;
|
|
592
662
|
const io = ensurePreloadObserver();
|
|
593
|
-
try { io && io.observe(
|
|
663
|
+
try { io && io.observe(wrap); } catch (e) {}
|
|
594
664
|
|
|
595
|
-
// If already
|
|
665
|
+
// If already near the fold, arm & fire immediately
|
|
596
666
|
try {
|
|
597
|
-
const r =
|
|
667
|
+
const r = wrap.getBoundingClientRect();
|
|
598
668
|
const screens = isBoosted() ? 5.0 : 3.0;
|
|
599
|
-
const
|
|
600
|
-
if (r.top <
|
|
669
|
+
const h = (window.innerHeight || 800);
|
|
670
|
+
if (r.top < screens * h && r.bottom > -screens * h) {
|
|
671
|
+
armPlaceholder(wrap, id);
|
|
672
|
+
enqueueShow(id);
|
|
673
|
+
}
|
|
601
674
|
} catch (e) {}
|
|
602
675
|
}
|
|
603
676
|
|
|
@@ -618,7 +691,8 @@ function startShow(id) {
|
|
|
618
691
|
|
|
619
692
|
const targets = computeTargets(items.length, interval, showFirst);
|
|
620
693
|
let inserted = 0;
|
|
621
|
-
const
|
|
694
|
+
const perf = getPerfProfile();
|
|
695
|
+
const maxInserts = perf.maxInsertsPerRun + (isBoosted() ? 1 : 0);
|
|
622
696
|
|
|
623
697
|
for (const afterPos of targets) {
|
|
624
698
|
if (inserted >= maxInserts) break;
|
|
@@ -787,11 +861,16 @@ function startShow(id) {
|
|
|
787
861
|
|
|
788
862
|
// remove all wrappers
|
|
789
863
|
try {
|
|
790
|
-
|
|
791
|
-
|
|
864
|
+
withInternalDomChange(() => {
|
|
865
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
866
|
+
try { el.remove(); } catch (e) {}
|
|
867
|
+
});
|
|
792
868
|
});
|
|
793
869
|
} catch (e) {}
|
|
794
870
|
|
|
871
|
+
// reset perf profile cache
|
|
872
|
+
state.perfProfile = null;
|
|
873
|
+
|
|
795
874
|
// reset state
|
|
796
875
|
state.cfg = null;
|
|
797
876
|
state.allTopics = [];
|
|
@@ -812,9 +891,11 @@ function startShow(id) {
|
|
|
812
891
|
|
|
813
892
|
function ensureDomObserver() {
|
|
814
893
|
if (state.domObs) return;
|
|
815
|
-
state.domObs = new MutationObserver(() => {
|
|
894
|
+
state.domObs = new MutationObserver((mutations) => {
|
|
816
895
|
if (state.internalDomChange > 0) return;
|
|
817
|
-
if (
|
|
896
|
+
if (isBlocked()) return;
|
|
897
|
+
// Only rescan when NodeBB actually added posts/topics/categories.
|
|
898
|
+
if (mutationHasRelevantAddedNodes(mutations)) scheduleRun();
|
|
818
899
|
});
|
|
819
900
|
try {
|
|
820
901
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
package/public/style.css
CHANGED
|
@@ -1,40 +1,9 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Minimal styling for injected Ezoic wrappers.
|
|
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
|
+
|
|
2
6
|
.ezoic-ad {
|
|
3
7
|
display: block;
|
|
4
8
|
width: 100%;
|
|
5
|
-
margin: 0 !important;
|
|
6
|
-
padding: 0 !important;
|
|
7
|
-
overflow: hidden;
|
|
8
|
-
}
|
|
9
|
-
|
|
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 */
|
|
14
|
-
}
|
|
15
|
-
|
|
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;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
-
.ezoic-ad.is-empty {
|
|
26
|
-
display: block !important;
|
|
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;
|
|
40
9
|
}
|