nodebb-plugin-ezoic-infinite 1.6.23 → 1.6.25

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.md ADDED
@@ -0,0 +1,15 @@
1
+ # NodeBB Plugin – Ezoic Infinite (Production)
2
+
3
+ This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
4
+ with full support for infinite scroll.
5
+
6
+ ## Key guarantees
7
+ - No duplicate ads back-to-back
8
+ - One showAds call per placeholder
9
+ - Fast reveal (MutationObserver on first child)
10
+ - Safe with ajaxify navigation
11
+ - Works with NodeBB 4.x + Harmony
12
+
13
+ ## Notes
14
+ - Placeholders must exist and be selected in Ezoic
15
+ - Use separate ID pools for topics vs messages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.23",
3
+ "version": "1.6.25",
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
@@ -1,6 +1,42 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ function ezoicClosestTopicLi(el) {
5
+ try {
6
+ if (!el) return null;
7
+ if (el.closest) {
8
+ return el.closest('li[component="category/topic"]') || el.closest('li');
9
+ }
10
+ } catch (e) {}
11
+ return null;
12
+ }
13
+
14
+ function ezoicPlaceBetweenInLi(target, wrap) {
15
+ // Place between-ad WRAP inside the previous topic <li>, not as a sibling in the <ul>.
16
+ try {
17
+ var li = ezoicClosestTopicLi(target) || target;
18
+ if (!li || !wrap) return false;
19
+
20
+ // avoid duplicates per anchor+after
21
+ try {
22
+ var after = wrap.getAttribute && wrap.getAttribute('data-ezoic-after');
23
+ if (after && li.querySelector) {
24
+ var existing = li.querySelector('.nodebb-ezoic-wrap.ezoic-ad-between[data-ezoic-after="' + after + '"]');
25
+ if (existing && existing !== wrap) {
26
+ try { wrap.remove(); } catch(e) {}
27
+ return true;
28
+ }
29
+ }
30
+ } catch (e) {}
31
+
32
+ try { wrap.setAttribute('data-ezoic-in-li', '1'); } catch (e) {}
33
+ li.appendChild(wrap);
34
+ return true;
35
+ } catch (e) {}
36
+ return false;
37
+ }
38
+
39
+
4
40
  // Track scroll direction to avoid aggressive recycling when the user scrolls upward.
5
41
  // Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
6
42
  let lastScrollY = 0;
