nodebb-plugin-ezoic-infinite 1.6.5 → 1.6.6

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 +128 -31
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.6",
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,35 @@ 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"
1357
- //
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.
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.
1360
1358
  //
1361
1359
  // 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.
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.)
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
1365
1367
 
1366
1368
  var BETWEEN_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-between';
1367
1369
  var MESSAGE_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-message';
1368
1370
  var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
1369
1371
 
1370
- // show tuning (safe)
1371
1372
  var MAX_SHOW_PER_TICK = 4;
1372
1373
 
1374
+ // validation throttles
1375
+ var VALIDATE_COOLDOWN_MS = 260;
1376
+ var lastValidate = 0;
1377
+
1373
1378
  // internal state
1374
- var activatedById = Object.create(null); // id -> ts
1379
+ var activatedById = Object.create(null);
1375
1380
  var showQueue = [];
1376
1381
  var showTicking = false;
1377
1382
 
@@ -1388,7 +1393,6 @@ function buildOrdinalMap(items) {
1388
1393
 
1389
1394
  function isFilled(w) {
1390
1395
  try {
1391
- // if Ezoic/Google already injected, there will be an iframe or an element with id starting google_ads_iframe
1392
1396
  if (w.querySelector('iframe')) return true;
1393
1397
  if (w.querySelector('[id^="google_ads_iframe"]')) return true;
1394
1398
  if (w.querySelector('.ezoic-ad')) return true;
@@ -1398,9 +1402,7 @@ function buildOrdinalMap(items) {
1398
1402
 
1399
1403
  function enqueueShow(id) {
1400
1404
  if (!id) return;
1401
- // show only once (avoid "already defined")
1402
1405
  if (activatedById[id]) return;
1403
-
1404
1406
  for (var i = 0; i < showQueue.length; i++) if (showQueue[i] === id) return;
1405
1407
  showQueue.push(id);
1406
1408
  scheduleShowTick();
@@ -1426,7 +1428,78 @@ function buildOrdinalMap(items) {
1426
1428
  });
1427
1429
  }
1428
1430
 
1429
- // De-duplicate placeholders to avoid Ezoic warnings.
1431
+ // --- Anchor guard ---
1432
+ function ensureAnchor(w) {
1433
+ try {
1434
+ if (!w || !w.parentNode) return;
1435
+ if (w.getAttribute('data-ezoic-anchored') === '1') return;
1436
+
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);
1441
+ }
1442
+
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;
1449
+ }
1450
+ }
1451
+
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');
1460
+ } catch (e) {}
1461
+ }
1462
+
1463
+ function validateAnchors() {
1464
+ var now = Date.now();
1465
+ if (now - lastValidate < VALIDATE_COOLDOWN_MS) return;
1466
+ lastValidate = now;
1467
+
1468
+ 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) {}
1486
+ }
1487
+
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) {}
1498
+ }
1499
+ } catch (e) {}
1500
+ }
1501
+
1502
+ // --- De-dupe placeholders to avoid Ezoic warnings ---
1430
1503
  function dedupePlaceholders() {
1431
1504
  var seen = Object.create(null);
1432
1505
  try {
@@ -1434,10 +1507,15 @@ function buildOrdinalMap(items) {
1434
1507
  try {
1435
1508
  var id = ph.getAttribute('data-ezoic-id');
1436
1509
  if (!id) return;
1510
+ var wrap = ph.closest('.nodebb-ezoic-wrap');
1511
+ if (!wrap) return;
1512
+
1513
+ // ensure anchor for any wrap we touch
1514
+ ensureAnchor(wrap);
1515
+
1437
1516
  if (seen[id]) {
1438
- // remove duplicate wrapper entirely (keep first occurrence)
1439
- var wrap = ph.closest('.nodebb-ezoic-wrap');
1440
- if (wrap) wrap.remove();
1517
+ // remove duplicates; keep first occurrence
1518
+ wrap.remove();
1441
1519
  } else {
1442
1520
  seen[id] = true;
1443
1521
  }
@@ -1446,7 +1524,7 @@ function buildOrdinalMap(items) {
1446
1524
  } catch (e) {}
1447
1525
  }
1448
1526
 
1449
- // Track visibility: when a wrap comes near viewport, trigger show once if empty.
1527
+ // --- Visibility: trigger show once if empty ---
1450
1528
  var io = null;
1451
1529
  function installIO() {
1452
1530
  if (io || typeof IntersectionObserver === 'undefined') return;
@@ -1455,8 +1533,11 @@ function buildOrdinalMap(items) {
1455
1533
  try {
1456
1534
  entries.forEach(function (e) {
1457
1535
  if (!e || !e.target) return;
1536
+
1537
+ // Always ensure anchor while visible/near
1538
+ ensureAnchor(e.target);
1539
+
1458
1540
  if (e.isIntersecting) {
1459
- // If the slot is already filled, mark it as activated to prevent re-define attempts.
1460
1541
  var id = getId(e.target);
1461
1542
  if (!id) return;
1462
1543
 
@@ -1470,17 +1551,16 @@ function buildOrdinalMap(items) {
1470
1551
  } catch (e) {}
1471
1552
  }, { root: null, rootMargin: '1200px 0px 1200px 0px', threshold: 0.01 });
1472
1553
 
1473
- try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { io.observe(w); } catch(e) {} }); } catch (e) {}
1554
+ try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { ensureAnchor(w); io.observe(w); } catch(e) {} }); } catch (e) {}
1474
1555
  }
