nodebb-plugin-ezoic-infinite 1.6.6 → 1.6.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +143 -104
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
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
@@ -1351,19 +1351,17 @@ function buildOrdinalMap(items) {
1351
1351
 
1352
1352
  // ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
1353
1353
  (function () {
1354
- // v2.4 (anchor-guard):
1355
- // Issue reported: when scrolling back up, ad wraps accumulate before the first post.
1356
- // Root cause (most likely): some script/virtualization detaches ad nodes from their original place and
1357
- // re-inserts them near the top, without their original context.
1354
+ // v2.5 (position-enforcer):
1355
+ // - Keep "no-redefine" (showAds only once per id, dedupe placeholders).
1356
+ // - Fix pile-up at top when scrolling up: wraps are being detached and re-inserted near the first post.
1357
+ // Adjacent-anchor can move with the wrap; so we anchor INSIDE the host post/topic element.
1358
1358
  //
1359
- // Fix:
1360
- // - On creation of each wrap, insert a hidden "anchor" node right before it and tag the wrap with that anchor id.
1361
- // - Periodically validate: if a wrap is no longer immediately preceded by its anchor, remove the wrap.
1362
- // (We do NOT recreate here; the normal injector will insert correct wraps as needed.)
1359
+ // Mechanism:
1360
+ // 1) For each wrap, find a "host" element (closest previous post/topic item).
1361
+ // 2) Insert an invisible marker span INSIDE that host (stays with host even if wrap is moved).
1362
+ // 3) On scroll/interval, if wrap is not located right AFTER its host element, move it back.
1363
1363
  //
1364
- // We also keep v2.3 no-redefine rules:
1365
- // - no wrap deletion for cleanup except for misplaced wraps / duplicate placeholders
1366
- // - showAds only once per placeholder id
1364
+ // This moves existing DOM nodes back (no placeholder recreation).
1367
1365
 
1368
1366
  var BETWEEN_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-between';
1369
1367
  var MESSAGE_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-message';
@@ -1372,8 +1370,8 @@ function buildOrdinalMap(items) {
1372
1370
  var MAX_SHOW_PER_TICK = 4;
1373
1371
 
1374
1372
  // validation throttles
1375
- var VALIDATE_COOLDOWN_MS = 260;
1376
- var lastValidate = 0;
1373
+ var ENFORCE_COOLDOWN_MS = 220;
1374
+ var lastEnforce = 0;
1377
1375
 
1378
1376
  // internal state
1379
1377
  var activatedById = Object.create(null);
@@ -1428,99 +1426,140 @@ function buildOrdinalMap(items) {
1428
1426
  });
1429
1427
  }
1430
1428
 
1431
- // --- Anchor guard ---
1432
- function ensureAnchor(w) {
1429
+ // --- De-dupe placeholders to avoid Ezoic warnings ---
1430
+ function dedupePlaceholders() {
1431
+ var seen = Object.create(null);
1433
1432
  try {
1434
- if (!w || !w.parentNode) return;
1435
- if (w.getAttribute('data-ezoic-anchored') === '1') return;
1433
+ document.querySelectorAll('.nodebb-ezoic-wrap [data-ezoic-id]').forEach(function (ph) {
1434
+ try {
1435
+ var id = ph.getAttribute('data-ezoic-id');
1436
+ if (!id) return;
1437
+ var wrap = ph.closest('.nodebb-ezoic-wrap');
1438
+ if (!wrap) return;
1439
+ if (seen[id]) {
1440
+ wrap.remove();
1441
+ } else {
1442
+ seen[id] = true;
1443
+ }
1444
+ } catch (e) {}
1445
+ });
1446
+ } catch (e) {}
1447
+ }
1436
1448
 
1437
- var wrapUid = w.getAttribute('data-ezoic-wrapuid');
1438
- if (!wrapUid) {
1439
- wrapUid = String(Date.now()) + '-' + Math.floor(Math.random() * 1e9);
1440
- w.setAttribute('data-ezoic-wrapuid', wrapUid);
1449
+ // --- Position Enforcer ---
1450
+ function ensureUid(w) {
1451
+ try {
1452
+ var uid = w.getAttribute('data-ezoic-wrapuid');
1453
+ if (!uid) {
1454
+ uid = String(Date.now()) + '-' + Math.floor(Math.random() * 1e9);
1455
+ w.setAttribute('data-ezoic-wrapuid', uid);
1441
1456
  }
1457
+ return uid;
1458
+ } catch (e) { return null; }
1459
+ }
1442
1460
 
1443
- // If already preceded by matching anchor, mark anchored.
1444
- var prev = w.previousSibling;
1445
- if (prev && prev.nodeType === 1 && prev.classList && prev.classList.contains('nodebb-ezoic-anchor')) {
1446
- if (prev.getAttribute('data-anchor-for') === wrapUid) {
1447
- w.setAttribute('data-ezoic-anchored', '1');
1448
- return;
1461
+ function findHostForWrap(w) {
1462
+ // Host should be a stable post/topic element, NOT the wrap itself.
1463
+ // Heuristics: previous siblings that look like topic/post items.
1464
+ try {
1465
+ var cur = w.previousElementSibling;
1466
+ // walk backward a bit
1467
+ for (var i = 0; i < 18 && cur; i++) {
1468
+ if (cur.classList && (cur.classList.contains('topic-item') || cur.classList.contains('posts-list-item') || cur.classList.contains('post') || cur.classList.contains('category-item'))) {
1469
+ return cur;
1449
1470
  }
1471
+ if (cur.getAttribute) {
1472
+ if (cur.getAttribute('data-pid') || cur.getAttribute('data-tid') || cur.getAttribute('data-topic-id') || cur.getAttribute('data-index')) return cur;
1473
+ }
1474
+ cur = cur.previousElementSibling;
1450
1475
  }
1451
1476
 
1452
- // Create anchor node
1453
- var a = document.createElement('span');
1454
- a.className = 'nodebb-ezoic-anchor';
1455
- a.setAttribute('data-anchor-for', wrapUid);
1456
- a.style.display = 'none';
1457
-
1458
- w.parentNode.insertBefore(a, w);
1459
- w.setAttribute('data-ezoic-anchored', '1');
1477
+ // fallback: walk up from wrap to find nearest list container, then approximate by data-ezoic-after index
1478
+ var afterPos = parseInt(w.getAttribute('data-ezoic-after') || '0', 10) || 0;
1479
+ if (afterPos > 0) {
1480
+ var list = w.parentElement;
1481
+ for (var up = 0; up < 6 && list && list !== document.body; up++) {
1482
+ if (list.classList && (list.classList.contains('topic-list') || list.classList.contains('topics') || list.classList.contains('posts') || list.classList.contains('category'))) break;
1483
+ list = list.parentElement;
1484
+ }
1485
+ if (list && list.querySelectorAll) {
1486
+ var items = list.querySelectorAll('.topic-item, .posts-list-item, .post, [data-pid], [data-tid], [data-topic-id]');
1487
+ if (items && items.length >= afterPos) {
1488
+ return items[Math.min(items.length - 1, afterPos - 1)];
1489
+ }
1490
+ }
1491
+ }
1460
1492
  } catch (e) {}
1493
+ return null;
1461
1494
  }
1462
1495
 
1463
- function validateAnchors() {
1464
- var now = Date.now();
1465
- if (now - lastValidate < VALIDATE_COOLDOWN_MS) return;
1466
- lastValidate = now;
1467
-
1496
+ function ensureMarkerInHost(w) {
1468
1497
  try {
1469
- // Remove wraps that lost their anchor adjacency (they have been moved)
1470
- var wraps = document.querySelectorAll('.nodebb-ezoic-wrap');
1471
- for (var i = 0; i < wraps.length; i++) {
1472
- var w = wraps[i];
1473
- try {
1474
- var uid = w.getAttribute('data-ezoic-wrapuid');
1475
- if (!uid) {
1476
- // If it's an old wrap with no uid, anchor it once.
1477
- ensureAnchor(w);
1478
- continue;
1479
- }
1480
- var prev = w.previousSibling;
1481
- if (!(prev && prev.nodeType === 1 && prev.classList && prev.classList.contains('nodebb-ezoic-anchor') && prev.getAttribute('data-anchor-for') === uid)) {
1482
- // Misplaced wrap -> remove it (prevents pile-up before first post)
1483
- w.remove();
1484
- }
1485
- } catch (e) {}
1498
+ var uid = ensureUid(w);
1499
+ if (!uid) return;
1500
+
1501
+ var markerId = w.getAttribute('data-ezoic-markerid');
1502
+ if (markerId) {
1503
+ var existing = document.getElementById(markerId);
1504
+ if (existing) return;
1486
1505
  }
1487
1506
 
1488
- // Remove orphan anchors (anchor without following wrap)
1489
- var anchors = document.querySelectorAll('.nodebb-ezoic-anchor');
1490
- for (var j = 0; j < anchors.length; j++) {
1491
- var a = anchors[j];
1492
- try {
1493
- var next = a.nextSibling;
1494
- if (!(next && next.nodeType === 1 && next.classList && next.classList.contains('nodebb-ezoic-wrap'))) {
1495
- a.remove();
1496
- }
1497
- } catch (e) {}
1507
+ var host = findHostForWrap(w);
1508
+ if (!host) return;
1509
+
1510
+ markerId = 'nodebb-ezoic-marker-' + uid;
1511
+ // if already exists somewhere else, reuse
1512
+ var m = document.getElementById(markerId);
1513
+ if (!m) {
1514
+ m = document.createElement('span');
1515
+ m.id = markerId;
1516
+ m.className = 'nodebb-ezoic-marker';
1517
+ m.setAttribute('data-marker-for', uid);
1518
+ m.style.display = 'none';
1519
+ host.appendChild(m);
1520
+ } else {
1521
+ // ensure marker is inside host (if moved, re-append)
1522
+ if (m.parentElement !== host) host.appendChild(m);
1498
1523
  }
1524
+
1525
+ w.setAttribute('data-ezoic-markerid', markerId);
1499
1526
  } catch (e) {}
1500
1527
  }
1501
1528
 
1502
- // --- De-dupe placeholders to avoid Ezoic warnings ---
1503
- function dedupePlaceholders() {
1504
- var seen = Object.create(null);
1529
+ function insertAfter(ref, node) {
1505
1530
  try {
1506
- document.querySelectorAll('.nodebb-ezoic-wrap [data-ezoic-id]').forEach(function (ph) {
1531
+ if (!ref || !ref.parentNode || !node) return;
1532
+ if (ref.nextSibling) ref.parentNode.insertBefore(node, ref.nextSibling);
1533
+ else ref.parentNode.appendChild(node);
1534
+ } catch (e) {}
1535
+ }
1536
+
1537
+ function enforcePositions() {
1538
+ var now = Date.now();
1539
+ if (now - lastEnforce < ENFORCE_COOLDOWN_MS) return;
1540
+ lastEnforce = now;
1541
+
1542
+ try {
1543
+ var wraps = document.querySelectorAll('.nodebb-ezoic-wrap');
1544
+ for (var i = 0; i < wraps.length; i++) {
1545
+ var w = wraps[i];
1507
1546
  try {
1508
- var id = ph.getAttribute('data-ezoic-id');
1509
- if (!id) return;
1510
- var wrap = ph.closest('.nodebb-ezoic-wrap');
1511
- if (!wrap) return;
1547
+ ensureMarkerInHost(w);
1512
1548
 
1513
- // ensure anchor for any wrap we touch
1514
- ensureAnchor(wrap);
1549
+ var markerId = w.getAttribute('data-ezoic-markerid');
1550
+ if (!markerId) continue;
1515
1551
 
1516
- if (seen[id]) {
1517
- // remove duplicates; keep first occurrence
1518
- wrap.remove();
1519
- } else {
1520
- seen[id] = true;
1552
+ var marker = document.getElementById(markerId);
1553
+ if (!marker || !marker.parentElement) continue;
1554
+
1555
+ var host = marker.parentElement;
1556
+ // desired: wrap should be directly after host (as sibling)
1557
+ if (!host.parentElement) continue;
1558
+ if (w.parentElement !== host.parentElement || w.previousElementSibling !== host) {
1559
+ insertAfter(host, w);
1521
1560
  }
1522
1561
  } catch (e) {}
1523
- });
1562
+ }
1524
1563
  } catch (e) {}
1525
1564
  }
1526
1565
 
@@ -1534,8 +1573,8 @@ function buildOrdinalMap(items) {
1534
1573
  entries.forEach(function (e) {
1535
1574
  if (!e || !e.target) return;
1536
1575
 
1537
- // Always ensure anchor while visible/near
1538
- ensureAnchor(e.target);
1576
+ // ensure host marker on sight
1577
+ ensureMarkerInHost(e.target);
1539
1578
 
1540
1579
  if (e.isIntersecting) {
1541
1580
  var id = getId(e.target);
@@ -1551,10 +1590,10 @@ function buildOrdinalMap(items) {
1551
1590
  } catch (e) {}
1552
1591
  }, { root: null, rootMargin: '1200px 0px 1200px 0px', threshold: 0.01 });
1553
1592
 
1554
- try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { ensureAnchor(w); io.observe(w); } catch(e) {} }); } catch (e) {}
1593
+ try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { ensureMarkerInHost(w); io.observe(w); } catch(e) {} }); } catch (e) {}
1555
1594
  }
1556
1595
 
1557
- // Observe newly added wraps and anchor them
1596
+ // Observe newly added wraps and marker them
1558
1597
  var moInstalled = false;
1559
1598
  function installMO() {
1560
1599
  if (moInstalled || typeof MutationObserver === 'undefined') return;
@@ -1562,8 +1601,8 @@ function buildOrdinalMap(items) {
1562
1601
 
1563
1602
  var mo = new MutationObserver(function (muts) {
1564
1603
  try { dedupePlaceholders(); } catch (e) {}
1565
-
1566
1604
  if (!io) return;
1605
+
1567
1606
  try {
1568
1607
  for (var i = 0; i < muts.length; i++) {
1569
1608
  var m = muts[i];
@@ -1573,7 +1612,7 @@ function buildOrdinalMap(items) {
1573
1612
  if (!n || n.nodeType !== 1) continue;
1574
1613
 
1575
1614
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1576
- ensureAnchor(n);
1615
+ ensureMarkerInHost(n);
1577
1616
  try { io.observe(n); } catch (e) {}
1578
1617
  var id = getId(n);
1579
1618
  if (id && !isFilled(n)) enqueueShow(id);
@@ -1581,29 +1620,32 @@ function buildOrdinalMap(items) {
1581
1620
  } else if (n.querySelectorAll) {
1582
1621
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1583
1622
  for (var k = 0; k < inner.length; k++) {
1584
- ensureAnchor(inner[k]);
1623
+ ensureMarkerInHost(inner[k]);
1585
1624
  try { io.observe(inner[k]); } catch (e) {}
1586
1625
  }
1587
1626
  }
1588
1627
  }
1589
1628
  }
1590
1629
  } catch (e) {}
1630
+
1631
+ // enforce after mutations (cheap throttle will apply)
1632
+ enforcePositions();
1591
1633
  });
1592
1634
 
1593
1635
  try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
1594
1636
  }
1595
1637
 
1596
1638
  function init() {
1597
- // anchor + dedupe existing
1639
+ dedupePlaceholders();
1640
+
1598
1641
  try {
1599
- document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); });
1642
+ document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureMarkerInHost(w); });
1600
1643
  } catch (e) {}
1601
- dedupePlaceholders();
1602
1644
 
1603
1645
  installIO();
1604
1646
  installMO();
1605
1647
 
1606
- // initial eager show for empty visible-ish wraps (bounded)
1648
+ // initial eager show (bounded)
1607
1649
  try {
1608
1650
  var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1609
1651
  var margin = 900;
@@ -1611,7 +1653,7 @@ function buildOrdinalMap(items) {
1611
1653
  var budget = 12;
1612
1654
  for (var i = 0; i < wraps.length && budget > 0; i++) {
1613
1655
  var w = wraps[i];
1614
- ensureAnchor(w);
1656
+ ensureMarkerInHost(w);
1615
1657
  var r = w.getBoundingClientRect();
1616
1658
  if (r.bottom >= -margin && r.top <= (vh + margin)) {
1617
1659
  var id = getId(w);
@@ -1624,31 +1666,28 @@ function buildOrdinalMap(items) {
1624
1666
  }
1625
1667
  } catch (e) {}
1626
1668
 
1627
- // validate regularly on scroll/resize (cheap)
1628
1669
  window.addEventListener('scroll', function () {
1629
- validateAnchors();
1670
+ enforcePositions();
1630
1671
  scheduleShowTick();
1631
1672
  }, { passive: true });
1632
1673
  window.addEventListener('resize', function () {
1633
- validateAnchors();
1674
+ enforcePositions();
1634
1675
  scheduleShowTick();
1635
1676
  }, { passive: true });
1636
1677
 
1637
1678
  if (window.jQuery) {
1638
1679
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1639
1680
  setTimeout(function () {
1640
- try { document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); }); } catch (e) {}
1641
1681
  dedupePlaceholders();
1642
- validateAnchors();
1682
+ try { document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureMarkerInHost(w); }); } catch (e) {}
1683
+ enforcePositions();
1643
1684
  scheduleShowTick();
1644
1685
  }, 0);
1645
1686
  });
1646
1687
  }
1647
1688
 
1648
- // periodic validate safety net
1649
- setInterval(validateAnchors, 1200);
1650
-
1651
- setTimeout(function () { validateAnchors(); scheduleShowTick(); }, 0);
1689
+ setInterval(enforcePositions, 1000);
1690
+ setTimeout(function () { enforcePositions(); scheduleShowTick(); }, 0);
1652
1691
  }
1653
1692
 
1654
1693
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);