nodebb-plugin-ezoic-infinite 1.6.21 → 1.6.23

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.21",
3
+ "version": "1.6.23",
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
@@ -1511,75 +1511,47 @@ function buildOrdinalMap(items) {
1511
1511
 
1512
1512
 
1513
1513
 
1514
- // ===== V14.1.1 Hook between-wrap (safe): host repair always, reconcile ONLY on upscroll, never append host to bottom =====
1514
+ // ===== V14.1.3: Mutation-based reconcile (no scroll listener), place by data-ezoic-after =====
1515
1515
  (function () {
1516
- var BETWEEN_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1517
- var HOST_CLASS = 'nodebb-ezoic-host';
1518
- var TOKEN_ATTR = 'data-ezoic-anchor-token';
1516
+ var WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1517
+ var HOST_SEL = 'li.nodebb-ezoic-host';
1518
+ var TOPIC_LI_SEL = 'li[component="category/topic"]';
1519
1519
 
1520
- var lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
1520
+ var freezeUntil = 0;
1521
1521
  var pending = false;
1522
- var lastRun = 0;
1523
- var COOLDOWN = 140;
1522
+ var queue = new Set();
1524
1523
 
1525
- function getY() {
1526
- return window.pageYOffset || document.documentElement.scrollTop || 0;
1527
- }
1528
-
1529
- function token() {
1530
- try { return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } catch (e) { return String(Date.now()); }
1531
- }
1524
+ function now() { return Date.now(); }
1532
1525
 
1533
- function getListContainer(node) {
1526
+ function getTopicList() {
1534
1527
  try {
1535
- var ul = node && node.closest ? node.closest('ul,ol') : null;
1536
- if (ul && (ul.tagName === 'UL' || ul.tagName === 'OL')) return ul;
1537
- var p = node && node.parentElement;
1538
- while (p) {
1539
- if (p.tagName === 'UL' || p.tagName === 'OL') return p;
1540
- p = p.parentElement;
1541
- }
1542
- } catch (e) {}
1543
- return null;
1528
+ var topic = document.querySelector(TOPIC_LI_SEL);
1529
+ if (!topic) return null;
1530
+ return topic.closest ? topic.closest('ul,ol') : null;
1531
+ } catch (e) { return null; }
1544
1532
  }
1545
1533
 
1546
- function closestNonHostLi(node) {
1534
+ function isLoading() {
1547
1535
  try {
1548
- var cur = node;
1549
- if (!cur) return null;
1550
- if (cur.closest) {
1551
- var host = cur.closest('li.' + HOST_CLASS);
1552
- if (host) cur = host;
1553
- }
1554
- var prev = cur.previousElementSibling;
1555
- while (prev) {
1556
- if (prev.tagName === 'LI' && !(prev.classList && prev.classList.contains(HOST_CLASS))) return prev;
1557
- prev = prev.previousElementSibling;
1558
- }
1559
- } catch (e) {}
1560
- return null;
1536
+ if (now() < freezeUntil) return true;
1537
+ return !!document.querySelector('.infinite-loading, .infinite-scroll-loading, .loading-indicator, .spinner, .topic-loading');
1538
+ } catch (e) { return false; }
1561
1539
  }
1562
1540
 
1563
- function ensureHost(wrap) {
1564
- // Always ensure UL/OL doesn't directly contain DIV wraps.
1541
+ function ensureHostForWrap(wrap, ul) {
1565
1542
  try {
1566
1543
  if (!wrap || wrap.nodeType !== 1) return null;
1567
- if (!(wrap.matches && wrap.matches(BETWEEN_SEL))) return null;
1568
-
1569
- var ul = getListContainer(wrap);
1570
- if (!ul) return null;
1571
-
1572
- var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1544
+ if (!(wrap.matches && wrap.matches(WRAP_SEL))) return null;
1545
+ var host = wrap.closest ? wrap.closest(HOST_SEL) : null;
1573
1546
  if (host) return host;
1574
-
1575
- // If direct child of UL/OL, create host in-place (does not affect infinite scroll measurements much)
1547
+ ul = ul || (wrap.closest ? wrap.closest('ul,ol') : null);
1548
+ if (!ul) return null;
1576
1549
  if (wrap.parentElement === ul) {
1577
1550
  host = document.createElement('li');
1578
- host.className = HOST_CLASS;
1551
+ host.className = 'nodebb-ezoic-host';
1579
1552
  host.setAttribute('role', 'listitem');
1580
1553
  host.style.listStyle = 'none';
1581
1554
  host.style.width = '100%';
1582
-
1583
1555
  ul.insertBefore(host, wrap);
1584
1556
  host.appendChild(wrap);
1585
1557
  try { wrap.style.width = '100%'; } catch (e) {}
@@ -1589,118 +1561,177 @@ function buildOrdinalMap(items) {
1589
1561
  return null;
1590
1562
  }
1591
1563
 
1592
- function tetherHostToAnchor(host, ul) {
1593
- // Only called on upscroll to avoid breaking load logic.
1564
+ function nthTopic(ul, n) {
1594
1565
  try {
1595
- if (!host) return;
1596
- var wrap = host.querySelector && host.querySelector(BETWEEN_SEL);
1597
- if (!wrap) { host.remove(); return; }
1566
+ var topics = ul.querySelectorAll(TOPIC_LI_SEL);
1567
+ if (!topics || !topics.length) return null;
1568
+ if (n < 1) n = 1;
1569
+ if (n > topics.length) n = topics.length;
1570
+ return topics[n-1] || null;
1571
+ } catch (e) { return null; }
1572
+ }
1598
1573
 
1599
- // Determine anchor = nearest previous non-host li
1600
- var anchorLi = closestNonHostLi(host);
1601
- if (!anchorLi) {
1602
- // No anchor in DOM -> mark orphan (handled on upscroll only)
1603
- host.setAttribute('data-ezoic-orphan', '1');
1604
- return;
1574
+ function previousTopicLi(host) {
1575
+ try {
1576
+ var prev = host.previousElementSibling;
1577
+ while (prev) {
1578
+ if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1579
+ prev = prev.previousElementSibling;
1605
1580
  }
1581
+ } catch (e) {}
1582
+ return null;
1583
+ }
1606
1584
 
1607
- var t = anchorLi.getAttribute(TOKEN_ATTR);
1608
- if (!t) {
1609
- t = token();
1610
- anchorLi.setAttribute(TOKEN_ATTR, t);
1585
+ function placeHost(host, ul) {
1586
+ try {
1587
+ if (!host || host.nodeType !== 1) return;
1588
+ ul = ul || host.parentElement;
1589
+ if (!ul) return;
1590
+
1591
+ var wrap = host.querySelector && host.querySelector(WRAP_SEL);
1592
+ if (!wrap) { host.remove(); return; }
1593
+
1594
+ var after = wrap.getAttribute('data-ezoic-after');
1595
+ var anchor = null;
1596
+
1597
+ if (after) {
1598
+ anchor = nthTopic(ul, parseInt(after, 10));
1599
+ }
1600
+ if (!anchor) {
1601
+ anchor = previousTopicLi(host);
1602
+ }
1603
+ if (!anchor) {
1604
+ // Can't place reliably; if host drifted to top area, remove to avoid pile-up.
1605
+ // Otherwise keep as-is.
1606
+ var firstTopic = nthTopic(ul, 1);
1607
+ if (firstTopic && host !== firstTopic && host.compareDocumentPosition(firstTopic) & Node.DOCUMENT_POSITION_FOLLOWING) {
1608
+ host.remove();
1609
+ }
1610
+ return;
1611
1611
  }
1612
- host.setAttribute(TOKEN_ATTR, t);
1613
- host.removeAttribute('data-ezoic-orphan');
1614
1612
 
1615
- // If host drifted, put it back right after anchor
1616
- if (host.previousElementSibling !== anchorLi) {
1617
- anchorLi.insertAdjacentElement('afterend', host);
1613
+ if (host.previousElementSibling !== anchor) {
1614
+ anchor.insertAdjacentElement('afterend', host);
1618
1615
  }
1619
1616
  } catch (e) {}
1620
1617
  }
1621
1618
 
1622
- function reconcileUpScroll() {
1623
- var y = getY();
1624
- var dy = y - lastY;
1625
- lastY = y;
1626
-
1627
- // ALWAYS: repair invalid ul>div occurrences (cheap) but don't reorder
1628
- try {
1629
- var bad = document.querySelectorAll('ul > ' + BETWEEN_SEL + ', ol > ' + BETWEEN_SEL);
1630
- bad.forEach(function(w){ ensureHost(w); });
1631
- } catch (e) {}
1632
-
1633
- // If NOT upscroll, do nothing else (prevents "ads accumulate at bottom" and avoids scroll blocking)
1634
- if (dy > -8) return;
1619
+ function drain() {
1620
+ pending = false;
1621
+ if (isLoading()) {
1622
+ // retry later
1623
+ scheduleDrain(250);
1624
+ return;
1625
+ }
1626
+ var ul = getTopicList();
1627
+ if (!ul) return;
1635
1628
 
1636
- // Up-scroll: tether/reconcile and drop orphan hosts (they cause pile-ups)
1629
+ // repair invalid ul>div once
1637
1630
  try {
1638
- var wraps = document.querySelectorAll(BETWEEN_SEL);
1639
- wraps.forEach(function(w){
1640
- var host = ensureHost(w);
1641
- if (!host) host = w.closest ? w.closest('li.' + HOST_CLASS) : null;
1642
- var ul = getListContainer(host || w);
1643
- if (host && ul) tetherHostToAnchor(host, ul);
1644
- });
1645
-
1646
- // Drop orphans only on upscroll
1647
- var orphans = document.querySelectorAll('li.' + HOST_CLASS + '[data-ezoic-orphan="1"]');
1648
- orphans.forEach(function(h){ try { h.remove(); } catch(e) {} });
1649
-
1650
- // Drop empty hosts
1651
- var empties = document.querySelectorAll('li.' + HOST_CLASS);
1652
- empties.forEach(function(h){
1653
- try { if (!(h.querySelector && h.querySelector(BETWEEN_SEL))) h.remove(); } catch(e) {}
1631
+ ul.querySelectorAll(':scope > ' + WRAP_SEL).forEach(function(w){
1632
+ var h = ensureHostForWrap(w, ul);
1633
+ if (h) queue.add(h);
1654
1634
  });
1655
1635
  } catch (e) {}
1636
+
1637
+ var count = 0;
1638
+ for (var host of Array.from(queue)) {
1639
+ queue.delete(host);
1640
+ placeHost(host, ul);
1641
+ count++;
1642
+ if (count >= 8) break; // keep it light
1643
+ }
1644
+ if (queue.size) scheduleDrain(60);
1656
1645
  }
1657
1646
 
1658
- function schedule() {
1659
- var now = Date.now();
1660
- if (now - lastRun < COOLDOWN) return;
1647
+ function scheduleDrain(delay) {
1661
1648
  if (pending) return;
1662
1649
  pending = true;
1663
- requestAnimationFrame(function(){
1664
- pending = false;
1665
- lastRun = Date.now();
1666
- reconcileUpScroll();
1667
- });
1650
+ if (delay) {
1651
+ setTimeout(function(){ requestAnimationFrame(drain); }, delay);
1652
+ } else {
1653
+ requestAnimationFrame(drain);
1654
+ }
1668
1655
  }
1669
1656
 
1670
- function init() {
1671
- // Initial repair only
1657
+ function enqueueFromNode(node) {
1672
1658
  try {
1673
- var bad = document.querySelectorAll('ul > ' + BETWEEN_SEL + ', ol > ' + BETWEEN_SEL);
1674
- bad.forEach(function(w){ ensureHost(w); });
1659
+ if (!node || node.nodeType !== 1) return;
1660
+ var ul = getTopicList();
1661
+ if (!ul) return;
1662
+
1663
+ if (node.matches && node.matches(WRAP_SEL)) {
1664
+ var h = ensureHostForWrap(node, ul) || (node.closest ? node.closest(HOST_SEL) : null);
1665
+ if (h) queue.add(h);
1666
+ } else if (node.matches && node.matches(HOST_SEL)) {
1667
+ queue.add(node);
1668
+ } else if (node.querySelectorAll) {
1669
+ node.querySelectorAll(WRAP_SEL).forEach(function(w){
1670
+ var h2 = ensureHostForWrap(w, ul) || (w.closest ? w.closest(HOST_SEL) : null);
1671
+ if (h2) queue.add(h2);
1672
+ });
1673
+ node.querySelectorAll(HOST_SEL).forEach(function(h3){ queue.add(h3); });
1674
+ }
1675
1675
  } catch (e) {}
1676
+ }
1676
1677
 
1677
- window.addEventListener('scroll', schedule, { passive: true });
1678
- window.addEventListener('resize', schedule, { passive: true });
1679
-
1680
- if (window.jQuery) {
1681
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1682
- setTimeout(schedule, 0);
1683
- setTimeout(schedule, 250);
1684
- setTimeout(schedule, 900);
1685
- });
1686
- }
1678
+ function init() {
1679
+ // initial scan (light)
1680
+ try {
1681
+ var ul = getTopicList();
1682
+ if (ul) {
1683
+ ul.querySelectorAll(WRAP_SEL).forEach(function(w){
1684
+ var h = ensureHostForWrap(w, ul) || (w.closest ? w.closest(HOST_SEL) : null);
1685
+ if (h) queue.add(h);
1686
+ });
1687
+ scheduleDrain(0);
1688
+ }
1689
+ } catch (e) {}
1687
1690
 
1688
- // MutationObserver: only schedule (no immediate heavy work)
1691
+ // Observe ONLY the topic list container once available
1689
1692
  try {
1690
1693
  if (typeof MutationObserver !== 'undefined') {
1691
1694
  var mo = new MutationObserver(function(muts){
1692
1695
  for (var i=0;i<muts.length;i++){
1693
- var m=muts[i];
1694
- if ((m.addedNodes && m.addedNodes.length) || (m.removedNodes && m.removedNodes.length)) { schedule(); break; }
1696
+ var m = muts[i];
1697
+ if (m.addedNodes && m.addedNodes.length) {
1698
+ for (var j=0;j<m.addedNodes.length;j++){
1699
+ enqueueFromNode(m.addedNodes[j]);
1700
+ }
1701
+ scheduleDrain(0);
1702
+ }
1695
1703
  }
1696
1704
  });
1697
- mo.observe(document.documentElement || document.body, { childList:true, subtree:true });
1705
+
1706
+ var ul = getTopicList();
1707
+ if (ul) mo.observe(ul, { childList:true, subtree:true });
1708
+ else {
1709
+ // wait for ajaxify
1710
+ var mo2 = new MutationObserver(function(){
1711
+ var u2 = getTopicList();
1712
+ if (u2) {
1713
+ try { mo.observe(u2, { childList:true, subtree:true }); } catch(e){}
1714
+ try { mo2.disconnect(); } catch(e){}
1715
+ }
1716
+ });
1717
+ mo2.observe(document.documentElement || document.body, { childList:true, subtree:true });
1718
+ }
1698
1719
  }
1699
1720
  } catch (e) {}
1721
+
1722
+ if (window.jQuery) {
1723
+ try {
1724
+ window.jQuery(window).on('action:infiniteScroll.loaded action:ajaxify.end', function(){
1725
+ freezeUntil = now() + 900; // allow NodeBB to settle
1726
+ // then reconcile any drift
1727
+ setTimeout(function(){ scheduleDrain(0); }, 950);
1728
+ });
1729
+ } catch (e) {}
1730
+ }
1700
1731
  }
1701
1732
 
1702
1733
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1703
1734
  else init();
1704
1735
  })();
1705
- // ===== /V14.1.1 =====
1736
+ // ===== /V14.1.3 =====
1706
1737
 
package/public/style.css CHANGED
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
 
84
- /* ===== V14.1.1 host ===== */
84
+ /* ===== V14.1.3 host ===== */
85
85
  li.nodebb-ezoic-host { list-style: none; width: 100%; }
86
- /* ===== /V14.1.1 ===== */
86
+ /* ===== /V14.1.3 ===== */
87
87
 
package/README.md DELETED
@@ -1,15 +0,0 @@
1
- # NodeBB Plugin – Ezoic Infinite (Production)
2
-
3
- This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
4
- with full support for infinite scroll.
5
-
6
- ## Key guarantees
7
- - No duplicate ads back-to-back
8
- - One showAds call per placeholder
9
- - Fast reveal (MutationObserver on first child)
10
- - Safe with ajaxify navigation
11
- - Works with NodeBB 4.x + Harmony
12
-
13
- ## Notes
14
- - Placeholders must exist and be selected in Ezoic
15
- - Use separate ID pools for topics vs messages