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.
- package/package.json +1 -1
- package/public/client.js +65 -29
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
510
|
-
const vh
|
|
511
|
-
|
|
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)) {
|
|
563
|
+
if (!prevFilled && !isFresh(prev)) { evict(prev); removed++; }
|
|
542
564
|
break;
|
|
543
565
|
}
|
|
544
|
-
if (prevFilled || !isFresh(w)) {
|
|
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
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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;
|