nodebb-plugin-ezoic-infinite 1.6.47 → 1.6.49

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.47",
3
+ "version": "1.6.49",
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.
@@ -679,6 +704,9 @@ function globalGapFixInit() {
679
704
  } catch (e) {}
680
705
  }
681
706
 
707
+ // Help creatives render promptly without requiring an actual user scroll.
708
+ scheduleViewportPoke(`afterInsert:${kindClass}:${id}`);
709
+
682
710
  return wrap;
683
711
  } finally {
684
712
  insertingIds.delete(id);
@@ -1511,16 +1539,21 @@ function buildOrdinalMap(items) {
1511
1539
 
1512
1540
 
1513
1541
 
1514
- // ===== V17 minimal pile-fix (no insert hooks) =====
1542
+
1543
+
1544
+
1545
+ // ===== V17.8: No pile-up by design — keep "future" between slots pending/collapsed until their anchor topic exists =====
1515
1546
  (function () {
1516
- // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1517
1547
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1518
1548
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1519
1549
  var HOST_CLASS = 'nodebb-ezoic-host';
1550
+ var PENDING_CLASS = 'nodebb-ezoic-pending';
1520
1551
 
1552
+ var ul = null;
1553
+ var mo = null;
1521
1554
  var scheduled = false;
1522
1555
  var lastRun = 0;
1523
- var COOLDOWN = 180;
1556
+ var COOLDOWN = 80;
1524
1557
 
1525
1558
  function getTopicList() {
1526
1559
  try {
@@ -1530,29 +1563,41 @@ function buildOrdinalMap(items) {
1530
1563
  } catch (e) { return null; }
1531
1564
  }
1532
1565
 
1533
- function isHost(node) {
1534
- return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
1566
+ function ensureUL() { ul = ul || getTopicList(); return ul; }
1567
+
1568
+ function isHost(el) {
1569
+ return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1535
1570
  }
1536
1571
 
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;
1572
+ function isBetweenWrap(el) {
1573
+ try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1574
+ }
1541
1575
 
1576
+ function ensureHostForWrap(wrap, ulEl) {
1577
+ // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1578
+ try {
1579
+ if (!wrap || !isBetweenWrap(wrap)) return null;
1542
1580
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1543
1581
  if (host) return host;
1544
1582
 
1545
- if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1546
- if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1583
+ ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1584
+ if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1547
1585
 
1548
- // Only wrap if direct child of list (invalid / fragile)
1549
- if (wrap.parentElement === ul) {
1586
+ if (wrap.parentElement === ulEl) {
1550
1587
  host = document.createElement('li');
1551
1588
  host.className = HOST_CLASS;
1552
1589
  host.setAttribute('role', 'listitem');
1553
1590
  host.style.listStyle = 'none';
1554
1591
  host.style.width = '100%';
1555
- ul.insertBefore(host, wrap);
1592
+
1593
+ try {
1594
+ var after = wrap.getAttribute('data-ezoic-after');
1595
+ if (after) host.setAttribute('data-ezoic-after', after);
1596
+ var pin = wrap.getAttribute('data-ezoic-pin');
1597
+ if (pin) host.setAttribute('data-ezoic-pin', pin);
1598
+ } catch (e) {}
1599
+
1600
+ ulEl.insertBefore(host, wrap);
1556
1601
  host.appendChild(wrap);
1557
1602
  try { wrap.style.width = '100%'; } catch (e) {}
1558
1603
  return host;
@@ -1561,262 +1606,147 @@ function buildOrdinalMap(items) {
1561
1606
  return null;
1562
1607
  }
1563
1608
 
1564
- function previousTopicLi(node) {
1609
+ function getAfter(el) {
1565
1610
  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;
1611
+ var v = null;
1612
+ if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1613
+ if (!v && el && el.querySelector) {
1614
+ var w = el.querySelector(BETWEEN_WRAP_SEL);
1615
+ if (w) v = w.getAttribute('data-ezoic-after');
1571
1616
  }
1572
- } catch (e) {}
1573
- return null;
1617
+ var n = parseInt(v, 10);
1618
+ return isNaN(n) ? null : n;
1619
+ } catch (e) { return null; }
1574
1620
  }
1575
1621
 
1576
- function detectPileUp(ul) {
1577
- // Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
1622
+ function getTopics(ulEl) {
1623
+ try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1624
+ }
1625
+
1626
+ function lastTopic(ulEl, topics) {
1578
1627
  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;
1628
+ topics = topics || getTopics(ulEl);
1629
+ return topics && topics.length ? topics[topics.length - 1] : null;
1630
+ } catch (e) { return null; }
1631
+ }
1632
+
1633
+ function moveAfter(node, anchor) {
1634
+ try {
1635
+ if (!node || !anchor || !anchor.insertAdjacentElement) return;
1636
+ if (node.previousElementSibling === anchor) return;
1637
+ anchor.insertAdjacentElement('afterend', node);
1601
1638
  } catch (e) {}
1602
- return false;
1603
1639
  }
1604
1640
 
1605
- function redistribute(ul) {
1641
+ function placeOrPend(host, ulEl, topics) {
1606
1642
  try {
1607
- if (!ul) return;
1643
+ if (!host) return;
1644
+ ulEl = ulEl || ensureUL();
1645
+ if (!ulEl) return;
1608
1646
 
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); });
1647
+ topics = topics || getTopics(ulEl);
1611
1648
 
1612
- // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
- if (!detectPileUp(ul)) return;
1649
+ var after = getAfter(host);
1650
+ if (!after) {
1651
+ var lt = lastTopic(ulEl, topics);
1652
+ if (lt) moveAfter(host, lt);
1653
+ host.classList && host.classList.remove(PENDING_CLASS);
1654
+ return;
1655
+ }
1614
1656
 
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;
1657
+ if (after > topics.length) {
1658
+ var lt2 = lastTopic(ulEl, topics);
1659
+ if (lt2) moveAfter(host, lt2);
1660
+ host.classList && host.classList.add(PENDING_CLASS);
1661
+ return;
1662
+ }
1621
1663
 
1622
- var anchor = previousTopicLi(host);
1623
- if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1664
+ var anchor = topics[after - 1];
1665
+ if (anchor) moveAfter(host, anchor);
1666
+ host.classList && host.classList.remove(PENDING_CLASS);
1667
+ } catch (e) {}
1668
+ }
1624
1669
 
1625
- if (host.previousElementSibling !== anchor) {
1626
- anchor.insertAdjacentElement('afterend', host);
1627
- }
1628
- } catch (e) {}
1670
+ function reconcile() {
1671
+ var ulEl = ensureUL();
1672
+ if (!ulEl) return;
1673
+
1674
+ var topics = getTopics(ulEl);
1675
+
1676
+ try {
1677
+ ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1678
+ var h = ensureHostForWrap(w, ulEl);
1679
+ if (h) placeOrPend(h, ulEl, topics);
1680
+ });
1681
+
1682
+ ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1683
+ placeOrPend(h, ulEl, topics);
1684
+ });
1685
+
1686
+ ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1687
+ var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1688
+ if (h) placeOrPend(h, ulEl, topics);
1629
1689
  });
1630
1690
  } catch (e) {}
1631
1691
  }
