nodebb-plugin-ezoic-infinite 1.6.34 → 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.34",
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,189 +1514,222 @@ function buildOrdinalMap(items) {
1514
1514
 
1515
1515
 
1516
1516
 
1517
- // ===== V17.3: keep injection; hide top pile on upscroll; rehome EMPTY hosts on downscroll =====
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 lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
1525
- var scheduled = false;
1526
- var lastRun = 0;
1527
- var COOLDOWN = 120;
1522
+ var ANCHOR_ATTR = 'data-ezoic-anchor-tid';
1528
1523
 
1529
- function getY(){ return window.pageYOffset || document.documentElement.scrollTop || 0; }
1524
+ var pending = false;
1525
+ var queue = new Set();
1530
1526
 
1531
- function getTopicList(){
1527
+ function getTopicList() {
1532
1528
  try {
1533
1529
  var li = document.querySelector(TOPIC_LI_SEL);
1534
1530
  if (!li) return null;
1535
1531
  return li.closest ? li.closest('ul,ol') : null;
1536
- } catch(e){ return null; }
1532
+ } catch (e) { return null; }
1533
+ }
1534
+
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];
1554
+ }
1555
+ } catch (e) {}
1556
+ return map;
1557
+ }
1558
+
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;
1537
1568
  }
1538
1569
 
1539
- function ensureHostForWrap(wrap, ul){
1540
- try{
1541
- if (!wrap || wrap.nodeType!==1) return null;
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;
1542
1574
  if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1543
1575
 
1544
- var host = wrap.closest ? wrap.closest('li.'+HOST_CLASS) : null;
1576
+ var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1545
1577
  if (host) return host;
1546
1578
 
1547
- if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1548
- if (!ul || !(ul.tagName==='UL' || ul.tagName==='OL')) return null;
1579
+ ul = ul || (wrap.closest ? wrap.closest('ul,ol') : null);
1580
+ if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1549
1581
 
1550
- if (wrap.parentElement === ul){
1582
+ // Only wrap if it's a direct child of the list
1583
+ if (wrap.parentElement === ul) {
1551
1584
  host = document.createElement('li');
1552
1585
  host.className = HOST_CLASS;
1553
- host.setAttribute('role','listitem');
1554
- host.style.listStyle='none';
1555
- host.style.width='100%';
1556
- // preserve after
1557
- try {
1558
- var after = wrap.getAttribute('data-ezoic-after');
1559
- if (after) host.setAttribute('data-ezoic-after', after);
1560
- } catch(e){}
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
+
1561
1597
  ul.insertBefore(host, wrap);
1562
1598
  host.appendChild(wrap);
1563
- try { wrap.style.width='100%'; } catch(e){}
1599
+ try { wrap.style.width = '100%'; } catch (e) {}
1564
1600
  return host;
1565
1601
  }
1566
- }catch(e){}
1602
+ } catch (e) {}
1567
1603
  return null;
1568
1604
  }
1569
1605
 
1570
- function hostHasAdContent(host){
1571
- // iframes/amp ads mean it's already rendered; moving might reduce fill
1572
- try{
1573
- return !!(host.querySelector && host.querySelector('iframe, amp-ad, amp-iframe, .amp-ads, .ezoic-ad iframe'));
1574
- }catch(e){ return false; }
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) {}
1575
1615
  }
1576
1616
 
1577
- function nthTopic(ul, n){
1578
- try{
1579
- var topics = ul.querySelectorAll(TOPIC_LI_SEL);
1580
- if (!topics || !topics.length) return null;
1581
- if (n<1) n=1;
1582
- if (n>topics.length) n=topics.length;
1583
- return topics[n-1] || null;
1584
- }catch(e){ return null; }
1585
- }
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;
1586
1622
 
1587
- function getAfter(host){
1588
- try{
1589
- var v = host.getAttribute('data-ezoic-after');
1590
- if (!v){
1591
- var wrap = host.querySelector && host.querySelector(BETWEEN_WRAP_SEL);
1592
- if (wrap) v = wrap.getAttribute('data-ezoic-after');
1593
- }
1594
- var n = parseInt(v,10);
1595
- return isNaN(n)? null : n;
1596
- }catch(e){ return null; }
1597
- }
1623
+ // if host got detached from list, skip
1624
+ if (!(ul.tagName === 'UL' || ul.tagName === 'OL')) return;
1598
1625
 
