nodebb-plugin-ezoic-infinite 1.6.50 → 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.50",
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.
@@ -661,8 +636,8 @@ function globalGapFixInit() {
661
636
 
662
637
  // ---------------- insertion primitives ----------------
663
638
 
664
- function buildWrap(id, kindClass, afterPos, createPlaceholder, tagName) {
665
- const wrap = document.createElement(tagName || 'div');
639
+ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
640
+ const wrap = document.createElement('div');
666
641
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
667
642
  wrap.setAttribute('data-ezoic-after', String(afterPos));
668
643
  wrap.setAttribute('data-ezoic-wrapid', String(id));
@@ -673,15 +648,6 @@ function globalGapFixInit() {
673
648
  }
674
649
  wrap.style.width = '100%';
675
650
 
676
- // If we're inserted into a UL/OL list, use an LI wrapper to keep valid markup.
677
- // Otherwise browsers can re-parent the DIV, which looks like items/ads “remontent”
678
- // and can break topic list rendering.
679
- if ((tagName || '').toLowerCase() === 'li') {
680
- wrap.style.listStyle = 'none';
681
- wrap.style.padding = '0';
682
- wrap.style.margin = '0';
683
- }
684
-
685
651
  if (createPlaceholder) {
686
652
  const ph = document.createElement('div');
687
653
  ph.id = `${PLACEHOLDER_PREFIX}${id}`;
@@ -701,10 +667,7 @@ function globalGapFixInit() {
701
667
 
702
668
  insertingIds.add(id);
703
669
  try {
704
- const parent = target.parentElement;
705
- const parentTag = parent && parent.tagName ? parent.tagName.toUpperCase() : '';
706
- const tagName = (parentTag === 'UL' || parentTag === 'OL') ? 'li' : 'div';
707
- const wrap = buildWrap(id, kindClass, afterPos, !existingPh, tagName);
670
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
708
671
  target.insertAdjacentElement('afterend', wrap);
709
672
 
710
673
  // If placeholder exists elsewhere (including pool), move it into the wrapper.
@@ -716,9 +679,6 @@ function globalGapFixInit() {
716
679
  } catch (e) {}
717
680
  }
718
681
 
719
- // Help creatives render promptly without requiring an actual user scroll.
720
- scheduleViewportPoke(`afterInsert:${kindClass}:${id}`);
721
-
722
682
  return wrap;
723
683
  } finally {
724
684
  insertingIds.delete(id);
@@ -1551,21 +1511,16 @@ function buildOrdinalMap(items) {
1551
1511
 
1552
1512
 
1553
1513
 
1554
-
1555
-
1556
-
1557
- // ===== 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) =====
1558
1515
  (function () {
1516
+ // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1559
1517
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1560
1518
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1561
1519
  var HOST_CLASS = 'nodebb-ezoic-host';
1562
- var PENDING_CLASS = 'nodebb-ezoic-pending';
1563
1520
 
1564
- var ul = null;
1565
- var mo = null;
1566
1521
  var scheduled = false;
1567
1522
  var lastRun = 0;
1568
- var COOLDOWN = 80;
1523
+ var COOLDOWN = 180;
1569
1524
 
1570
1525
  function getTopicList() {
1571
1526
  try {
@@ -1575,41 +1530,29 @@ function buildOrdinalMap(items) {
1575
1530
  } catch (e) { return null; }
1576
1531
  }
1577
1532
 
1578
- function ensureUL() { ul = ul || getTopicList(); return ul; }
1579
-
1580
- function isHost(el) {
1581
- return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1582
- }
1583
-
1584
- function isBetweenWrap(el) {
1585
- 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));
1586
1535
  }
1587
1536
 
1588
- function ensureHostForWrap(wrap, ulEl) {
1589
- // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1537
+ function ensureHostForWrap(wrap, ul) {
1590
1538
  try {
1591
- 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
+
1592
1542
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1593
1543
  if (host) return host;
1594
1544
 
1595
- ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1596
- 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;
1597
1547
 
1598
- if (wrap.parentElement === ulEl) {
1548
+ // Only wrap if direct child of list (invalid / fragile)
1549
+ if (wrap.parentElement === ul) {
1599
1550
  host = document.createElement('li');
1600
1551
  host.className = HOST_CLASS;
1601
1552
  host.setAttribute('role', 'listitem');
1602
1553
  host.style.listStyle = 'none';
1603
1554
  host.style.width = '100%';
1604
-
1605
- try {
1606
- var after = wrap.getAttribute('data-ezoic-after');
1607
- if (after) host.setAttribute('data-ezoic-after', after);
1608
- var pin = wrap.getAttribute('data-ezoic-pin');
1609
- if (pin) host.setAttribute('data-ezoic-pin', pin);
1610
- } catch (e) {}
1611
-
1612
- ulEl.insertBefore(host, wrap);
1555
+ ul.insertBefore(host, wrap);
1613
1556
  host.appendChild(wrap);
1614
1557
  try { wrap.style.width = '100%'; } catch (e) {}
1615
1558
  return host;
@@ -1618,147 +1561,134 @@ function buildOrdinalMap(items) {
1618
1561
  return null;
1619
1562
  }
1620
1563
 
1621
- function getAfter(el) {
1564
+ function previousTopicLi(node) {
1622
1565
  try {
1623
- var v = null;
1624
- if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1625
- if (!v && el && el.querySelector) {
1626
- var w = el.querySelector(BETWEEN_WRAP_SEL);
1627
- 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;
1628
1571
  }
1629
- var n = parseInt(v, 10);
1630
- return isNaN(n) ? null : n;
1631
- } catch (e) { return null; }
1632
- }
1633
-
1634
- function getTopics(ulEl) {
1635
- try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1636
- }
1637
-
1638
- function lastTopic(ulEl, topics) {
1639
- try {
1640
- topics = topics || getTopics(ulEl);
1641
- return topics && topics.length ? topics[topics.length - 1] : null;
1642
- } catch (e) { return null; }
1643
- }
1644
-
1645
- function moveAfter(node, anchor) {
1646
- try {
1647
- if (!node || !anchor || !anchor.insertAdjacentElement) return;
1648
- if (node.previousElementSibling === anchor) return;
1649
- anchor.insertAdjacentElement('afterend', node);
1650
1572
  } catch (e) {}
1573
+ return null;
1651
1574
  }
1652
1575
 
1653
- 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.
1654
1578
  try {
1655
- if (!host) return;
1656
- ulEl = ulEl || ensureUL();
1657
- if (!ulEl) return;
1658
-
1659
- topics = topics || getTopics(ulEl);
1660
-
1661
- var after = getAfter(host);
1662
- if (!after) {
1663
- var lt = lastTopic(ulEl, topics);
1664
- if (lt) moveAfter(host, lt);
1665
- host.classList && host.classList.remove(PENDING_CLASS);
1666
- return;
1667
- }
1668
-
1669
- if (after > topics.length) {
1670
- var lt2 = lastTopic(ulEl, topics);
1671
- if (lt2) moveAfter(host, lt2);
1672
- host.classList && host.classList.add(PENDING_CLASS);
1673
- 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
+ }
1674
1599
  }
1675
-
1676
- var anchor = topics[after - 1];
1677
- if (anchor) moveAfter(host, anchor);
1678
- host.classList && host.classList.remove(PENDING_CLASS);
1600
+ return maxRun >= 2;
1679
1601
  } catch (e) {}
1602
+ return false;
1680
1603
  }
1681
1604
 
1682
- function reconcile() {
1683
- var ulEl = ensureUL();
1684
- if (!ulEl) return;
1605
+ function redistribute(ul) {
1606
+ try {
1607
+ if (!ul) return;
1608
+
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); });
1685
1611
 
1686
- var topics = getTopics(ulEl);
1612
+ // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
+ if (!detectPileUp(ul)) return;
1687
1614
 
1688
- try {
1689
- ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1690
- var h = ensureHostForWrap(w, ulEl);
1691
- if (h) placeOrPend(h, ulEl, topics);
1692
- });
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;
1693
1621
 
1694
- ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1695
- placeOrPend(h, ulEl, topics);
1696
- });
1622
+ var anchor = previousTopicLi(host);
1623
+ if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1697
1624
 
1698
- ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1699
- var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1700
- if (h) placeOrPend(h, ulEl, topics);
1625
+ if (host.previousElementSibling !== anchor) {
1626
+ anchor.insertAdjacentElement('afterend', host);
1627
+ }
1628
+ } catch (e) {}
1701
1629
  });
1702
1630
  } catch (e) {}
1703
1631
  }
1704
1632
 
1705
- function scheduleReconcile() {
1633
+ function schedule(reason) {
1706
1634
  var now = Date.now();
1707
- if (scheduled) return;
1708
1635
  if (now - lastRun < COOLDOWN) return;
1636
+ if (scheduled) return;
1709
1637
  scheduled = true;
1710
- lastRun = now;
1711
1638
  requestAnimationFrame(function () {
1712
1639
  scheduled = false;
1713
- reconcile();
1640
+ lastRun = Date.now();
1641
+ try {
1642
+ var ul = getTopicList();
1643
+ if (!ul) return;
1644
+ redistribute(ul);
1645
+ } catch (e) {}
1714
1646
  });
1715
1647
  }
1716
1648
 
1717
- function initObserver() {
1718
- var ulEl = ensureUL();
1719
- if (!ulEl || mo) return;
1720
-
1721
- mo = new MutationObserver(function () {
1722
- scheduleReconcile();
1723
- });
1649
+ function init() {
1650
+ schedule('init');
1724
1651
 
1725
- mo.observe(ulEl, { childList: true, subtree: true });
1726
- }
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
+ };
1727
1663
 
1728
- function init() {
1729
- scheduleViewportPoke('init');
1730
- initObserver();
1731
- 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) {}
1732
1678
 
1679
+ // NodeBB events: run after infinite scroll batches
1733
1680
  if (window.jQuery) {
1734
1681
  try {
1735
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1736
- ul = null;
1737
- try { if (mo) mo.disconnect(); } catch (e) {}
1738
- mo = null;
1739
- initObserver();
1740
- scheduleReconcile();
1741
- scheduleViewportPoke('ajaxify.end');
1742
- setTimeout(scheduleReconcile, 120);
1743
- 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);
1744
1685
  });
1745
1686
  } catch (e) {}
1746
1687
  }
1747
-
1748
- var tries = 0;
1749
- var t = setInterval(function () {
1750
- tries++;
1751
- if (ensureUL()) {
1752
- clearInterval(t);
1753
- initObserver();
1754
- scheduleReconcile();
1755
- }
1756
- if (tries > 30) clearInterval(t);
1757
- }, 200);
1758
1688
  }
1759
1689
 
1760
1690
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1761
1691
  else init();
1762
1692
  })();
1763
- // ===== /V17.8 =====
1693
+ // ===== /V17 =====
1764
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
-