nodebb-plugin-ezoic-infinite 1.6.49 → 1.6.51

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/README-V17.md ADDED
@@ -0,0 +1,17 @@
1
+ # V17 – Minimal pile-up fixer (base: ezoic-infinite-fixed (1).zip)
2
+
3
+ Tu as demandé de repartir du zip d'origine: certaines variantes ont cassé l'injection.
4
+
5
+ ## Principe
6
+ V17 **ne touche pas** au code d'injection (pas de hook dans insertAfter/moveWrapAfter).
7
+ Les pubs continuent de se créer comme avant.
8
+
9
+ V17 intervient seulement quand on détecte un symptôme "pile-up" (2+ pubs between consécutives sans topics entre):
10
+ - encapsule les `ul > div.nodebb-ezoic-wrap.ezoic-ad-between` dans un `<li class="nodebb-ezoic-host">` (DOM stable)
11
+ - redistribue les hosts en les replaçant juste après le topic `<li>` précédent dans la liste
12
+
13
+ ## Déclencheurs
14
+ - MutationObserver uniquement sur la liste de topics (guarded par détection pile-up)
15
+ - events NodeBB ajaxify / infiniteScroll
16
+
17
+ Build: 2026-02-18T14:53:42.167388Z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.49",
3
+ "version": "1.6.51",
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
@@ -63,31 +63,6 @@
63
63
  // Production build: debug disabled
64
64
  function dbg() {}
65
65
 
66
- // Some ad providers (notably Google Safeframe) can defer creative rendering until a
67
- // scroll/resize tick is observed. On SPA/infinite-scroll pages we can end up with
68
- // valid iframes that stay visually empty until the user nudges the scroll.
69
- // We gently "poke" observers by dispatching scroll/resize events (debounced).
70
- let _ezPokeScheduled = false;
71
- function scheduleViewportPoke(reason) {
72
- if (_ezPokeScheduled) return;
73
- _ezPokeScheduled = true;
74
- try {
75
- requestAnimationFrame(() => {
76
- _ezPokeScheduled = false;
77
- try {
78
- window.dispatchEvent(new Event('scroll'));
79
- window.dispatchEvent(new Event('resize'));
80
- } catch (e) {}
81
- try {
82
- document.dispatchEvent(new Event('scroll'));
83
- } catch (e) {}
84
- dbg('viewportPoke', reason || '');
85
- });
86
- } catch (e) {
87
- _ezPokeScheduled = false;
88
- }
89
- }
90
-
91
66
  // Ezoic (and some partner scripts) can be very noisy in console on SPA/Ajaxify setups.
92
67
  // These warnings are not actionable for end-users and can flood the console.
93
68
  // We selectively silence the known spam patterns while keeping other warnings intact.
@@ -704,9 +679,6 @@ function globalGapFixInit() {
704
679
  } catch (e) {}
705
680
  }
706
681
 
707
- // Help creatives render promptly without requiring an actual user scroll.
708
- scheduleViewportPoke(`afterInsert:${kindClass}:${id}`);
709
-
710
682
  return wrap;
