nodebb-plugin-ezoic-infinite 1.6.4 → 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 +194 -155
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.4",
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
@@ -1347,55 +1347,62 @@ function buildOrdinalMap(items) {
1347
1347
 
1348
1348
 
1349
1349
 
1350
+
1351
+
1350
1352
  // ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
1351
1353
  (function () {
1352
- // v2.2:
1353
- // - Fix "after long scroll: no ads down / no ads up" caused by O(n) scans with a small budget.
1354
- // - Replace the near-viewport scan with a viewport-walk using elementFromPoint + sibling traversal.
1355
- // This stays O(k) even with thousands of posts.
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.
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.)
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
1356
1367
 
1357
1368
  var BETWEEN_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-between';
1358
1369
  var MESSAGE_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-message';
1359
1370
  var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
1360
1371
 
1361
- var KEEP_MARGIN_BETWEEN_DESKTOP = 2600;
1362
- var KEEP_MARGIN_BETWEEN_MOBILE = 1900;
1372
+ var MAX_SHOW_PER_TICK = 4;
1363
1373
 
1364
- // Show tuning (safe/moderate)
1365
- var SHOW_COOLDOWN_MS = 900;
1366
- var MAX_SHOW_PER_TICK = 6;
1374
+ // validation throttles
1375
+ var VALIDATE_COOLDOWN_MS = 260;
1376
+ var lastValidate = 0;
1367
1377
 
1368
- // Viewport-walk tuning
1369
- var WALK_COOLDOWN_MS = 180;
1370
- var lastWalk = 0;
1371
- var WALK_STEPS = 28; // siblings in each direction
1372
- var WALK_POINTS = 2; // sample top & bottom area of viewport
1373
-
1374
- function isMobile() {
1375
- try { return window.matchMedia && window.matchMedia('(max-width: 767px)').matches; } catch (e) { return false; }
1376
- }
1377
- function keepMarginBetween() { return isMobile() ? KEEP_MARGIN_BETWEEN_MOBILE : KEEP_MARGIN_BETWEEN_DESKTOP; }
1378
-
1379
- var lastShowById = Object.create(null);
1378
+ // internal state
1379
+ var activatedById = Object.create(null);
1380
1380
  var showQueue = [];
1381
1381
  var showTicking = false;
1382
1382
 
1383
- function getWrapId(w) {
1383
+ function getIdFromWrap(w) {
1384
1384
  try { return w.getAttribute('data-ezoic-wrapid'); } catch (e) { return null; }
1385
1385
  }
1386
- function getPlaceholderId(w) {
1386
+ function getIdFromPlaceholder(w) {
1387
1387
  try {
1388
1388
  var ph = w.querySelector('[data-ezoic-id]');
1389
1389
  return ph ? ph.getAttribute('data-ezoic-id') : null;
1390
1390
  } catch (e) { return null; }
1391
1391
  }
1392
+ function getId(w) { return getIdFromWrap(w) || getIdFromPlaceholder(w); }
1393
+
1394
+ function isFilled(w) {
1395
+ try {
1396
+ if (w.querySelector('iframe')) return true;
1397
+ if (w.querySelector('[id^="google_ads_iframe"]')) return true;
1398
+ if (w.querySelector('.ezoic-ad')) return true;
1399
+ } catch (e) {}
1400
+ return false;
1401
+ }
1392
1402
 
1393
1403
  function enqueueShow(id) {
1394
1404
  if (!id) return;
1395
- var now = Date.now();
1396
- var last = lastShowById[id] || 0;
1397
- if (now - last < SHOW_COOLDOWN_MS) return;
1398
-
1405
+ if (activatedById[id]) return;
1399
1406
  for (var i = 0; i < showQueue.length; i++) if (showQueue[i] === id) return;
1400
1407
  showQueue.push(id);
1401
1408
  scheduleShowTick();
@@ -1410,9 +1417,9 @@ function buildOrdinalMap(items) {
1410
1417
  while (showQueue.length && n < MAX_SHOW_PER_TICK) {
1411
1418
  var id = showQueue.shift();
1412
1419
  try {
1413
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1420
+ if (!activatedById[id] && window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1414
1421
  window.ezstandalone.showAds(String(id));
1415
- lastShowById[id] = Date.now();
1422
+ activatedById[id] = Date.now();
1416
1423
  }
1417
1424
  } catch (e) {}
1418
1425
  n++;
@@ -1421,28 +1428,103 @@ function buildOrdinalMap(items) {
1421
1428
  });
1422
1429
  }
1423
1430
 
1424
- function removeFarBetweenWraps() {
1425
- var margin = keepMarginBetween();
1426
- var removed = 0;
1427
- var wraps;
1428
- try { wraps = document.querySelectorAll(BETWEEN_SELECTOR); } catch (e) { return; }
1431
+ // --- Anchor guard ---
1432
+ function ensureAnchor(w) {
1433
+ try {
1434
+ if (!w || !w.parentNode) return;
1435
+ if (w.getAttribute('data-ezoic-anchored') === '1') return;
1429
1436
 
1430
- wraps.forEach(function (w) {
1431
- if (removed >= 3) return;
1432
- try {
1433
- var lv = parseInt(w.getAttribute('data-last-visible') || '0', 10);
1434
- if (lv && (Date.now() - lv) < 12000) return;
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
+ }
1435
1442
 
1436
- var r = w.getBoundingClientRect();
1437
- if (r.bottom < -margin || r.top > ((window.innerHeight || 0) + margin)) {
1438
- w.remove();
1439
- removed += 1;
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;
1440
1449
  }
1441
- } catch (e) {}
1442
- });
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) {}
1443
1500
  }
1444
1501
 
1445
- // IO: preload earlier for faster display
1502
+ // --- De-dupe placeholders to avoid Ezoic warnings ---
1503
+ function dedupePlaceholders() {
1504
+ var seen = Object.create(null);
1505
+ try {
1506
+ document.querySelectorAll('.nodebb-ezoic-wrap [data-ezoic-id]').forEach(function (ph) {
1507
+ 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;
1512
+
1513
+ // ensure anchor for any wrap we touch
1514
+ ensureAnchor(wrap);
1515
+
1516
+ if (seen[id]) {
1517
+ // remove duplicates; keep first occurrence
1518
+ wrap.remove();
1519
+ } else {
1520
+ seen[id] = true;
1521
+ }
1522
+ } catch (e) {}
1523
+ });
1524
+ } catch (e) {}
1525
+ }
1526
+
1527
+ // --- Visibility: trigger show once if empty ---
1446
1528
  var io = null;
1447
1529
  function installIO() {
1448
1530
  if (io || typeof IntersectionObserver === 'undefined') return;
@@ -1451,24 +1533,36 @@ function buildOrdinalMap(items) {
1451
1533
  try {
1452
1534
  entries.forEach(function (e) {
1453
1535
  if (!e || !e.target) return;
1536
+
1537
+ // Always ensure anchor while visible/near
1538
+ ensureAnchor(e.target);
1539
+
1454
1540
  if (e.isIntersecting) {
1455
- try { e.target.setAttribute('data-last-visible', String(Date.now())); } catch (err) {}
1456
- var id = getWrapId(e.target) || getPlaceholderId(e.target);
1457
- if (id) enqueueShow(id);
1541
+ var id = getId(e.target);
1542
+ if (!id) return;
1543
+
1544
+ if (isFilled(e.target)) {
1545
+ activatedById[id] = activatedById[id] || Date.now();
1546
+ return;
1547
+ }
1548
+ enqueueShow(id);
1458
1549
  }
1459
1550
  });
1460
1551
  } catch (e) {}
1461
1552
  }, { root: null, rootMargin: '1200px 0px 1200px 0px', threshold: 0.01 });
1462
1553
 
1463
- 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) {}
1464
1555
  }
1465
1556
 
1557
+ // Observe newly added wraps and anchor them
1466
1558
  var moInstalled = false;
1467
1559
  function installMO() {
1468
1560
  if (moInstalled || typeof MutationObserver === 'undefined') return;
1469
1561
  moInstalled = true;
1470
1562
 
1471
1563
  var mo = new MutationObserver(function (muts) {
1564
+ try { dedupePlaceholders(); } catch (e) {}
1565
+
1472
1566
  if (!io) return;
1473
1567
  try {
1474
1568
  for (var i = 0; i < muts.length; i++) {
@@ -1479,12 +1573,15 @@ function buildOrdinalMap(items) {
1479
1573
  if (!n || n.nodeType !== 1) continue;
1480
1574
 
1481
1575
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1576
+ ensureAnchor(n);
1482
1577
  try { io.observe(n); } catch (e) {}
1483
- var id = getWrapId(n) || getPlaceholderId(n);
1484
- if (id) enqueueShow(id);
1578
+ var id = getId(n);
1579
+ if (id && !isFilled(n)) enqueueShow(id);
1580
+ else if (id) activatedById[id] = activatedById[id] || Date.now();
1485
1581
  } else if (n.querySelectorAll) {
1486
1582
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1487
1583
  for (var k = 0; k < inner.length; k++) {
1584
+ ensureAnchor(inner[k]);
1488
1585
  try { io.observe(inner[k]); } catch (e) {}
1489
1586
  }
1490
1587
  }
@@ -1496,122 +1593,62 @@ function buildOrdinalMap(items) {
1496
1593
  try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
1497
1594
  }
1498
1595
 
1499
- function closestWrap(el) {
1596
+ function init() {
1597
+ // anchor + dedupe existing
1500
1598
  try {
1501
- while (el && el !== document.body && el !== document.documentElement) {
1502
- if (el.classList && el.classList.contains('nodebb-ezoic-wrap')) return el;
1503
- el = el.parentElement;
1504
- }
1599
+ document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); });
1505
1600
  } catch (e) {}
1506
- return null;
1507
- }
1508
-
1509
- function enqueueWrap(w) {
1510
- if (!w) return;
1511
- var id = getWrapId(w) || getPlaceholderId(w);
1512
- if (id) enqueueShow(id);
1513
- }
1514
-
1515
- // Viewport-walk: find content near viewport and walk siblings to pick nearby wraps.
1516
- function viewportWalkEnqueue() {
1517
- var now = Date.now();
1518
- if (now - lastWalk < WALK_COOLDOWN_MS) return;
1519
- lastWalk = now;
1520
-
1521
- if (typeof document.elementFromPoint !== 'function') return;
1522
-
1523
- var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1524
- if (!vh) return;
1601
+ dedupePlaceholders();
1525
1602
 
1526
- // sample points: near top and near bottom
1527
- var ys = [Math.min(vh - 10, 140), Math.max(10, vh - 180)];
1528
- if (WALK_POINTS === 1) ys = [Math.min(vh - 10, 180)];
1529
-
1530
- for (var p = 0; p < ys.length; p++) {
1531
- var y = ys[p];
1532
- var el = null;
1533
- try { el = document.elementFromPoint(10, y); } catch (e) {}
1534
- if (!el) continue;
1535
-
1536
- // ascend to a stable list item, then traverse siblings
1537
- var cursor = el;
1538
- // try to find an item container to traverse in topic lists
1539
- for (var i = 0; i < 8 && cursor && cursor.parentElement; i++) {
1540
- if (cursor.classList && (cursor.classList.contains('topic-item') || cursor.classList.contains('posts-list') || cursor.classList.contains('category-item'))) break;
1541
- cursor = cursor.parentElement;
1542
- }
1543
- // if cursor isn't traversable, just use the element itself
1544
- cursor = cursor || el;
1545
-
1546
- // find nearest wrap around this point
1547
- enqueueWrap(closestWrap(el));
1548
-
1549
- // walk next/prev siblings and enqueue wraps found
1550
- var forward = cursor;
1551
- for (var s = 0; s < WALK_STEPS; s++) {
1552
- if (!forward) break;
1553
- // check within forward node for wraps
1554
- try {
1555
- if (forward.matches && forward.matches(WRAP_SELECTOR)) enqueueWrap(forward);
1556
- if (forward.querySelectorAll) {
1557
- var w1 = forward.querySelectorAll(WRAP_SELECTOR);
1558
- for (var wi = 0; wi < w1.length; wi++) enqueueWrap(w1[wi]);
1559
- }
1560
- } catch (e) {}
1561
- forward = forward.nextElementSibling;
1562
- }
1603
+ installIO();
1604
+ installMO();
1563
1605
 
1564
- var backward = cursor;
1565
- for (var s2 = 0; s2 < WALK_STEPS; s2++) {
1566
- if (!backward) break;
1567
- try {
1568
- if (backward.matches && backward.matches(WRAP_SELECTOR)) enqueueWrap(backward);
1569
- if (backward.querySelectorAll) {
1570
- var w2 = backward.querySelectorAll(WRAP_SELECTOR);
1571
- for (var wj = 0; wj < w2.length; wj++) enqueueWrap(w2[wj]);
1606
+ // initial eager show for empty visible-ish wraps (bounded)
1607
+ try {
1608
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1609
+ var margin = 900;
1610
+ var wraps = document.querySelectorAll(WRAP_SELECTOR);
1611
+ var budget = 12;
1612
+ for (var i = 0; i < wraps.length && budget > 0; i++) {
1613
+ var w = wraps[i];
1614
+ ensureAnchor(w);
1615
+ var r = w.getBoundingClientRect();
1616
+ if (r.bottom >= -margin && r.top <= (vh + margin)) {
1617
+ var id = getId(w);
1618
+ if (id) {
1619
+ if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
1620
+ else enqueueShow(id);
1572
1621
  }
1573
- } catch (e) {}
1574
- backward = backward.previousElementSibling;
1622
+ budget--;
1623
+ }
1575
1624
  }
1576
- }
1577
- }
1578
-
1579
- var sweepPending = false;
1580
- var lastSweep = 0;
1581
- var SWEEP_COOLDOWN_MS = 600;
1582
-
1583
- function scheduleSweep() {
1584
- var now = Date.now();
1585
- if (now - lastSweep < SWEEP_COOLDOWN_MS) return;
1586
- if (sweepPending) return;
1587
- sweepPending = true;
1588
- requestAnimationFrame(function () {
1589
- sweepPending = false;
1590
- lastSweep = Date.now();
1591
- removeFarBetweenWraps();
1592
- });
1593
- }
1594
-
1595
- function onScroll() {
1596
- scheduleSweep();
1597
- viewportWalkEnqueue();
1598
- scheduleShowTick();
1599
- }
1600
-
1601
- function init() {
1602
- installIO();
1603
- installMO();
1625
+ } catch (e) {}
1604
1626
 
1605
- window.addEventListener('scroll', onScroll, { passive: true });
1606
- window.addEventListener('resize', onScroll, { 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 });
1607
1636
 
1608
1637
  if (window.jQuery) {
1609
1638
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1610
- setTimeout(function () { installIO(); onScroll(); }, 0);
1639
+ setTimeout(function () {
1640
+ try { document.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (w) { ensureAnchor(w); }); } catch (e) {}
1641
+ dedupePlaceholders();
1642
+ validateAnchors();
1643
+ scheduleShowTick();
1644
+ }, 0);
1611
1645
  });
1612
1646
  }
1613
1647
 
1614
- setTimeout(function () { installIO(); onScroll(); }, 0);
1648
+ // periodic validate safety net
1649
+ setInterval(validateAnchors, 1200);
1650
+
1651
+ setTimeout(function () { validateAnchors(); scheduleShowTick(); }, 0);
1615
1652
  }
1616
1653
 
1617
1654
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -1622,3 +1659,5 @@ function buildOrdinalMap(items) {
1622
1659
 
1623
1660
 
1624
1661
 
1662
+
1663
+