nodebb-plugin-ezoic-infinite 1.5.85 → 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 +55 -181
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;
|
|
@@ -704,143 +728,12 @@ function globalGapFixInit() {
|
|
|
704
728
|
}
|
|
705
729
|
|
|
706
730
|
function pruneOrphanWraps(kindClass, items) {
|
|
707
|
-
|
|
708
|
-
// When that happens, previously-inserted ad wraps may become "orphan" nodes with no
|
|
709
|
-
// nearby post containers, which leads to ads clustering together when scrolling back up.
|
|
710
|
-
// We prune only *true* orphans that are far offscreen to keep the UI stable.
|
|
711
|
-
if (!items || !items.length) return 0;
|
|
712
|
-
const itemSet = new Set(items);
|
|
713
|
-
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
714
|
-
let removed = 0;
|
|
715
|
-
|
|
716
|
-
const isFilled = (wrap) => {
|
|
717
|
-
return !!(wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
const hasNearbyItem = (wrap) => {
|
|
721
|
-
// NodeBB/skins can inject separators/spacers; be tolerant.
|
|
722
|
-
let prev = wrap.previousElementSibling;
|
|
723
|
-
for (let i = 0; i < 14 && prev; i++) {
|
|
724
|
-
if (itemSet.has(prev)) return true;
|
|
725
|
-
prev = prev.previousElementSibling;
|
|
726
|
-
}
|
|
727
|
-
let next = wrap.nextElementSibling;
|
|
728
|
-
for (let i = 0; i < 14 && next; i++) {
|
|
729
|
-
if (itemSet.has(next)) return true;
|
|
730
|
-
next = next.nextElementSibling;
|
|
731
|
-
}
|
|
732
|
-
return false;
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
wraps.forEach((wrap) => {
|
|
736
|
-
// Never prune pinned placements.
|
|
737
|
-
try {
|
|
738
|
-
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
739
|
-
} catch (e) {}
|
|
740
|
-
|
|
741
|
-
// For message/topic pages we may prune filled or empty orphans if they are far away,
|
|
742
|
-
// otherwise consecutive "stacks" can appear when posts are virtualized.
|
|
743
|
-
const isMessage = (kindClass === 'ezoic-ad-message');
|
|
744
|
-
if (!isMessage && isFilled(wrap)) return; // never prune filled ads for non-message lists
|
|
745
|
-
|
|
746
|
-
// Never prune a fresh wrap: it may fill late.
|
|
747
|
-
try {
|
|
748
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
749
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
750
|
-
} catch (e) {}
|
|
751
|
-
|
|
752
|
-
if (hasNearbyItem(wrap)) {
|
|
753
|
-
try { wrap.classList && wrap.classList.remove('ez-orphan-hidden'); wrap.style && (wrap.style.display = ''); } catch (e) {}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// If the anchor item is no longer in the DOM (virtualized), hide the wrap so ads never "stack"
|
|
758
|
-
// back-to-back while scrolling. We'll recycle it when its anchor comes back.
|
|
759
|
-
try { wrap.classList && wrap.classList.add('ez-orphan-hidden'); wrap.style && (wrap.style.display = 'none'); } catch (e) {}
|
|
760
|
-
|
|
761
|
-
// For message ads: only release if far offscreen to avoid perceived "vanishing" during fast scroll.
|
|
762
|
-
if (isMessage) {
|
|
763
|
-
try {
|
|
764
|
-
const r = wrap.getBoundingClientRect();
|
|
765
|
-
const vh = Math.max(1, window.innerHeight || 1);
|
|
766
|
-
const farAbove = r.bottom < -vh * 2;
|
|
767
|
-
const farBelow = r.top > vh * 4;
|
|
768
|
-
if (!farAbove && !farBelow) return;
|
|
769
|
-
} catch (e) {
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
731
|
+
return;
|
|
772
732
|
}
|
|
773
733
|
|
|
774
|
-
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
775
|
-
removed++;
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
return removed;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
734
|
function decluster(kindClass) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (wraps.length < 2) return 0;
|
|
785
|
-
|
|
786
|
-
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
787
|
-
|
|
788
|
-
const isFilled = (wrap) => {
|
|
789
|
-
return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
const isFresh = (wrap) => {
|
|
793
|
-
try {
|
|
794
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
795
|
-
return created && (now() - created) < keepEmptyWrapMs();
|
|
796
|
-
} catch (e) {
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
let removed = 0;
|
|
802
|
-
for (const w of wraps) {
|
|
803
|
-
// Never decluster pinned placements.
|
|
804
|
-
try {
|
|
805
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
806
|
-
} catch (e) {}
|
|
807
|
-
|
|
808
|
-
let prev = w.previousElementSibling;
|
|
809
|
-
for (let i = 0; i < 3 && prev; i++) {
|
|
810
|
-
if (isWrap(prev)) {
|
|
811
|
-
// If the previous wrap is pinned, keep this one (spacing is intentional).
|
|
812
|
-
try {
|
|
813
|
-
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
814
|
-
} catch (e) {}
|
|
815
|
-
|
|
816
|
-
// Never remove a wrap that is already filled; otherwise it looks like
|
|
817
|
-
// ads "disappear" while scrolling. Only remove the empty neighbour.
|
|
818
|
-
const prevFilled = isFilled(prev);
|
|
819
|
-
const curFilled = isFilled(w);
|
|
820
|
-
|
|
821
|
-
if (curFilled) {
|
|
822
|
-
// If the previous one is empty (and not fresh), drop the previous instead.
|
|
823
|
-
if (!prevFilled && !isFresh(prev)) {
|
|
824
|
-
withInternalDomChange(() => releaseWrapNode(prev));
|
|
825
|
-
removed++;
|
|
826
|
-
}
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Current is empty.
|
|
831
|
-
// Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
|
|
832
|
-
// Only decluster when previous is filled, or when current is stale.
|
|
833
|
-
if (prevFilled || !isFresh(w)) {
|
|
834
|
-
withInternalDomChange(() => releaseWrapNode(w));
|
|
835
|
-
removed++;
|
|
836
|
-
}
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
839
|
-
prev = prev.previousElementSibling;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return removed;
|
|
843
|
-
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
844
737
|
|
|
845
738
|
// ---------------- show (preload / fast fill) ----------------
|
|
846
739
|
|
|
@@ -1079,6 +972,8 @@ function buildOrdinalMap(items) {
|
|
|
1079
972
|
}
|
|
1080
973
|
|
|
1081
974
|
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
975
|
+
try { if (containerEl) purgeAllEzoicWraps(containerEl); } catch (e) {}
|
|
976
|
+
|
|
1082
977
|
if (!items.length) return 0;
|
|
1083
978
|
|
|
1084
979
|
const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
|
|
@@ -1102,10 +997,9 @@ function buildOrdinalMap(items) {
|
|
|
1102
997
|
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1103
998
|
// appearing on very long infinite scroll sessions.
|
|
1104
999
|
if (!id) {
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
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;
|
|
1109
1003
|
if (recycledWrap) {
|
|
1110
1004
|
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1111
1005
|
}
|
|
@@ -1128,48 +1022,12 @@ function buildOrdinalMap(items) {
|
|
|
1128
1022
|
}
|
|
1129
1023
|
|
|
1130
1024
|
function pickRecyclableWrap(kindClass) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
// on long topics without redefining placeholders.
|
|
1134
|
-
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
1135
|
-
if (!wraps || !wraps.length) return null;
|
|
1136
|
-
|
|
1137
|
-
const vh = Math.max(300, window.innerHeight || 800);
|
|
1138
|
-
// Recycle only when the wrapper is far above the viewport.
|
|
1139
|
-
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1140
|
-
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1141
|
-
|
|
1142
|
-
let best = null;
|
|
1143
|
-
let bestBottom = Infinity;
|
|
1144
|
-
for (const w of wraps) {
|
|
1145
|
-
if (!w || !w.isConnected) continue;
|
|
1146
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
1147
|
-
const rect = w.getBoundingClientRect();
|
|
1148
|
-
if (rect.bottom < threshold) {
|
|
1149
|
-
if (rect.bottom < bestBottom) {
|
|
1150
|
-
bestBottom = rect.bottom;
|
|
1151
|
-
best = w;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return best;
|
|
1156
|
-
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1157
1027
|
|
|
1158
1028
|
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
1163
|
-
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
1164
|
-
|
|
1165
|
-
// Ensure minimal layout impact.
|
|
1166
|
-
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
1167
|
-
try { tightenStickyIn(wrap); } catch (e) {}
|
|
1168
|
-
return wrap;
|
|
1169
|
-
} catch (e) {
|
|
1170
|
-
return null;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1173
1031
|
|
|
1174
1032
|
async function runCore() {
|
|
1175
1033
|
if (isBlocked()) return 0;
|
|
@@ -1195,7 +1053,7 @@ function buildOrdinalMap(items) {
|
|
|
1195
1053
|
state.allPosts,
|
|
1196
1054
|
'curPosts'
|
|
1197
1055
|
);
|
|
1198
|
-
|
|
1056
|
+
decluster('ezoic-ad-message');
|
|
1199
1057
|
}
|
|
1200
1058
|
} else if (kind === 'categoryTopics') {
|
|
1201
1059
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
@@ -1209,7 +1067,7 @@ function buildOrdinalMap(items) {
|
|
|
1209
1067
|
state.allTopics,
|
|
1210
1068
|
'curTopics'
|
|
1211
1069
|
);
|
|
1212
|
-
|
|
1070
|
+
decluster('ezoic-ad-between');
|
|
1213
1071
|
}
|
|
1214
1072
|
} else if (kind === 'categories') {
|
|
1215
1073
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
@@ -1223,7 +1081,7 @@ function buildOrdinalMap(items) {
|
|
|
1223
1081
|
state.allCategories,
|
|
1224
1082
|
'curCategories'
|
|
1225
1083
|
);
|
|
1226
|
-
|
|
1084
|
+
decluster('ezoic-ad-categories');
|
|
1227
1085
|
}
|
|
1228
1086
|
}
|
|
1229
1087
|
|
|
@@ -1508,3 +1366,19 @@ function buildOrdinalMap(items) {
|
|
|
1508
1366
|
insertHeroAdEarly().catch(() => {});
|
|
1509
1367
|
requestBurst();
|
|
1510
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
|
+
|