nodebb-plugin-ezoic-infinite 1.5.41 → 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.41",
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
 
@@ -542,7 +543,7 @@ function pruneOrphanWraps(kindClass, items) {
542
543
  }
543
544
  function buildWrap(id, kindClass, afterPos) {
544
545
  const wrap = document.createElement('div');
545
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
546
+ wrap.className = `${WRAP_CLASS} ${kindClass} ez-pending`;
546
547
  wrap.setAttribute('data-ezoic-after', String(afterPos));
547
548
  wrap.setAttribute('data-ezoic-wrapid', String(id));
548
549
  wrap.style.width = '100%';
@@ -557,6 +558,103 @@ function buildWrap(id, kindClass, afterPos) {
557
558
  return wrap;
558
559
  }
559
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
+
560
658
  function findWrap(kindClass, afterPos) {
561
659
  return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
562
660
  }
@@ -679,7 +777,15 @@ function startShow(id) {
679
777
  try {
680
778
  if (isBlocked()) return;
681
779
 
682
- 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);
683
789
  if (!ph || !ph.isConnected) return;
684
790
 
685
791
  const now2 = Date.now();
@@ -691,6 +797,15 @@ function startShow(id) {
691
797
  const ez = window.ezstandalone;
692
798
 
693
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
+
694
809
  try {
695
810
  if (state.usedOnce && state.usedOnce.has(id)) {
696
811
  safeDestroyById(id);
@@ -1022,6 +1137,7 @@ function startShow(id) {
1022
1137
  warmUpNetwork();
1023
1138
  patchShowAds();
1024
1139
  ensureTightenObserver();
1140
+ ensureFillObserver();
1025
1141
  ensurePreloadObserver();
1026
1142
  ensureDomObserver();
1027
1143
 
@@ -1045,6 +1161,7 @@ function startShow(id) {
1045
1161
  warmUpNetwork();
1046
1162
  patchShowAds();
1047
1163
  ensureTightenObserver();
1164
+ ensureFillObserver();
1048
1165
  ensurePreloadObserver();
1049
1166
  ensureDomObserver();
1050
1167
  bindNodeBB();
package/public/style.css CHANGED
@@ -9,3 +9,26 @@
9
9
  clear: both;
10
10
  position: relative;
11
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;
34
+ }