711
683
  } finally {
712
684
  insertingIds.delete(id);
@@ -1539,21 +1511,16 @@ function buildOrdinalMap(items) {
1539
1511
 
1540
1512
 
1541
1513
 
1542
-
1543
-
1544
-
1545
- // ===== V17.8: No pile-up by design — keep "future" between slots pending/collapsed until their anchor topic exists =====
1514
+ // ===== V17 minimal pile-fix (no insert hooks) =====
1546
1515
  (function () {
1516
+ // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1547
1517
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1548
1518
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1549
1519
  var HOST_CLASS = 'nodebb-ezoic-host';
1550
- var PENDING_CLASS = 'nodebb-ezoic-pending';
1551
1520
 
1552
- var ul = null;
1553
- var mo = null;
1554
1521
  var scheduled = false;
1555
1522
  var lastRun = 0;
1556
- var COOLDOWN = 80;
1523
+ var COOLDOWN = 180;
1557
1524
 
1558
1525
  function getTopicList() {
1559
1526
  try {
@@ -1563,41 +1530,29 @@ function buildOrdinalMap(items) {
1563
1530
  } catch (e) { return null; }
1564
1531
  }
1565
1532
 
1566
- function ensureUL() { ul = ul || getTopicList(); return ul; }
1567
-
1568
- function isHost(el) {
1569
- return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1570
- }
1571
-
1572
- function isBetweenWrap(el) {
1573
- try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1533
+ function isHost(node) {
1534
+ return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
1574
1535
  }
1575
1536
 
1576
- function ensureHostForWrap(wrap, ulEl) {
1577
- // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1537
+ function ensureHostForWrap(wrap, ul) {
1578
1538
  try {
1579
- if (!wrap || !isBetweenWrap(wrap)) return null;
1539
+ if (!wrap || wrap.nodeType !== 1) return null;
1540
+ if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1541
+
1580
1542
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1581
1543
  if (host) return host;
1582
1544
 
1583
- ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1584
- if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1545
+ if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1546
+ if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1585
1547
 
1586
- if (wrap.parentElement === ulEl) {
1548
+ // Only wrap if direct child of list (invalid / fragile)
1549
+ if (wrap.parentElement === ul) {
1587
1550
  host = document.createElement('li');
1588
1551
  host.className = HOST_CLASS;
1589
1552
  host.setAttribute('role', 'listitem');
1590
1553
  host.style.listStyle = 'none';
1591
1554
  host.style.width = '100%';
1592
-
1593
- try {
1594
- var after = wrap.getAttribute('data-ezoic-after');
1595
- if (after) host.setAttribute('data-ezoic-after', after);
1596
- var pin = wrap.getAttribute('data-ezoic-pin');
1597
- if (pin) host.setAttribute('data-ezoic-pin', pin);
1598
- } catch (e) {}
1599
-
1600
- ulEl.insertBefore(host, wrap);
1555
+ ul.insertBefore(host, wrap);
1601
1556
  host.appendChild(wrap);
1602
1557
  try { wrap.style.width = '100%'; } catch (e) {}
1603
1558
  return host;
@@ -1606,147 +1561,134 @@ function buildOrdinalMap(items) {
1606
1561
  return null;
1607
1562
  }
1608
1563
 
1609
- function getAfter(el) {
1564
+ function previousTopicLi(node) {
1610
1565
  try {
1611
- var v = null;
1612
- if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1613
- if (!v && el && el.querySelector) {
1614
- var w = el.querySelector(BETWEEN_WRAP_SEL);
1615
- if (w) v = w.getAttribute('data-ezoic-after');
1566
+ var prev = node.previousElementSibling;
1567
+ while (prev) {
1568
+ if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1569
+ // skip other hosts/wraps
1570
+ prev = prev.previousElementSibling;
1616
1571
  }
1617
- var n = parseInt(v, 10);
1618
- return isNaN(n) ? null : n;
1619
- } catch (e) { return null; }
1620
- }
1621
-
1622
- function getTopics(ulEl) {
1623
- try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1624
- }
1625
-
1626
- function lastTopic(ulEl, topics) {
1627
- try {
1628
- topics = topics || getTopics(ulEl);
1629
- return topics && topics.length ? topics[topics.length - 1] : null;
1630
- } catch (e) { return null; }
1631
- }
1632
-
1633
- function moveAfter(node, anchor) {
1634
- try {
1635
- if (!node || !anchor || !anchor.insertAdjacentElement) return;
1636
- if (node.previousElementSibling === anchor) return;
1637
- anchor.insertAdjacentElement('afterend', node);
1638
1572
  } catch (e) {}
1573
+ return null;
1639
1574
  }
1640
1575
 
1641
- function placeOrPend(host, ulEl, topics) {
1576
+ function detectPileUp(ul) {
1577
+ // Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
1642
1578
  try {
1643
- if (!host) return;
1644
- ulEl = ulEl || ensureUL();
1645
- if (!ulEl) return;
1646
-
1647
- topics = topics || getTopics(ulEl);
1648
-
1649
- var after = getAfter(host);
1650
- if (!after) {
1651
- var lt = lastTopic(ulEl, topics);
1652
- if (lt) moveAfter(host, lt);
1653
- host.classList && host.classList.remove(PENDING_CLASS);
1654
- return;
1655
- }
1656
-
1657
- if (after > topics.length) {
1658
- var lt2 = lastTopic(ulEl, topics);
1659
- if (lt2) moveAfter(host, lt2);
1660
- host.classList && host.classList.add(PENDING_CLASS);
1661
- return;
1579
+ var kids = ul.children;
1580
+ var run = 0;
1581
+ var maxRun = 0;
1582
+ for (var i = 0; i < kids.length; i++) {
1583
+ var el = kids[i];
1584
+ var isBetween = false;
1585
+ if (isHost(el)) {
1586
+ isBetween = !!(el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
1587
+ } else if (el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1588
+ isBetween = true;
1589
+ }
1590
+ if (isBetween) {
1591
+ run++;
1592
+ if (run > maxRun) maxRun = run;
1593
+ } else if (el.matches && el.matches(TOPIC_LI_SEL)) {
1594
+ run = 0;
1595
+ } else {
1596
+ // other nodes reset lightly
1597
+ run = 0;
1598
+ }
1662
1599
  }
1663
-
1664
- var anchor = topics[after - 1];
1665
- if (anchor) moveAfter(host, anchor);
1666
- host.classList && host.classList.remove(PENDING_CLASS);
1600
+ return maxRun >= 2;
1667
1601
  } catch (e) {}
1602
+ return false;
1668
1603
  }
1669
1604
 
1670
- function reconcile() {
1671
- var ulEl = ensureUL();
1672
- if (!ulEl) return;
1605
+ function redistribute(ul) {
1606
+ try {
1607
+ if (!ul) return;
1673
1608
 
1674
- var topics = getTopics(ulEl);
1609
+ // Step 1: wrap any direct child between DIVs into LI hosts (makes list stable)
1610
+ ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); });
1675
1611
 
1676
- try {
1677
- ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1678
- var h = ensureHostForWrap(w, ulEl);
1679
- if (h) placeOrPend(h, ulEl, topics);
1680
- });
1612
+ // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
+ if (!detectPileUp(ul)) return;
1681
1614
 
1682
- ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1683
- placeOrPend(h, ulEl, topics);
1684
- });
1615
+ // Move each host to immediately after the closest previous topic LI at its current position.
1616
+ var hosts = ul.querySelectorAll(':scope > li.' + HOST_CLASS);
1617
+ hosts.forEach(function(host){
1618
+ try {
1619
+ var wrap = host.querySelector && host.querySelector(BETWEEN_WRAP_SEL);
1620
+ if (!wrap) return;
1621
+
1622
+ var anchor = previousTopicLi(host);
1623
+ if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1685
1624
 
1686
- ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1687
- var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1688
- if (h) placeOrPend(h, ulEl, topics);
1625
+ if (host.previousElementSibling !== anchor) {
1626
+ anchor.insertAdjacentElement('afterend', host);
1627
+ }
1628
+ } catch (e) {}
1689
1629
  });
1690
1630
  } catch (e) {}
1691
1631
  }
1692
1632
 
1693
- function scheduleReconcile() {
1633
+ function schedule(reason) {
1694
1634
  var now = Date.now();
1695
- if (scheduled) return;
1696
1635
  if (now - lastRun < COOLDOWN) return;
1636
+ if (scheduled) return;
1697
1637
  scheduled = true;
1698
- lastRun = now;
1699
1638
  requestAnimationFrame(function () {
1700
1639
  scheduled = false;
1701
- reconcile();
1640
+ lastRun = Date.now();
1641
+ try {
1642
+ var ul = getTopicList();
1643
+ if (!ul) return;
1644
+ redistribute(ul);
1645
+ } catch (e) {}
1702
1646
  });
1703
1647
  }
1704
1648
 
1705
- function initObserver() {
1706
- var ulEl = ensureUL();
1707
- if (!ulEl || mo) return;
1708
-
1709
- mo = new MutationObserver(function () {
1710
- scheduleReconcile();
1711
- });
1649
+ function init() {
1650
+ schedule('init');
1712
1651
 
1713
- mo.observe(ulEl, { childList: true, subtree: true });
1714
- }
1652
+ // Observe only the topic list once available
1653
+ try {
1654
+ if (typeof MutationObserver !== 'undefined') {
1655
+ var observeList = function(ul){
1656
+ if (!ul) return;
1657
+ var mo = new MutationObserver(function(muts){
1658
+ // schedule on any change; redistribute() itself is guarded by pile-up detection
1659
+ schedule('mo');
1660
+ });
1661
+ mo.observe(ul, { childList: true, subtree: true });
1662
+ };
1715
1663
 
1716
- function init() {
1717
- scheduleViewportPoke('init');
1718
- initObserver();
1719
- scheduleReconcile();
1664
+ var ul = getTopicList();
1665
+ if (ul) observeList(ul);
1666
+ else {
1667
+ var mo2 = new MutationObserver(function(){
1668
+ var u2 = getTopicList();
1669
+ if (u2) {
1670
+ try { observeList(u2); } catch(e){}
1671
+ try { mo2.disconnect(); } catch(e){}
1672
+ }
1673
+ });
1674
+ mo2.observe(document.documentElement || document.body, { childList: true, subtree: true });
1675
+ }
1676
+ }
1677
+ } catch (e) {}
1720
1678
 
1679
+ // NodeBB events: run after infinite scroll batches
1721
1680
  if (window.jQuery) {
1722
1681
  try {
1723
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1724
- ul = null;
1725
- try { if (mo) mo.disconnect(); } catch (e) {}
1726
- mo = null;
1727
- initObserver();
1728
- scheduleReconcile();
1729
- scheduleViewportPoke('ajaxify.end');
1730
- setTimeout(scheduleReconcile, 120);
1731
- setTimeout(scheduleReconcile, 500);
1682
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1683
+ setTimeout(function(){ schedule('event'); }, 50);
1684
+ setTimeout(function(){ schedule('event2'); }, 400);
1732
1685
  });
1733
1686
  } catch (e) {}
1734
1687
  }
1735
-
1736
- var tries = 0;
1737
- var t = setInterval(function () {
1738
- tries++;
1739
- if (ensureUL()) {
1740
- clearInterval(t);
1741
- initObserver();
1742
- scheduleReconcile();
1743
- }
1744
- if (tries > 30) clearInterval(t);
1745
- }, 200);
1746
1688
  }
1747
1689
 
1748
1690
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1749
1691
  else init();
1750
1692
  })();
1751
- // ===== /V17.8 =====
1693
+ // ===== /V17 =====
1752
1694
 
package/public/style.css CHANGED
@@ -86,20 +86,3 @@ li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
86
86
  li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
87
87
  /* ===== /V17 ===== */
88
88
 
89
-
90
-
91
- /* ===== V17.8 pending slots (prevents pile-up top/bottom) ===== */
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
-
95
- /* slots whose anchor topic isn't loaded yet */
96
- li.nodebb-ezoic-host.nodebb-ezoic-pending{
97
- max-height: 0 !important;
98
- margin: 0 !important;
99
- padding: 0 !important;
100
- overflow: hidden !important;
101
- opacity: 0 !important;
102
- pointer-events: none !important;
103
- }
104
- /* ===== /V17.8 ===== */
105
-