nodebb-plugin-ezoic-infinite 1.6.48 → 1.6.50

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.6.48",
3
+ "version": "1.6.50",
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
@@ -63,6 +63,31 @@
63
63
  // Production build: debug disabled
64
64
  function dbg() {}
65
65
 
66
+ // Some ad providers (notably Google Safeframe) can defer creative rendering until a
67
+ // scroll/resize tick is observed. On SPA/infinite-scroll pages we can end up with
68
+ // valid iframes that stay visually empty until the user nudges the scroll.
69
+ // We gently "poke" observers by dispatching scroll/resize events (debounced).
70
+ let _ezPokeScheduled = false;
71
+ function scheduleViewportPoke(reason) {
72
+ if (_ezPokeScheduled) return;
73
+ _ezPokeScheduled = true;
74
+ try {
75
+ requestAnimationFrame(() => {
76
+ _ezPokeScheduled = false;
77
+ try {
78
+ window.dispatchEvent(new Event('scroll'));
79
+ window.dispatchEvent(new Event('resize'));
80
+ } catch (e) {}
81
+ try {
82
+ document.dispatchEvent(new Event('scroll'));
83
+ } catch (e) {}
84
+ dbg('viewportPoke', reason || '');
85
+ });
86
+ } catch (e) {
87
+ _ezPokeScheduled = false;
88
+ }
89
+ }
90
+
66
91
  // Ezoic (and some partner scripts) can be very noisy in console on SPA/Ajaxify setups.
67
92
  // These warnings are not actionable for end-users and can flood the console.
68
93
  // We selectively silence the known spam patterns while keeping other warnings intact.
