nodebb-plugin-ezoic-infinite 1.6.62 → 1.6.64
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 +125 -38
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -401,12 +401,23 @@
|
|
|
401
401
|
}
|
|
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
413
|
const wrap = document.createElement('div');
|
|
406
414
|
wrap.className = WRAP_CLASS + ' ' + kindClass;
|
|
407
415
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
408
416
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
409
417
|
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));
|
|
410
421
|
if (afterPos === 1) wrap.setAttribute('data-ezoic-pin', '1');
|
|
411
422
|
wrap.style.width = '100%';
|
|
412
423
|
if (createPlaceholder) {
|
|
@@ -418,14 +429,27 @@
|
|
|
418
429
|
return wrap;
|
|
419
430
|
}
|
|
420
431
|
|
|
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
|
+
|
|
421
443
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
422
444
|
if (!target || !target.insertAdjacentElement) return null;
|
|
423
445
|
if (findWrap(kindClass, afterPos)) return null;
|
|
446
|
+
const anchorId = getAnchorStableId(target);
|
|
447
|
+
if (anchorId && findWrapByAnchor(kindClass, anchorId)) return null; // already anchored
|
|
424
448
|
if (insertingIds.has(id)) return null;
|
|
425
449
|
const existingPh = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
426
450
|
insertingIds.add(id);
|
|
427
451
|
try {
|
|
428
|
-
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
452
|
+
const wrap = buildWrap(id, kindClass, afterPos, !existingPh, anchorId);
|
|
429
453
|
target.insertAdjacentElement('afterend', wrap);
|
|
430
454
|
if (existingPh) {
|
|
431
455
|
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
@@ -469,23 +493,25 @@
|
|
|
469
493
|
if (!items || !items.length) return 0;
|
|
470
494
|
const itemSet = new Set(items);
|
|
471
495
|
const isMessage = kindClass === 'ezoic-ad-message';
|
|
472
|
-
// Between
|
|
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.
|
|
496
|
+
// Between/category ads are NEVER fully released — only hidden/shown.
|
|
478
497
|
const allowRelease = isMessage;
|
|
479
498
|
let removed = 0;
|
|
480
499
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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).
|
|
484
512
|
const pivot = (wrap.parentElement && wrap.parentElement.classList &&
|
|
485
513
|
wrap.parentElement.classList.contains(HOST_CLASS))
|
|
486
|
-
? wrap.parentElement
|
|
487
|
-
: wrap;
|
|
488
|
-
|
|
514
|
+
? wrap.parentElement : wrap;
|
|
489
515
|
let prev = pivot.previousElementSibling;
|
|
490
516
|
for (let i = 0; i < 14 && prev; i++) {
|
|
491
517
|
if (itemSet.has(prev)) return true;
|
|
@@ -500,31 +526,24 @@
|
|
|
500
526
|
};
|
|
501
527
|
|
|
502
528
|
document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
|
|
503
|
-
// Never touch pinned placements.
|
|
504
529
|
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
505
|
-
|
|
506
|
-
// Never prune very fresh wraps (slow auction / CMP fills).
|
|
507
530
|
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
508
531
|
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
509
532
|
|
|
510
|
-
if (
|
|
511
|
-
// Anchor back in DOM → restore visibility.
|
|
533
|
+
if (anchorIsLive(wrap)) {
|
|
512
534
|
try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
|
|
513
535
|
return;
|
|
514
536
|
}
|
|
515
537
|
|
|
516
|
-
// Anchor
|
|
538
|
+
// Anchor not in DOM → hide.
|
|
517
539
|
try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
|
|
518
|
-
|
|
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
540
|
if (!allowRelease) return;
|
|
522
541
|
|
|
523
|
-
//
|
|
542
|
+
// Message-ads only: release when far off-screen.
|
|
524
543
|
try {
|
|
525
544
|
const r = wrap.getBoundingClientRect();
|
|
526
545
|
const vh = Math.max(1, window.innerHeight || 1);
|
|
527
|
-
if (r.bottom > -vh * 2 && r.top < vh * 4) return;
|
|
546
|
+
if (r.bottom > -vh * 2 && r.top < vh * 4) return;
|
|
528
547
|
} catch (e) { return; }
|
|
529
548
|
|
|
530
549
|
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
@@ -533,6 +552,55 @@
|
|
|
533
552
|
return removed;
|
|
534
553
|
}
|
|
535
554
|
|
|
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
|
+
|
|
536
604
|
function decluster(kindClass) {
|
|
537
605
|
const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
|
|
538
606
|
if (wraps.length < 2) return 0;
|
|
@@ -965,30 +1033,49 @@
|
|
|
965
1033
|
const kind = getKind();
|
|
966
1034
|
let inserted = 0;
|
|
967
1035
|
|
|
1036
|
+
// When the user is scrolling UP, NodeBB loads content above the viewport.
|
|
1037
|
+
// Injecting new ad wraps at that moment targets those freshly-loaded top items
|
|
1038
|
+
// (low ordinals) and makes ads appear right at the top of the list.
|
|
1039
|
+
// While scrolling up we only restore previously-hidden wraps (pruneOrphanWraps
|
|
1040
|
+
// un-hides them as their anchor posts return) — no new injections.
|
|
1041
|
+
const canInject = scrollDir >= 0;
|
|
1042
|
+
|
|
968
1043
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
969
1044
|
const items = getPostContainers();
|
|
970
1045
|
pruneOrphanWraps('ezoic-ad-message', items);
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1046
|
+
if (canInject) {
|
|
1047
|
+
inserted += injectBetween('ezoic-ad-message', items,
|
|
1048
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
1049
|
+
normalizeBool(cfg.showFirstMessageAd), state.allPosts, 'curPosts');
|
|
1050
|
+
decluster('ezoic-ad-message');
|
|
1051
|
+
}
|
|
975
1052
|
|
|
976
1053
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
977
1054
|
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);
|
|
978
1060
|
pruneOrphanWraps('ezoic-ad-between', items);
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1061
|
+
if (canInject) {
|
|
1062
|
+
inserted += injectBetween('ezoic-ad-between', items,
|
|
1063
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
1064
|
+
normalizeBool(cfg.showFirstTopicAd), state.allTopics, 'curTopics');
|
|
1065
|
+
decluster('ezoic-ad-between');
|
|
1066
|
+
schedulePileFix();
|
|
1067
|
+
}
|
|
984
1068
|
|
|
985
1069
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
986
1070
|
const items = getCategoryItems();
|
|
1071
|
+
reanchorWraps('ezoic-ad-categories', items);
|
|
987
1072
|
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1073
|
+
if (canInject) {
|
|
1074
|
+
inserted += injectBetween('ezoic-ad-categories', items,
|
|
1075
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
1076
|
+
normalizeBool(cfg.showFirstCategoryAd), state.allCategories, 'curCategories');
|
|
1077
|
+
decluster('ezoic-ad-categories');
|
|
1078
|
+
}
|
|
992
1079
|
}
|
|
993
1080
|
|
|
994
1081
|
return inserted;
|