@@ -668,7 +704,10 @@ function globalGapFixInit() {
668
704
  insertingIds.add(id);
669
705
  try {
670
706
  const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
671
- target.insertAdjacentElement('afterend', wrap);
707
+ if (kindClass === 'ezoic-ad-between') {
708
+ if (ezoicPlaceBetweenInLi(target, wrap)) return;
709
+ }
710
+ target.insertAdjacentElement('afterend', wrap);
672
711
 
673
712
  // If placeholder exists elsewhere (including pool), move it into the wrapper.
674
713
  if (existingPh) {
@@ -1160,7 +1199,10 @@ function buildOrdinalMap(items) {
1160
1199
  if (!anchorEl || !wrap || !wrap.isConnected) return null;
1161
1200
 
1162
1201
  wrap.setAttribute('data-ezoic-after', String(afterPos));
1163
- anchorEl.insertAdjacentElement('afterend', wrap);
1202
+ if (kindClass === 'ezoic-ad-between') {
1203
+ if (ezoicPlaceBetweenInLi(anchorEl, wrap)) return;
1204
+ }
1205
+ anchorEl.insertAdjacentElement('afterend', wrap);
1164
1206
 
1165
1207
  // Ensure minimal layout impact.
1166
1208
  try { wrap.style.contain = 'layout style paint'; } catch (e) {}
@@ -1511,227 +1553,26 @@ function buildOrdinalMap(items) {
1511
1553
 
1512
1554
 
1513
1555
 
1514
- // ===== V14.1.3: Mutation-based reconcile (no scroll listener), place by data-ezoic-after =====
1515
- (function () {
1516
- var WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1517
- var HOST_SEL = 'li.nodebb-ezoic-host';
1518
- var TOPIC_LI_SEL = 'li[component="category/topic"]';
1519
-
1520
- var freezeUntil = 0;
1521
- var pending = false;
1522
- var queue = new Set();
1523
-
1524
- function now() { return Date.now(); }
1525
-
1526
- function getTopicList() {
1527
- try {
1528
- var topic = document.querySelector(TOPIC_LI_SEL);
1529
- if (!topic) return null;
1530
- return topic.closest ? topic.closest('ul,ol') : null;
1531
- } catch (e) { return null; }
1532
- }
1533
-
1534
- function isLoading() {
1535
- try {
1536
- if (now() < freezeUntil) return true;
1537
- return !!document.querySelector('.infinite-loading, .infinite-scroll-loading, .loading-indicator, .spinner, .topic-loading');
1538
- } catch (e) { return false; }
1539
- }
1540
-
1541
- function ensureHostForWrap(wrap, ul) {
1542
- try {
1543
- if (!wrap || wrap.nodeType !== 1) return null;
1544
- if (!(wrap.matches && wrap.matches(WRAP_SEL))) return null;
1545
- var host = wrap.closest ? wrap.closest(HOST_SEL) : null;
1546
- if (host) return host;
1547
- ul = ul || (wrap.closest ? wrap.closest('ul,ol') : null);
1548
- if (!ul) return null;
1549
- if (wrap.parentElement === ul) {
1550
- host = document.createElement('li');
1551
- host.className = 'nodebb-ezoic-host';
1552
- host.setAttribute('role', 'listitem');
1553
- host.style.listStyle = 'none';
1554
- host.style.width = '100%';
1555
- ul.insertBefore(host, wrap);
1556
- host.appendChild(wrap);
1557
- try { wrap.style.width = '100%'; } catch (e) {}
1558
- return host;
1559
- }
1560
- } catch (e) {}
1561
- return null;
1562
- }
1563
-
1564
- function nthTopic(ul, n) {
1565
- try {
1566
- var topics = ul.querySelectorAll(TOPIC_LI_SEL);
1567
- if (!topics || !topics.length) return null;
1568
- if (n < 1) n = 1;
1569
- if (n > topics.length) n = topics.length;
1570
- return topics[n-1] || null;
1571
- } catch (e) { return null; }
1572
- }
1573
-
1574
- function previousTopicLi(host) {
1575
- try {
1576
- var prev = host.previousElementSibling;
1577
- while (prev) {
1578
- if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1579
- prev = prev.previousElementSibling;
1580
- }
1581
- } catch (e) {}
1582
- return null;
1583
- }
1584
-
1585
- function placeHost(host, ul) {
1586
- try {
1587
- if (!host || host.nodeType !== 1) return;
1588
- ul = ul || host.parentElement;
1589
- if (!ul) return;
1590
-
1591
- var wrap = host.querySelector && host.querySelector(WRAP_SEL);
1592
- if (!wrap) { host.remove(); return; }
1593
-
1594
- var after = wrap.getAttribute('data-ezoic-after');
1595
- var anchor = null;
1596
-
1597
- if (after) {
1598
- anchor = nthTopic(ul, parseInt(after, 10));
1599
- }
1600
- if (!anchor) {
1601
- anchor = previousTopicLi(host);
1602
- }
1603
- if (!anchor) {
1604
- // Can't place reliably; if host drifted to top area, remove to avoid pile-up.
1605
- // Otherwise keep as-is.
1606
- var firstTopic = nthTopic(ul, 1);
1607
- if (firstTopic && host !== firstTopic && host.compareDocumentPosition(firstTopic) & Node.DOCUMENT_POSITION_FOLLOWING) {
1608
- host.remove();
1609
- }
1610
- return;
1611
- }
1612
-
1613
- if (host.previousElementSibling !== anchor) {
1614
- anchor.insertAdjacentElement('afterend', host);
1615
- }
1616
- } catch (e) {}
1617
- }
1618
-
1619
- function drain() {
1620
- pending = false;
1621
- if (isLoading()) {
1622
- // retry later
1623
- scheduleDrain(250);
1624
- return;
1625
- }
1626
- var ul = getTopicList();
1627
- if (!ul) return;
1628
-
1629
- // repair invalid ul>div once
1630
- try {
1631
- ul.querySelectorAll(':scope > ' + WRAP_SEL).forEach(function(w){
1632
- var h = ensureHostForWrap(w, ul);
1633
- if (h) queue.add(h);
1634
- });
1635
- } catch (e) {}
1636
-
1637
- var count = 0;
1638
- for (var host of Array.from(queue)) {
1639
- queue.delete(host);
1640
- placeHost(host, ul);
1641
- count++;
1642
- if (count >= 8) break; // keep it light
1643
- }
1644
- if (queue.size) scheduleDrain(60);
1645
- }
1646
-
1647
- function scheduleDrain(delay) {
1648
- if (pending) return;
1649
- pending = true;
1650
- if (delay) {
1651
- setTimeout(function(){ requestAnimationFrame(drain); }, delay);
1652
- } else {
1653
- requestAnimationFrame(drain);
1654
- }
1655
- }
1656
-
1657
- function enqueueFromNode(node) {
1658
- try {
1659
- if (!node || node.nodeType !== 1) return;
1660
- var ul = getTopicList();
1661
- if (!ul) return;
1662
-
1663
- if (node.matches && node.matches(WRAP_SEL)) {
1664
- var h = ensureHostForWrap(node, ul) || (node.closest ? node.closest(HOST_SEL) : null);
1665
- if (h) queue.add(h);
1666
- } else if (node.matches && node.matches(HOST_SEL)) {
1667
- queue.add(node);
1668
- } else if (node.querySelectorAll) {
1669
- node.querySelectorAll(WRAP_SEL).forEach(function(w){
1670
- var h2 = ensureHostForWrap(w, ul) || (w.closest ? w.closest(HOST_SEL) : null);
1671
- if (h2) queue.add(h2);
1672
- });
1673
- node.querySelectorAll(HOST_SEL).forEach(function(h3){ queue.add(h3); });
1674
- }
1675
- } catch (e) {}
1676
- }
1677
-
1678
- function init() {
1679
- // initial scan (light)
1680
- try {
1681
- var ul = getTopicList();
1682
- if (ul) {
1683
- ul.querySelectorAll(WRAP_SEL).forEach(function(w){
1684
- var h = ensureHostForWrap(w, ul) || (w.closest ? w.closest(HOST_SEL) : null);
1685
- if (h) queue.add(h);
1686
- });
1687
- scheduleDrain(0);
1688
- }
1689
- } catch (e) {}
1690
-
1691
- // Observe ONLY the topic list container once available
1692
- try {
1693
- if (typeof MutationObserver !== 'undefined') {
1694
- var mo = new MutationObserver(function(muts){
1695
- for (var i=0;i<muts.length;i++){
1696
- var m = muts[i];
1697
- if (m.addedNodes && m.addedNodes.length) {
1698
- for (var j=0;j<m.addedNodes.length;j++){
1699
- enqueueFromNode(m.addedNodes[j]);
1700
- }
1701
- scheduleDrain(0);
1702
- }
1703
- }
1704
- });
1705
-
1706
- var ul = getTopicList();
1707
- if (ul) mo.observe(ul, { childList:true, subtree:true });
1708
- else {
1709
- // wait for ajaxify
1710
- var mo2 = new MutationObserver(function(){
1711
- var u2 = getTopicList();
1712
- if (u2) {
1713
- try { mo.observe(u2, { childList:true, subtree:true }); } catch(e){}
1714
- try { mo2.disconnect(); } catch(e){}
1715
- }
1716
- });
1717
- mo2.observe(document.documentElement || document.body, { childList:true, subtree:true });
1718
- }
1719
- }
1720
- } catch (e) {}
1721
-
1722
- if (window.jQuery) {
1556
+ // ===== V15 one-shot repair (no heavy observers) =====
1557
+ function ezoicRepairBetweenPlacement() {
1558
+ try {
1559
+ var bad = document.querySelectorAll('ul > div.nodebb-ezoic-wrap.ezoic-ad-between, ol > div.nodebb-ezoic-wrap.ezoic-ad-between');
1560
+ bad.forEach(function(wrap){
1723
1561
  try {
1724
- window.jQuery(window).on('action:infiniteScroll.loaded action:ajaxify.end', function(){
1725
- freezeUntil = now() + 900; // allow NodeBB to settle
1726
- // then reconcile any drift
1727
- setTimeout(function(){ scheduleDrain(0); }, 950);
1728
- });
1562
+ var prev = wrap.previousElementSibling;
1563
+ if (prev) ezoicPlaceBetweenInLi(prev, wrap);
1729
1564
  } catch (e) {}
1730
- }
1731
- }
1732
-
1733
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1734
- else init();
1735
- })();
1736
- // ===== /V14.1.3 =====
1565
+ });
1566
+ } catch (e) {}
1567
+ }
1568
+ try { ezoicRepairBetweenPlacement(); } catch (e) {}
1569
+ if (window.jQuery) {
1570
+ try {
1571
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1572
+ setTimeout(function(){ try { ezoicRepairBetweenPlacement(); } catch(e) {} }, 0);
1573
+ setTimeout(function(){ try { ezoicRepairBetweenPlacement(); } catch(e) {} }, 250);
1574
+ });
1575
+ } catch (e) {}
1576
+ }
1577
+ // ===== /V15 =====
1737
1578
 
package/public/style.css CHANGED
@@ -81,7 +81,15 @@
81
81
  }
82
82
 
83
83
 
84
- /* ===== V14.1.3 host ===== */
85
- li.nodebb-ezoic-host { list-style: none; width: 100%; }
86
- /* ===== /V14.1.3 ===== */
84
+ /* ===== V15 between in li ===== */
85
+ li[component="category/topic"] > .nodebb-ezoic-wrap.ezoic-ad-between[data-ezoic-in-li="1"] {
86
+ display: block;
87
+ width: 100%;
88
+ margin-top: 12px;
89
+ margin-bottom: 12px;
90
+ }
91
+ li[component="category/topic"] > .nodebb-ezoic-wrap.ezoic-ad-between[data-ezoic-in-li="1"] * {
92
+ max-width: 100%;
93
+ }
94
+ /* ===== /V15 ===== */
87
95