nodebb-plugin-ezoic-infinite 1.5.41 → 1.5.44

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.44",
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
@@ -117,6 +117,7 @@ function mutationHasRelevantAddedNodes(mutations) {
117
117
  pageKey: null,
118
118
  cfg: null,
119
119
 
120
+ navSeq: 0,
120
121
  // Full lists (never consumed) + cursors for round-robin reuse
121
122
  allTopics: [],
122
123
  allPosts: [],
@@ -136,6 +137,7 @@ function mutationHasRelevantAddedNodes(mutations) {
136
137
  // observers / schedulers
137
138
  domObs: null,
138
139
  tightenObs: null,
140
+ fillObs: null,
139
141
  io: null,
140
142
  runQueued: false,
141
143
 
@@ -265,6 +267,7 @@ function mutationHasRelevantAddedNodes(mutations) {
265
267
 
266
268
  ez.showAds = function (...args) {
267
269
  if (isBlocked()) return;
270
+ if (seq !== state.navSeq) return;
268
271
 
269
272
  let ids = [];
270
273
  if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
@@ -542,7 +545,7 @@ function pruneOrphanWraps(kindClass, items) {
542
545
  }
543
546
  function buildWrap(id, kindClass, afterPos) {
544
547
  const wrap = document.createElement('div');
545
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
548
+ wrap.className = `${WRAP_CLASS} ${kindClass} ez-pending`;
546
549
  wrap.setAttribute('data-ezoic-after', String(afterPos));
547
550
  wrap.setAttribute('data-ezoic-wrapid', String(id));
548
551
  wrap.style.width = '100%';
@@ -557,6 +560,103 @@ function buildWrap(id, kindClass, afterPos) {
557
560
  return wrap;
558
561
  }
559
562
 
563
+ // ---------- Fill detection & collapse handling (lightweight) ----------
564
+ // If ad fill is slow, showing a big empty slot is visually jarring. We keep
565
+ // our injected wrapper collapsed (ez-pending) until a creative is present,
566
+ // then mark it ez-ready.
567
+
568
+ function wrapHasFilledCreative(wrap) {
569
+ try {
570
+ if (!wrap || !wrap.isConnected) return false;
571
+ // Safeframe container (most common)
572
+ const c = wrap.querySelector('div[id$="__container__"]');
573
+ if (c && c.offsetHeight > 10) return true;
574
+ // Any iframe with non-trivial height
575
+ const f = wrap.querySelector('iframe');
576
+ if (!f) return false;
577
+ if (f.getAttribute('data-load-complete') === 'true') return true;
578
+ if (f.offsetHeight > 10) return true;
579
+ return false;
580
+ } catch (e) {}
581
+ return false;
582
+ }
583
+
584
+ function markWrapFilledIfNeeded(wrap) {
585
+ try {
586
+ if (!wrap || !wrap.isConnected) return;
587
+ if (!wrap.classList || !wrap.classList.contains(WRAP_CLASS)) return;
588
+ // Only our injected wrappers are DIVs with data-ezoic-wrapid.
589
+ if (wrap.tagName !== 'DIV') return;
590
+ if (!wrap.getAttribute('data-ezoic-wrapid')) return;
591
+
592
+ if (wrapHasFilledCreative(wrap)) {
593
+ wrap.classList.remove('ez-pending');
594
+ wrap.classList.add('ez-ready');
595
+ }
596
+ } catch (e) {}
597
+ }
598
+
599
+ function ensureFillObserver() {
600
+ if (state.fillObs) return;
601
+
602
+ let raf = 0;
603
+ const pending = new Set();
604
+ const schedule = (wrap) => {
605
+ if (!wrap) return;
606
+ pending.add(wrap);
607
+ if (raf) return;
608
+ raf = requestAnimationFrame(() => {
609
+ raf = 0;
610
+ for (const w of pending) markWrapFilledIfNeeded(w);
611
+ pending.clear();
612
+ });
613
+ };
614
+
615
+ const closestWrap = (node) => {
616
+ try {
617
+ if (!node || node.nodeType !== 1) return null;
618
+ const el = /** @type {Element} */ (node);
619
+ if (el.tagName === 'DIV' && el.classList && el.classList.contains(WRAP_CLASS) && el.getAttribute('data-ezoic-wrapid')) return el;
620
+ if (el.closest) return el.closest(`div.${WRAP_CLASS}[data-ezoic-wrapid]`);
621
+ } catch (e) {}
622
+ return null;
623
+ };
624
+
625
+ state.fillObs = new MutationObserver((mutations) => {
626
+ try {
627
+ for (const m of mutations) {
628
+ if (m.type === 'attributes') {
629
+ const w = closestWrap(m.target);
630
+ if (w) schedule(w);
631
+ continue;
632
+ }
633
+ if (!m.addedNodes || !m.addedNodes.length) continue;
634
+ for (const n of m.addedNodes) {
635
+ const w = closestWrap(n);
636
+ if (w) schedule(w);
637
+ if (n && n.nodeType === 1 && n.querySelectorAll) {
638
+ n.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(schedule);
639
+ }
640
+ }
641
+ }
642
+ } catch (e) {}
643
+ });
644
+
645
+ try {
646
+ state.fillObs.observe(document.documentElement, {
647
+ subtree: true,
648
+ childList: true,
649
+ attributes: true,
650
+ attributeFilter: ['style', 'class', 'data-load-complete', 'height', 'src'],
651
+ });
652
+ } catch (e) {}
653
+
654
+ // Kick once for already-present wraps
655
+ try {
656
+ document.querySelectorAll(`div.${WRAP_CLASS}[data-ezoic-wrapid]`).forEach(markWrapFilledIfNeeded);
657
+ } catch (e) {}
658
+ }
659
+
560
660
  function findWrap(kindClass, afterPos) {
561
661
  return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
562
662
  }
@@ -664,6 +764,7 @@ function drainQueue() {
664
764
  function startShow(id) {
665
765
  if (!id || isBlocked()) return;
666
766
 
767
+ const seq = state.navSeq;
667
768
  state.inflight++;
668
769
  let released = false;
669
770
  const release = () => {
@@ -679,7 +780,15 @@ function startShow(id) {
679
780
  try {
680
781
  if (isBlocked()) return;
681
782
 
682
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
783
+ // Ensure placeholder is armed (arm-on-load). On some NodeBB transitions the
784
+ // wrapper may exist but the placeholder id is not yet assigned.
785
+ const wrap = findWrapById(id);
786
+ if (wrap && wrap.isConnected) {
787
+ try { armPlaceholder(wrap, id); } catch (e) {}
788
+ }
789
+
790
+ const domId = `${PLACEHOLDER_PREFIX}${id}`;
791
+ const ph = document.getElementById(domId);
683
792
  if (!ph || !ph.isConnected) return;
684
793
 
685
794
  const now2 = Date.now();
@@ -691,13 +800,29 @@ function startShow(id) {
691
800
  const ez = window.ezstandalone;
692
801
 
693
802
  const doShow = () => {
803
+ // Re-check right before showing: the placeholder can disappear between
804
+ // scheduling and execution (ajaxify/infinite scroll DOM churn).
805
+ if (seq !== state.navSeq) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
806
+ const phNow = document.getElementById(domId);
807
+ if (!phNow || !phNow.isConnected) {
808
+ try { clearTimeout(hardTimer); } catch (e) {}
809
+ release();
810
+ return;
811
+ }
812
+
694
813
  try {
695
814
  if (state.usedOnce && state.usedOnce.has(id)) {
696
815
  safeDestroyById(id);
697
816
  }
698
817
  } catch (e) {}
699
818
 
700
- try { ez.showAds(id); } catch (e) {}
819
+ // Let the DOM settle for one macrotask; NodeBB can reflow/replace nodes right after mutations.
820
+ setTimeout(() => {
821
+ if (seq !== state.navSeq) { return; }
822
+ const phFinal = document.getElementById(domId);
823
+ if (!phFinal || !phFinal.isConnected) { return; }
824
+ try { ez.showAds(id); } catch (e) {}
825
+ }, 0);
701
826
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
702
827
 
703
828
  setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
@@ -960,6 +1085,12 @@ function startShow(id) {
960
1085
  function cleanup() {
961
1086
  blockedUntil = Date.now() + 1200;
962
1087
 
1088
+ // invalidate any queued showAds from previous view
1089
+ state.navSeq++;
1090
+ state.inflight = 0;
1091
+ state.pending = [];
1092
+ state.pendingSet = new Set();
1093
+
963
1094
  // remove all wrappers
964
1095
  try {
965
1096
  withInternalDomChange(() => {
@@ -1022,6 +1153,7 @@ function startShow(id) {
1022
1153
  warmUpNetwork();
1023
1154
  patchShowAds();
1024
1155
  ensureTightenObserver();
1156
+ ensureFillObserver();
1025
1157
  ensurePreloadObserver();
1026
1158
  ensureDomObserver();
1027
1159
 
@@ -1045,6 +1177,7 @@ function startShow(id) {
1045
1177
  warmUpNetwork();
1046
1178
  patchShowAds();
1047
1179
  ensureTightenObserver();
1180
+ ensureFillObserver();
1048
1181
  ensurePreloadObserver();
1049
1182
  ensureDomObserver();
1050
1183
  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
+ }