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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.40",
3
+ "version": "1.5.43",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
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
  }