1632
1692
 
1633
- function schedule(reason) {
1693
+ function scheduleReconcile() {
1634
1694
  var now = Date.now();
1635
- if (now - lastRun < COOLDOWN) return;
1636
1695
  if (scheduled) return;
1696
+ if (now - lastRun < COOLDOWN) return;
1637
1697
  scheduled = true;
1698
+ lastRun = now;
1638
1699
  requestAnimationFrame(function () {
1639
1700
  scheduled = false;
1640
- lastRun = Date.now();
1641
- try {
1642
- var ul = getTopicList();
1643
- if (!ul) return;
1644
- redistribute(ul);
1645
- } catch (e) {}
1701
+ reconcile();
1646
1702
  });
1647
1703
  }
1648
1704
 
1649
- function init() {
1650
- schedule('init');
1705
+ function initObserver() {
1706
+ var ulEl = ensureUL();
1707
+ if (!ulEl || mo) return;
1651
1708
 
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
- };
1709
+ mo = new MutationObserver(function () {
1710
+ scheduleReconcile();
1711
+ });
1663
1712
 
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) {}
1713
+ mo.observe(ulEl, { childList: true, subtree: true });
1714
+ }
1715
+
1716
+ function init() {
1717
+ scheduleViewportPoke('init');
1718
+ initObserver();
1719
+ scheduleReconcile();
1678
1720
 
1679
- // NodeBB events: run after infinite scroll batches
1680
1721
  if (window.jQuery) {
1681
1722
  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);
1723
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1724
+ ul = null;
1725
+ try { if (mo) mo.disconnect(); } catch (e) {}
1726
+ mo = null;
1727
+ initObserver();
1728
+ scheduleReconcile();
1729
+ scheduleViewportPoke('ajaxify.end');
1730
+ setTimeout(scheduleReconcile, 120);
1731
+ setTimeout(scheduleReconcile, 500);
1685
1732
  });
1686
1733
  } catch (e) {}
1687
1734
  }
1735
+
1736
+ var tries = 0;
1737
+ var t = setInterval(function () {
1738
+ tries++;
1739
+ if (ensureUL()) {
1740
+ clearInterval(t);
1741
+ initObserver();
1742
+ scheduleReconcile();
1743
+ }
1744
+ if (tries > 30) clearInterval(t);
1745
+ }, 200);
1688
1746
  }
1689
1747
 
