nodebb-plugin-ezoic-infinite 1.6.20 → 1.6.21

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.20",
3
+ "version": "1.6.21",
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,46 +1511,75 @@ function buildOrdinalMap(items) {
1511
1511
 
1512
1512
 
1513
1513
 
1514
- // ===== V14.3 reconcile by data-ezoic-after (upscroll only, no DOM scanning on downscroll) =====
1514
+ // ===== V14.1.1 Hook between-wrap (safe): host repair always, reconcile ONLY on upscroll, never append host to bottom =====
1515
1515
  (function () {
1516
+ var BETWEEN_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1516
1517
  var HOST_CLASS = 'nodebb-ezoic-host';
1517
- var WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1518
- var TOPIC_LI_SEL = 'li[component="category/topic"]';
1518
+ var TOKEN_ATTR = 'data-ezoic-anchor-token';
1519
+
1519
1520
  var lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
1521
+ var pending = false;
1522
+ var lastRun = 0;
1523
+ var COOLDOWN = 140;
1520
1524
 
1521
1525
  function getY() {
1522
1526
  return window.pageYOffset || document.documentElement.scrollTop || 0;
1523
1527
  }
1524
1528
 
1525
- function getTopicList() {
1529
+ function token() {
1530
+ try { return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } catch (e) { return String(Date.now()); }
1531
+ }
1532
+
1533
+ function getListContainer(node) {
1526
1534
  try {
1527
- var topicLi = document.querySelector(TOPIC_LI_SEL);
1528
- if (!topicLi) return null;
1529
- var ul = topicLi.closest ? topicLi.closest('ul,ol') : null;
1530
- return ul;
1531
- } catch (e) { return null; }
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;
1532
1544
  }
1533
1545
 
1534
- function ensureValidHostForBetween(wrap) {
1535
- // Ensure ul/ol does not directly contain div wraps: wrap them in LI host.
1546
+ function closestNonHostLi(node) {
1547
+ 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;
1561
+ }
1562
+
1563
+ function ensureHost(wrap) {
1564
+ // Always ensure UL/OL doesn't directly contain DIV wraps.
1536
1565
  try {
1537
1566
  if (!wrap || wrap.nodeType !== 1) return null;
1538
- if (!(wrap.matches && wrap.matches(WRAP_SEL))) return null;
1539
- var ul = wrap.parentElement;
1540
- while (ul && !(ul.tagName === 'UL' || ul.tagName === 'OL')) ul = ul.parentElement;
1567
+ if (!(wrap.matches && wrap.matches(BETWEEN_SEL))) return null;
1568
+
1569
+ var ul = getListContainer(wrap);
1541
1570
  if (!ul) return null;
1542
1571
 
1543
- // If already inside host, return host
1544
1572
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1545
1573
  if (host) return host;
1546
1574
 
1547
- // If direct child of UL/OL, create host and move wrap into it
1575
+ // If direct child of UL/OL, create host in-place (does not affect infinite scroll measurements much)
1548
1576
  if (wrap.parentElement === ul) {
1549
1577
  host = document.createElement('li');
1550
1578
  host.className = HOST_CLASS;
1551
1579
  host.setAttribute('role', 'listitem');
1552
1580
  host.style.listStyle = 'none';
1553
1581
  host.style.width = '100%';
1582
+
1554
1583
  ul.insertBefore(host, wrap);
1555
1584
  host.appendChild(wrap);
1556
1585
  try { wrap.style.width = '100%'; } catch (e) {}
@@ -1560,66 +1589,72 @@ function buildOrdinalMap(items) {
1560
1589
  return null;
1561
1590
  }
1562
1591
 
1563
- function desiredAnchorByAfter(ul, afterNum) {
1564
- // afterNum is like 6/12/18 etc. It means "after nth topic".
1565
- // We'll locate the nth topic LI currently in DOM (1-indexed), then anchor after it.
1592
+ function tetherHostToAnchor(host, ul) {
1593
+ // Only called on upscroll to avoid breaking load logic.
1566
1594
  try {
1567
- if (!ul || !afterNum) return null;
1568
- var n = parseInt(afterNum, 10);
1569
- if (!n || n < 1) return null;
1570
- var topics = ul.querySelectorAll(TOPIC_LI_SEL);
1571
- if (!topics || !topics.length) return null;
1572
- // clamp to range
1573
- if (n > topics.length) n = topics.length;
1574
- return topics[n - 1];
1575
- } catch (e) { return null; }
1576
- }
1595
+ if (!host) return;
1596
+ var wrap = host.querySelector && host.querySelector(BETWEEN_SEL);
1597
+ if (!wrap) { host.remove(); return; }
1598
+
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;
1605
+ }
1577
1606
 
1578
- function reconcileHostsUpScroll() {
1579
- var ul = getTopicList();
1580
- if (!ul) return;
1607
+ var t = anchorLi.getAttribute(TOKEN_ATTR);
1608
+ if (!t) {
1609
+ t = token();
1610
+ anchorLi.setAttribute(TOKEN_ATTR, t);
1611
+ }
1612
+ host.setAttribute(TOKEN_ATTR, t);
1613
+ host.removeAttribute('data-ezoic-orphan');
1581
1614
 
1582
- // 1) Repair invalid wraps (cheap)
1583
- try {
1584
- ul.querySelectorAll(':scope > ' + WRAP_SEL).forEach(function(w){ ensureValidHostForBetween(w); });
1615
+ // If host drifted, put it back right after anchor
1616
+ if (host.previousElementSibling !== anchorLi) {
1617
+ anchorLi.insertAdjacentElement('afterend', host);
1618
+ }
1585
1619
  } catch (e) {}
1620
+ }
1586
1621
 
1587
- // 2) Only on upscroll: move hosts back to their after-index positions if they drifted.
1622
+ function reconcileUpScroll() {
1588
1623
  var y = getY();
1589
1624
  var dy = y - lastY;
1590
1625
  lastY = y;
1591
- if (dy >= -10) return;
1592
1626
 
1627
+ // ALWAYS: repair invalid ul>div occurrences (cheap) but don't reorder
1593
1628
  try {
1594
- var hosts = ul.querySelectorAll('li.' + HOST_CLASS);
1595
- hosts.forEach(function(host){
1596
- try {
1597
- var wrap = host.querySelector && host.querySelector(WRAP_SEL);
1598
- if (!wrap) { host.remove(); return; }
1629
+ var bad = document.querySelectorAll('ul > ' + BETWEEN_SEL + ', ol > ' + BETWEEN_SEL);
1630
+ bad.forEach(function(w){ ensureHost(w); });
1631
+ } catch (e) {}
1599
1632
 
1600
- var afterNum = wrap.getAttribute('data-ezoic-after') || host.getAttribute('data-ezoic-after') || '';
1601
- if (!afterNum) return;
1633
+ // If NOT upscroll, do nothing else (prevents "ads accumulate at bottom" and avoids scroll blocking)
1634
+ if (dy > -8) return;
1602
1635
 
1603
- var anchor = desiredAnchorByAfter(ul, afterNum);
1604
- if (!anchor) {
1605
- // if we can't anchor, remove to prevent piling
1606
- host.remove();
1607
- return;
1608
- }
1636
+ // Up-scroll: tether/reconcile and drop orphan hosts (they cause pile-ups)
1637
+ 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
+ });
1609
1645
 
1610
- if (host.previousElementSibling !== anchor) {
1611
- anchor.insertAdjacentElement('afterend', host);
1612
- }
1613
- } catch (e) {}
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) {}
1614
1654
  });
1615
1655
  } catch (e) {}
1616
1656
  }
1617
1657
 
1618
- // Throttled scheduler (scroll)
1619
- var pending = false;
1620
- var lastRun = 0;
1621
- var COOLDOWN = 160;
1622
-
1623
1658
  function schedule() {
1624
1659
  var now = Date.now();
1625
1660
  if (now - lastRun < COOLDOWN) return;
@@ -1628,15 +1663,15 @@ function buildOrdinalMap(items) {
1628
1663
  requestAnimationFrame(function(){
1629
1664
  pending = false;
1630
1665
  lastRun = Date.now();
1631
- reconcileHostsUpScroll();
1666
+ reconcileUpScroll();
1632
1667
  });
1633
1668
  }
1634
1669
 
1635
1670
  function init() {
1636
- // initial repair only
1671
+ // Initial repair only
1637
1672
  try {
1638
- var ul = getTopicList();
1639
- if (ul) ul.querySelectorAll(':scope > ' + WRAP_SEL).forEach(function(w){ ensureValidHostForBetween(w); });
1673
+ var bad = document.querySelectorAll('ul > ' + BETWEEN_SEL + ', ol > ' + BETWEEN_SEL);
1674
+ bad.forEach(function(w){ ensureHost(w); });
1640
1675
  } catch (e) {}
1641
1676
 
1642
1677
  window.addEventListener('scroll', schedule, { passive: true });
@@ -1645,13 +1680,27 @@ function buildOrdinalMap(items) {
1645
1680
  if (window.jQuery) {
1646
1681
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1647
1682
  setTimeout(schedule, 0);
1648
- setTimeout(schedule, 300);
1683
+ setTimeout(schedule, 250);
1684
+ setTimeout(schedule, 900);
1649
1685
  });
1650
1686
  }
1687
+
1688
+ // MutationObserver: only schedule (no immediate heavy work)
1689
+ try {
1690
+ if (typeof MutationObserver !== 'undefined') {
1691
+ var mo = new MutationObserver(function(muts){
1692
+ 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; }
1695
+ }
1696
+ });
1697
+ mo.observe(document.documentElement || document.body, { childList:true, subtree:true });
1698
+ }
1699
+ } catch (e) {}
1651
1700
  }
1652
1701
 
1653
1702
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1654
1703
  else init();
1655
1704
  })();
1656
- // ===== /V14.3 =====
1705
+ // ===== /V14.1.1 =====
1657
1706
 
package/public/style.css CHANGED
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
 
84
- /* ===== V14.3 host ===== */
84
+ /* ===== V14.1.1 host ===== */
85
85
  li.nodebb-ezoic-host { list-style: none; width: 100%; }
86
- /* ===== /V14.3 ===== */
86
+ /* ===== /V14.1.1 ===== */
87
87