nodebb-plugin-ezoic-infinite 1.6.4 → 1.6.5

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 +101 -159
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.5",
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,54 +1347,59 @@ function buildOrdinalMap(items) {
1347
1347
 
1348
1348
 
1349
1349
 
1350
+
1350
1351
  // ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
1351
1352
  (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.
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.
1360
+ //
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.
1356
1365
 
1357
1366
  var BETWEEN_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-between';
1358
1367
  var MESSAGE_SELECTOR = '.nodebb-ezoic-wrap.ezoic-ad-message';
1359
1368
  var WRAP_SELECTOR = BETWEEN_SELECTOR + ', ' + MESSAGE_SELECTOR;
1360
1369
 
1361
- var KEEP_MARGIN_BETWEEN_DESKTOP = 2600;
1362
- var KEEP_MARGIN_BETWEEN_MOBILE = 1900;
1363
-
1364
- // Show tuning (safe/moderate)
1365
- var SHOW_COOLDOWN_MS = 900;
1366
- var MAX_SHOW_PER_TICK = 6;
1367
-
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
1370
+ // show tuning (safe)
1371
+ var MAX_SHOW_PER_TICK = 4;
1373
1372
 
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);
1373
+ // internal state
1374
+ var activatedById = Object.create(null); // id -> ts
1380
1375
  var showQueue = [];
1381
1376
  var showTicking = false;
1382
1377
 
1383
- function getWrapId(w) {
1378
+ function getIdFromWrap(w) {
1384
1379
  try { return w.getAttribute('data-ezoic-wrapid'); } catch (e) { return null; }
1385
1380
  }
1386
- function getPlaceholderId(w) {
1381
+ function getIdFromPlaceholder(w) {
1387
1382
  try {
1388
1383
  var ph = w.querySelector('[data-ezoic-id]');
1389
1384
  return ph ? ph.getAttribute('data-ezoic-id') : null;
1390
1385
  } catch (e) { return null; }
1391
1386
  }
1387
+ function getId(w) { return getIdFromWrap(w) || getIdFromPlaceholder(w); }
1388
+
1389
+ function isFilled(w) {
1390
+ try {
1391
+ // if Ezoic/Google already injected, there will be an iframe or an element with id starting google_ads_iframe
1392
+ if (w.querySelector('iframe')) return true;
1393
+ if (w.querySelector('[id^="google_ads_iframe"]')) return true;
1394
+ if (w.querySelector('.ezoic-ad')) return true;
1395
+ } catch (e) {}
1396
+ return false;
1397
+ }
1392
1398
 
1393
1399
  function enqueueShow(id) {
1394
1400
  if (!id) return;
1395
- var now = Date.now();
1396
- var last = lastShowById[id] || 0;
1397
- if (now - last < SHOW_COOLDOWN_MS) return;
1401
+ // show only once (avoid "already defined")
1402
+ if (activatedById[id]) return;
1398
1403
 
1399
1404
  for (var i = 0; i < showQueue.length; i++) if (showQueue[i] === id) return;
1400
1405
  showQueue.push(id);
@@ -1410,9 +1415,9 @@ function buildOrdinalMap(items) {
1410
1415
  while (showQueue.length && n < MAX_SHOW_PER_TICK) {
1411
1416
  var id = showQueue.shift();
1412
1417
  try {
1413
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1418
+ if (!activatedById[id] && window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1414
1419
  window.ezstandalone.showAds(String(id));
1415
- lastShowById[id] = Date.now();
1420
+ activatedById[id] = Date.now();
1416
1421
  }
1417
1422
  } catch (e) {}
1418
1423
  n++;
@@ -1421,28 +1426,27 @@ function buildOrdinalMap(items) {
1421
1426
  });
1422
1427
  }
1423
1428
 
1424
- function removeFarBetweenWraps() {
1425
- var margin = keepMarginBetween();
1426
- var removed = 0;
1427
- var wraps;
1428
- try { wraps = document.querySelectorAll(BETWEEN_SELECTOR); } catch (e) { return; }
1429
-
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;
1435
-
1436
- var r = w.getBoundingClientRect();
1437
- if (r.bottom < -margin || r.top > ((window.innerHeight || 0) + margin)) {
1438
- w.remove();
1439
- removed += 1;
1440
- }
1441
- } catch (e) {}
1442
- });
1429
+ // De-duplicate placeholders to avoid Ezoic warnings.
1430
+ function dedupePlaceholders() {
1431
+ var seen = Object.create(null);
1432
+ try {
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
+ if (seen[id]) {
1438
+ // remove duplicate wrapper entirely (keep first occurrence)
1439
+ var wrap = ph.closest('.nodebb-ezoic-wrap');
1440
+ if (wrap) wrap.remove();
1441
+ } else {
1442
+ seen[id] = true;
1443
+ }
1444
+ } catch (e) {}
1445
+ });
1446
+ } catch (e) {}
1443
1447
  }
