nodebb-plugin-ezoic-infinite 1.6.39 → 1.6.41

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.39",
3
+ "version": "1.6.41",
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
@@ -498,48 +498,6 @@ function globalGapFixInit() {
498
498
  return el;
499
499
  }
500
500
 
501
- // Best-effort refresh for Ezoic standalone when we defer placement.
502
- // Some stacks won't fill ads if a placeholder is rendered while collapsed/0px.
503
- let _refreshTimer = null;
504
-
505
- function collectEzoicIds(root) {
506
- const ids = new Set();
507
- if (!root || !root.querySelectorAll) return [];
508
- // Common Ezoic placeholder ids
509
- root.querySelectorAll('[id^="ezoic-pub-ad-placeholder-"]').forEach(el => ids.add(el.id));
510
- // Some integrations keep ids on wrappers
511
- root.querySelectorAll('[data-ezoic-id]').forEach(el => {
512
- if (el.id) ids.add(el.id);
513
- });
514
- return Array.from(ids);
515
- }
516
-
517
- function refreshAds(ids) {
518
- clearTimeout(_refreshTimer);
519
- _refreshTimer = setTimeout(() => {
520
- try {
521
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
522
- // Prefer a full rescan; fall back to ids if supported.
523
- try {
524
- window.ezstandalone.showAds();
525
- } catch (e) {
526
- if (ids && ids.length) window.ezstandalone.showAds(ids);
527
- }
528
- if (window.__EZ_DEBUG) console.debug('[ezoic-infinite] refreshAds (ezstandalone)', { ids });
529
- return;
530
- }
531
- if (window.ez && typeof window.ez.showAds === 'function') {
532
- try {
533
- window.ez.showAds();
534
- } catch (e) {
535
- if (ids && ids.length) window.ez.showAds(ids);
536
- }
537
- if (window.__EZ_DEBUG) console.debug('[ezoic-infinite] refreshAds (ez)', { ids });
538
- }
539
- } catch (e) {}
540
- }, 75);
541
- }
542
-
543
501
  function primePlaceholderPool(allIds) {
544
502
  try {
545
503
  if (!Array.isArray(allIds) || !allIds.length) return;
@@ -1553,21 +1511,16 @@ function buildOrdinalMap(items) {
1553
1511
 
1554
1512
 
1555
1513
 
1556
-
1557
-
1558
-
1559
- // ===== V17.8: No pile-up by design — keep "future" between slots pending/collapsed until their anchor topic exists =====
1514
+ // ===== V17 minimal pile-fix (no insert hooks) =====
1560
1515
  (function () {
1516
+ // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1561
1517
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1562
1518
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1563
1519
  var HOST_CLASS = 'nodebb-ezoic-host';
1564
- var PENDING_CLASS = 'nodebb-ezoic-pending';
1565
1520
 
1566
- var ul = null;
1567
- var mo = null;
1568
1521
  var scheduled = false;
1569
1522
  var lastRun = 0;
1570
- var COOLDOWN = 80;
1523
+ var COOLDOWN = 180;
1571
1524
 
1572
1525
  function getTopicList() {
1573
1526
  try {
@@ -1577,41 +1530,29 @@ function buildOrdinalMap(items) {
1577
1530
  } catch (e) { return null; }
1578
1531
  }
1579
1532
 
1580
- function ensureUL() { ul = ul || getTopicList(); return ul; }
1581
-
1582
- function isHost(el) {
1583
- return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1533
+ function isHost(node) {
1534
+ return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
1584
1535
  }
1585
1536
 
1586
- function isBetweenWrap(el) {
1587
- try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1588
- }
1589
-
1590
- function ensureHostForWrap(wrap, ulEl) {
1591
- // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1537
+ function ensureHostForWrap(wrap, ul) {
1592
1538
  try {
1593
- if (!wrap || !isBetweenWrap(wrap)) return null;
1539
+ if (!wrap || wrap.nodeType !== 1) return null;
1540
+ if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1541
+
1594
1542
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1595
1543
  if (host) return host;
1596
1544
 
1597
- ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1598
- if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1545
+ if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1546
+ if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1599
1547
 
1600
- if (wrap.parentElement === ulEl) {
1548
+ // Only wrap if direct child of list (invalid / fragile)
1549
+ if (wrap.parentElement === ul) {
1601
1550
  host = document.createElement('li');
1602
1551
  host.className = HOST_CLASS;
1603
1552
  host.setAttribute('role', 'listitem');
1604
1553
  host.style.listStyle = 'none';
1605
1554
  host.style.width = '100%';
1606
-
1607
- try {
1608
- var after = wrap.getAttribute('data-ezoic-after');
1609
- if (after) host.setAttribute('data-ezoic-after', after);
1610
- var pin = wrap.getAttribute('data-ezoic-pin');
1611
- if (pin) host.setAttribute('data-ezoic-pin', pin);
1612
- } catch (e) {}
1613
-
1614
- ulEl.insertBefore(host, wrap);
1555
+ ul.insertBefore(host, wrap);
1615
1556
  host.appendChild(wrap);
1616
1557
  try { wrap.style.width = '100%'; } catch (e) {}
1617
1558
  return host;
@@ -1620,151 +1561,296 @@ function buildOrdinalMap(items) {
1620
1561
  return null;
1621
1562
  }
1622
1563
 
1623
- function getAfter(el) {
1564
+ function previousTopicLi(node) {
1624
1565
  try {
1625
- var v = null;
1626
- if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1627
- if (!v && el && el.querySelector) {
1628
- var w = el.querySelector(BETWEEN_WRAP_SEL);
1629
- if (w) v = w.getAttribute('data-ezoic-after');
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;
1630
1571
  }
1631
- var n = parseInt(v, 10);
1632
- return isNaN(n) ? null : n;
1633
- } catch (e) { return null; }
1634
- }
1635
-
1636
- function getTopics(ulEl) {
1637
- try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1572
+ } catch (e) {}
1573
+ return null;
1638
1574
  }
1639
1575
 
1640
- function lastTopic(ulEl, topics) {
1576
+ function detectPileUp(ul) {
1577
+ // Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
1641
1578
  try {
1642
- topics = topics || getTopics(ulEl);
1643
- return topics && topics.length ? topics[topics.length - 1] : null;
1644
- } catch (e) { return null; }
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;
1601
+ } catch (e) {}
1602
+ return false;
1645
1603
  }
1646
1604
 
1647
- function moveAfter(node, anchor) {
1605
+ function redistribute(ul) {
1648
1606
  try {
1649
- if (!node || !anchor || !anchor.insertAdjacentElement) return;
1650
- if (node.previousElementSibling === anchor) return;
1651
- anchor.insertAdjacentElement('afterend', node);
1607
+ if (!ul) return;
1608
+
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); });
1611
+
1612
+ // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
+ if (!detectPileUp(ul)) return;
1614
+
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;
1621
+
1622
+ var anchor = previousTopicLi(host);
1623
+ if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1624
+
1625
+ if (host.previousElementSibling !== anchor) {
1626
+ anchor.insertAdjacentElement('afterend', host);
1627
+ }
1628
+ } catch (e) {}
1629
+ });
1652
1630
  } catch (e) {}
1653
1631
  }
1654
1632
 
1655
- function placeOrPend(host, ulEl, topics) {
1656
- try {
1657
- if (!host) return;
1658
- ulEl = ulEl || ensureUL();
1659
- if (!ulEl) return;
1633
+ function schedule(reason) {
1634
+ var now = Date.now();
1635
+ if (now - lastRun < COOLDOWN) return;
1636
+ if (scheduled) return;
1637
+ scheduled = true;
1638
+ requestAnimationFrame(function () {
1639
+ scheduled = false;
1640
+ lastRun = Date.now();
1641
+ try {
1642
+ var ul = getTopicList();
1643
+ if (!ul) return;
1644
+ redistribute(ul);
1645
+ } catch (e) {}
1646
+ });
1647
+ }
1648
+
1649
+ function init() {
1650
+ schedule('init');
1660
1651
 
1661
- topics = topics || getTopics(ulEl);
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
+ };
1662
1663
 
1663
- var after = getAfter(host);
1664
- if (!after) {
1665
- var lt = lastTopic(ulEl, topics);
1666
- if (lt) moveAfter(host, lt);
1667
- host.classList && host.classList.remove(PENDING_CLASS);
1668
- return;
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
+ }
1669
1676
  }
1677
+ } catch (e) {}
1670
1678
 
1671
- if (after > topics.length) {
1672
- // Defer placement until the anchor exists.
1673
- // Keeping a placeholder in a collapsed/0px container can prevent ad fill on some stacks.
1674
- host.classList && host.classList.add(PENDING_CLASS);
1675
- try {
1676
- getPoolEl().appendChild(host);
1677
- } catch (e) {}
1679
+ // NodeBB events: run after infinite scroll batches
1680
+ if (window.jQuery) {
1681
+ 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);
1685
+ });
1686
+ } catch (e) {}
1687
+ }
1688
+ }
1689
+
1690
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1691
+ else init();
1692
+ })();
1693
+ // ===== /V17 =====
1694
+
1695
+ // ===== V17.13: empty GPT "poke" (keeps V17 pile-fix intact) =====
1696
+ // Goal: some GPT containers render empty until a tiny scroll triggers a refresh.
1697
+ // This watcher detects visible empty GPT slots and triggers a lightweight refresh.
1698
+ (function () {
1699
+ var TAG = '[EZ EMPTY]';
1700
+ var seen = Object.create(null);
1701
+
1702
+ function dbg() {
1703
+ if (!window.__EZ_DEBUG) return;
1704
+ try { console.log.apply(console, arguments); } catch (e) {}
1705
+ }
1706
+
1707
+ function throttle(fn, wait) {
1708
+ var t = 0;
1709
+ var timer = null;
1710
+ return function () {
1711
+ var now = Date.now();
1712
+ var args = arguments;
1713
+ if (now - t >= wait) {
1714
+ t = now;
1715
+ fn.apply(null, args);
1678
1716
  return;
1679
1717
  }
1718
+ if (timer) return;
1719
+ timer = setTimeout(function () {
1720
+ timer = null;
1721
+ t = Date.now();
1722
+ fn.apply(null, args);
1723
+ }, wait);
1724
+ };
1725
+ }
1680
1726
 
1681
- var anchor = topics[after - 1];
1682
- if (anchor) moveAfter(host, anchor);
1683
- if (host.classList && host.classList.contains(PENDING_CLASS)) {
1684
- host.classList.remove(PENDING_CLASS);
1685
- if (host.dataset && host.dataset.ezoicId) refreshAds([host.dataset.ezoicId]);
1686
- }
1727
+ function isVisible(el) {
1728
+ try {
1729
+ if (!el || el.nodeType !== 1) return false;
1730
+ var r = el.getBoundingClientRect();
1731
+ if (!r) return false;
1732
+ if (r.width <= 0 || r.height <= 0) return false;
1733
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1734
+ var vw = window.innerWidth || document.documentElement.clientWidth || 0;
1735
+ if (vh <= 0 || vw <= 0) return false;
1736
+ return r.bottom > 0 && r.right > 0 && r.top < vh && r.left < vw;
1687
1737
  } catch (e) {}
1738
+ return false;
1688
1739
  }
1689
1740
 
1690
- function reconcile() {
1691
- var ulEl = ensureUL();
1692
- if (!ulEl) return;
1741
+ function slotHasFill(slot) {
1742
+ try {
1743
+ if (slot.querySelector && slot.querySelector('iframe')) return true;
1744
+ var cid = 'google_ads_iframe_';
1745
+ var any = slot.querySelector && slot.querySelector('[id^="' + cid + '"] iframe');
1746
+ return !!any;
1747
+ } catch (e) {}
1748
+ return false;
1749
+ }
1693
1750
 
1694
- var topics = getTopics(ulEl);
1751
+ function pokeRefresh() {
1752
+ try {
1753
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1754
+ window.ezstandalone.showAds();
1755
+ dbg(TAG, 'showAds called');
1756
+ return true;
1757
+ }
1758
+ } catch (e) {}
1759
+ try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1760
+ try { window.dispatchEvent(new Event('resize')); } catch (e) {}
1761
+ return false;
1762
+ }
1695
1763
 
1764
+ function checkSlot(slot) {
1696
1765
  try {
1697
- ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1698
- var h = ensureHostForWrap(w, ulEl);
1699
- if (h) placeOrPend(h, ulEl, topics);
1700
- });
1766
+ if (!slot || !slot.id) return;
1767
+ if (slot.id.indexOf('div-gpt-ad-') !== 0) return;
1768
+ if (slotHasFill(slot)) return;
1769
+ if (!isVisible(slot)) return;
1701
1770
 
1702
- ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1703
- placeOrPend(h, ulEl, topics);
1704
- });
1771
+ var k = slot.id;
1772
+ var now = Date.now();
1773
+ if (seen[k] && (now - seen[k]) < 2000) return;
1774
+ seen[k] = now;
1705
1775
 
1706
- ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1707
- var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1708
- if (h) placeOrPend(h, ulEl, topics);
1709
- });
1776
+ setTimeout(function () {
1777
+ try {
1778
+ if (!slotHasFill(slot) && isVisible(slot)) {
1779
+ console.log(TAG, k);
1780
+ pokeRefresh();
1781
+ }
1782
+ } catch (e) {}
1783
+ }, 120);
1710
1784
  } catch (e) {}
