nodebb-plugin-ezoic-infinite 1.6.17 → 1.6.19

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.17",
3
+ "version": "1.6.19",
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,116 +1,6 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
- function ezoicAnchorToken() {
5
- // cheap unique-ish token
6
- try { return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } catch (e) { return String(Date.now()); }
7
- }
8
-
9
- function ezoicInsertAfterWithToken(target, wrap, kindClass) {
10
- try {
11
- if (!target || !wrap) return;
12
-
13
- // Tag the anchor LI (topic item) with a stable token the first time we attach a between ad to it.
14
- // If NodeBB re-renders/virtualizes and recreates the LI, this tag disappears => we can drop orphan ads.
15
- var anchorLi = null;
16
- try { anchorLi = target.closest ? target.closest('li') : null; } catch (e) {}
17
- if (!anchorLi && target.tagName === 'LI') anchorLi = target;
18
-
19
- if (kindClass === 'ezoic-ad-between' && anchorLi) {
20
- var token = anchorLi.getAttribute('data-ezoic-anchor-token');
21
- if (!token) {
22
- token = ezoicAnchorToken();
23
- try { anchorLi.setAttribute('data-ezoic-anchor-token', token); } catch (e) {}
24
- }
25
-
26
- // If inside UL/OL, keep valid DOM with LI host, but keep wrap as DIV (compat).
27
- var p = anchorLi.parentElement;
28
- if (p && (p.tagName === 'UL' || p.tagName === 'OL')) {
29
- var host = document.createElement('li');
30
- host.className = 'nodebb-ezoic-host';
31
- host.setAttribute('role', 'listitem');
32
- host.style.listStyle = 'none';
33
- host.style.width = '100%';
34
- try { host.setAttribute('data-ezoic-anchor-token', token); } catch (e) {}
35
-
36
- if (anchorLi.insertAdjacentElement) anchorLi.insertAdjacentElement('afterend', host);
37
- else if (p.insertBefore) p.insertBefore(host, anchorLi.nextSibling);
38
-
39
- try { host.appendChild(wrap); } catch (e) {}
40
- try { wrap.style.width = '100%'; } catch (e) {}
41
- return;
42
- }
43
-
44
- // Non-list parent: just insert sibling but still tag wrap so we can reconcile
45
- try { wrap.setAttribute('data-ezoic-anchor-token', token); } catch (e) {}
46
- }
47
-
48
- if (target.insertAdjacentElement) ezoicInsertAfterWithToken(target, wrap, kindClass);
49
- else if (target.parentNode) target.parentNode.insertBefore(wrap, target.nextSibling);
50
- } catch (e) {}
51
- }
52
-
53
- function ezoicRepairBetweenWrapsToHost() {
54
- // Repair any invalid UL children and ensure hosts/wraps carry token if possible.
55
- try {
56
- var bad = document.querySelectorAll('ul > div.nodebb-ezoic-wrap.ezoic-ad-between, ol > div.nodebb-ezoic-wrap.ezoic-ad-between');
57
- bad.forEach(function (wrap) {
58
- try {
59
- var ul = wrap.parentElement;
60
- if (!ul) return;
61
- var prevLi = wrap.previousElementSibling && wrap.previousElementSibling.tagName === 'LI' ? wrap.previousElementSibling : null;
62
- var token = prevLi ? prevLi.getAttribute('data-ezoic-anchor-token') : null;
63
- if (!token && prevLi) {
64
- token = ezoicAnchorToken();
65
- prevLi.setAttribute('data-ezoic-anchor-token', token);
66
- }
67
-
68
- var host = document.createElement('li');
69
- host.className = 'nodebb-ezoic-host';
70
- host.setAttribute('role', 'listitem');
71
- host.style.listStyle = 'none';
72
- host.style.width = '100%';
73
- if (token) host.setAttribute('data-ezoic-anchor-token', token);
74
-
75
- ul.insertBefore(host, wrap);
76
- host.appendChild(wrap);
77
- wrap.style.width = '100%';
78
- } catch (e) {}
79
- });
80
- } catch (e) {}
81
- }
82
-
83
- function ezoicReconcileOrDropHostsByToken(scope) {
84
- // Core fix: if NodeBB virtualizes and anchors disappear, DROP hosts so they can't pile up at top.
85
- // If anchor exists, ensure host sits right after it.
86
- try {
87
- var root = scope || document;
88
- var hosts = root.querySelectorAll('li.nodebb-ezoic-host[data-ezoic-anchor-token]');
89
- if (!hosts || !hosts.length) return;
90
-
91
- hosts.forEach(function(host){
92
- try {
93
- var token = host.getAttribute('data-ezoic-anchor-token');
94
- if (!token) return;
95
- var listEl = host.parentElement;
96
- if (!listEl) return;
97
-
98
- // Find the exact anchor LI that still carries the token.
99
- var anchor = listEl.querySelector('li[data-ezoic-anchor-token="' + token + '"]');
100
- if (!anchor) {
101
- // anchor not in DOM => virtualized/re-rendered: remove host to avoid stacking
102
- host.remove();
103
- return;
104
- }
105
- if (host.previousElementSibling !== anchor) {
106
- anchor.insertAdjacentElement('afterend', host);
107
- }
108
- } catch (e) {}
109
- });
110
- } catch (e) {}
111
- }
112
-
113
-
114
4
  // Track scroll direction to avoid aggressive recycling when the user scrolls upward.
