nodebb-plugin-ezoic-infinite 1.5.40 → 1.5.43
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 +159 -3
- package/public/style.css +25 -0
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -136,6 +136,7 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
136
136
|
// observers / schedulers
|
|
137
137
|
domObs: null,
|
|
138
138
|
tightenObs: null,
|
|
139
|
+
fillObs: null,
|
|
139
140
|
io: null,
|
|
140
141
|
runQueued: false,
|
|
141
142
|
|
|
@@ -318,6 +319,27 @@ function mutationHasRelevantAddedNodes(mutations) {
|
|
|
318
319
|
if (adSpan.tagName !== 'SPAN') return;
|
|
319
320
|
if (!adSpan.classList || !adSpan.classList.contains('ezoic-ad')) return;
|
|
320
321
|
|
|
322
|
+
// Some Ezoic templates apply sticky/fixed positioning inside the ad slot
|
|
323
|
+
// (e.g. .ezads-sticky-intradiv) which can make the creative appear to
|
|
324
|
+
// "slide" within an oversized container. Neutralize it inside the slot.
|
|
325
|
+
try {
|
|
326
|
+
const sticky = adSpan.querySelectorAll('.ezads-sticky-intradiv');
|
|
327
|
+
sticky.forEach((el) => {
|
|
328
|
+
el.style.setProperty('position', 'static', 'important');
|
|
329
|
+
el.style.setProperty('top', 'auto', 'important');
|
|
330
|
+
el.style.setProperty('bottom', 'auto', 'important');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Safety net: any descendant that ends up sticky/fixed via inline style
|
|
334
|
+
// (rare, but causes "floating" creatives).
|
|
335
|
+
const positioned = adSpan.querySelectorAll('[style*="position: sticky"], [style*="position:sticky"], [style*="position: fixed"], [style*="position:fixed"]');
|
|
336
|
+
positioned.forEach((el) => {
|
|
337
|
+
el.style.setProperty('position', 'static', 'important');
|
|
338
|
+
el.style.setProperty('top', 'auto', 'important');
|
|
339
|
+
el.style.setProperty('bottom', 'auto', 'important');
|
|
340
|
+
});
|
|
341
|
+
} catch (e) {}
|
|
342
|
+
|
|
321
343
|
const mhStr = adSpan.style && adSpan.style.minHeight ? String(adSpan.style.minHeight) : '';
|
|
322
344
|
const mh = mhStr ? parseInt(mhStr, 10) : 0;
|
|
323
345
|
if (!mh || mh < 350) return; // only fix the "400px"-style reservations
|
|
@@ -521,7 +543,7 @@ function pruneOrphanWraps(kindClass, items) {
|
|
|
521
543
|
}
|
|
522
544
|
function buildWrap(id, kindClass, afterPos) {
|
|
523
545
|
const wrap = document.createElement('div');
|
|
524
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
546
|
+
wrap.className = `${WRAP_CLASS} ${kindClass} ez-pending`;
|
|
525
547
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
526
548
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
527
549
|
wrap.style.width = '100%';
|
|
@@ -536,6 +558,103 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
536
558
|
return wrap;
|
|
537
559
|
}
|
|
538
560
|
|
|
561
|
+
// ---------- Fill detection & collapse handling (lightweight) ----------
|
|
562
|
+
// If ad fill is slow, showing a big empty slot is visually jarring. We keep
|
|
563
|
+
// our injected wrapper collapsed (ez-pending) until a creative is present,
|
|
564
|
+
// then mark it ez-ready.
|
|
565
|
+
|
|
566
|
+
function wrapHasFilledCreative(wrap) {
|
|
567
|
+
try {
|
|
568
|
+
if (!wrap || !wrap.isConnected) return false;
|
|
569
|
+
// Safeframe container (most common)
|
|
570
|
+
const c = wrap.querySelector('div[id$="__container__"]');
|
|
571
|
+
if (c && c.offsetHeight > 10) return true;
|
|
572
|
+
// Any iframe with non-trivial height
|
|
573
|
+
const f = wrap.querySelector('iframe');
|
|
574
|
+
if (!f) return false;
|
|
575
|
+
if (f.getAttribute('data-load-complete') === 'true') return true;
|
|
576
|
+
if (f.offsetHeight > 10) return true;
|
|
577
|
+
return false;
|
|
578
|
+
} catch (e) {}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function markWrapFilledIfNeeded(wrap) {
|
|
583
|
+
try {
|
|
584
|
+
if (!wrap || !wrap.isConnected) return;
|
|
585
|
+
if (!wrap.classList || !wrap.classList.contains(WRAP_CLASS)) return;
|
|
586
|
+
// Only our injected wrappers are DIVs with data-ezoic-wrapid.
|
|
587
|
+
if (wrap.tagName !== 'DIV') return;
|
|
588
|
+
if (!wrap.getAttribute('data-ezoic-wrapid')) return;
|
|
589
|
+
|
|
590
|
+
if (wrapHasFilledCreative(wrap)) {
|
|
591
|
+
wrap.classList.remove('ez-pending');
|
|
592
|
+
wrap.classList.add('ez-ready');
|
|
593
|
+
}
|
|
594
|
+
} catch (e) {}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function ensureFillObserver() {
|
|
598
|
+
if (state.fillObs) return;
|
|
599
|
+
|
|
600
|
+
let raf = 0;
|
|
601
|
+
const pending = new Set();
|
|
602
|
+
const schedule = (wrap) => {
|
|
603
|
+
if (!wrap) return;
|
|
604
|
+
pending.add(wrap);
|
|
605
|
+
if (raf) return;
|
|
606
|
+
raf = requestAnimationFrame(() => {
|
|
607
|
+
raf = 0;
|
|
608
|
+
for (const w of pending) markWrapFilledIfNeeded(w);
|
|
609
|
+
pending.clear();
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const closestWrap = (node) => {
|
|
614
|
+
try {
|
|
615
|
+
if (!node || node.nodeType !== 1) return null;
|
|
616
|
+
const el = /** @type {Element} */ (node);
|
|
617
|
+
if (el.tagName === 'DIV' && el.classList && el.classList.contains(WRAP_CLASS) && el.getAttribute('data-ezoic-wrapid')) return el;
|
|
618
|
+
if (el.closest) return el.closest(`div.${WRAP_CLASS}[data-ezoic-wrapid]`);
|
|
619
|
+
} catch (e) {}
|
|
620
|
+
return null;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
state.fillObs = new MutationObserver((mutations) => {
|
|
624
|
+
try {
|
|
625
|
+
for (const m of mutations) {
|
|
626
|
+
if (m.type === 'attributes') {
|
|
627
|
+
const w = closestWrap(m.target);
|
|
628
|
+
if (w) schedule(w);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
632
|
+
for (const n of m.addedNodes) {
|
|
633
|
+
const w = closestWrap(n);
|
|
634
|
+
if (w) schedule(w);
|
|
635
|
+
if (n && n.nodeType === 1 && n.querySelectorAll) {
|
|
636
|
+
n.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(schedule);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} catch (e) {}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
state.fillObs.observe(document.documentElement, {
|
|
645
|
+
subtree: true,
|
|
646
|
+
childList: true,
|
|
647
|
+
attributes: true,
|
|
648
|
+
attributeFilter: ['style', 'class', 'data-load-complete', 'height', 'src'],
|
|
649
|
+
});
|
|
650
|
+
} catch (e) {}
|
|
651
|
+
|
|
652
|
+
// Kick once for already-present wraps
|
|
653
|
+
try {
|
|
654
|
+
document.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(markWrapFilledIfNeeded);
|
|
655
|
+
} catch (e) {}
|
|
656
|
+
}
|
|
657
|
+
|
|
539
658
|
function findWrap(kindClass, afterPos) {
|
|
540
659
|
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
541
660
|
}
|
|
@@ -658,7 +777,15 @@ function startShow(id) {
|
|
|
658
777
|
try {
|
|
659
778
|
if (isBlocked()) return;
|
|
660
779
|
|
|
661
|
-
|
|
780
|
+
// Ensure placeholder is armed (arm-on-load). On some NodeBB transitions the
|
|
781
|
+
// wrapper may exist but the placeholder id is not yet assigned.
|
|
782
|
+
const wrap = findWrapById(id);
|
|
783
|
+
if (wrap && wrap.isConnected) {
|
|
784
|
+
try { armPlaceholder(wrap, id); } catch (e) {}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
788
|
+
const ph = document.getElementById(domId);
|
|
662
789
|
if (!ph || !ph.isConnected) return;
|
|
663
790
|
|
|
664
791
|
const now2 = Date.now();
|
|
@@ -670,6 +797,15 @@ function startShow(id) {
|
|
|
670
797
|
const ez = window.ezstandalone;
|
|
671
798
|
|
|
672
799
|
const doShow = () => {
|
|
800
|
+
// Re-check right before showing: the placeholder can disappear between
|
|
801
|
+
// scheduling and execution (ajaxify/infinite scroll DOM churn).
|
|
802
|
+
const phNow = document.getElementById(domId);
|
|
803
|
+
if (!phNow || !phNow.isConnected) {
|
|
804
|
+
try { clearTimeout(hardTimer); } catch (e) {}
|
|
805
|
+
release();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
673
809
|
try {
|
|
674
810
|
if (state.usedOnce && state.usedOnce.has(id)) {
|
|
675
811
|
safeDestroyById(id);
|
|
@@ -1001,6 +1137,7 @@ function startShow(id) {
|
|
|
1001
1137
|
warmUpNetwork();
|
|
1002
1138
|
patchShowAds();
|
|
1003
1139
|
ensureTightenObserver();
|
|
1140
|
+
ensureFillObserver();
|
|
1004
1141
|
ensurePreloadObserver();
|
|
1005
1142
|
ensureDomObserver();
|
|
1006
1143
|
|
|
@@ -1024,11 +1161,30 @@ function startShow(id) {
|
|
|
1024
1161
|
warmUpNetwork();
|
|
1025
1162
|
patchShowAds();
|
|
1026
1163
|
ensureTightenObserver();
|
|
1164
|
+
ensureFillObserver();
|
|
1027
1165
|
ensurePreloadObserver();
|
|
1028
1166
|
ensureDomObserver();
|
|
1029
|
-
|
|
1030
1167
|
bindNodeBB();
|
|
1031
1168
|
|
|
1169
|
+
// Lightweight scroll kick: NodeBB infinite scroll can keep many nodes and only append occasionally.
|
|
1170
|
+
// Without a scroll trigger, we might not inject new placeholders until another DOM mutation occurs.
|
|
1171
|
+
// This is throttled and only triggers near the bottom to keep CPU usage minimal.
|
|
1172
|
+
state.lastScrollKick = 0;
|
|
1173
|
+
window.addEventListener('scroll', () => {
|
|
1174
|
+
const now = Date.now();
|
|
1175
|
+
if (now - state.lastScrollKick < 250) return;
|
|
1176
|
+
state.lastScrollKick = now;
|
|
1177
|
+
|
|
1178
|
+
// Only kick when user is approaching the end of currently rendered content
|
|
1179
|
+
const doc = document.documentElement;
|
|
1180
|
+
const scrollTop = window.pageYOffset || doc.scrollTop || 0;
|
|
1181
|
+
const viewportH = window.innerHeight || doc.clientHeight || 0;
|
|
1182
|
+
const fullH = Math.max(doc.scrollHeight, document.body ? document.body.scrollHeight : 0);
|
|
1183
|
+
if (scrollTop + viewportH > fullH - 2000) {
|
|
1184
|
+
if (!isBlocked()) scheduleRun();
|
|
1185
|
+
}
|
|
1186
|
+
}, { passive: true });
|
|
1187
|
+
|
|
1032
1188
|
// First paint: try hero + run
|
|
1033
1189
|
blockedUntil = 0;
|
|
1034
1190
|
insertHeroAdEarly().catch(() => {});
|
package/public/style.css
CHANGED
|
@@ -6,4 +6,29 @@
|
|
|
6
6
|
.ezoic-ad {
|
|
7
7
|
display: block;
|
|
8
8
|
width: 100%;
|
|
9
|
+
clear: both;
|
|
10
|
+
position: relative;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
UX: collapse injected wrapper slots until the creative is actually filled.
|
|
15
|
+
This avoids showing a large empty placeholder when ad fill is slow.
|
|
16
|
+
Only applies to our injected wrapper DIVs (not Ezoic's internal SPANs).
|
|
17
|
+
*/
|
|
18
|
+
div.ezoic-ad.ez-pending {
|
|
19
|
+
height: 1px !important;
|
|
20
|
+
min-height: 1px !important;
|
|
21
|
+
overflow: hidden !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
div.ezoic-ad.ez-ready {
|
|
25
|
+
height: auto !important;
|
|
26
|
+
overflow: visible !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Remove baseline gaps under iframes inside Ezoic creatives */
|
|
30
|
+
span.ezoic-ad iframe,
|
|
31
|
+
span.ezoic-ad div[id$="__container__"] iframe {
|
|
32
|
+
display: block !important;
|
|
33
|
+
vertical-align: top !important;
|
|
9
34
|
}
|