1711
1785
  }
1712
1786
 
1713
- function scheduleReconcile() {
1714
- var now = Date.now();
1715
- if (scheduled) return;
1716
- if (now - lastRun < COOLDOWN) return;
1717
- scheduled = true;
1718
- lastRun = now;
1719
- requestAnimationFrame(function () {
1720
- scheduled = false;
1721
- reconcile();
1722
- });
1787
+ function scan() {
1788
+ try {
1789
+ var slots = document.querySelectorAll('[id^="div-gpt-ad-"]');
1790
+ if (!slots || !slots.length) {
1791
+ dbg(TAG, 'none');
1792
+ return;
1793
+ }
1794
+ for (var i = 0; i < slots.length; i++) checkSlot(slots[i]);
1795
+ } catch (e) {}
1723
1796
  }
1724
1797
 
1725
- function initObserver() {
1726
- var ulEl = ensureUL();
1727
- if (!ulEl || mo) return;
1728
-
1729
- mo = new MutationObserver(function () {
1730
- scheduleReconcile();
1731
- });
1732
-
1733
- mo.observe(ulEl, { childList: true, subtree: true });
1798
+ function observeNewSlots(io) {
1799
+ try {
1800
+ if (typeof MutationObserver === 'undefined') return;
1801
+ var mo = new MutationObserver(function (muts) {
1802
+ try {
1803
+ for (var i = 0; i < muts.length; i++) {
1804
+ var m = muts[i];
1805
+ if (!m.addedNodes) continue;
1806
+ for (var j = 0; j < m.addedNodes.length; j++) {
1807
+ var n = m.addedNodes[j];
1808
+ if (!n || n.nodeType !== 1) continue;
1809
+ if (n.id && n.id.indexOf('div-gpt-ad-') === 0) {
1810
+ try { io.observe(n); } catch (e) {}
1811
+ checkSlot(n);
1812
+ } else if (n.querySelectorAll) {
1813
+ var inner = n.querySelectorAll('[id^="div-gpt-ad-"]');
1814
+ for (var k = 0; k < inner.length; k++) {
1815
+ try { io.observe(inner[k]); } catch (e) {}
1816
+ checkSlot(inner[k]);
1817
+ }
1818
+ }
1819
+ }
1820
+ }
1821
+ } catch (e) {}
1822
+ });
1823
+ mo.observe(document.documentElement || document.body, { childList: true, subtree: true });
1824
+ } catch (e) {}
1734
1825
  }
1735
1826
 
1736
- function init() {
1737
- initObserver();
1738
- scheduleReconcile();
1827
+ function initEmptyWatcher() {
1828
+ scan();
1739
1829
 
1740
- if (window.jQuery) {
1830
+ if ('IntersectionObserver' in window) {
1741
1831
  try {
1742
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1743
- ul = null;
1744
- try { if (mo) mo.disconnect(); } catch (e) {}
1745
- mo = null;
1746
- initObserver();
1747
- scheduleReconcile();
1748
- setTimeout(scheduleReconcile, 120);
1749
- setTimeout(scheduleReconcile, 500);
1750
- });
1832
+ var io = new IntersectionObserver(function (entries) {
1833
+ for (var i = 0; i < entries.length; i++) {
1834
+ var ent = entries[i];
1835
+ if (ent && ent.isIntersecting) checkSlot(ent.target);
1836
+ }
1837
+ }, { root: null, threshold: 0.12 });
1838
+
1839
+ var slots = document.querySelectorAll('[id^="div-gpt-ad-"]');
1840
+ for (var s = 0; s < slots.length; s++) {
1841
+ try { io.observe(slots[s]); } catch (e) {}
1842
+ }
1843
+ observeNewSlots(io);
1751
1844
  } catch (e) {}
1752
1845
  }
1753
1846
 
1754
- var tries = 0;
1755
- var t = setInterval(function () {
1756
- tries++;
1757
- if (ensureUL()) {
1758
- clearInterval(t);
1759
- initObserver();
1760
- scheduleReconcile();
1761
- }
1762
- if (tries > 30) clearInterval(t);
1763
- }, 200);
1847
+ try { window.addEventListener('scroll', throttle(scan, 250), { passive: true }); } catch (e) {}
1848
+ try { window.addEventListener('resize', throttle(scan, 500)); } catch (e) {}
1849
+ try { document.addEventListener('visibilitychange', function () { if (!document.hidden) setTimeout(scan, 150); }); } catch (e) {}
1764
1850
  }
1765
1851
 
1766
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1767
- else init();
1852
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initEmptyWatcher);
1853
+ else initEmptyWatcher();
1768
1854
  })();
1769
- // ===== /V17.8 =====
1855
+ // ===== /V17.13 =====
1770
1856
 
package/public/style.css CHANGED
@@ -86,15 +86,3 @@ 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
- display: none !important;
98
- }
99
- /* ===== /V17.8 ===== */
100
-