1599
- function detectAndHideTopPile(ul){
1600
- // Hide piled hosts near top so user doesn't see the stack.
1601
- try{
1602
- var kids = ul.children;
1603
- var limit = Math.min(kids.length, 60);
1604
- var run = 0;
1605
- for (var i=0;i<limit;i++){
1606
- var el = kids[i];
1607
- var isBetweenHost = (el.tagName==='LI' && el.classList && el.classList.contains(HOST_CLASS) && el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
1608
- var isTopic = (el.matches && el.matches(TOPIC_LI_SEL));
1609
- if (isBetweenHost){
1610
- run++;
1611
- if (run>=2){
1612
- // mark piled beyond the first
1613
- el.setAttribute(PILED_ATTR,'1');
1614
- }
1615
- } else if (isTopic){
1616
- run=0;
1617
- } else {
1618
- // reset on any non-topic
1619
- }
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);
1620
1636
  }
1621
- }catch(e){}
1637
+ } catch (e) {}
1622
1638
  }
1623
1639
 
1624
- function rehomePiledEmptyOnDownscroll(ul){
1625
- // On downscroll, try to move only EMPTY (not yet rendered) piled hosts to intended anchors and unhide them.
1626
- try{
1627
- var piled = ul.querySelectorAll('li.'+HOST_CLASS+'['+PILED_ATTR+'="1"]');
1628
- if (!piled.length) return;
1629
-
1630
- piled.forEach(function(host){
1631
- try{
1632
- // if it already has rendered ad, just unhide; do not move
1633
- if (hostHasAdContent(host)){
1634
- host.removeAttribute(PILED_ATTR);
1635
- return;
1636
- }
1637
- var after = getAfter(host);
1638
- var anchor = after!=null ? nthTopic(ul, after) : null;
1639
- if (anchor){
1640
- anchor.insertAdjacentElement('afterend', host);
1641
- }
1642
- host.removeAttribute(PILED_ATTR);
1643
- }catch(e){}
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);
1644
1650
  });
1645
- }catch(e){}
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();
1646
1664
  }
1647
1665
 
1648
- function schedule(fn){
1649
- var now = Date.now();
1650
- if (now-lastRun < COOLDOWN) return;
1651
- if (scheduled) return;
1652
- scheduled = true;
1653
- requestAnimationFrame(function(){
1654
- scheduled=false;
1655
- lastRun = Date.now();
1656
- fn();
1657
- });
1666
+ function schedule() {
1667
+ if (pending) return;
1668
+ pending = true;
1669
+ requestAnimationFrame(drain);
1670
+ }
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) {}
1658
1689
  }
1659
1690
 
1660
- function onScroll(){
1661
- var y = getY();
1662
- var dy = y - lastY;
1663
- lastY = y;
1691
+ function init() {
1664
1692
  var ul = getTopicList();
1665
1693
  if (!ul) return;
1666
1694
 
1667
- // always wrap invalid ul>div (cheap)
1668
- try { ul.querySelectorAll(':scope > '+BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); }); } catch(e){}
1695
+ // Initial pass: host all between DIVs and reconcile once.
1696
+ schedule();
1669
1697
 
1670
- if (dy < -8 && y < 900){
1671
- // upscroll near top: hide pile quickly (no moving)
1672
- schedule(function(){ detectAndHideTopPile(ul); });
1673
- } else if (dy > 8){
1674
- // downscroll: rehome only empty piled hosts (safe) and unhide
1675
- schedule(function(){ rehomePiledEmptyOnDownscroll(ul); });
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) {}
1676
1717
  }
1677
- }
1678
1718
 
1679
- function init(){
1680
- window.addEventListener('scroll', onScroll, { passive:true });
1681
-
1682
- if (window.jQuery){
1683
- try{
1684
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1685
- // wrap invalids only
1686
- setTimeout(function(){
1687
- try{
1688
- var ul = getTopicList();
1689
- if (!ul) return;
1690
- ul.querySelectorAll(':scope > '+BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); });
1691
- }catch(e){}
1692
- }, 50);
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);
1693
1726
  });
1694
- }catch(e){}
1727
+ } catch (e) {}
1695
1728
  }
1696
1729
  }
1697
1730
 
1698
1731
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1699
1732
  else init();
1700
1733
  })();
1701
- // ===== /V17.3 =====
1734
+ // ===== /V18 =====
1702
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.3 piled hide ===== */
92
- li.nodebb-ezoic-host[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.3 ===== */
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