nodebb-plugin-ezoic-infinite 1.6.35 → 1.6.37

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.35",
3
+ "version": "1.6.37",
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
@@ -1514,15 +1514,18 @@ function buildOrdinalMap(items) {
1514
1514
 
1515
1515
 
1516
1516
 
1517
- // ===== V18: Always host between DIVs in <li> + anchor by topic tid (MutationObserver, no scroll hooks to injection) =====
1517
+ // ===== V17.8: No pile-up by design keep "future" between slots pending/collapsed until their anchor topic exists =====
1518
1518
  (function () {
1519
1519
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1520
1520
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1521
1521
  var HOST_CLASS = 'nodebb-ezoic-host';
1522
- var ANCHOR_ATTR = 'data-ezoic-anchor-tid';
1522
+ var PENDING_CLASS = 'nodebb-ezoic-pending';
1523
1523
 
1524
- var pending = false;
1525
- var queue = new Set();
1524
+ var ul = null;
1525
+ var mo = null;
1526
+ var scheduled = false;
1527
+ var lastRun = 0;
1528
+ var COOLDOWN = 80;
1526
1529
 
1527
1530
  function getTopicList() {
1528
1531
  try {
@@ -1532,69 +1535,41 @@ function buildOrdinalMap(items) {
1532
1535
  } catch (e) { return null; }
1533
1536
  }
1534
1537
 
1535
- function extractTidFromTopicLi(li) {
1536
- // Parse tid from a topic link (/topic/{tid}/...)
1537
- try {
1538
- if (!li || !li.querySelector) return null;
1539
- var a = li.querySelector('a[href^="/topic/"], a[href*="/topic/"]');
1540
- if (!a) return null;
1541
- var href = a.getAttribute('href') || '';
1542
- var m = href.match(/\/topic\/(\d+)\b/);
1543
- return m ? m[1] : null;
1544
- } catch (e) { return null; }
1545
- }
1538
+ function ensureUL() { ul = ul || getTopicList(); return ul; }
1546
1539
 
1547
- function buildTidMap(ul) {
1548
- var map = Object.create(null);
1549
- try {
1550
- var topics = ul.querySelectorAll(TOPIC_LI_SEL);
1551
- for (var i = 0; i < topics.length; i++) {
1552
- var tid = extractTidFromTopicLi(topics[i]);
1553
- if (tid && !map[tid]) map[tid] = topics[i];
1554
- }
1555
- } catch (e) {}
1556
- return map;
1540
+ function isHost(el) {
1541
+ return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1557
1542
  }
1558
1543
 
1559
- function previousTopicLi(node) {
1560
- try {
1561
- var prev = node.previousElementSibling;
1562
- while (prev) {
1563
- if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1564
- prev = prev.previousElementSibling;
1565
- }
1566
- } catch (e) {}
1567
- return null;
1544
+ function isBetweenWrap(el) {
1545
+ try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1568
1546
  }
1569
1547
 
1570
- function ensureHostForWrap(wrap, ul) {
1571
- // Always ensure UL children are LI, to avoid NodeBB reparenting DIVs (root cause of pile-up).
1548
+ function ensureHostForWrap(wrap, ulEl) {
1549
+ // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1572
1550
  try {
1573
- if (!wrap || wrap.nodeType !== 1) return null;
1574
- if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1575
-
1551
+ if (!wrap || !isBetweenWrap(wrap)) return null;
1576
1552
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1577
1553
  if (host) return host;
1578
1554
 
1579
- ul = ul || (wrap.closest ? wrap.closest('ul,ol') : null);
1580
- if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1555
+ ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1556
+ if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1581
1557
 
1582
- // Only wrap if it's a direct child of the list
1583
- if (wrap.parentElement === ul) {
1558
+ if (wrap.parentElement === ulEl) {
1584
1559
  host = document.createElement('li');
1585
1560
  host.className = HOST_CLASS;
1586
1561
  host.setAttribute('role', 'listitem');
1587
1562
  host.style.listStyle = 'none';
1588
1563
  host.style.width = '100%';
1589
1564
 
1590
- // anchor tid from previous topic
1591
- var anchor = previousTopicLi(wrap) || wrap.previousElementSibling;
1592
- if (anchor && anchor.matches && anchor.matches(TOPIC_LI_SEL)) {
1593
- var tid = extractTidFromTopicLi(anchor);
1594
- if (tid) host.setAttribute(ANCHOR_ATTR, tid);
1595
- }
1565
+ try {
1566
+ var after = wrap.getAttribute('data-ezoic-after');
1567
+ if (after) host.setAttribute('data-ezoic-after', after);
1568
+ var pin = wrap.getAttribute('data-ezoic-pin');
1569
+ if (pin) host.setAttribute('data-ezoic-pin', pin);
1570
+ } catch (e) {}
1596
1571
 
1597
- ul.insertBefore(host, wrap);
1572
+ ulEl.insertBefore(host, wrap);
1598
1573
  host.appendChild(wrap);
1599
1574
  try { wrap.style.width = '100%'; } catch (e) {}
1600
1575
  return host;
@@ -1603,133 +1578,145 @@ function buildOrdinalMap(items) {
1603
1578
  return null;
1604
1579
  }
1605
1580
 
1606
- function ensureAnchorTid(host) {
1581
+ function getAfter(el) {
1607
1582
  try {
1608
- if (!host || host.nodeType !== 1) return;
1609
- if (host.getAttribute(ANCHOR_ATTR)) return;
1610
- var anchor = previousTopicLi(host);
1611
- if (!anchor) return;
1612
- var tid = extractTidFromTopicLi(anchor);
1613
- if (tid) host.setAttribute(ANCHOR_ATTR, tid);
1614
- } catch (e) {}
1583
+ var v = null;
1584
+ if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1585
+ if (!v && el && el.querySelector) {
1586
+ var w = el.querySelector(BETWEEN_WRAP_SEL);
1587
+ if (w) v = w.getAttribute('data-ezoic-after');
1588
+ }
1589
+ var n = parseInt(v, 10);
1590
+ return isNaN(n) ? null : n;
1591
+ } catch (e) { return null; }
1615
1592
  }
1616
1593
 
1617
- function reconcileHost(host, ul, tidMap) {
1618
- try {
1619
- if (!host || host.nodeType !== 1 || !host.isConnected) return;
1620
- ul = ul || host.parentElement;
1621
- if (!ul) return;
1622
-
1623
- // if host got detached from list, skip
1624
- if (!(ul.tagName === 'UL' || ul.tagName === 'OL')) return;
1594
+ function getTopics(ulEl) {
1595
+ try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1596
+ }
1625
1597
 
1626
- ensureAnchorTid(host);
1598
+ function lastTopic(ulEl, topics) {
1599
+ try {
1600
+ topics = topics || getTopics(ulEl);
1601
+ return topics && topics.length ? topics[topics.length - 1] : null;
1602
+ } catch (e) { return null; }
1603
+ }
1627
1604
 
1628
- var tid = host.getAttribute(ANCHOR_ATTR);
1629
- if (!tid) return;
1605
+ function moveAfter(node, anchor) {
1606
+ try {
1607
+ if (!node || !anchor || !anchor.insertAdjacentElement) return;
1608
+ if (node.previousElementSibling === anchor) return;
1609
+ anchor.insertAdjacentElement('afterend', node);
1610
+ } catch (e) {}
1611
+ }
1630
1612
 
1631
- var anchorLi = tidMap[tid];
1632
- if (!anchorLi || !anchorLi.isConnected) return;
1613
+ function placeOrPend(host, ulEl, topics) {
1614
+ try {
1615
+ if (!host) return;
1616
+ ulEl = ulEl || ensureUL();
1617
+ if (!ulEl) return;
1618
+
1619
+ topics = topics || getTopics(ulEl);
1620
+
1621
+ var after = getAfter(host);
1622
+ if (!after) {
1623
+ var lt = lastTopic(ulEl, topics);
1624
+ if (lt) moveAfter(host, lt);
1625
+ host.classList && host.classList.remove(PENDING_CLASS);
1626
+ return;
1627
+ }
1633
1628
 
1634
- if (host.previousElementSibling !== anchorLi) {
1635
- anchorLi.insertAdjacentElement('afterend', host);
1629
+ if (after > topics.length) {
1630
+ var lt2 = lastTopic(ulEl, topics);
1631
+ if (lt2) moveAfter(host, lt2);
1632
+ host.classList && host.classList.add(PENDING_CLASS);
1633
+ return;
1636
1634
  }
1635
+
1636
+ var anchor = topics[after - 1];
1637
+ if (anchor) moveAfter(host, anchor);
1638
+ host.classList && host.classList.remove(PENDING_CLASS);
1637
1639
  } catch (e) {}
1638
1640
  }
1639
1641
 
1640
- function drain() {
1641
- pending = false;
1642
- var ul = getTopicList();
1643
- if (!ul) return;
1642
+ function reconcile() {
1643
+ var ulEl = ensureUL();
1644
+ if (!ulEl) return;
1645
+
1646
+ var topics = getTopics(ulEl);
1644
1647
 
1645
- // First, host any direct-child between DIVs currently in list (cheap, keeps DOM valid).
1646
1648
  try {
1647
- ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1648
- var h = ensureHostForWrap(w, ul);
1649
- if (h) queue.add(h);
1649
+ ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1650
+ var h = ensureHostForWrap(w, ulEl);
1651
+ if (h) placeOrPend(h, ulEl, topics);
1650
1652
  });
1651
- } catch (e) {}
1652
1653
 
1653
- var tidMap = buildTidMap(ul);
1654
+ ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1655
+ placeOrPend(h, ulEl, topics);
1656
+ });
1654
1657
 
1655
- // Process a limited batch per frame
1656
- var processed = 0;
1657
- for (var host of Array.from(queue)) {
1658
- queue.delete(host);
1659
- reconcileHost(host, ul, tidMap);
1660
- processed++;
1661
- if (processed >= 12) break;
1662
- }
1663
- if (queue.size) schedule();
1658
+ ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1659
+ var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1660
+ if (h) placeOrPend(h, ulEl, topics);
1661
+ });
1662
+ } catch (e) {}
1664
1663
  }
1665
1664
 
1666
- function schedule() {
1667
- if (pending) return;
1668
- pending = true;
1669
- requestAnimationFrame(drain);
1665
+ function scheduleReconcile() {
1666
+ var now = Date.now();
1667
+ if (scheduled) return;
1668
+ if (now - lastRun < COOLDOWN) return;
1669
+ scheduled = true;
1670
+ lastRun = now;
1671
+ requestAnimationFrame(function () {
1672
+ scheduled = false;
1673
+ reconcile();
1674
+ });
1670
1675
  }
1671
1676
 
1672
- function enqueueNode(node, ul) {
1673
- try {
1674
- if (!node || node.nodeType !== 1) return;
1675
- if (node.matches && node.matches(BETWEEN_WRAP_SEL)) {
1676
- var h = ensureHostForWrap(node, ul);
1677
- if (h) queue.add(h);
1678
- } else if (node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS)) {
1679
- queue.add(node);
1680
- } else if (node.querySelectorAll) {
1681
- // only check within added subtree, not whole doc
1682
- node.querySelectorAll(':scope ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1683
- var h2 = ensureHostForWrap(w, ul);
1684
- if (h2) queue.add(h2);
1685
- });
1686
- node.querySelectorAll(':scope li.' + HOST_CLASS).forEach(function (h3) { queue.add(h3); });
1687
- }
1688
- } catch (e) {}
1689
- }
1677
+ function initObserver() {
1678
+ var ulEl = ensureUL();
1679
+ if (!ulEl || mo) return;
1690
1680
 
1691
- function init() {
1692
- var ul = getTopicList();
1693
- if (!ul) return;
1681
+ mo = new MutationObserver(function () {
1682
+ scheduleReconcile();
1683
+ });
1694
1684
 
1695
- // Initial pass: host all between DIVs and reconcile once.
1696
- schedule();
1685
+ mo.observe(ulEl, { childList: true, subtree: true });
1686
+ }
1697
1687
 
1698
- // Observe only the topic list
1699
- if (typeof MutationObserver !== 'undefined') {
1700
- var mo = new MutationObserver(function (muts) {
1701
- try {
1702
- var u = getTopicList();
1703
- if (!u) return;
1704
-
1705
- for (var i = 0; i < muts.length; i++) {
1706
- var m = muts[i];
1707
- if (m.addedNodes && m.addedNodes.length) {
1708
- for (var j = 0; j < m.addedNodes.length; j++) {
1709
- enqueueNode(m.addedNodes[j], u);
1710
- }
1711
- }
1712
- }
1713
- if (queue.size) schedule();
1714
- } catch (e) {}
1715
- });
1716
- try { mo.observe(ul, { childList: true, subtree: true }); } catch (e) {}
1717
- }
1688
+ function init() {
1689
+ initObserver();
1690
+ scheduleReconcile();
1718
1691
 
1719
- // NodeBB events: run a settle pass after batches
1720
1692
  if (window.jQuery) {
1721
1693
  try {
1722
1694
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1723
- // Let NodeBB append topics, then reconcile hosts by anchor tid.
1724
- setTimeout(function(){ schedule(); }, 120);
1725
- setTimeout(function(){ schedule(); }, 520);
1695
+ ul = null;
1696
+ try { if (mo) mo.disconnect(); } catch (e) {}
1697
+ mo = null;
1698
+ initObserver();
1699
+ scheduleReconcile();
1700
+ setTimeout(scheduleReconcile, 120);
1701
+ setTimeout(scheduleReconcile, 500);
1726
1702
  });
1727
1703
  } catch (e) {}
1728
1704
  }
1705
+
1706
+ var tries = 0;
1707
+ var t = setInterval(function () {
1708
+ tries++;
1709
+ if (ensureUL()) {
1710
+ clearInterval(t);
1711
+ initObserver();
1712
+ scheduleReconcile();
1713
+ }
1714
+ if (tries > 30) clearInterval(t);
1715
+ }, 200);
1729
1716
  }
1730
1717
 
1731
1718
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1732
1719
  else init();
1733
1720
  })();
1734
- // ===== /V18 =====
1721
+ // ===== /V17.8 =====
1735
1722
 
package/public/style.css CHANGED
@@ -88,8 +88,18 @@ li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; displa
88
88
 
89
89
 
90
90
 
91
- /* ===== V18 hosts ===== */
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
- /* ===== /V18 ===== */
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 ===== */
95
105