nodebb-plugin-ezoic-infinite 1.6.35 → 1.6.36

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.36",
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,20 @@ 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.7: Keep injection, anchor each between slot to topic TID and repair on DOM moves (minimal, targeted) =====
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
1522
  var ANCHOR_ATTR = 'data-ezoic-anchor-tid';
1523
1523
 
1524
+ var ul = null;
1525
+ var mo = null;
1526
+
1524
1527
  var pending = false;
1525
1528
  var queue = new Set();
1529
+ var lastFlush = 0;
1530
+ var FLUSH_COOLDOWN = 60;
1526
1531
 
1527
1532
  function getTopicList() {
1528
1533
  try {
@@ -1532,43 +1537,14 @@ function buildOrdinalMap(items) {
1532
1537
  } catch (e) { return null; }
1533
1538
  }
1534
1539
 
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
- }
1540
+ function ensureUL() { ul = ul || getTopicList(); return ul; }
1546
1541
 
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;
1542
+ function isHost(el) {
1543
+ return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1557
1544
  }
1558
1545
 
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;
1568
- }
1569
-
1570
- function ensureHostForWrap(wrap, ul) {
1571
- // Always ensure UL children are LI, to avoid NodeBB reparenting DIVs (root cause of pile-up).
1546
+ function ensureHostForWrap(wrap, ulEl) {
1547
+ // Only wrap invalid ul>div cases (same idea as V17), no injection hook.
1572
1548
  try {
1573
1549
  if (!wrap || wrap.nodeType !== 1) return null;
1574
1550
  if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
@@ -1576,25 +1552,20 @@ function buildOrdinalMap(items) {
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
-
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
- }
1596
-
1597
- ul.insertBefore(host, wrap);
1564
+ try {
1565
+ var after = wrap.getAttribute('data-ezoic-after');
1566
+ if (after) host.setAttribute('data-ezoic-after', after);
1567
+ } catch (e) {}
1568
+ ulEl.insertBefore(host, wrap);
1598
1569
  host.appendChild(wrap);
1599
1570
  try { wrap.style.width = '100%'; } catch (e) {}
1600
1571
  return host;
@@ -1603,133 +1574,227 @@ function buildOrdinalMap(items) {
1603
1574
  return null;
1604
1575
  }
1605
1576
 
1606
- function ensureAnchorTid(host) {
1577
+ function getTidFromTopicLi(li) {
1607
1578
  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);
1579
+ if (!li) return null;
1580
+ var tid = li.getAttribute('data-tid') || li.getAttribute('data-topic-id') || li.getAttribute('data-topicid') || li.getAttribute('data-id');
1581
+ if (tid && /^\\d+$/.test(tid)) return tid;
1582
+
1583
+ var a = li.querySelector && li.querySelector('a[href*="/topic/"]');
1584
+ if (a) {
1585
+ var href = a.getAttribute('href') || '';
1586
+ var m = href.match(/\\/topic\\/(\\d+)(\\/|$)/);
1587
+ if (m) return m[1];
1588
+ }
1614
1589
  } catch (e) {}
1590
+ return null;
1615
1591
  }
1616
1592
 
1617
- function reconcileHost(host, ul, tidMap) {
1593
+ function closestPrevTopicLi(node) {
1618
1594
  try {
1619
- if (!host || host.nodeType !== 1 || !host.isConnected) return;
1620
- ul = ul || host.parentElement;
1621
- if (!ul) return;
1595
+ var cur = node;
1596
+ if (!cur) return null;
1597
+ if (cur.closest) {
1598
+ var h = cur.closest('li.' + HOST_CLASS);
1599
+ if (h) cur = h;
1600
+ }
1601
+ var prev = cur.previousElementSibling;
1602
+ while (prev) {
1603
+ if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1604
+ prev = prev.previousElementSibling;
1605
+ }
1606
+ } catch (e) {}
1607
+ return null;
1608
+ }
1622
1609
 
1623
- // if host got detached from list, skip
1624
- if (!(ul.tagName === 'UL' || ul.tagName === 'OL')) return;
1610
+ function topicLiByTid(ulEl, tid) {
1611
+ try {
1612
+ if (!ulEl || !tid) return null;
1613
+ var q = ulEl.querySelector(
1614
+ TOPIC_LI_SEL + '[data-tid="' + tid + '"], ' +
1615
+ TOPIC_LI_SEL + '[data-topic-id="' + tid + '"], ' +
1616
+ TOPIC_LI_SEL + '[data-topicid="' + tid + '"]'
1617
+ );
1618
+ if (q) return q;
1619
+
1620
+ var topics = ulEl.querySelectorAll(TOPIC_LI_SEL);
1621
+ for (var i=0;i<topics.length;i++){
1622
+ var t = getTidFromTopicLi(topics[i]);
1623
+ if (t === tid) return topics[i];
1624
+ }
1625
+ } catch (e) {}
1626
+ return null;
1627
+ }
1625
1628
 
1626
- ensureAnchorTid(host);
1629
+ function rememberAnchor(el) {
1630
+ try {
1631
+ if (!el) return;
1632
+
1633
+ // normalize to host if wrap
1634
+ if (el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1635
+ var h = el.closest ? el.closest('li.' + HOST_CLASS) : null;
1636
+ if (h) el = h;
1637
+ }
1638
+ if (!isHost(el) && !(el.matches && el.matches(BETWEEN_WRAP_SEL))) return;
1627
1639
 
1628
- var tid = host.getAttribute(ANCHOR_ATTR);
1640
+ if (el.getAttribute && el.getAttribute(ANCHOR_ATTR)) return;
1641
+
1642
+ var prevTopic = closestPrevTopicLi(el);
1643
+ if (!prevTopic) return;
1644
+ var tid = getTidFromTopicLi(prevTopic);
1629
1645
  if (!tid) return;
1630
1646
 
1631
- var anchorLi = tidMap[tid];
1632
- if (!anchorLi || !anchorLi.isConnected) return;
1647
+ el.setAttribute(ANCHOR_ATTR, tid);
1648
+ } catch (e) {}
1649
+ }
1650
+
1651
+ function repairOne(node, ulEl) {
1652
+ try {
1653
+ ulEl = ulEl || ensureUL();
1654
+ if (!ulEl) return;
1655
+
1656
+ var el = node;
1633
1657
 
1634
- if (host.previousElementSibling !== anchorLi) {
1635
- anchorLi.insertAdjacentElement('afterend', host);
1658
+ if (el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1659
+ var host = ensureHostForWrap(el, ulEl) || (el.closest ? el.closest('li.' + HOST_CLASS) : null);
1660
+ if (host) el = host;
1661
+ }
1662
+
1663
+ if (isHost(el)) {
1664
+ rememberAnchor(el);
1665
+ var tid = el.getAttribute(ANCHOR_ATTR);
1666
+ if (!tid) return;
1667
+ var anchorTopic = topicLiByTid(ulEl, tid);
1668
+ if (!anchorTopic) return;
1669
+ if (el.previousElementSibling !== anchorTopic) {
1670
+ anchorTopic.insertAdjacentElement('afterend', el);
1671
+ }
1672
+ return;
1673
+ }
1674
+
1675
+ if (el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1676
+ rememberAnchor(el);
1677
+ var tid2 = el.getAttribute(ANCHOR_ATTR);
1678
+ if (!tid2) return;
1679
+ var anchor2 = topicLiByTid(ulEl, tid2);
1680
+ if (!anchor2) return;
1681
+ if (el.previousElementSibling !== anchor2) {
1682
+ anchor2.insertAdjacentElement('afterend', el);
1683
+ }
1636
1684
  }
1637
1685
  } catch (e) {}
1638
1686
  }
1639
1687
 
1640
- function drain() {
1688
+ function enqueue(node) {
1689
+ try {
1690
+ if (!node || node.nodeType !== 1) return;
1691
+
1692
+ if (isHost(node) || (node.matches && node.matches(BETWEEN_WRAP_SEL))) {
1693
+ queue.add(node);
1694
+ scheduleFlush();
1695
+ return;
1696
+ }
1697
+ if (node.querySelectorAll) {
1698
+ var wraps = node.querySelectorAll(BETWEEN_WRAP_SEL);
1699
+ if (wraps && wraps.length) wraps.forEach(function(w){ queue.add(w); });
1700
+ var hosts = node.querySelectorAll('li.' + HOST_CLASS);
1701
+ if (hosts && hosts.length) hosts.forEach(function(h){ queue.add(h); });
1702
+ if ((wraps && wraps.length) || (hosts && hosts.length)) scheduleFlush();
1703
+ }
1704
+ } catch (e) {}
1705
+ }
1706
+
1707
+ function flush() {
1641
1708
  pending = false;
1642
- var ul = getTopicList();
1643
- if (!ul) return;
1709
+ var ulEl = ensureUL();
1710
+ if (!ulEl) return;
1644
1711
 
1645
- // First, host any direct-child between DIVs currently in list (cheap, keeps DOM valid).
1712
+ // host invalid ul>div and capture anchors
1646
1713
  try {
1647
- ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1648
- var h = ensureHostForWrap(w, ul);
1649
- if (h) queue.add(h);
1714
+ ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function(w){
1715
+ var host = ensureHostForWrap(w, ulEl);
1716
+ if (host) queue.add(host);
1650
1717
  });
1718
+ ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function(h){ rememberAnchor(h); });
1719
+ ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function(w){ rememberAnchor(w); });
1651
1720
  } catch (e) {}
1652
1721
 
1653
- var tidMap = buildTidMap(ul);
1654
-
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;
1722
+ var n = 0;
1723
+ for (var item of Array.from(queue)) {
1724
+ queue.delete(item);
1725
+ repairOne(item, ulEl);
1726
+ n++;
1727
+ if (n >= 10) break;
1662
1728
  }
1663
- if (queue.size) schedule();
1729
+ if (queue.size) scheduleFlush();
1664
1730
  }
1665
1731
 
1666
- function schedule() {
1732
+ function scheduleFlush() {
1733
+ var now = Date.now();
1667
1734
  if (pending) return;
1735
+ if (now - lastFlush < FLUSH_COOLDOWN) return;
1668
1736
  pending = true;
1669
- requestAnimationFrame(drain);
1737
+ lastFlush = now;
1738
+ requestAnimationFrame(flush);
1670
1739
  }
1671
1740
 
1672
- function enqueueNode(node, ul) {
1741
+ function initObserver() {
1742
+ var ulEl = ensureUL();
1743
+ if (!ulEl || mo) return;
1744
+
1745
+ // initial capture
1673
1746
  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
- }
1747
+ ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function(w){
1748
+ var host = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1749
+ rememberAnchor(host || w);
1750
+ });
1688
1751
  } catch (e) {}
1752
+
1753
+ mo = new MutationObserver(function(muts){
1754
+ try {
1755
+ for (var i=0;i<muts.length;i++){
1756
+ var m = muts[i];
1757
+ if (m.addedNodes && m.addedNodes.length) {
1758
+ for (var j=0;j<m.addedNodes.length;j++) enqueue(m.addedNodes[j]);
1759
+ }
1760
+ if (m.target) enqueue(m.target);
1761
+ }
1762
+ } catch (e) {}
1763
+ });
1764
+ mo.observe(ulEl, { childList:true, subtree:true });
1689
1765
  }
1690
1766
 
1691
1767
  function init() {
1692
- var ul = getTopicList();
1693
- if (!ul) return;
1768
+ initObserver();
1769
+ scheduleFlush();
1694
1770
 
1695
- // Initial pass: host all between DIVs and reconcile once.
1696
- schedule();
1697
-
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
- }
1718
-
1719
- // NodeBB events: run a settle pass after batches
1720
1771
  if (window.jQuery) {
1721
1772
  try {
1722
- 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);
1773
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1774
+ ul = null;
1775
+ try { if (mo) { mo.disconnect(); } } catch(e){}
1776
+ mo = null;
1777
+ initObserver();
1778
+ setTimeout(function(){ scheduleFlush(); }, 80);
1779
+ setTimeout(function(){ scheduleFlush(); }, 450);
1726
1780
  });
1727
1781
  } catch (e) {}
1728
1782
  }
1783
+
1784
+ var tries = 0;
1785
+ var t = setInterval(function(){
1786
+ tries++;
1787
+ if (ensureUL()) {
1788
+ clearInterval(t);
1789
+ initObserver();
1790
+ scheduleFlush();
1791
+ }
1792
+ if (tries > 25) clearInterval(t);
1793
+ }, 200);
1729
1794
  }
1730
1795
 
1731
1796
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1732
1797
  else init();
1733
1798
  })();
1734
- // ===== /V18 =====
1799
+ // ===== /V17.7 =====
1735
1800
 
package/public/style.css CHANGED
@@ -88,8 +88,8 @@ 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.7 host styling ===== */
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
+ /* ===== /V17.7 ===== */
95
95