1444
1448
 
1445
- // IO: preload earlier for faster display
1449
+ // Track visibility: when a wrap comes near viewport, trigger show once if empty.
1446
1450
  var io = null;
1447
1451
  function installIO() {
1448
1452
  if (io || typeof IntersectionObserver === 'undefined') return;
@@ -1452,9 +1456,15 @@ function buildOrdinalMap(items) {
1452
1456
  entries.forEach(function (e) {
1453
1457
  if (!e || !e.target) return;
1454
1458
  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);
1459
+ // If the slot is already filled, mark it as activated to prevent re-define attempts.
1460
+ var id = getId(e.target);
1461
+ if (!id) return;
1462
+
1463
+ if (isFilled(e.target)) {
1464
+ activatedById[id] = activatedById[id] || Date.now();
1465
+ return;
1466
+ }
1467
+ enqueueShow(id);
1458
1468
  }
1459
1469
  });
1460
1470
  } catch (e) {}
@@ -1463,12 +1473,16 @@ function buildOrdinalMap(items) {
1463
1473
  try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { io.observe(w); } catch(e) {} }); } catch (e) {}
1464
1474
  }
1465
1475
 
1476
+ // Observe newly added wraps and observe them + dedupe
1466
1477
  var moInstalled = false;
1467
1478
  function installMO() {
1468
1479
  if (moInstalled || typeof MutationObserver === 'undefined') return;
1469
1480
  moInstalled = true;
1470
1481
 
1471
1482
  var mo = new MutationObserver(function (muts) {
1483
+ // dedupe quickly, then observe new wraps
1484
+ try { dedupePlaceholders(); } catch (e) {}
1485
+
1472
1486
  if (!io) return;
1473
1487
  try {
1474
1488
  for (var i = 0; i < muts.length; i++) {
@@ -1480,8 +1494,10 @@ function buildOrdinalMap(items) {
1480
1494
 
1481
1495
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1482
1496
  try { io.observe(n); } catch (e) {}
1483
- var id = getWrapId(n) || getPlaceholderId(n);
1484
- if (id) enqueueShow(id);
1497
+ // eager show if empty
1498
+ var id = getId(n);
1499
+ if (id && !isFilled(n)) enqueueShow(id);
1500
+ else if (id) activatedById[id] = activatedById[id] || Date.now();
1485
1501
  } else if (n.querySelectorAll) {
1486
1502
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1487
1503
  for (var k = 0; k < inner.length; k++) {
@@ -1496,122 +1512,47 @@ function buildOrdinalMap(items) {
1496
1512
  try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
1497
1513
  }
1498
1514
 
1499
- function closestWrap(el) {
1500
- 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
- }
1505
- } 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;
1525
-
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));
1515
+ function init() {
1516
+ // 1) dedupe existing
1517
+ dedupePlaceholders();
1548
1518
 
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
- }
1519
+ // 2) install observers
1520
+ installIO();
1521
+ installMO();
1563
1522
 
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]);
1523
+ // 3) initial eager show for empty visible-ish wraps (bounded)
1524
+ try {
1525
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1526
+ var margin = 900;
1527
+ var wraps = document.querySelectorAll(WRAP_SELECTOR);
1528
+ var budget = 12;
1529
+ for (var i = 0; i < wraps.length && budget > 0; i++) {
1530
+ var w = wraps[i];
1531
+ var r = w.getBoundingClientRect();
1532
+ if (r.bottom >= -margin && r.top <= (vh + margin)) {
1533
+ var id = getId(w);
1534
+ if (id) {
1535
+ if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
1536
+ else enqueueShow(id);
1572
1537
  }
1573
- } catch (e) {}
1574
- backward = backward.previousElementSibling;
1538
+ budget--;
1539
+ }
1575
1540
  }
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();
1541
+ } catch (e) {}
1604
1542
 
1605
- window.addEventListener('scroll', onScroll, { passive: true });
1606
- window.addEventListener('resize', onScroll, { passive: true });
1543
+ window.addEventListener('scroll', scheduleShowTick, { passive: true });
1544
+ window.addEventListener('resize', scheduleShowTick, { passive: true });
1607
1545
 
1608
1546
  if (window.jQuery) {
1609
1547
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1610
- setTimeout(function () { installIO(); onScroll(); }, 0);
1548
+ setTimeout(function () {
1549
+ dedupePlaceholders();
1550
+ scheduleShowTick();
1551
+ }, 0);
1611
1552
  });
1612
1553
  }
1613
1554
 
1614
- setTimeout(function () { installIO(); onScroll(); }, 0);
1555
+ setTimeout(scheduleShowTick, 0);
1615
1556
  }
1616
1557
 
1617
1558
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -1622,3 +1563,4 @@ function buildOrdinalMap(items) {
1622
1563
 
1623
1564
 
1624
1565
 
1566
+