nodebb-plugin-ezoic-infinite 1.5.86 → 1.5.88
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 +21 -214
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
var EZOIC_SAFE_MODE = true;
|
|
5
|
+
|
|
6
|
+
|
|
4
7
|
// Track scroll direction to avoid aggressive recycling when the user scrolls upward.
|
|
5
8
|
// Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
|
|
6
9
|
let lastScrollY = 0;
|
|
@@ -486,24 +489,6 @@ function globalGapFixInit() {
|
|
|
486
489
|
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
487
490
|
}
|
|
488
491
|
|
|
489
|
-
function getStableAnchorKey(el) {
|
|
490
|
-
try {
|
|
491
|
-
if (!el) return '';
|
|
492
|
-
const pid = el.getAttribute('data-pid');
|
|
493
|
-
if (pid) return 'pid:' + pid;
|
|
494
|
-
const tid = el.getAttribute('data-tid');
|
|
495
|
-
if (tid) return 'tid:' + tid;
|
|
496
|
-
const slug = el.getAttribute('data-slug');
|
|
497
|
-
if (slug) return 'slug:' + slug;
|
|
498
|
-
} catch (e) {}
|
|
499
|
-
return '';
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function findWrapByAnchor(kindClass, anchorKey) {
|
|
503
|
-
if (!anchorKey) return null;
|
|
504
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-anchor="${anchorKey}"]`);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
492
|
// ---------------- placeholder pool ----------------
|
|
508
493
|
|
|
509
494
|
function getPoolEl() {
|
|
@@ -654,13 +639,12 @@ function globalGapFixInit() {
|
|
|
654
639
|
|
|
655
640
|
// ---------------- insertion primitives ----------------
|
|
656
641
|
|
|
657
|
-
function buildWrap(id, kindClass, afterPos, createPlaceholder
|
|
642
|
+
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
658
643
|
const wrap = document.createElement('div');
|
|
659
644
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
660
645
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
661
646
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
662
647
|
wrap.setAttribute('data-created', String(now()));
|
|
663
|
-
if (anchorKey) wrap.setAttribute('data-ezoic-anchor', String(anchorKey));
|
|
664
648
|
// "Pinned" placements (after the first item) should remain stable.
|
|
665
649
|
if (afterPos === 1) {
|
|
666
650
|
wrap.setAttribute('data-ezoic-pin', '1');
|
|
@@ -677,16 +661,16 @@ function globalGapFixInit() {
|
|
|
677
661
|
return wrap;
|
|
678
662
|
}
|
|
679
663
|
|
|
680
|
-
function insertAfter(target, id, kindClass, afterPos
|
|
664
|
+
function insertAfter(target, id, kindClass, afterPos) {
|
|
681
665
|
if (!target || !target.insertAdjacentElement) return null;
|
|
682
|
-
if (findWrap(kindClass, afterPos)
|
|
666
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
683
667
|
if (insertingIds.has(id)) return null;
|
|
684
668
|
|
|
685
669
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
686
670
|
|
|
687
671
|
insertingIds.add(id);
|
|
688
672
|
try {
|
|
689
|
-
const wrap = buildWrap(id, kindClass, afterPos, !existingPh
|
|
673
|
+
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
690
674
|
target.insertAdjacentElement('afterend', wrap);
|
|
691
675
|
|
|
692
676
|
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
@@ -723,143 +707,13 @@ function globalGapFixInit() {
|
|
|
723
707
|
}
|
|
724
708
|
|
|
725
709
|
function pruneOrphanWraps(kindClass, items) {
|
|
726
|
-
|
|
727
|
-
// When that happens, previously-inserted ad wraps may become "orphan" nodes with no
|
|
728
|
-
// nearby post containers, which leads to ads clustering together when scrolling back up.
|
|
729
|
-
// We prune only *true* orphans that are far offscreen to keep the UI stable.
|
|
730
|
-
if (!items || !items.length) return 0;
|
|
731
|
-
const itemSet = new Set(items);
|
|
732
|
-
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
733
|
-
let removed = 0;
|
|
734
|
-
|
|
735
|
-
const isFilled = (wrap) => {
|
|
736
|
-
return !!(wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
737
|
-
};
|
|
738
|
-
|
|
739
|
-
const hasNearbyItem = (wrap) => {
|
|
740
|
-
// NodeBB/skins can inject separators/spacers; be tolerant.
|
|
741
|
-
let prev = wrap.previousElementSibling;
|
|
742
|
-
for (let i = 0; i < 14 && prev; i++) {
|
|
743
|
-
if (itemSet.has(prev)) return true;
|
|
744
|
-
prev = prev.previousElementSibling;
|
|
745
|
-
}
|
|
746
|
-
let next = wrap.nextElementSibling;
|
|
747
|
-
for (let i = 0; i < 14 && next; i++) {
|
|
748
|
-
if (itemSet.has(next)) return true;
|
|
749
|
-
next = next.nextElementSibling;
|
|
750
|
-
}
|
|
751
|
-
return false;
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
wraps.forEach((wrap) => {
|
|
755
|
-
// Never prune pinned placements.
|
|
756
|
-
try {
|
|
757
|
-
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
758
|
-
} catch (e) {}
|
|
759
|
-
|
|
760
|
-
// For message/topic pages we may prune filled or empty orphans if they are far away,
|
|
761
|
-
// otherwise consecutive "stacks" can appear when posts are virtualized.
|
|
762
|
-
const isMessage = (kindClass === 'ezoic-ad-message');
|
|
763
|
-
if (!isMessage && isFilled(wrap)) return; // never prune filled ads for non-message lists
|
|
764
|
-
|
|
765
|
-
// Never prune a fresh wrap: it may fill late.
|
|
766
|
-
try {
|
|
767
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
768
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
769
|
-
} catch (e) {}
|
|
770
|
-
|
|
771
|
-
if (hasNearbyItem(wrap)) {
|
|
772
|
-
try { wrap.classList && wrap.classList.remove('ez-orphan-hidden'); wrap.style && (wrap.style.display = ''); } catch (e) {}
|
|
710
|
+
// V4.1 safe mode: no DOM removals here to avoid breaking infinite-scroll internals.
|
|
773
711
|
return;
|
|
774
712
|
}
|
|
775
713
|
|
|
776
|
-
// If the anchor item is no longer in the DOM (virtualized), hide the wrap so ads never "stack"
|
|
777
|
-
// back-to-back while scrolling. We'll recycle it when its anchor comes back.
|
|
778
|
-
try { wrap.classList && wrap.classList.add('ez-orphan-hidden'); wrap.style && (wrap.style.display = 'none'); } catch (e) {}
|
|
779
|
-
|
|
780
|
-
// For message ads: only release if far offscreen to avoid perceived "vanishing" during fast scroll.
|
|
781
|
-
if (isMessage) {
|
|
782
|
-
try {
|
|
783
|
-
const r = wrap.getBoundingClientRect();
|
|
784
|
-
const vh = Math.max(1, window.innerHeight || 1);
|
|
785
|
-
const farAbove = r.bottom < -vh * 2;
|
|
786
|
-
const farBelow = r.top > vh * 4;
|
|
787
|
-
if (!farAbove && !farBelow) return;
|
|
788
|
-
} catch (e) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
794
|
-
removed++;
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
return removed;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
714
|
function decluster(kindClass) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (wraps.length < 2) return 0;
|
|
804
|
-
|
|
805
|
-
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
806
|
-
|
|
807
|
-
const isFilled = (wrap) => {
|
|
808
|
-
return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
const isFresh = (wrap) => {
|
|
812
|
-
try {
|
|
813
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
814
|
-
return created && (now() - created) < keepEmptyWrapMs();
|
|
815
|
-
} catch (e) {
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
let removed = 0;
|
|
821
|
-
for (const w of wraps) {
|
|
822
|
-
// Never decluster pinned placements.
|
|
823
|
-
try {
|
|
824
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
825
|
-
} catch (e) {}
|
|
826
|
-
|
|
827
|
-
let prev = w.previousElementSibling;
|
|
828
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
829
|
-
if (isWrap(prev)) {
|
|
830
|
-
// If the previous wrap is pinned, keep this one (spacing is intentional).
|
|
831
|
-
try {
|
|
832
|
-
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
833
|
-
} catch (e) {}
|
|
834
|
-
|
|
835
|
-
// Never remove a wrap that is already filled; otherwise it looks like
|
|
836
|
-
// ads "disappear" while scrolling. Only remove the empty neighbour.
|
|
837
|
-
const prevFilled = isFilled(prev);
|
|
838
|
-
const curFilled = isFilled(w);
|
|
839
|
-
|
|
840
|
-
if (curFilled) {
|
|
841
|
-
// If the previous one is empty (and not fresh), drop the previous instead.
|
|
842
|
-
if (!prevFilled && !isFresh(prev)) {
|
|
843
|
-
withInternalDomChange(() => releaseWrapNode(prev));
|
|
844
|
-
removed++;
|
|
845
|
-
}
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Current is empty.
|
|
850
|
-
// Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
|
|
851
|
-
// Only decluster when previous is filled, or when current is stale.
|
|
852
|
-
if (prevFilled || !isFresh(w)) {
|
|
853
|
-
withInternalDomChange(() => releaseWrapNode(w));
|
|
854
|
-
removed++;
|
|
855
|
-
}
|
|
856
|
-
break;
|
|
857
|
-
}
|
|
858
|
-
prev = prev.previousElementSibling;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return removed;
|
|
862
|
-
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
863
717
|
|
|
864
718
|
// ---------------- show (preload / fast fill) ----------------
|
|
865
719
|
|
|
@@ -1111,16 +965,6 @@ function buildOrdinalMap(items) {
|
|
|
1111
965
|
if (!el) continue;
|
|
1112
966
|
if (!el || !el.isConnected) continue;
|
|
1113
967
|
if (isAdjacentAd(el)) continue;
|
|
1114
|
-
const anchorKey = getStableAnchorKey(el);
|
|
1115
|
-
const existingByAnchor = findWrapByAnchor(kindClass, anchorKey);
|
|
1116
|
-
if (existingByAnchor && existingByAnchor.isConnected) {
|
|
1117
|
-
const next = el.nextElementSibling;
|
|
1118
|
-
if (next !== existingByAnchor) {
|
|
1119
|
-
try { el.insertAdjacentElement('afterend', existingByAnchor); } catch (e) {}
|
|
1120
|
-
}
|
|
1121
|
-
existingByAnchor.setAttribute('data-ezoic-after', String(afterPos));
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1124
968
|
if (findWrap(kindClass, afterPos)) continue;
|
|
1125
969
|
|
|
1126
970
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
@@ -1131,10 +975,9 @@ function buildOrdinalMap(items) {
|
|
|
1131
975
|
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1132
976
|
// appearing on very long infinite scroll sessions.
|
|
1133
977
|
if (!id) {
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
recycledWrap = (allowRecycle && scrollDir > 0) ? pickRecyclableWrap(kindClass) : null;
|
|
978
|
+
// Only recycle while scrolling down. Recycling while scrolling up tends to
|
|
979
|
+
// cause perceived instability (ads bunching/disappearing).
|
|
980
|
+
recycledWrap = null;
|
|
1138
981
|
if (recycledWrap) {
|
|
1139
982
|
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1140
983
|
}
|
|
@@ -1144,7 +987,7 @@ function buildOrdinalMap(items) {
|
|
|
1144
987
|
|
|
1145
988
|
const wrap = recycledWrap
|
|
1146
989
|
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1147
|
-
: insertAfter(el, id, kindClass, afterPos
|
|
990
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
1148
991
|
if (!wrap) continue;
|
|
1149
992
|
|
|
1150
993
|
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
@@ -1157,48 +1000,12 @@ function buildOrdinalMap(items) {
|
|
|
1157
1000
|
}
|
|
1158
1001
|
|
|
1159
1002
|
function pickRecyclableWrap(kindClass) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
// on long topics without redefining placeholders.
|
|
1163
|
-
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
1164
|
-
if (!wraps || !wraps.length) return null;
|
|
1165
|
-
|
|
1166
|
-
const vh = Math.max(300, window.innerHeight || 800);
|
|
1167
|
-
// Recycle only when the wrapper is far above the viewport.
|
|
1168
|
-
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1169
|
-
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1170
|
-
|
|
1171
|
-
let best = null;
|
|
1172
|
-
let bestBottom = Infinity;
|
|
1173
|
-
for (const w of wraps) {
|
|
1174
|
-
if (!w || !w.isConnected) continue;
|
|
1175
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
1176
|
-
const rect = w.getBoundingClientRect();
|
|
1177
|
-
if (rect.bottom < threshold) {
|
|
1178
|
-
if (rect.bottom < bestBottom) {
|
|
1179
|
-
bestBottom = rect.bottom;
|
|
1180
|
-
best = w;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return best;
|
|
1185
|
-
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1186
1005
|
|
|
1187
1006
|
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
1192
|
-
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
1193
|
-
|
|
1194
|
-
// Ensure minimal layout impact.
|
|
1195
|
-
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
1196
|
-
try { tightenStickyIn(wrap); } catch (e) {}
|
|
1197
|
-
return wrap;
|
|
1198
|
-
} catch (e) {
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1202
1009
|
|
|
1203
1010
|
async function runCore() {
|
|
1204
1011
|
if (isBlocked()) return 0;
|
|
@@ -1224,7 +1031,7 @@ function buildOrdinalMap(items) {
|
|
|
1224
1031
|
state.allPosts,
|
|
1225
1032
|
'curPosts'
|
|
1226
1033
|
);
|
|
1227
|
-
|
|
1034
|
+
decluster('ezoic-ad-message');
|
|
1228
1035
|
}
|
|
1229
1036
|
} else if (kind === 'categoryTopics') {
|
|
1230
1037
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
@@ -1238,7 +1045,7 @@ function buildOrdinalMap(items) {
|
|
|
1238
1045
|
state.allTopics,
|
|
1239
1046
|
'curTopics'
|
|
1240
1047
|
);
|
|
1241
|
-
|
|
1048
|
+
decluster('ezoic-ad-between');
|
|
1242
1049
|
}
|
|
1243
1050
|
} else if (kind === 'categories') {
|
|
1244
1051
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
@@ -1252,7 +1059,7 @@ function buildOrdinalMap(items) {
|
|
|
1252
1059
|
state.allCategories,
|
|
1253
1060
|
'curCategories'
|
|
1254
1061
|
);
|
|
1255
|
-
|
|
1062
|
+
decluster('ezoic-ad-categories');
|
|
1256
1063
|
}
|
|
1257
1064
|
}
|
|
1258
1065
|
|