115
5
  // Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
116
6
  let lastScrollY = 0;
@@ -778,7 +668,7 @@ function globalGapFixInit() {
778
668
  insertingIds.add(id);
779
669
  try {
780
670
  const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
781
- ezoicInsertAfterWithToken(target, wrap, kindClass);
671
+ target.insertAdjacentElement('afterend', wrap);
782
672
 
783
673
  // If placeholder exists elsewhere (including pool), move it into the wrapper.
784
674
  if (existingPh) {
@@ -1270,7 +1160,7 @@ function buildOrdinalMap(items) {
1270
1160
  if (!anchorEl || !wrap || !wrap.isConnected) return null;
1271
1161
 
1272
1162
  wrap.setAttribute('data-ezoic-after', String(afterPos));
1273
- ezoicInsertAfterWithToken(anchorEl, wrap, kindClass);
1163
+ anchorEl.insertAdjacentElement('afterend', wrap);
1274
1164
 
1275
1165
  // Ensure minimal layout impact.
1276
1166
  try { wrap.style.contain = 'layout style paint'; } catch (e) {}
@@ -1620,49 +1510,206 @@ function buildOrdinalMap(items) {
1620
1510
  })();
1621
1511
 
1622
1512
 
1623
- // ===== V14 token reconcile scheduler =====
1624
- (function(){
1625
- var pending=false, last=0, COOLDOWN=140;
1626
- function schedule(scope){
1627
- var now=Date.now();
1628
- if (now-last<COOLDOWN) return;
1629
- if (pending) return;
1630
- pending=true;
1631
- requestAnimationFrame(function(){
1632
- pending=false; last=Date.now();
1633
- try { ezoicRepairBetweenWrapsToHost(); } catch(e) {}
1634
- try { ezoicReconcileOrDropHostsByToken(scope); } catch(e) {}
1635
- });
1513
+
1514
+ // ===== V14.2 targeted hook (topic list only) + upscroll reconcile =====
1515
+ (function () {
1516
+ var BETWEEN_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1517
+ var HOST_CLASS = 'nodebb-ezoic-host';
1518
+ var TOKEN_ATTR = 'data-ezoic-anchor-token';
1519
+ var TOPIC_LI_SEL = 'li[component="category/topic"]';
1520
+
1521
+ var lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
1522
+
1523
+ function token() {
1524
+ try { return String(Date.now()) + '-' + Math.random().toString(16).slice(2); } catch (e) { return String(Date.now()); }
1636
1525
  }
1637
1526
 
1638
- window.addEventListener('scroll', function(){ schedule(document); }, { passive:true });
1639
- window.addEventListener('resize', function(){ schedule(document); }, { passive:true });
1527
+ function getScrollY() {
1528
+ return window.pageYOffset || document.documentElement.scrollTop || 0;
1529
+ }
1640
1530
 
1641
- if (window.jQuery) {
1642
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1643
- setTimeout(function(){ schedule(document); }, 0);
1644
- setTimeout(function(){ schedule(document); }, 220);
1645
- setTimeout(function(){ schedule(document); }, 800);
1646
- });
1531
+ function isTopicList(ul) {
1532
+ try { return !!(ul && ul.querySelector && ul.querySelector(TOPIC_LI_SEL)); } catch (e) { return false; }
1647
1533
  }
1648
1534
 
1649
- try {
1650
- if (typeof MutationObserver !== 'undefined') {
1535
+ function closestTopicList(node) {
1536
+ try {
1537
+ var ul = node && node.closest ? node.closest('ul,ol') : null;
1538
+ if (ul && isTopicList(ul)) return ul;
1539
+ // sometimes wrap is outside: search up
1540
+ var p = node && node.parentElement;
1541
+ while (p) {
1542
+ if ((p.tagName === 'UL' || p.tagName === 'OL') && isTopicList(p)) return p;
1543
+ p = p.parentElement;
1544
+ }
1545
+ } catch (e) {}
1546
+ return null;
1547
+ }
1548
+
1549
+ function closestAnchorTopicLi(hostOrWrap, ul) {
1550
+ try {
1551
+ var cur = hostOrWrap;
1552
+ if (!cur) return null;
1553
+ // if wrap inside host li, start from host
1554
+ if (cur.closest) {
1555
+ var h = cur.closest('li.' + HOST_CLASS);
1556
+ if (h) cur = h;
1557
+ }
1558
+ var prev = cur.previousElementSibling;
1559
+ while (prev) {
1560
+ if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1561
+ prev = prev.previousElementSibling;
1562
+ }
1563
+ } catch (e) {}
1564
+ return null;
1565
+ }
1566
+
1567
+ function ensureHostAndTokenForWrap(wrap) {
1568
+ try {
1569
+ if (!wrap || wrap.nodeType !== 1) return;
1570
+ if (!(wrap.matches && wrap.matches(BETWEEN_SEL))) return;
1571
+
1572
+ var ul = closestTopicList(wrap);
1573
+ if (!ul) return;
1574
+
1575
+ // Ensure wrap is not direct child of UL/OL
1576
+ var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1577
+ if (!host) {
1578
+ if (wrap.parentElement === ul) {
1579
+ host = document.createElement('li');
1580
+ host.className = HOST_CLASS;
1581
+ host.setAttribute('role', 'listitem');
1582
+ host.style.listStyle = 'none';
1583
+ host.style.width = '100%';
1584
+ ul.insertBefore(host, wrap);
1585
+ host.appendChild(wrap);
1586
+ }
1587
+ }
1588
+
1589
+ if (!host) return;
1590
+
1591
+ // Anchor = previous topic LI only
1592
+ var anchorLi = closestAnchorTopicLi(host, ul);
1593
+ if (!anchorLi) {
1594
+ host.setAttribute('data-ezoic-orphan', '1');
1595
+ return;
1596
+ }
1597
+
1598
+ var t = anchorLi.getAttribute(TOKEN_ATTR);
1599
+ if (!t) {
1600
+ t = token();
1601
+ anchorLi.setAttribute(TOKEN_ATTR, t);
1602
+ }
1603
+ host.setAttribute(TOKEN_ATTR, t);
1604
+ host.removeAttribute('data-ezoic-orphan');
1605
+ } catch (e) {}
1606
+ }
1607
+
1608
+ function repairInvalidUlChildren(ul) {
1609
+ try {
1610
+ if (!ul) return;
1611
+ // Fix ul>div wraps
1612
+ var bad = ul.querySelectorAll(':scope > ' + BETWEEN_SEL);
1613
+ bad.forEach(function(wrap){ ensureHostAndTokenForWrap(wrap); });
1614
+ } catch (e) {}
1615
+ }
1616
+
1617
+ function cleanupEmptyHosts(ul) {
1618
+ try {
1619
+ if (!ul) return;
1620
+ var hosts = ul.querySelectorAll('li.' + HOST_CLASS);
1621
+ hosts.forEach(function(host){
1622
+ try {
1623
+ var hasWrap = host.querySelector && host.querySelector(BETWEEN_SEL);
1624
+ if (!hasWrap) host.remove();
1625
+ } catch (e) {}
1626
+ });
1627
+ } catch (e) {}
1628
+ }
1629
+
1630
+ function reconcileUpScroll(ul) {
1631
+ // Only reconcile (move/drop) on upscroll to avoid breaking infinite scroll load calculations.
1632
+ try {
1633
+ if (!ul) return;
1634
+ var y = getScrollY();
1635
+ var dy = y - lastY;
1636
+ lastY = y;
1637
+ if (dy >= -10) return; // not an upscroll
1638
+
1639
+ // Reconcile / drop
1640
+ var hosts = ul.querySelectorAll('li.' + HOST_CLASS + '[' + TOKEN_ATTR + '], li.' + HOST_CLASS + '[data-ezoic-orphan="1"]');
1641
+ hosts.forEach(function(host){
1642
+ try {
1643
+ if (host.getAttribute('data-ezoic-orphan') === '1') { host.remove(); return; }
1644
+ var t = host.getAttribute(TOKEN_ATTR);
1645
+ if (!t) { host.remove(); return; }
1646
+ var anchor = ul.querySelector(TOPIC_LI_SEL + '[' + TOKEN_ATTR + '="' + t + '"]');
1647
+ if (!anchor) { host.remove(); return; }
1648
+ if (host.previousElementSibling !== anchor) anchor.insertAdjacentElement('afterend', host);
1649
+ } catch (e) {}
1650
+ });
1651
+ } catch (e) {}
1652
+ }
1653
+
1654
+ function sweepAll(reasonNode) {
1655
+ try {
1656
+ var ul = closestTopicList(reasonNode || document.querySelector(TOPIC_LI_SEL));
1657
+ if (!ul) return;
1658
+ repairInvalidUlChildren(ul);
1659
+
1660
+ // Tokenize any new wraps within this list
1661
+ ul.querySelectorAll(BETWEEN_SEL).forEach(function(wrap){ ensureHostAndTokenForWrap(wrap); });
1662
+
1663
+ // Don't move on downscroll; only clean empties
1664
+ cleanupEmptyHosts(ul);
1665
+ reconcileUpScroll(ul);
1666
+ } catch (e) {}
1667
+ }
1668
+
1669
+ // Process mutations incrementally (no full-document scan)
1670
+ function installMO() {
1671
+ try {
1672
+ if (typeof MutationObserver === 'undefined') return;
1651
1673
  var mo = new MutationObserver(function(muts){
1652
1674
  for (var i=0;i<muts.length;i++){
1653
- var m=muts[i];
1654
- if ((m.addedNodes && m.addedNodes.length) || (m.removedNodes && m.removedNodes.length)) {
1655
- schedule(m.target);
1675
+ var m = muts[i];
1676
+ if (m.addedNodes && m.addedNodes.length) {
1677
+ for (var j=0;j<m.addedNodes.length;j++){
1678
+ var n = m.addedNodes[j];
1679
+ if (!n || n.nodeType !== 1) continue;
1680
+ if (n.matches && n.matches(BETWEEN_SEL)) {
1681
+ ensureHostAndTokenForWrap(n);
1682
+ } else if (n.querySelector && n.querySelector(BETWEEN_SEL)) {
1683
+ n.querySelectorAll(BETWEEN_SEL).forEach(function(w){ ensureHostAndTokenForWrap(w); });
1684
+ }
1685
+ }
1686
+ // light sweep on the container only
1687
+ sweepAll(m.target);
1656
1688
  break;
1657
1689
  }
1658
1690
  }
1659
1691
  });
1660
- document.querySelectorAll('ul,ol').forEach(function(list){
1661
- try { mo.observe(list, {childList:true}); } catch(e) {}
1692
+ mo.observe(document.documentElement || document.body, { childList: true, subtree: true });
1693
+ } catch (e) {}
1694
+ }
1695
+
1696
+ function init() {
1697
+ // initial tokenizing/repair
1698
+ sweepAll(document);
1699
+ window.addEventListener('scroll', function(){ sweepAll(document); }, { passive: true });
1700
+ window.addEventListener('resize', function(){ sweepAll(document); }, { passive: true });
1701
+
1702
+ if (window.jQuery) {
1703
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1704
+ setTimeout(function(){ sweepAll(document); }, 0);
1705
+ setTimeout(function(){ sweepAll(document); }, 250);
1662
1706
  });
1663
1707
  }
1664
- } catch(e){}
1708
+ installMO();
1709
+ }
1665
1710
 
1666
- setTimeout(function(){ schedule(document); }, 0);
1711
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1712
+ else init();
1667
1713
  })();
1668
- // ===== /V14 =====
1714
+ // ===== /V14.2 =====
1715
+
package/public/style.css CHANGED
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
 
84
- /* ===== V14 token host styles ===== */
84
+ /* ===== V14.2 host ===== */
85
85
  li.nodebb-ezoic-host { list-style: none; width: 100%; }
86
- /* ===== /V14 ===== */
86
+ /* ===== /V14.2 ===== */
87
87