nodebb-plugin-ezoic-infinite 1.6.5 → 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 +168 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.5",
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
@@ -1348,30 +1348,33 @@ function buildOrdinalMap(items) {
1348
1348
 
1349
1349
 
1350
1350
 
1351
+
1351
1352
  // ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
1352
1353
  (function () {
1353
- // v2.3 (no-redefine):
1354
- // Your logs show:
1355
- // - "Placeholder Id XXX has already been defined"
1356
- // - "No valid placeholders for loadMore"
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.
1357
1358
  //
1358
- // Root cause: calling showAds repeatedly for the same placeholder id and/or
1359
- // removing/recreating wrappers with the same IDs causes Ezoic to treat them as re-defined.
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.
1360
1363
  //
1361
- // Fix:
1362
- // - NEVER remove wraps/placeholders here (no DOM deletion of ads).
1363
- // - Only call showAds ONCE per placeholder per page lifetime (unless the placeholder is truly empty).
1364
- // - De-duplicate placeholders in DOM: if the same data-ezoic-id appears multiple times, keep the first.
1364
+ // This moves existing DOM nodes back (no placeholder recreation).
1365
1365
 
1366
1366
  var BETWEEN_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-between';
1367
1367
  var MESSAGE_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-message';
1368
1368
  var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
1369
1369
 
1370
- // show tuning (safe)
1371
1370
  var MAX_SHOW_PER_TICK = 4;
1372
1371
 
1372
+ // validation throttles
1373
+ var ENFORCE_COOLDOWN_MS = 220;
1374
+ var lastEnforce = 0;
1375
+
1373
1376
  // internal state
1374
- var activatedById = Object.create(null); // id -> ts
1377
+ var activatedById = Object.create(null);
1375
1378
  var showQueue = [];
1376
1379
  var showTicking = false;
1377
1380
 
@@ -1388,7 +1391,6 @@ function buildOrdinalMap(items) {
1388
1391
 
1389
1392
  function isFilled(w) {
1390
1393
  try {
1391
- // if Ezoic/Google already injected, there will be an iframe or an element with id starting google_ads_iframe
1392
1394
  if (w.querySelector('iframe')) return true;
1393
1395
  if (w.querySelector('[id^="google_ads_iframe"]')) return true;
1394
1396
  if (w.querySelector('.ezoic-ad')) return true;
@@ -1398,9 +1400,7 @@ function buildOrdinalMap(items) {
1398
1400
 
1399
1401
  function enqueueShow(id) {
1400
1402
  if (!id) return;
1401
- // show only once (avoid "already defined")
1402
1403
  if (activatedById[id]) return;
1403
-
1404
1404
  for (var i = 0; i < showQueue.length; i++) if (showQueue[i] === id) return;
1405
1405
  showQueue.push(id);
1406
1406
  scheduleShowTick();
@@ -1426,7 +1426,7 @@ function buildOrdinalMap(items) {
1426
1426
  });
1427
1427
  }
1428
1428
 
1429
- // De-duplicate placeholders to avoid Ezoic warnings.
1429
+ // --- De-dupe placeholders to avoid Ezoic warnings ---
1430
1430
  function dedupePlaceholders() {
1431
1431
  var seen = Object.create(null);
1432
1432
  try {
@@ -1434,10 +1434,10 @@ function buildOrdinalMap(items) {
1434
1434
  try {
1435
1435
  var id = ph.getAttribute('data-ezoic-id');
1436
1436
  if (!id) return;
1437
+ var wrap = ph.closest('.nodebb-ezoic-wrap');
1438
+ if (!wrap) return;
1437
1439
  if (seen[id]) {
1438
- // remove duplicate wrapper entirely (keep first occurrence)
1439
- var wrap = ph.closest('.nodebb-ezoic-wrap');
1440
- if (wrap) wrap.remove();
1440
+ wrap.remove();
1441
1441
  } else {
1442
1442
  seen[id] = true;
1443
1443
  }
@@ -1446,7 +1446,124 @@ function buildOrdinalMap(items) {
1446
1446
  } catch (e) {}
1447
1447
  }
1448
1448
 
1449
- // Track visibility: when a wrap comes near viewport, trigger show once if empty.
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);
1456
+ }
1457
+ return uid;
1458
+ } catch (e) { return null; }
1459
+ }
1460
+
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;
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;
1475
+ }
1476
+
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
+ }
1492
+ } catch (e) {}
1493
+ return null;
1494
+ }
1495
+
1496
+ function ensureMarkerInHost(w) {
1497
+ try {
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;
1505
+ }
1506
+
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);
1523
+ }
1524
+
1525
+ w.setAttribute('data-ezoic-markerid', markerId);
1526
+ } catch (e) {}
1527
+ }
1528
+
1529
+ function insertAfter(ref, node) {
1530
+ try {
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];
1546
+ try {
1547
+ ensureMarkerInHost(w);
1548
+
1549
+ var markerId = w.getAttribute('data-ezoic-markerid');
1550
+ if (!markerId) continue;
1551
+
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);
1560
+ }
1561
+ } catch (e) {}
1562
+ }
1563
+ } catch (e) {}
1564
+ }
1565
+
1566
+ // --- Visibility: trigger show once if empty ---
1450
1567
  var io = null;
