nodebb-plugin-ezoic-infinite 1.5.86 → 1.5.87
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 +60 -215
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// ===== V4 stateless anti-remontée =====
|
|
5
|
+
function purgeAllEzoicWraps(root) {
|
|
6
|
+
var scope = root || document;
|
|
7
|
+
try {
|
|
8
|
+
scope.querySelectorAll('.nodebb-ezoic-wrap').forEach(function (n) {
|
|
9
|
+
try { n.remove(); } catch (e) {}
|
|
10
|
+
});
|
|
11
|
+
} catch (e) {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resetEzoicState() {
|
|
15
|
+
try { if (typeof shownAdsSet !== 'undefined' && shownAdsSet && shownAdsSet.clear) shownAdsSet.clear(); } catch (e) {}
|
|
16
|
+
try { if (typeof pendingShowSet !== 'undefined' && pendingShowSet && pendingShowSet.clear) pendingShowSet.clear(); } catch (e) {}
|
|
17
|
+
try { if (typeof showQueue !== 'undefined') showQueue = []; } catch (e) {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hardResetAndReinject() {
|
|
21
|
+
purgeAllEzoicWraps(document);
|
|
22
|
+
resetEzoicState();
|
|
23
|
+
try { if (typeof scheduleInject === 'function') scheduleInject(); } catch (e) {}
|
|
24
|
+
}
|
|
25
|
+
// ===== /V4 =====
|
|
26
|
+
|
|
27
|
+
|
|
4
28
|
// Track scroll direction to avoid aggressive recycling when the user scrolls upward.
|
|
5
29
|
// Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
|
|
6
30
|
let lastScrollY = 0;
|
|
@@ -486,24 +510,6 @@ function globalGapFixInit() {
|
|
|
486
510
|
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
487
511
|
}
|
|
488
512
|
|
|
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
513
|
// ---------------- placeholder pool ----------------
|
|
508
514
|
|
|
509
515
|
function getPoolEl() {
|
|
@@ -654,13 +660,12 @@ function globalGapFixInit() {
|
|
|
654
660
|
|
|
655
661
|
// ---------------- insertion primitives ----------------
|
|
656
662
|
|
|
657
|
-
function buildWrap(id, kindClass, afterPos, createPlaceholder
|
|
663
|
+
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
658
664
|
const wrap = document.createElement('div');
|
|
659
665
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
660
666
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
661
667
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
662
668
|
wrap.setAttribute('data-created', String(now()));
|
|
663
|
-
if (anchorKey) wrap.setAttribute('data-ezoic-anchor', String(anchorKey));
|
|
664
669
|
// "Pinned" placements (after the first item) should remain stable.
|
|
665
670
|
if (afterPos === 1) {
|
|
666
671
|
wrap.setAttribute('data-ezoic-pin', '1');
|
|
@@ -677,16 +682,16 @@ function globalGapFixInit() {
|
|
|
677
682
|
return wrap;
|
|
678
683
|
}
|
|
679
684
|
|
|
680
|
-
function insertAfter(target, id, kindClass, afterPos
|
|
685
|
+
function insertAfter(target, id, kindClass, afterPos) {
|
|
681
686
|
if (!target || !target.insertAdjacentElement) return null;
|
|
682
|
-
if (findWrap(kindClass, afterPos)
|
|
687
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
683
688
|
if (insertingIds.has(id)) return null;
|
|
684
689
|
|
|
685
690
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
686
691
|
|
|
687
692
|
insertingIds.add(id);
|
|
688
693
|
try {
|
|
689
|
-
const wrap = buildWrap(id, kindClass, afterPos, !existingPh
|
|
694
|
+
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
690
695
|
target.insertAdjacentElement('afterend', wrap);
|
|
691
696
|
|
|
692
697
|
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
@@ -723,143 +728,12 @@ function globalGapFixInit() {
|
|
|
723
728
|
}
|
|
724
729
|
|
|
725
730
|
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) {}
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
|
|
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
|
-
}
|
|
731
|
+
return;
|
|
791
732
|
}
|
|
792
733
|
|
|
793
|
-
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
794
|
-
removed++;
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
return removed;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
734
|
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
|
-
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
863
737
|
|
|
864
738
|
// ---------------- show (preload / fast fill) ----------------
|
|
865
739
|
|
|
@@ -1098,6 +972,8 @@ function buildOrdinalMap(items) {
|
|
|
1098
972
|
}
|
|
1099
973
|
|
|
1100
974
|
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
975
|
+
try { if (containerEl) purgeAllEzoicWraps(containerEl); } catch (e) {}
|
|
976
|
+
|
|
1101
977
|
if (!items.length) return 0;
|
|
1102
978
|
|
|
1103
979
|
const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
|
|
@@ -1111,16 +987,6 @@ function buildOrdinalMap(items) {
|
|
|
1111
987
|
if (!el) continue;
|
|
1112
988
|
if (!el || !el.isConnected) continue;
|
|
1113
989
|
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
990
|
if (findWrap(kindClass, afterPos)) continue;
|
|
1125
991
|
|
|
1126
992
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
@@ -1131,10 +997,9 @@ function buildOrdinalMap(items) {
|
|
|
1131
997
|
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1132
998
|
// appearing on very long infinite scroll sessions.
|
|
1133
999
|
if (!id) {
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
recycledWrap = (allowRecycle && scrollDir > 0) ? pickRecyclableWrap(kindClass) : null;
|
|
1000
|
+
// Only recycle while scrolling down. Recycling while scrolling up tends to
|
|
1001
|
+
// cause perceived instability (ads bunching/disappearing).
|
|
1002
|
+
recycledWrap = scrollDir > 0 ? pickRecyclableWrap(kindClass) : null;
|
|
1138
1003
|
if (recycledWrap) {
|
|
1139
1004
|
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1140
1005
|
}
|
|
@@ -1144,7 +1009,7 @@ function buildOrdinalMap(items) {
|
|
|
1144
1009
|
|
|
1145
1010
|
const wrap = recycledWrap
|
|
1146
1011
|
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1147
|
-
: insertAfter(el, id, kindClass, afterPos
|
|
1012
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
1148
1013
|
if (!wrap) continue;
|
|
1149
1014
|
|
|
1150
1015
|
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
@@ -1157,48 +1022,12 @@ function buildOrdinalMap(items) {
|
|
|
1157
1022
|
}
|
|
1158
1023
|
|
|
1159
1024
|
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
|
-
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1186
1027
|
|
|
1187
1028
|
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
|
-
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1202
1031
|
|
|
1203
1032
|
async function runCore() {
|
|
1204
1033
|
if (isBlocked()) return 0;
|
|
@@ -1224,7 +1053,7 @@ function buildOrdinalMap(items) {
|
|
|
1224
1053
|
state.allPosts,
|
|
1225
1054
|
'curPosts'
|
|
1226
1055
|
);
|
|
1227
|
-
|
|
1056
|
+
decluster('ezoic-ad-message');
|
|
1228
1057
|
}
|
|
1229
1058
|
} else if (kind === 'categoryTopics') {
|
|
1230
1059
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
@@ -1238,7 +1067,7 @@ function buildOrdinalMap(items) {
|
|
|
1238
1067
|
state.allTopics,
|
|
1239
1068
|
'curTopics'
|
|
1240
1069
|
);
|
|
1241
|
-
|
|
1070
|
+
decluster('ezoic-ad-between');
|
|
1242
1071
|
}
|
|
1243
1072
|
} else if (kind === 'categories') {
|
|
1244
1073
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
@@ -1252,7 +1081,7 @@ function buildOrdinalMap(items) {
|
|
|
1252
1081
|
state.allCategories,
|
|
1253
1082
|
'curCategories'
|
|
1254
1083
|
);
|
|
1255
|
-
|
|
1084
|
+
decluster('ezoic-ad-categories');
|
|
1256
1085
|
}
|
|
1257
1086
|
}
|
|
1258
1087
|
|
|
@@ -1537,3 +1366,19 @@ function buildOrdinalMap(items) {
|
|
|
1537
1366
|
insertHeroAdEarly().catch(() => {});
|
|
1538
1367
|
requestBurst();
|
|
1539
1368
|
})();
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
// V4 stateless hooks
|
|
1373
|
+
try {
|
|
1374
|
+
if (typeof window !== 'undefined' && window.jQuery) {
|
|
1375
|
+
window.jQuery(window).off('action:ajaxify.start.ezoicv4 action:infiniteScroll.start.ezoicv4 action:ajaxify.end.ezoicv4 action:infiniteScroll.loaded.ezoicv4');
|
|
1376
|
+
window.jQuery(window).on('action:ajaxify.start.ezoicv4 action:infiniteScroll.start.ezoicv4', function () {
|
|
1377
|
+
hardResetAndReinject();
|
|
1378
|
+
});
|
|
1379
|
+
window.jQuery(window).on('action:ajaxify.end.ezoicv4 action:infiniteScroll.loaded.ezoicv4', function () {
|
|
1380
|
+
hardResetAndReinject();
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
} catch (e) {}
|
|
1384
|
+
|