@@ -636,8 +661,8 @@ function globalGapFixInit() {
636
661
 
637
662
  // ---------------- insertion primitives ----------------
638
663
 
639
- function buildWrap(id, kindClass, afterPos, createPlaceholder) {
640
- const wrap = document.createElement('div');
664
+ function buildWrap(id, kindClass, afterPos, createPlaceholder, tagName) {
665
+ const wrap = document.createElement(tagName || 'div');
641
666
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
642
667
  wrap.setAttribute('data-ezoic-after', String(afterPos));
643
668
  wrap.setAttribute('data-ezoic-wrapid', String(id));
@@ -648,6 +673,15 @@ function globalGapFixInit() {
648
673
  }
649
674
  wrap.style.width = '100%';
650
675
 
676
+ // If we're inserted into a UL/OL list, use an LI wrapper to keep valid markup.
677
+ // Otherwise browsers can re-parent the DIV, which looks like items/ads “remontent”
678
+ // and can break topic list rendering.
679
+ if ((tagName || '').toLowerCase() === 'li') {
680
+ wrap.style.listStyle = 'none';
681
+ wrap.style.padding = '0';
682
+ wrap.style.margin = '0';
683
+ }
684
+
651
685
  if (createPlaceholder) {
652
686
  const ph = document.createElement('div');
653
687
  ph.id = `${PLACEHOLDER_PREFIX}${id}`;
@@ -667,7 +701,10 @@ function globalGapFixInit() {
667
701
 
668
702
  insertingIds.add(id);
669
703
  try {
670
- const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
704
+ const parent = target.parentElement;
705
+ const parentTag = parent && parent.tagName ? parent.tagName.toUpperCase() : '';
706
+ const tagName = (parentTag === 'UL' || parentTag === 'OL') ? 'li' : 'div';
707
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh, tagName);
671
708
  target.insertAdjacentElement('afterend', wrap);
672
709
 
673
710
  // If placeholder exists elsewhere (including pool), move it into the wrapper.
@@ -679,6 +716,9 @@ function globalGapFixInit() {
679
716
  } catch (e) {}
680
717
  }
681
718
 
719
+ // Help creatives render promptly without requiring an actual user scroll.
720
+ scheduleViewportPoke(`afterInsert:${kindClass}:${id}`);
721
+
682
722
  return wrap;
683
723
  } finally {
684
724
  insertingIds.delete(id);
@@ -1511,16 +1551,21 @@ function buildOrdinalMap(items) {
1511
1551
 
1512
1552
 
1513
1553
 
1514
- // ===== V17 minimal pile-fix (no insert hooks) =====
1554
+
1555
+
1556
+
1557
+ // ===== V17.8: No pile-up by design — keep "future" between slots pending/collapsed until their anchor topic exists =====
1515
1558
  (function () {
1516
- // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1517
1559
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1518
1560
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1519
1561
  var HOST_CLASS = 'nodebb-ezoic-host';
1562
+ var PENDING_CLASS = 'nodebb-ezoic-pending';
1520
1563
 
1564
+ var ul = null;
1565
+ var mo = null;
1521
1566
  var scheduled = false;
1522
1567
  var lastRun = 0;
1523
- var COOLDOWN = 180;
1568
+ var COOLDOWN = 80;
1524
1569
 
1525
1570
  function getTopicList() {
1526
1571
  try {
@@ -1530,29 +1575,41 @@ function buildOrdinalMap(items) {
1530
1575
  } catch (e) { return null; }
1531
1576
  }
1532
1577
 
1533
- function isHost(node) {
1534
- return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
1578
+ function ensureUL() { ul = ul || getTopicList(); return ul; }
1579
+
1580
+ function isHost(el) {
1581
+ return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1535
1582
  }
1536
1583
 
1537
- function ensureHostForWrap(wrap, ul) {
1538
- try {
1539
- if (!wrap || wrap.nodeType !== 1) return null;
1540
- if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1584
+ function isBetweenWrap(el) {
1585
+ try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1586
+ }
1541
1587
 
1588
+ function ensureHostForWrap(wrap, ulEl) {
1589
+ // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1590
+ try {
1591
+ if (!wrap || !isBetweenWrap(wrap)) return null;
1542
1592
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1543
1593
  if (host) return host;
1544
1594
 
1545
- if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1546
- if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1595
+ ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1596
+ if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1547
1597
 
1548
- // Only wrap if direct child of list (invalid / fragile)
1549
- if (wrap.parentElement === ul) {
1598
+ if (wrap.parentElement === ulEl) {
1550
1599
  host = document.createElement('li');
1551
1600
  host.className = HOST_CLASS;
1552
1601
  host.setAttribute('role', 'listitem');
1553
1602
  host.style.listStyle = 'none';
1554
1603
  host.style.width = '100%';
1555
- ul.insertBefore(host, wrap);
1604
+
1605
+ try {
1606
+ var after = wrap.getAttribute('data-ezoic-after');
1607
+ if (after) host.setAttribute('data-ezoic-after', after);
1608
+ var pin = wrap.getAttribute('data-ezoic-pin');
1609
+ if (pin) host.setAttribute('data-ezoic-pin', pin);
1610
+ } catch (e) {}
1611
+
1612
+ ulEl.insertBefore(host, wrap);
1556
1613
  host.appendChild(wrap);
1557
1614
  try { wrap.style.width = '100%'; } catch (e) {}
1558
1615
  return host;
@@ -1561,268 +1618,147 @@ function buildOrdinalMap(items) {
1561
1618
  return null;
1562
1619
  }
1563
1620
 
1564
- function previousTopicLi(node) {
1621
+ function getAfter(el) {
1565
1622
  try {
1566
- var prev = node.previousElementSibling;
1567
- while (prev) {
1568
- if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1569
- // skip other hosts/wraps
1570
- prev = prev.previousElementSibling;
1623
+ var v = null;
1624
+ if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1625
+ if (!v && el && el.querySelector) {
1626
+ var w = el.querySelector(BETWEEN_WRAP_SEL);
1627
+ if (w) v = w.getAttribute('data-ezoic-after');
1571
1628
  }
1572
- } catch (e) {}
1573
- return null;
1629
+ var n = parseInt(v, 10);
1630
+ return isNaN(n) ? null : n;
1631
+ } catch (e) { return null; }
1632
+ }
1633
+
1634
+ function getTopics(ulEl) {
1635
+ try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1574
1636
  }
1575
1637
 
1576
- function detectPileUp(ul) {
1577
- // Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
1638
+ function lastTopic(ulEl, topics) {
1578
1639
  try {
1579
- var kids = ul.children;
1580
- var run = 0;
1581
- var maxRun = 0;
1582
- for (var i = 0; i < kids.length; i++) {
1583
- var el = kids[i];
1584
- var isBetween = false;
1585
- if (isHost(el)) {
1586
- isBetween = !!(el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
1587
- } else if (el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1588
- isBetween = true;
1589
- }
1590
- if (isBetween) {
1591
- run++;
1592
- if (run > maxRun) maxRun = run;
1593
- } else if (el.matches && el.matches(TOPIC_LI_SEL)) {
1594
- run = 0;
1595
- } else {
1596
- // other nodes reset lightly
1597
- run = 0;
1598
- }
1599
- }
1600
- return maxRun >= 2;
1640
+ topics = topics || getTopics(ulEl);
1641
+ return topics && topics.length ? topics[topics.length - 1] : null;
1642
+ } catch (e) { return null; }
1643
+ }
1644
+
1645
+ function moveAfter(node, anchor) {
1646
+ try {
1647
+ if (!node || !anchor || !anchor.insertAdjacentElement) return;
1648
+ if (node.previousElementSibling === anchor) return;
1649
+ anchor.insertAdjacentElement('afterend', node);
1601
1650
  } catch (e) {}
1602
- return false;
1603
1651
  }
1604
1652
 
1605
- function redistribute(ul) {
1653
+ function placeOrPend(host, ulEl, topics) {
1606
1654
  try {
1607
- if (!ul) return;
1655
+ if (!host) return;
1656
+ ulEl = ulEl || ensureUL();
1657
+ if (!ulEl) return;
1608
1658
 
1609
- // Step 1: wrap any direct child between DIVs into LI hosts (makes list stable)
1610
- ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); });
1659
+ topics = topics || getTopics(ulEl);
1611
1660
 
1612
- // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
- if (!detectPileUp(ul)) return;
1661
+ var after = getAfter(host);
1662
+ if (!after) {
1663
+ var lt = lastTopic(ulEl, topics);
1664
+ if (lt) moveAfter(host, lt);
1665
+ host.classList && host.classList.remove(PENDING_CLASS);
1666
+ return;
1667
+ }
1614
1668
 
1615
- // Move each host to immediately after the closest previous topic LI at its current position.
1616
- var hosts = ul.querySelectorAll(':scope > li.' + HOST_CLASS);
1617
- hosts.forEach(function(host){
1618
- try {
1619
- var wrap = host.querySelector && host.querySelector(BETWEEN_WRAP_SEL);
1620
- if (!wrap) return;
1669
+ if (after > topics.length) {
1670
+ var lt2 = lastTopic(ulEl, topics);
1671
+ if (lt2) moveAfter(host, lt2);
1672
+ host.classList && host.classList.add(PENDING_CLASS);
1673
+ return;
1674
+ }
1621
1675
 
1622
- var anchor = previousTopicLi(host);
1623
- if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1676
+ var anchor = topics[after - 1];
1677
+ if (anchor) moveAfter(host, anchor);
1678
+ host.classList && host.classList.remove(PENDING_CLASS);
1679
+ } catch (e) {}
1680
+ }
1624
1681
 
1625
- if (host.previousElementSibling !== anchor) {
1626
- anchor.insertAdjacentElement('afterend', host);
1627
- }
1628
- } catch (e) {}
1682
+ function reconcile() {
1683
+ var ulEl = ensureUL();
1684
+ if (!ulEl) return;
1685
+
1686
+ var topics = getTopics(ulEl);
1687
+
1688
+ try {
1689
+ ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1690
+ var h = ensureHostForWrap(w, ulEl);
1691
+ if (h) placeOrPend(h, ulEl, topics);
1692
+ });
1693
+
1694
+ ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1695
+ placeOrPend(h, ulEl, topics);
1696
+ });
1697
+
1698
+ ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1699
+ var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1700
+ if (h) placeOrPend(h, ulEl, topics);
1629
1701
  });
1630
1702
  } catch (e) {}
1631
1703
  }
1632
1704
 
1633
- function schedule(reason) {
1705
+ function scheduleReconcile() {
1634
1706
  var now = Date.now();
1635
- if (now - lastRun < COOLDOWN) return;
1636
1707
  if (scheduled) return;
1708
+ if (now - lastRun < COOLDOWN) return;
1637
1709
  scheduled = true;
1710
+ lastRun = now;
1638
1711
  requestAnimationFrame(function () {
1639
1712
  scheduled = false;
1640
- lastRun = Date.now();
1641
- try {
1642
- var ul = getTopicList();
1643
- if (!ul) return;
1644
- redistribute(ul);
1645
- } catch (e) {}
1713
+ reconcile();
1646
1714
  });
1647
1715
  }
1648
1716
 
1649
- function init() {
1650
- schedule('init');
1717
+ function initObserver() {
1718
+ var ulEl = ensureUL();
1719
+ if (!ulEl || mo) return;
1651
1720
 
1652
- // Observe only the topic list once available
1653
- try {
1654
- if (typeof MutationObserver !== 'undefined') {
1655
- var observeList = function(ul){
1656
- if (!ul) return;
1657
- var mo = new MutationObserver(function(muts){
1658
- // schedule on any change; redistribute() itself is guarded by pile-up detection
1659
- schedule('mo');
1660
- });
1661
- mo.observe(ul, { childList: true, subtree: true });
1662
- };
1721
+ mo = new MutationObserver(function () {
1722
+ scheduleReconcile();
1723
+ });
1663
1724
 
1664
- var ul = getTopicList();
1665
- if (ul) observeList(ul);
1666
- else {
1667
- var mo2 = new MutationObserver(function(){
1668
- var u2 = getTopicList();
1669
- if (u2) {
1670
- try { observeList(u2); } catch(e){}
1671
- try { mo2.disconnect(); } catch(e){}
1672
- }
1673
- });
1674
- mo2.observe(document.documentElement || document.body, { childList: true, subtree: true });
1675
- }
1676
- }
1677
- } catch (e) {}
1725
+ mo.observe(ulEl, { childList: true, subtree: true });
1726
+ }
1727
+
1728
+ function init() {
1729
+ scheduleViewportPoke('init');
1730
+ initObserver();
1731
+ scheduleReconcile();
1678
1732
 
1679
- // NodeBB events: run after infinite scroll batches
1680
1733
  if (window.jQuery) {
1681
1734
  try {
1682
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1683
- setTimeout(function(){ schedule('event'); }, 50);
1684
- setTimeout(function(){ schedule('event2'); }, 400);
1735
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1736
+ ul = null;
1737
+ try { if (mo) mo.disconnect(); } catch (e) {}
1738
+ mo = null;
1739
+ initObserver();
1740
+ scheduleReconcile();
1741
+ scheduleViewportPoke('ajaxify.end');
1742
+ setTimeout(scheduleReconcile, 120);
1743
+ setTimeout(scheduleReconcile, 500);
1685
1744
  });
1686
1745
  } catch (e) {}
1687
1746
  }
1747
+
1748
+ var tries = 0;
1749
+ var t = setInterval(function () {
1750
+ tries++;
1751
+ if (ensureUL()) {
1752
+ clearInterval(t);
1753
+ initObserver();
1754
+ scheduleReconcile();
1755
+ }
1756
+ if (tries > 30) clearInterval(t);
1757
+ }, 200);
1688
1758
  }
1689
1759
 
1690
1760
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1691
1761
  else init();
1692
1762
  })();
1693
- // ===== /V17 =====
1694
-
1695
-
1696
-
1697
- // ===== V17 empty-refresh (no move) =====
1698
- // Goal: if a between-ad is visible but still has no ad iframe, gently poke Ezoic/GPT to render.
1699
- (function () {
1700
- var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1701
- var seen = new WeakMap();
1702
- var io = null;
1703
- // NOTE: Calling ezstandalone.showAds() was triggering extra bidder requests
1704
- // (and in your setup, surfacing ServiceWorker/CORS failures) which made
1705
- // fills slower and created visible repositioning. We only do a tiny scroll
1706
- // nudge here, because manual micro-scroll reliably triggers a fill.
1707
- var microScrollCooldownUntil = 0;
1708
-
1709
- function now() { return Date.now ? Date.now() : +new Date(); }
1710
-
1711
- function getSlotId(wrap) {
1712
- try {
1713
- var gpt = wrap.querySelector('[id^="div-gpt-ad-"]');
1714
- return gpt ? gpt.id : null;
1715
- } catch (e) { return null; }
1716
- }
1717
-
1718
- function hasAnyCreative(wrap) {
1719
- try {
1720
- // Any iframe (safeframe or normal) inside the wrap counts as rendered.
1721
- if (wrap.querySelector('iframe')) return true;
1722
- // Some formats use ins/amp/other nodes.
1723
- if (wrap.querySelector('amp-ad, amp-embed, ins.adsbygoogle')) return true;
1724
- } catch (e) {}
1725
- return false;
1726
- }
1727
-
1728
- function microScrollNudge() {
1729
- var t = now();
1730
- if (t < microScrollCooldownUntil) return;
1731
- microScrollCooldownUntil = t + 1200;
1732
-
1733
- // Prefer a real 1px scroll + restore: this matches the observed manual fix.
1734
- try {
1735
- var y = window.pageYOffset || document.documentElement.scrollTop || 0;
1736
- window.scrollTo(0, y + 1);
1737
- (window.requestAnimationFrame || setTimeout)(function () {
1738
- try { window.scrollTo(0, y); } catch (e) {}
1739
- try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1740
- }, 16);
1741
- return;
1742
- } catch (e) {}
1743
-
1744
- // Fallback: nudge listeners.
1745
- try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1746
- }
1747
-
1748
- function scheduleCheck(wrap) {
1749
- try {
1750
- if (!wrap || wrap.nodeType !== 1) return;
1751
- if (hasAnyCreative(wrap)) return;
1752
-
1753
- var meta = seen.get(wrap);
1754
- var t = now();
1755
- if (!meta) {
1756
- meta = { firstSeen: t, checks: 0 };
1757
- seen.set(wrap, meta);
1758
- }
1759
-
1760
- // Give the normal pipeline time to render.
1761
- if (meta.checks === 0) {
1762
- setTimeout(function () { scheduleCheck(wrap); }, 700);
1763
- meta.checks++;
1764
- return;
1765
- }
1766
-
1767
- if (hasAnyCreative(wrap)) return;
1768
-
1769
- // Still empty -> poke (micro-scroll only; no showAds/refresh calls).
1770
- var id = getSlotId(wrap) || 'none';
1771
- try { console.log('[EZ EMPTY]', id); } catch (e) {}
1772
- microScrollNudge();
1773
-
1774
- // Re-check once after the nudge, but do not loop.
1775
- setTimeout(function () {
1776
- if (!wrap.isConnected) return;
1777
- if (hasAnyCreative(wrap)) return;
1778
- // Still empty: leave it to the native pipeline; avoid repeated nudges.
1779
- }, 900);
1780
- } catch (e) {}
1781
- }
1782
-
1783
- function ensureIO() {
1784
- if (io) return;
1785
- if (!('IntersectionObserver' in window)) return;
1763
+ // ===== /V17.8 =====
1786
1764
 
1787
- io = new IntersectionObserver(function (entries) {
1788
- for (var i = 0; i < entries.length; i++) {
1789
- var e = entries[i];
1790
- if (!e.isIntersecting) continue;
1791
- scheduleCheck(e.target);
1792
- }
1793
- }, { root: null, rootMargin: '900px 0px 900px 0px', threshold: 0.01 });
1794
-
1795
- // Observe existing wraps.
1796
- try {
1797
- var wraps = document.querySelectorAll(BETWEEN_WRAP_SEL);
1798
- for (var j = 0; j < wraps.length; j++) io.observe(wraps[j]);
1799
- } catch (e) {}
1800
-
1801
- // Observe new wraps.
1802
- try {
1803
- var mo = new MutationObserver(function (muts) {
1804
- for (var k = 0; k < muts.length; k++) {
1805
- var m = muts[k];
1806
- if (!m.addedNodes) continue;
1807
- for (var n = 0; n < m.addedNodes.length; n++) {
1808
- var node = m.addedNodes[n];
1809
- if (!node || node.nodeType !== 1) continue;
1810
- if (node.matches && node.matches(BETWEEN_WRAP_SEL)) {
1811
- io.observe(node);
1812
- } else if (node.querySelectorAll) {
1813
- var inner = node.querySelectorAll(BETWEEN_WRAP_SEL);
1814
- for (var q = 0; q < inner.length; q++) io.observe(inner[q]);
1815
- }
1816
- }
1817
- }
1818
- });
1819
- mo.observe(document.body, { childList: true, subtree: true });
1820
- } catch (e) {}
1821
- }
1822
-
1823
- if (document.readyState === 'loading') {
1824
- document.addEventListener('DOMContentLoaded', ensureIO, { once: true });
1825
- } else {
1826
- ensureIO();
1827
- }
1828
- })();
package/public/style.css CHANGED
@@ -86,3 +86,20 @@ li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
86
86
  li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
87
87
  /* ===== /V17 ===== */
88
88
 
89
+
90
+
91
+ /* ===== V17.8 pending slots (prevents pile-up top/bottom) ===== */
92
+ li.nodebb-ezoic-host { list-style:none; width:100%; display:block; }
93
+ li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width:100%; display:block; }
94
+
95
+ /* slots whose anchor topic isn't loaded yet */
96
+ li.nodebb-ezoic-host.nodebb-ezoic-pending{
97
+ max-height: 0 !important;
98
+ margin: 0 !important;
99
+ padding: 0 !important;
100
+ overflow: hidden !important;
101
+ opacity: 0 !important;
102
+ pointer-events: none !important;
103
+ }
104
+ /* ===== /V17.8 ===== */
105
+