nodebb-plugin-ezoic-infinite 1.6.3 → 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 -102
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.3",
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
@@ -1346,54 +1346,60 @@ function buildOrdinalMap(items) {
1346
1346
 
1347
1347
 
1348
1348
 
1349
+
1350
+
1349
1351
  // ===== CLEAN REFRACTOR: visibility manager for Ezoic wraps =====
1350
1352
  (function () {
1351
- // v2.1 (safe):
1352
- // - Keep the v2 behavior that didn't break forum rendering.
1353
- // - Improve speed moderately (earlier preload + slightly higher show throughput).
1354
- // - Improve up-scroll reliability without scanning the whole DOM:
1355
- // * on scroll, enqueue showAds for wraps currently in/near viewport (small bounded loop).
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 (moderate)
1365
- var SHOW_COOLDOWN_MS = 900;
1366
- var MAX_SHOW_PER_TICK = 6;
1367
-
1368
- // Up-scroll helper: only check near-viewport wraps, bounded
1369
- var SCAN_COOLDOWN_MS = 220;
1370
- var lastScan = 0;
1371
- var SCAN_BUDGET = 10;
1372
-
1373
- function isMobile() {
1374
- try { return window.matchMedia && window.matchMedia('(max-width: 767px)').matches; } catch (e) { return false; }
1375
- }
1376
- function keepMarginBetween() { return isMobile() ? KEEP_MARGIN_BETWEEN_MOBILE : KEEP_MARGIN_BETWEEN_DESKTOP; }
1370
+ // show tuning (safe)
1371
+ var MAX_SHOW_PER_TICK = 4;
1377
1372
 
1378
- var lastShowById = Object.create(null);
1373
+ // internal state
1374
+ var activatedById = Object.create(null); // id -> ts
1379
1375
  var showQueue = [];
1380
1376
  var showTicking = false;
1381
1377
 
1382
- function getWrapId(w) {
1378
+ function getIdFromWrap(w) {
1383
1379
  try { return w.getAttribute('data-ezoic-wrapid'); } catch (e) { return null; }
1384
1380
  }
1385
- function getPlaceholderId(w) {
1381
+ function getIdFromPlaceholder(w) {
1386
1382
  try {
1387
1383
  var ph = w.querySelector('[data-ezoic-id]');
1388
1384
  return ph ? ph.getAttribute('data-ezoic-id') : null;
1389
1385
  } catch (e) { return null; }
1390
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
+ }
1391
1398
 
1392
1399
  function enqueueShow(id) {
1393
1400
  if (!id) return;
1394
- var now = Date.now();
1395
- var last = lastShowById[id] || 0;
1396
- if (now - last < SHOW_COOLDOWN_MS) return;
1401
+ // show only once (avoid "already defined")
1402
+ if (activatedById[id]) return;
1397
1403
 
1398
1404
  for (var i = 0; i < showQueue.length; i++) if (showQueue[i] === id) return;
1399
1405
  showQueue.push(id);
@@ -1409,9 +1415,9 @@ function buildOrdinalMap(items) {
1409
1415
  while (showQueue.length && n < MAX_SHOW_PER_TICK) {
1410
1416
  var id = showQueue.shift();
1411
1417
  try {
1412
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1418
+ if (!activatedById[id] && window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
1413
1419
  window.ezstandalone.showAds(String(id));
1414
- lastShowById[id] = Date.now();
1420
+ activatedById[id] = Date.now();
1415
1421
  }
1416
1422
  } catch (e) {}
1417
1423
  n++;
@@ -1420,28 +1426,27 @@ function buildOrdinalMap(items) {
1420
1426
  });
1421
1427
  }
1422
1428
 
1423
- function removeFarBetweenWraps() {
1424
- var margin = keepMarginBetween();
1425
- var removed = 0;
1426
- var wraps;
1427
- try { wraps = document.querySelectorAll(BETWEEN_SELECTOR); } catch (e) { return; }
1428
-
1429
- wraps.forEach(function (w) {
1430
- if (removed >= 3) return;
1431
- try {
1432
- var lv = parseInt(w.getAttribute('data-last-visible') || '0', 10);
1433
- if (lv && (Date.now() - lv) < 12000) return;
1434
-
1435
- var r = w.getBoundingClientRect();
1436
- if (r.bottom < -margin || r.top > ((window.innerHeight || 0) + margin)) {
1437
- w.remove();
1438
- removed += 1;
1439
- }
1440
- } catch (e) {}
1441
- });
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) {}
1442
1447
  }
1443
1448
 
1444
- // IO: preload earlier for faster display
1449
+ // Track visibility: when a wrap comes near viewport, trigger show once if empty.
1445
1450
  var io = null;
1446
1451
  function installIO() {
1447
1452
  if (io || typeof IntersectionObserver === 'undefined') return;
@@ -1451,9 +1456,15 @@ function buildOrdinalMap(items) {
1451
1456
  entries.forEach(function (e) {
1452
1457
  if (!e || !e.target) return;
1453
1458
  if (e.isIntersecting) {
1454
- try { e.target.setAttribute('data-last-visible', String(Date.now())); } catch (err) {}
1455
- var id = getWrapId(e.target) || getPlaceholderId(e.target);
1456
- 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);
1457
1468
  }
1458
1469
  });
1459
1470
  } catch (e) {}
@@ -1462,12 +1473,16 @@ function buildOrdinalMap(items) {
1462
1473
  try { document.querySelectorAll(WRAP_SELECTOR).forEach(function (w) { try { io.observe(w); } catch(e) {} }); } catch (e) {}
1463
1474
  }
1464
1475
 
1476
+ // Observe newly added wraps and observe them + dedupe
1465
1477
  var moInstalled = false;
1466
1478
  function installMO() {
1467
1479
  if (moInstalled || typeof MutationObserver === 'undefined') return;
1468
1480
  moInstalled = true;
1469
1481
 
1470
1482
  var mo = new MutationObserver(function (muts) {
1483
+ // dedupe quickly, then observe new wraps
1484
+ try { dedupePlaceholders(); } catch (e) {}
1485
+
1471
1486
  if (!io) return;
1472
1487
  try {
1473
1488
  for (var i = 0; i < muts.length; i++) {
@@ -1479,8 +1494,10 @@ function buildOrdinalMap(items) {
1479
1494
 
1480
1495
  if (n.matches && n.matches(WRAP_SELECTOR)) {
1481
1496
  try { io.observe(n); } catch (e) {}
1482
- var id = getWrapId(n) || getPlaceholderId(n);
1483
- 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();
1484
1501
  } else if (n.querySelectorAll) {
1485
1502
  var inner = n.querySelectorAll(WRAP_SELECTOR);
1486
1503
  for (var k = 0; k < inner.length; k++) {
@@ -1495,67 +1512,47 @@ function buildOrdinalMap(items) {
1495
1512
  try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}
1496
1513
  }
1497
1514
 
1498
- // Small bounded scan near viewport on scroll (helps on up-scroll)
1499
- function scanNearViewport() {
1500
- var now = Date.now();
1501
- if (now - lastScan < SCAN_COOLDOWN_MS) return;
1502
- lastScan = now;
1515
+ function init() {
1516
+ // 1) dedupe existing
1517
+ dedupePlaceholders();
1503
1518
 
1504
- var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1505
- var margin = 900; // near viewport window
1506
- var wraps;
1507
- try { wraps = document.querySelectorAll(WRAP_SELECTOR); } catch (e) { return; }
1519
+ // 2) install observers
1520
+ installIO();
1521
+ installMO();
1508
1522
 
1509
- var budget = SCAN_BUDGET;
1510
- for (var i = 0; i < wraps.length && budget > 0; i++) {
1511
- var w = wraps[i];
1512
- try {
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];
1513
1531
  var r = w.getBoundingClientRect();
1514
1532
  if (r.bottom >= -margin && r.top <= (vh + margin)) {
1515
- var id = getWrapId(w) || getPlaceholderId(w);
1516
- if (id) enqueueShow(id);
1533
+ var id = getId(w);
1534
+ if (id) {
1535
+ if (isFilled(w)) activatedById[id] = activatedById[id] || Date.now();
1536
+ else enqueueShow(id);
1537
+ }
1517
1538
  budget--;
1518
1539
  }
1519
- } catch (e) {}
1520
- }
1521
- }
1522
-
1523
- var sweepPending = false;
1524
- var lastSweep = 0;
1525
- var SWEEP_COOLDOWN_MS = 600;
1526
-
1527
- function scheduleSweep() {
1528
- var now = Date.now();
1529
- if (now - lastSweep < SWEEP_COOLDOWN_MS) return;
1530
- if (sweepPending) return;
1531
- sweepPending = true;
1532
- requestAnimationFrame(function () {
1533
- sweepPending = false;
1534
- lastSweep = Date.now();
1535
- removeFarBetweenWraps();
1536
- });
1537
- }
1538
-
1539
- function onScroll() {
1540
- scheduleSweep();
1541
- scanNearViewport();
1542
- scheduleShowTick();
1543
- }
1544
-
1545
- function init() {
1546
- installIO();
1547
- installMO();
1540
+ }
1541
+ } catch (e) {}
1548
1542
 
1549
- window.addEventListener('scroll', onScroll, { passive: true });
1550
- window.addEventListener('resize', onScroll, { passive: true });
1543
+ window.addEventListener('scroll', scheduleShowTick, { passive: true });
1544
+ window.addEventListener('resize', scheduleShowTick, { passive: true });
1551
1545
 
1552
1546
  if (window.jQuery) {
1553
1547
  window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1554
- setTimeout(function () { installIO(); onScroll(); }, 0);
1548
+ setTimeout(function () {
1549
+ dedupePlaceholders();
1550
+ scheduleShowTick();
1551
+ }, 0);
1555
1552
  });
1556
1553
  }
1557
1554
 
1558
- setTimeout(function () { installIO(); onScroll(); }, 0);
1555
+ setTimeout(scheduleShowTick, 0);
1559
1556
  }
1560
1557
 
1561
1558
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -1565,3 +1562,5 @@ function buildOrdinalMap(items) {
1565
1562
 
1566
1563
 
1567
1564
 
1565
+
1566
+