1475
1556
 
1476
- // Observe newly added wraps and observe them + dedupe
1557
+ // Observe newly added wraps and anchor them
1477
1558
  var moInstalled = false;
1478
1559
  function installMO() {
1479
1560
  if (moInstalled || typeof MutationObserver === 'undefined') return;
1480
1561
  moInstalled = true;
1481
1562
 
1482
1563
  var mo = new MutationObserver(function (muts) {
1483
- // dedupe quickly, then observe new wraps
1484
1564
  try { dedupePlaceholders(); } catch (e) {}
1485
1565
 
1486
1566
  if (!io) return;
@@ -1493,14 +1573,15 @@ function buildOrdinalMap(items) {
1493
1573
  if (!n || n.nodeType !== 1) continue;
1494
1574
 
1495
1575
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1576
+ ensureAnchor(n);
1496
1577
  try { io.observe(n); } catch (e) {}
1497
- // eager show if empty
1498
1578
  var id = getId(n);
1499
1579
  if (id && !isFilled(n)) enqueueShow(id);
1500
1580
  else if (id) activatedById[id] = activatedById[id] || Date.now();
1501
1581
  } else if (n.querySelectorAll) {
1502
1582
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1503
1583
  for (var k = 0; k < inner.length; k++) {
1584
+ ensureAnchor(inner[k]);
1504
1585
  try { io.observe(inner[k]); } catch (e) {}
1505
1586
  }
1506
1587
  }
@@ -1513,14 +1594,16 @@ function buildOrdinalMap(items) {
1513
1594
  }
1514
1595
 
1515
1596
  function init() {
1516
- // 1) dedupe existing
1597
+ // anchor + dedupe existing
1598
+ try {
1599
+ document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); });
1600
+ } catch (e) {}
1517
1601
  dedupePlaceholders();
1518
1602
 
1519
- // 2) install observers
1520
1603
  installIO();
1521
1604
  installMO();
1522
1605
 
1523
- // 3) initial eager show for empty visible-ish wraps (bounded)
1606
+ // initial eager show for empty visible-ish wraps (bounded)
1524
1607
  try {
1525
1608
  var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1526
1609
  var margin = 900;
@@ -1528,6 +1611,7 @@ function buildOrdinalMap(items) {
1528
1611
  var budget = 12;
1529
1612
  for (var i = 0; i < wraps.length && budget > 0; i++) {
1530
1613
  var w = wraps[i];
1614
+ ensureAnchor(w);
1531
1615
  var r = w.getBoundingClientRect();
1532
1616
  if (r.bottom >= -margin && r.top <= (vh + margin)) {
1533
1617
  var id = getId(w);
@@ -1540,19 +1624,31 @@ function buildOrdinalMap(items) {
1540
1624
  }
1541
1625
  } catch (e) {}
1542
1626
 
1543
- window.addEventListener('scroll', scheduleShowTick, { passive: true });
1544
- window.addEventListener('resize', scheduleShowTick, { passive: true });
1627
+ // validate regularly on scroll/resize (cheap)
1628
+ window.addEventListener('scroll', function () {
1629
+ validateAnchors();
1630
+ scheduleShowTick();
1631
+ }, { passive: true });
1632
+ window.addEventListener('resize', function () {
1633
+ validateAnchors();
1634
+ scheduleShowTick();
1635
+ }, { passive: true });
1545
1636
 
1546
1637
  if (window.jQuery) {
1547
1638
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1548
1639
  setTimeout(function () {
1640
+ try { document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); }); } catch (e) {}
1549
1641
  dedupePlaceholders();
1642
+ validateAnchors();
1550
1643
  scheduleShowTick();
1551
1644
  }, 0);
1552
1645
  });
1553
1646
  }
1554
1647
 
1555
- setTimeout(scheduleShowTick, 0);
1648
+ // periodic validate safety net
1649
+ setInterval(validateAnchors, 1200);
1650
+
1651
+ setTimeout(function () { validateAnchors(); scheduleShowTick(); }, 0);
1556
1652
  }
1557
1653
 
1558
1654
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -1564,3 +1660,4 @@ function buildOrdinalMap(items) {
1564
1660
 
1565
1661
 
1566
1662
 
1663
+