nodebb-plugin-ezoic-infinite 1.6.64 → 1.6.65

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 +22 -94
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.64",
3
+ "version": "1.6.65",
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
@@ -402,22 +402,12 @@
402
402
 
403
403
  // ─── Insertion primitives ───────────────────────────────────────────────────
404
404
 
405
- /** Extract the stable DOM id (tid/pid/cid) from an anchor element. */
406
- function getAnchorStableId(el) {
407
- if (!el) return '';
408
- return el.getAttribute('data-tid') || el.getAttribute('data-pid') ||
409
- el.getAttribute('data-cid') || el.getAttribute('data-index') || '';
410
- }
411
-
412
- function buildWrap(id, kindClass, afterPos, createPlaceholder, anchorStableId) {
405
+ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
413
406
  const wrap = document.createElement('div');
414
407
  wrap.className = WRAP_CLASS + ' ' + kindClass;
415
408
  wrap.setAttribute('data-ezoic-after', String(afterPos));
416
409
  wrap.setAttribute('data-ezoic-wrapid', String(id));
417
410
  wrap.setAttribute('data-created', String(now()));
418
- // Store the anchor element's stable DOM id so we can re-anchor after
419
- // NodeBB re-renders the list (which changes ordinal positions).
420
- if (anchorStableId) wrap.setAttribute('data-ezoic-anchor', String(anchorStableId));
421
411
  if (afterPos === 1) wrap.setAttribute('data-ezoic-pin', '1');
422
412
  wrap.style.width = '100%';
423
413
  if (createPlaceholder) {
@@ -429,27 +419,14 @@
429
419
  return wrap;
430
420
  }
431
421
 
432
- /**
433
- * Find a wrap that is already anchored to a specific stable id (tid/pid/cid).
434
- * Used to prevent double-injection when NodeBB re-renders and ordinals shift.
435
- */
436
- function findWrapByAnchor(kindClass, anchorStableId) {
437
- if (!anchorStableId) return null;
438
- return document.querySelector(
439
- '.' + WRAP_CLASS + '.' + kindClass + '[data-ezoic-anchor="' + CSS.escape(String(anchorStableId)) + '"]'
440
- );
441
- }
442
-
443
422
  function insertAfter(target, id, kindClass, afterPos) {
444
423
  if (!target || !target.insertAdjacentElement) return null;
445
424
  if (findWrap(kindClass, afterPos)) return null;
446
- const anchorId = getAnchorStableId(target);
447
- if (anchorId && findWrapByAnchor(kindClass, anchorId)) return null; // already anchored
448
425
  if (insertingIds.has(id)) return null;
449
426
  const existingPh = document.getElementById(PLACEHOLDER_PREFIX + id);
450
427
  insertingIds.add(id);
451
428
  try {
452
- const wrap = buildWrap(id, kindClass, afterPos, !existingPh, anchorId);
429
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
453
430
  target.insertAdjacentElement('afterend', wrap);
454
431
  if (existingPh) {
455
432
  existingPh.setAttribute('data-ezoic-id', String(id));
@@ -494,21 +471,12 @@
494
471
  const itemSet = new Set(items);
495
472
  const isMessage = kindClass === 'ezoic-ad-message';
496
473
  // Between/category ads are NEVER fully released — only hidden/shown.
474
+ // Releasing frees IDs into the pool → re-injection at wrong positions.
497
475
  const allowRelease = isMessage;
498
476
  let removed = 0;
499
477
 
500
- // Build a set of stable anchor ids currently in the DOM (tid/pid/cid).
501
- const liveAnchorIds = new Set();
502
- items.forEach((el) => {
503
- const sid = getAnchorStableId(el);
504
- if (sid) liveAnchorIds.add(sid);
505
- });
506
-
507
- const anchorIsLive = (wrap) => {
508
- // Primary: check by stable id (survives NodeBB re-renders).
509
- const sid = wrap.getAttribute('data-ezoic-anchor');
510
- if (sid) return liveAnchorIds.has(sid);
511
- // Fallback: DOM-proximity check (for wraps without stable id).
478
+ const hasNearbyItem = (wrap) => {
479
+ // If wrap is inside a li.nodebb-ezoic-host, check the HOST's siblings.
512
480
  const pivot = (wrap.parentElement && wrap.parentElement.classList &&
513
481
  wrap.parentElement.classList.contains(HOST_CLASS))
514
482
  ? wrap.parentElement : wrap;
@@ -530,12 +498,12 @@
530
498
  const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
531
499
  if (created && (now() - created) < keepEmptyWrapMs()) return;
532
500
 
533
- if (anchorIsLive(wrap)) {
501
+ if (hasNearbyItem(wrap)) {
534
502
  try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
535
503
  return;
536
504
  }
537
505
 
538
- // Anchor not in DOM → hide.
506
+ // Anchor gone → hide.
539
507
  try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
540
508
  if (!allowRelease) return;
541
509
 
@@ -552,55 +520,6 @@
552
520
  return removed;
553
521
  }
554
522
 
555
- /**
556
- * Re-anchor between/category wraps after a NodeBB list re-render.
557
- *
558
- * When NodeBB rebuilds the topic list (infinite scroll above, sort change…),
559
- * it re-inserts topic <li> elements in a new DOM order. Our wraps are still
560
- * in the DOM but they may now sit between wrong topics because their ordinals
561
- * changed. We use the stable anchor id (data-tid / data-pid) recorded at
562
- * insertion time to find each wrap's correct anchor topic and move it there.
563
- */
564
- function reanchorWraps(kindClass, items) {
565
- // Build tid → element map from the current live items.
566
- const tidMap = new Map();
567
- items.forEach((el) => {
568
- const sid = getAnchorStableId(el);
569
- if (sid) tidMap.set(sid, el);
570
- });
571
-
572
- document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
573
- try {
574
- const sid = wrap.getAttribute('data-ezoic-anchor');
575
- if (!sid) return;
576
- const anchor = tidMap.get(sid);
577
- if (!anchor || !anchor.isConnected) return;
578
-
579
- // Check if the wrap is already correctly positioned (immediately after anchor).
580
- // Account for li.nodebb-ezoic-host wrapper.
581
- const wrapOrHost = (wrap.parentElement && wrap.parentElement.classList &&
582
- wrap.parentElement.classList.contains(HOST_CLASS))
583
- ? wrap.parentElement : wrap;
584
-
585
- if (wrapOrHost.previousElementSibling === anchor) {
586
- // Already in the right place — just un-hide if it was hidden.
587
- try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
588
- return;
589
- }
590
-
591
- // Update data-ezoic-after to the anchor's current ordinal so findWrap still works.
592
- const newOrdinal = getAnchorStableId(anchor)
593
- ? (parseInt(anchor.getAttribute('data-index') || '-1', 10) + 1) || 0
594
- : 0;
595
- if (newOrdinal > 0) wrap.setAttribute('data-ezoic-after', String(newOrdinal));
596
-
597
- // Move the wrap (or its host) to after the anchor.
598
- anchor.insertAdjacentElement('afterend', wrapOrHost);
599
- try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
600
- } catch (e) {}
601
- });
602
- }
603
-
604
523
  function decluster(kindClass) {
605
524
  const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
606
525
  if (wraps.length < 2) return 0;
@@ -990,10 +909,25 @@
990
909
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
991
910
  let inserted = 0;
992
911
 
912
+ // Viewport top — we never inject after an element that is fully above this.
913
+ // This is the definitive guard against NodeBB loading content above the fold
914
+ // and our scanner immediately injecting ads on those newly-loaded top items.
915
+ // We use a generous negative margin so items just barely scrolled above still
916
+ // get ads injected (they'll be visible when the user scrolls back a little).
917
+ const viewportSafeTop = -(window.innerHeight || 800) * 0.5;
918
+
993
919
  for (const afterPos of targets) {
994
920
  if (inserted >= maxInserts) break;
995
921
  const el = ordinalMap.get(afterPos);
996
922
  if (!el || !el.isConnected) continue;
923
+
924
+ // Skip elements that are well above the viewport.
925
+ // getBoundingClientRect is cheap on modern engines (no forced layout).
926
+ try {
927
+ const rect = el.getBoundingClientRect();
928
+ if (rect.bottom < viewportSafeTop) continue;
929
+ } catch (e) {}
930
+
997
931
  if (isAdjacentAd(el)) continue;
998
932
  if (findWrap(kindClass, afterPos)) continue;
999
933
 
@@ -1052,11 +986,6 @@
1052
986
 
1053
987
  } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
1054
988
  const items = getTopicItems();
1055
- // Re-anchor FIRST: move existing wraps to their correct topic after any
1056
- // NodeBB list re-render (ordinals may have shifted). This must happen before
1057
- // pruneOrphanWraps (which uses live anchor ids) and before injectBetween
1058
- // (which checks findWrap/findWrapByAnchor to avoid duplicates).
1059
- reanchorWraps('ezoic-ad-between', items);
1060
989
  pruneOrphanWraps('ezoic-ad-between', items);
1061
990
  if (canInject) {
1062
991
  inserted += injectBetween('ezoic-ad-between', items,
@@ -1068,7 +997,6 @@
1068
997
 
1069
998
  } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
1070
999
  const items = getCategoryItems();
1071
- reanchorWraps('ezoic-ad-categories', items);
1072
1000
  pruneOrphanWraps('ezoic-ad-categories', items);
1073
1001
  if (canInject) {
1074
1002
  inserted += injectBetween('ezoic-ad-categories', items,