1690
1748
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1691
1749
  else init();
1692
1750
  })();
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
- var showAdsCooldownUntil = 0;
1751
+ // ===== /V17.8 =====
1704
1752
 
1705
- function now() { return Date.now ? Date.now() : +new Date(); }
1706
-
1707
- function getSlotId(wrap) {
1708
- try {
1709
- var gpt = wrap.querySelector('[id^="div-gpt-ad-"]');
1710
- return gpt ? gpt.id : null;
1711
- } catch (e) { return null; }
1712
- }
1713
-
1714
- function hasAnyCreative(wrap) {
1715
- try {
1716
- // Any iframe (safeframe or normal) inside the wrap counts as rendered.
1717
- if (wrap.querySelector('iframe')) return true;
1718
- // Some formats use ins/amp/other nodes.
1719
- if (wrap.querySelector('amp-ad, amp-embed, ins.adsbygoogle')) return true;
1720
- } catch (e) {}
1721
- return false;
1722
- }
1723
-
1724
- function safeShowAds() {
1725
- var t = now();
1726
- if (t < showAdsCooldownUntil) return;
1727
- showAdsCooldownUntil = t + 1200;
1728
-
1729
- try {
1730
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1731
- window.ezstandalone.showAds();
1732
- return;
1733
- }
1734
- } catch (e) {}
1735
-
1736
- // Fallback: nudge common listeners.
1737
- try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1738
- try { window.dispatchEvent(new Event('resize')); } catch (e) {}
1739
- }
1740
-
1741
- function scheduleCheck(wrap) {
1742
- try {
1743
- if (!wrap || wrap.nodeType !== 1) return;
1744
- if (hasAnyCreative(wrap)) return;
1745
-
1746
- var meta = seen.get(wrap);
1747
- var t = now();
1748
- if (!meta) {
1749
- meta = { firstSeen: t, checks: 0 };
1750
- seen.set(wrap, meta);
1751
- }
1752
-
1753
- // Give the normal pipeline time to render.
1754
- if (meta.checks === 0) {
1755
- setTimeout(function () { scheduleCheck(wrap); }, 700);
1756
- meta.checks++;
1757
- return;
1758
- }
1759
-
1760
- if (hasAnyCreative(wrap)) return;
1761
-
1762
- // Still empty -> poke.
1763
- var id = getSlotId(wrap) || 'none';
1764
- try { console.log('[EZ EMPTY]', id); } catch (e) {}
1765
- safeShowAds();
1766
-
1767
- // One more re-check shortly after the poke.
1768
- setTimeout(function () {
1769
- if (!wrap.isConnected) return;
1770
- if (hasAnyCreative(wrap)) return;
1771
- // If still empty after a poke, try once more (but do not loop forever).
1772
- safeShowAds();
1773
- }, 900);
1774
- } catch (e) {}
1775
- }
1776
-
1777
- function ensureIO() {
1778
- if (io) return;
1779
- if (!('IntersectionObserver' in window)) return;
1780
-
1781
- io = new IntersectionObserver(function (entries) {
1782
- for (var i = 0; i < entries.length; i++) {
1783
- var e = entries[i];
1784
- if (!e.isIntersecting) continue;
1785
- scheduleCheck(e.target);
1786
- }
1787
- }, { root: null, rootMargin: '900px 0px 900px 0px', threshold: 0.01 });
1788
-
1789
- // Observe existing wraps.
1790
- try {
1791
- var wraps = document.querySelectorAll(BETWEEN_WRAP_SEL);
1792
- for (var j = 0; j < wraps.length; j++) io.observe(wraps[j]);
1793
- } catch (e) {}
1794
-
1795
- // Observe new wraps.
1796
- try {
1797
- var mo = new MutationObserver(function (muts) {
1798
- for (var k = 0; k < muts.length; k++) {
1799
- var m = muts[k];
1800
- if (!m.addedNodes) continue;
1801
- for (var n = 0; n < m.addedNodes.length; n++) {
1802
- var node = m.addedNodes[n];
1803
- if (!node || node.nodeType !== 1) continue;
1804
- if (node.matches && node.matches(BETWEEN_WRAP_SEL)) {
1805
- io.observe(node);
1806
- } else if (node.querySelectorAll) {
1807
- var inner = node.querySelectorAll(BETWEEN_WRAP_SEL);
1808
- for (var q = 0; q < inner.length; q++) io.observe(inner[q]);
1809
- }
1810
- }
1811
- }
1812
- });
1813
- mo.observe(document.body, { childList: true, subtree: true });
1814
- } catch (e) {}
1815
- }
1816
-
1817
- if (document.readyState === 'loading') {
1818
- document.addEventListener('DOMContentLoaded', ensureIO, { once: true });
1819
- } else {
1820
- ensureIO();
1821
- }
1822
- })();
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
+