1451
1568
  function installIO() {
1452
1569
  if (io || typeof IntersectionObserver === 'undefined') return;
@@ -1455,8 +1572,11 @@ function buildOrdinalMap(items) {
1455
1572
  try {
1456
1573
  entries.forEach(function (e) {
1457
1574
  if (!e || !e.target) return;
1575
+
1576
+ // ensure host marker on sight
1577
+ ensureMarkerInHost(e.target);
1578
+
1458
1579
  if (e.isIntersecting) {
1459
- // If the slot is already filled, mark it as activated to prevent re-define attempts.
1460
1580
  var id = getId(e.target);
1461
1581
  if (!id) return;
1462
1582
 
@@ -1470,20 +1590,19 @@ function buildOrdinalMap(items) {
1470
1590
  } catch (e) {}
1471
1591
  }, { root: null, rootMargin: '1200px 0px 1200px 0px', threshold: 0.01 });
1472
1592
 
1473
- try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { 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) {}
1474
1594
  }
1475
1595
 
1476
- // Observe newly added wraps and observe them + dedupe
1596
+ // Observe newly added wraps and marker them
1477
1597
  var moInstalled = false;
1478
1598
  function installMO() {
1479
1599
  if (moInstalled || typeof MutationObserver === 'undefined') return;
1480
1600
  moInstalled = true;
1481
1601
 
1482
1602
  var mo = new MutationObserver(function (muts) {
1483
- // dedupe quickly, then observe new wraps
1484
1603
  try { dedupePlaceholders(); } catch (e) {}
1485
-
1486
1604
  if (!io) return;
1605
+
1487
1606
  try {
1488
1607
  for (var i = 0; i < muts.length; i++) {
1489
1608
  var m = muts[i];
@@ -1493,34 +1612,40 @@ function buildOrdinalMap(items) {
1493
1612
  if (!n || n.nodeType !== 1) continue;
1494
1613
 
1495
1614
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1615
+ ensureMarkerInHost(n);
1496
1616
  try { io.observe(n); } catch (e) {}
1497
- // eager show if empty
1498
1617
  var id = getId(n);
1499
1618
  if (id && !isFilled(n)) enqueueShow(id);
1500
1619
  else if (id) activatedById[id] = activatedById[id] || Date.now();
1501
1620
  } else if (n.querySelectorAll) {
1502
1621
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1503
1622
  for (var k = 0; k < inner.length; k++) {
1623
+ ensureMarkerInHost(inner[k]);
1504
1624
  try { io.observe(inner[k]); } catch (e) {}
1505
1625
  }
1506
1626
  }
1507
1627
  }
1508
1628
  }
1509
1629
  } catch (e) {}
1630
+
1631
+ // enforce after mutations (cheap throttle will apply)
1632
+ enforcePositions();
1510
1633
  });
1511
1634
 
1512
1635
  try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
1513
1636
  }
1514
1637
 
1515
1638
  function init() {
1516
- // 1) dedupe existing
1517
1639
  dedupePlaceholders();
1518
1640
 
1519
- // 2) install observers
1641
+ try {
1642
+ document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureMarkerInHost(w); });
1643
+ } catch (e) {}
1644
+
1520
1645
  installIO();
1521
1646
  installMO();
1522
1647
 
1523
- // 3) initial eager show for empty visible-ish wraps (bounded)
1648
+ // initial eager show (bounded)
1524
1649
  try {
1525
1650
  var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1526
1651
  var margin = 900;
@@ -1528,6 +1653,7 @@ function buildOrdinalMap(items) {
1528
1653
  var budget = 12;
1529
1654
  for (var i = 0; i < wraps.length && budget > 0; i++) {
1530
1655
  var w = wraps[i];
1656
+ ensureMarkerInHost(w);
1531
1657
  var r = w.getBoundingClientRect();
1532
1658
  if (r.bottom >= -margin && r.top <= (vh + margin)) {
1533
1659
  var id = getId(w);
@@ -1540,19 +1666,28 @@ function buildOrdinalMap(items) {
1540
1666
  }
1541
1667
  } catch (e) {}
1542
1668
 
1543
- window.addEventListener('scroll', scheduleShowTick, { passive: true });
1544
- window.addEventListener('resize', scheduleShowTick, { passive: true });
1669
+ window.addEventListener('scroll', function () {
1670
+ enforcePositions();
1671
+ scheduleShowTick();
1672
+ }, { passive: true });
1673
+ window.addEventListener('resize', function () {
1674
+ enforcePositions();
1675
+ scheduleShowTick();
1676
+ }, { passive: true });
1545
1677
 
1546
1678
  if (window.jQuery) {
1547
1679
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1548
1680
  setTimeout(function () {
1549
1681
  dedupePlaceholders();
1682
+ try { document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureMarkerInHost(w); }); } catch (e) {}
1683
+ enforcePositions();
1550
1684
  scheduleShowTick();
1551
1685
  }, 0);
1552
1686
  });
1553
1687
  }
1554
1688
 
1555
- setTimeout(scheduleShowTick, 0);
1689
+ setInterval(enforcePositions, 1000);
1690
+ setTimeout(function () { enforcePositions(); scheduleShowTick(); }, 0);
1556
1691
  }
1557
1692
 
1558
1693
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -1564,3 +1699,4 @@ function buildOrdinalMap(items) {
1564
1699
 
1565
1700
 
1566
1701
 
1702
+