nodebb-plugin-ezoic-infinite 1.6.33 → 1.6.35

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.33",
3
+ "version": "1.6.35",
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,162 +1514,222 @@ function buildOrdinalMap(items) {
1514
1514
 
1515
1515
 
1516
1516
 
1517
- // ===== V17.5: Viewport-based pile masking (no moves, keeps injection intact) =====
1518
- (function(){
1517
+ // ===== V18: Always host between DIVs in <li> + anchor by topic tid (MutationObserver, no scroll hooks to injection) =====
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 PILED_ATTR = 'data-ezoic-piled';
1523
-
1524
- var scheduled = false;
1525
- var lastRun = 0;
1526
- var COOLDOWN = 70;
1522
+ var ANCHOR_ATTR = 'data-ezoic-anchor-tid';
1527
1523
 
1528
- var lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
1524
+ var pending = false;
1525
+ var queue = new Set();
1529
1526
 
1530
- function getY(){ return window.pageYOffset || document.documentElement.scrollTop || 0; }
1531
-
1532
- function getTopicList(){
1533
- try{
1527
+ function getTopicList() {
1528
+ try {
1534
1529
  var li = document.querySelector(TOPIC_LI_SEL);
1535
1530
  if (!li) return null;
1536
1531
  return li.closest ? li.closest('ul,ol') : null;
1537
- }catch(e){ return null; }
1532
+ } catch (e) { return null; }
1538
1533
  }
1539
1534
 
1540
- function isBetweenNode(el){
1541
- try{
1542
- if (!el || el.nodeType!==1) return false;
1543
- if (el.matches && el.matches(BETWEEN_WRAP_SEL)) return true; // ul>div
1544
- if (el.tagName==='LI' && el.classList && el.classList.contains(HOST_CLASS)) {
1545
- return !!(el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
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
+ }
1546
+
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];
1546
1554
  }
1547
- }catch(e){}
1548
- return false;
1555
+ } catch (e) {}
1556
+ return map;
1549
1557
  }
1550
1558
 
1551
- function isTopicNode(el){
1552
- try{ return !!(el && el.nodeType===1 && el.matches && el.matches(TOPIC_LI_SEL)); }catch(e){ return false; }
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;
1553
1568
  }
1554
1569
 
1555
- function setPiled(el, piled){
1556
- try{
1557
- if (!el) return;
1558
- if (piled) el.setAttribute(PILED_ATTR,'1');
1559
- else el.removeAttribute(PILED_ATTR);
1560
- }catch(e){}
1570
+ function ensureHostForWrap(wrap, ul) {
1571
+ // Always ensure UL children are LI, to avoid NodeBB reparenting DIVs (root cause of pile-up).
1572
+ try {
1573
+ if (!wrap || wrap.nodeType !== 1) return null;
1574
+ if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1575
+
1576
+ var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1577
+ if (host) return host;
1578
+
1579
+ ul = ul || (wrap.closest ? wrap.closest('ul,ol') : null);
1580
+ if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1581
+
1582
+ // Only wrap if it's a direct child of the list
1583
+ if (wrap.parentElement === ul) {
1584
+ host = document.createElement('li');
1585
+ host.className = HOST_CLASS;
1586
+ host.setAttribute('role', 'listitem');
1587
+ host.style.listStyle = 'none';
1588
+ 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);
1598
+ host.appendChild(wrap);
1599
+ try { wrap.style.width = '100%'; } catch (e) {}
1600
+ return host;
1601
+ }
1602
+ } catch (e) {}
1603
+ return null;
1561
1604
  }
1562
1605
 
1563
- function clearAll(ul){
1564
- try{
1565
- if (!ul) return;
1566
- ul.querySelectorAll('['+PILED_ATTR+'="1"]').forEach(function(n){ setPiled(n,false); });
1567
- }catch(e){}
1606
+ function ensureAnchorTid(host) {
1607
+ 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) {}
1568
1615
  }
1569
1616
 
1570
- function maskViewportPile(ul){
1571
- // We mask piled ads based on what is actually near/in the viewport, not just first N children.
1572
- // This catches cases where virtualization moves some ads but not others.
1573
- try{
1617
+ function reconcileHost(host, ul, tidMap) {
1618
+ try {
1619
+ if (!host || host.nodeType !== 1 || !host.isConnected) return;
1620
+ ul = ul || host.parentElement;
1574
1621
  if (!ul) return;
1575
- var vh = window.innerHeight || 800;
1576
- var bandTop = -200; // slightly above viewport
1577
- var bandBottom = vh * 1.6; // covers top area user sees when returning up
1578
-
1579
- // collect candidates in band in DOM order
1580
- var kids = ul.children;
1581
- var candidates = [];
1582
- for (var i=0;i<kids.length;i++){
1583
- var el = kids[i];
1584
- if (!isBetweenNode(el) && !isTopicNode(el)) continue;
1585
- // compute rect (cheap enough on limited band; stop after we've passed far below band)
1586
- var r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
1587
- if (!r) continue;
1588
- if (r.top > bandBottom && candidates.length > 0) {
1589
- // Once we've started collecting and we are past band, we can stop scanning further.
1590
- break;
1591
- }
1592
- if (r.bottom < bandTop) continue;
1593
- if (r.top > bandBottom) continue;
1594
1622
 
1595
- candidates.push(el);
1596
- }
1623
+ // if host got detached from list, skip
1624
+ if (!(ul.tagName === 'UL' || ul.tagName === 'OL')) return;
1597
1625
 
1598
- // Now walk candidates and hide consecutive between nodes (keep first between in a run)
1599
- var run = 0;
1600
- for (var j=0;j<candidates.length;j++){
1601
- var el2 = candidates[j];
1602
- if (isTopicNode(el2)){
1603
- run = 0;
1604
- continue;
1605
- }
1606
- if (isBetweenNode(el2)){
1607
- run++;
1608
- setPiled(el2, run >= 2);
1609
- }
1626
+ ensureAnchorTid(host);
1627
+
1628
+ var tid = host.getAttribute(ANCHOR_ATTR);
1629
+ if (!tid) return;
1630
+
1631
+ var anchorLi = tidMap[tid];
1632
+ if (!anchorLi || !anchorLi.isConnected) return;
1633
+
1634
+ if (host.previousElementSibling !== anchorLi) {
1635
+ anchorLi.insertAdjacentElement('afterend', host);
1610
1636
  }
1611
- }catch(e){}
1637
+ } catch (e) {}
1612
1638
  }
1613
1639
 
1614
- function schedule(fn){
1615
- var now = Date.now();
1616
- if (now-lastRun < COOLDOWN) return;
1617
- if (scheduled) return;
1618
- scheduled = true;
1619
- requestAnimationFrame(function(){
1620
- scheduled = false;
1621
- lastRun = Date.now();
1622
- fn();
1623
- });
1640
+ function drain() {
1641
+ pending = false;
1642
+ var ul = getTopicList();
1643
+ if (!ul) return;
1644
+
1645
+ // First, host any direct-child between DIVs currently in list (cheap, keeps DOM valid).
1646
+ try {
1647
+ ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1648
+ var h = ensureHostForWrap(w, ul);
1649
+ if (h) queue.add(h);
1650
+ });
1651
+ } catch (e) {}
1652
+
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;
1662
+ }
1663
+ if (queue.size) schedule();
1624
1664
  }
1625
1665
 
1626
- function onScroll(){
1627
- var y = getY();
1628
- var dy = y - lastY;
1629
- lastY = y;
1666
+ function schedule() {
1667
+ if (pending) return;
1668
+ pending = true;
1669
+ requestAnimationFrame(drain);
1670
+ }
1630
1671
 
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
+ }
1690
+
1691
+ function init() {
1631
1692
  var ul = getTopicList();
1632
1693
  if (!ul) return;
1633
1694
 
1634
- // Only manage pile masking when user is heading upward OR is near top-ish.
1635
- if (dy < -6 || y < 1100){
1636
- schedule(function(){ maskViewportPile(ul); });
1637
- } else if (dy > 12 && y > 1400){
1638
- // when leaving the top area, restore visibility
1639
- schedule(function(){ clearAll(ul); });
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) {}
1640
1717
  }
1641
- }
1642
1718
 
1643
- function init(){
1644
- window.addEventListener('scroll', onScroll, { passive:true });
1645
- window.addEventListener('resize', function(){
1646
- var ul = getTopicList();
1647
- if (!ul) return;
1648
- schedule(function(){ maskViewportPile(ul); });
1649
- }, { passive:true });
1650
-
1651
- if (window.jQuery){
1652
- try{
1653
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1654
- setTimeout(function(){
1655
- var ul = getTopicList();
1656
- if (!ul) return;
1657
- schedule(function(){ maskViewportPile(ul); });
1658
- }, 120);
1719
+ // NodeBB events: run a settle pass after batches
1720
+ if (window.jQuery) {
1721
+ 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);
1659
1726
  });
1660
- }catch(e){}
1727
+ } catch (e) {}
1661
1728
  }
1662
-
1663
- // initial
1664
- setTimeout(function(){
1665
- var ul = getTopicList();
1666
- if (!ul) return;
1667
- schedule(function(){ maskViewportPile(ul); });
1668
- }, 250);
1669
1729
  }
1670
1730
 
1671
1731
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1672
1732
  else init();
1673
1733
  })();
1674
- // ===== /V17.5 =====
1734
+ // ===== /V18 =====
1675
1735
 
package/public/style.css CHANGED
@@ -88,10 +88,8 @@ li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; displa
88
88
 
89
89
 
90
90
 
91
- /* ===== V17.5 pile hide ===== */
92
- [data-ezoic-piled="1"] { display: none !important; }
93
- /* keep host stable */
94
- li.nodebb-ezoic-host { list-style:none; width:100%; display:block; }
95
- li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width:100%; display:block; }
96
- /* ===== /V17.5 ===== */
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 ===== */
97
95