nodebb-plugin-ezoic-infinite 1.6.61 → 1.6.63

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 +65 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.61",
3
+ "version": "1.6.63",
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
@@ -467,17 +467,31 @@
467
467
  // ─── Orphan / cluster management ────────────────────────────────────────────
468
468
  function pruneOrphanWraps(kindClass, items) {
469
469
  if (!items || !items.length) return 0;
470
- const itemSet = new Set(items);
470
+ const itemSet = new Set(items);
471
471
  const isMessage = kindClass === 'ezoic-ad-message';
472
+ // Between-ads and category-ads are NEVER fully released — only hidden/shown.
473
+ // Releasing them frees their placeholder IDs back into the pool. When the
474
+ // anchor topics return (scroll-up / NodeBB re-render), injectBetween would
475
+ // re-insert those IDs, but DOM ordinals have shifted → all wraps pile at top.
476
+ // Keeping the wrap node in DOM at its original position means findWrap() will
477
+ // always find it and injectBetween() will never create a duplicate.
478
+ const allowRelease = isMessage;
472
479
  let removed = 0;
473
480
 
474
481
  const hasNearbyItem = (wrap) => {
475
- let prev = wrap.previousElementSibling;
482
+ // If the wrap is inside a li.nodebb-ezoic-host, we must check the HOST's
483
+ // siblings, not the wrap's own siblings (the wrap has no siblings inside host).
484
+ const pivot = (wrap.parentElement && wrap.parentElement.classList &&
485
+ wrap.parentElement.classList.contains(HOST_CLASS))
486
+ ? wrap.parentElement
487
+ : wrap;
488
+
489
+ let prev = pivot.previousElementSibling;
476
490
  for (let i = 0; i < 14 && prev; i++) {
477
491
  if (itemSet.has(prev)) return true;
478
492
  prev = prev.previousElementSibling;
479
493
  }
480
- let next = wrap.nextElementSibling;
494
+ let next = pivot.nextElementSibling;
481
495
  for (let i = 0; i < 14 && next; i++) {
482
496
  if (itemSet.has(next)) return true;
483
497
  next = next.nextElementSibling;
@@ -494,6 +508,7 @@
494
508
  if (created && (now() - created) < keepEmptyWrapMs()) return;
495
509
 
496
510
  if (hasNearbyItem(wrap)) {
511
+ // Anchor back in DOM → restore visibility.
497
512
  try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
498
513
  return;
499
514
  }
@@ -501,17 +516,15 @@
501
516
  // Anchor is gone (virtualized) → hide so ads don't stack visually.
502
517
  try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
503
518
 
504
- // Only fully release when far off-screen (avoids perceived "vanishing").
505
- // FIX: between-ads are now also subject to this offscreen check, not
506
- // just message-ads. Previously, filled between-ad wraps were NEVER pruned,
507
- // causing orphaned filled wraps to drift near the top of the list.
519
+ // For between/category ads: stop here never release the node.
520
+ // The wrap stays in DOM (hidden) at its correct data-ezoic-after position.
521
+ if (!allowRelease) return;
522
+
523
+ // For message-ads only: fully release when far off-screen.
508
524
  try {
509
- const r = wrap.getBoundingClientRect();
510
- const vh = Math.max(1, window.innerHeight || 1);
511
- const near = isMessage
512
- ? (r.bottom > -vh * 2 && r.top < vh * 4)
513
- : (r.bottom > -vh * 3 && r.top < vh * 5);
514
- if (near) return;
525
+ const r = wrap.getBoundingClientRect();
526
+ const vh = Math.max(1, window.innerHeight || 1);
527
+ if (r.bottom > -vh * 2 && r.top < vh * 4) return; // still near viewport
515
528
  } catch (e) { return; }
516
529
 
517
530
  withInternalDomChange(() => releaseWrapNode(wrap));
@@ -523,6 +536,13 @@
523
536
  function decluster(kindClass) {
524
537
  const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
525
538
  if (wraps.length < 2) return 0;
539
+ // Between/category ads: hide the duplicate rather than releasing it.
540
+ // Releasing frees IDs into the pool → re-injection at wrong positions on scroll-up.
541
+ const allowRelease = kindClass === 'ezoic-ad-message';
542
+ const evict = (n) => {
543
+ if (allowRelease) { withInternalDomChange(() => releaseWrapNode(n)); }
544
+ else { try { n.classList.add('ez-orphan-hidden'); n.style.display = 'none'; } catch (e) {} }
545
+ };
526
546
  const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
527
547
  const isFresh = (w) => {
528
548
  const c = parseInt(w.getAttribute('data-created') || '0', 10);
@@ -531,17 +551,19 @@
531
551
  let removed = 0;
532
552
  for (const w of wraps) {
533
553
  if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
554
+ if (w.classList && w.classList.contains('ez-orphan-hidden')) continue;
534
555
  let prev = w.previousElementSibling;
535
556
  for (let i = 0; i < 3 && prev; i++) {
536
557
  if (isWrap(prev)) {
537
558
  if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
559
+ if (prev.classList && prev.classList.contains('ez-orphan-hidden')) break;
538
560
  const prevFilled = isFilled(prev);
539
561
  const curFilled = isFilled(w);
540
562
  if (curFilled) {
541
- if (!prevFilled && !isFresh(prev)) { withInternalDomChange(() => releaseWrapNode(prev)); removed++; }
563
+ if (!prevFilled && !isFresh(prev)) { evict(prev); removed++; }
542
564
  break;
543
565
  }
544
- if (prevFilled || !isFresh(w)) { withInternalDomChange(() => releaseWrapNode(w)); removed++; }
566
+ if (prevFilled || !isFresh(w)) { evict(w); removed++; }
545
567
  break;
546
568
  }
547
569
  prev = prev.previousElementSibling;
@@ -913,7 +935,8 @@
913
935
  if (!id) {
914
936
  // Pool exhausted: try to recycle a wrap that is far above the viewport.
915
937
  // Recycling is disabled when scrolling up and for message ads.
916
- const allowRecycle = kindClass !== 'ezoic-ad-message' && scrollDir > 0;
938
+ // Recycling is disabled for between/category ads (we never release those wraps).
939
+ const allowRecycle = kindClass === 'ezoic-ad-message' && scrollDir > 0;
917
940
  recycledWrap = allowRecycle ? pickRecyclableWrap(kindClass) : null;
918
941
  if (recycledWrap) id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
919
942
  }
@@ -942,30 +965,43 @@
942
965
  const kind = getKind();
943
966
  let inserted = 0;
944
967
 
968
+ // When the user is scrolling UP, NodeBB loads content above the viewport.
969
+ // Injecting new ad wraps at that moment targets those freshly-loaded top items
970
+ // (low ordinals) and makes ads appear right at the top of the list.
971
+ // While scrolling up we only restore previously-hidden wraps (pruneOrphanWraps
972
+ // un-hides them as their anchor posts return) — no new injections.
973
+ const canInject = scrollDir >= 0;
974
+
945
975
  if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
946
976
  const items = getPostContainers();
947
977
  pruneOrphanWraps('ezoic-ad-message', items);
948
- inserted += injectBetween('ezoic-ad-message', items,
949
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
950
- normalizeBool(cfg.showFirstMessageAd), state.allPosts, 'curPosts');
951
- decluster('ezoic-ad-message');
978
+ if (canInject) {
979
+ inserted += injectBetween('ezoic-ad-message', items,
980
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
981
+ normalizeBool(cfg.showFirstMessageAd), state.allPosts, 'curPosts');
982
+ decluster('ezoic-ad-message');
983
+ }
952
984
 
953
985
  } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
954
986
  const items = getTopicItems();
955
987
  pruneOrphanWraps('ezoic-ad-between', items);
956
- inserted += injectBetween('ezoic-ad-between', items,
957
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
958
- normalizeBool(cfg.showFirstTopicAd), state.allTopics, 'curTopics');
959
- decluster('ezoic-ad-between');
960
- schedulePileFix();
988
+ if (canInject) {
989
+ inserted += injectBetween('ezoic-ad-between', items,
990
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
991
+ normalizeBool(cfg.showFirstTopicAd), state.allTopics, 'curTopics');
992
+ decluster('ezoic-ad-between');
993
+ schedulePileFix();
994
+ }
961
995
 
962
996
  } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
963
997
  const items = getCategoryItems();
964
998
  pruneOrphanWraps('ezoic-ad-categories', items);
965
- inserted += injectBetween('ezoic-ad-categories', items,
966
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
967
- normalizeBool(cfg.showFirstCategoryAd), state.allCategories, 'curCategories');
968
- decluster('ezoic-ad-categories');
999
+ if (canInject) {
1000
+ inserted += injectBetween('ezoic-ad-categories', items,
1001
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
1002
+ normalizeBool(cfg.showFirstCategoryAd), state.allCategories, 'curCategories');
1003
+ decluster('ezoic-ad-categories');
1004
+ }
969
1005
  }
970
1006
 
971
1007
  return inserted;