nodebb-plugin-ezoic-infinite 1.6.68 → 1.6.70
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 +156 -3
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -119,6 +119,10 @@
|
|
|
119
119
|
burstDeadline:0,
|
|
120
120
|
burstCount: 0,
|
|
121
121
|
lastBurstTs: 0,
|
|
122
|
+
|
|
123
|
+
// Detached wraps cache (to survive NodeBB virtual/infinite scroll recycling)
|
|
124
|
+
// kindClass -> Map(afterPos -> wrapNode)
|
|
125
|
+
detached: new Map(),
|
|
122
126
|
};
|
|
123
127
|
|
|
124
128
|
let blockedUntil = 0;
|
|
@@ -228,7 +232,22 @@
|
|
|
228
232
|
try {
|
|
229
233
|
if (!wrap) return;
|
|
230
234
|
const iframes = wrap.querySelectorAll('iframe');
|
|
231
|
-
|
|
235
|
+
|
|
236
|
+
// If no iframe yet (or no-fill), Ezoic can still reserve a large gap via
|
|
237
|
+
// inline `min-height:400px !important` on `.ezoic-ad` elements.
|
|
238
|
+
// Inline !important beats our CSS, so we proactively shrink it.
|
|
239
|
+
if (!iframes.length) {
|
|
240
|
+
const bad = wrap.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"], span.ezoic-ad[style*="min-height"]');
|
|
241
|
+
bad.forEach(n => {
|
|
242
|
+
const s = (n.getAttribute('style') || '').toLowerCase();
|
|
243
|
+
if (s.includes('min-height:400') || s.includes('min-height: 400')) {
|
|
244
|
+
try { n.style.setProperty('min-height', '1px', 'important'); } catch (e) { n.style.minHeight = '1px'; }
|
|
245
|
+
try { n.style.setProperty('height', 'auto', 'important'); } catch (e) {}
|
|
246
|
+
try { n.style.setProperty('line-height', '0', 'important'); } catch (e) {}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
232
251
|
|
|
233
252
|
// Find the nearest ezoic-ad ancestor with the 400px inline min-height.
|
|
234
253
|
let ref = null;
|
|
@@ -394,6 +413,54 @@
|
|
|
394
413
|
try { wrap.remove(); } catch (e) {}
|
|
395
414
|
}
|
|
396
415
|
|
|
416
|
+
function getDetachedMap(kindClass) {
|
|
417
|
+
let m = st.detached.get(kindClass);
|
|
418
|
+
if (!m) { m = new Map(); st.detached.set(kindClass, m); }
|
|
419
|
+
return m;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function detachWrap(kindClass, wrap) {
|
|
423
|
+
// Keep a wrap node in memory so we can re-attach it later without
|
|
424
|
+
// re-requesting the same placement (avoids GPT "format already created" and
|
|
425
|
+
// prevents ads from vanishing permanently when NodeBB recycles DOM nodes).
|
|
426
|
+
try {
|
|
427
|
+
const afterPos = parseInt(wrap.getAttribute('data-ezoic-after') || '0', 10);
|
|
428
|
+
if (!afterPos) return dropWrap(wrap);
|
|
429
|
+
|
|
430
|
+
// Unobserve placeholder while detached
|
|
431
|
+
try {
|
|
432
|
+
const ph = wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
433
|
+
if (ph) try { st.io && st.io.unobserve(ph); } catch (e) {}
|
|
434
|
+
} catch (e) {}
|
|
435
|
+
|
|
436
|
+
const m = getDetachedMap(kindClass);
|
|
437
|
+
// If a cached wrap for that position already exists, prefer the newest
|
|
438
|
+
// and fully drop the old one.
|
|
439
|
+
if (m.has(afterPos)) {
|
|
440
|
+
try { dropWrap(m.get(afterPos)); } catch (e) {}
|
|
441
|
+
}
|
|
442
|
+
m.set(afterPos, wrap);
|
|
443
|
+
|
|
444
|
+
// Hard cap cache size per kind to avoid unbounded memory growth.
|
|
445
|
+
if (m.size > 80) {
|
|
446
|
+
const oldest = [...m.entries()].sort((a, b) => {
|
|
447
|
+
const ta = parseInt((a[1] && a[1].getAttribute('data-created')) || '0', 10);
|
|
448
|
+
const tb = parseInt((b[1] && b[1].getAttribute('data-created')) || '0', 10);
|
|
449
|
+
return ta - tb;
|
|
450
|
+
}).slice(0, m.size - 80);
|
|
451
|
+
oldest.forEach(([pos, node]) => {
|
|
452
|
+
m.delete(pos);
|
|
453
|
+
try { dropWrap(node); } catch (e) {}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Actually detach from DOM
|
|
458
|
+
try { wrap.remove(); } catch (e) {}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
try { dropWrap(wrap); } catch (x) {}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
397
464
|
function pickId(poolKey, cursorKey) {
|
|
398
465
|
const pool = st.pools[poolKey];
|
|
399
466
|
if (!pool.length) return null;
|
|
@@ -477,11 +544,44 @@
|
|
|
477
544
|
const w = ph.closest('.' + WRAP_CLASS);
|
|
478
545
|
if (!w) return;
|
|
479
546
|
if (isFilled(ph)) { w.classList.remove('is-empty'); tightenMinHeight(w); }
|
|
480
|
-
else {
|
|
547
|
+
else {
|
|
548
|
+
w.classList.add('is-empty');
|
|
549
|
+
// Kill any large reserved gap even when there is no iframe.
|
|
550
|
+
tightenMinHeight(w);
|
|
551
|
+
watchFill(w);
|
|
552
|
+
scheduleHardPruneEmpty(id);
|
|
553
|
+
}
|
|
481
554
|
} catch (e) {}
|
|
482
555
|
}, 15_000);
|
|
483
556
|
}
|
|
484
557
|
|
|
558
|
+
// If a placement stays empty for a long time, remove it entirely so the user
|
|
559
|
+
// doesn't get persistent blank blocks. This also frees the placeholder ID so
|
|
560
|
+
// a future injection can try a different one.
|
|
561
|
+
function scheduleHardPruneEmpty(id) {
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
try {
|
|
564
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
565
|
+
if (!ph || !ph.isConnected) return;
|
|
566
|
+
const w = ph.closest('.' + WRAP_CLASS);
|
|
567
|
+
if (!w) return;
|
|
568
|
+
// If still not filled after 45s, drop it.
|
|
569
|
+
if (!isFilled(w)) {
|
|
570
|
+
// Also ensure it isn't cached as detached.
|
|
571
|
+
try {
|
|
572
|
+
const kind = Array.from(w.classList).find(c => c.startsWith('ezoic-ad-'));
|
|
573
|
+
if (kind) {
|
|
574
|
+
const afterPos = parseInt(w.getAttribute('data-ezoic-after') || '0', 10);
|
|
575
|
+
const m = st.detached.get(kind);
|
|
576
|
+
if (m && afterPos && m.get(afterPos) === w) m.delete(afterPos);
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {}
|
|
579
|
+
withInternal(() => dropWrap(w));
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {}
|
|
582
|
+
}, 45_000);
|
|
583
|
+
}
|
|
584
|
+
|
|
485
585
|
function startShow(id) {
|
|
486
586
|
if (!id || isBlocked()) return;
|
|
487
587
|
st.inflight++;
|
|
@@ -573,16 +673,38 @@
|
|
|
573
673
|
if (itemSet.has(el)) { found = true; break; }
|
|
574
674
|
}
|
|
575
675
|
|
|
576
|
-
if (!found)
|
|
676
|
+
if (!found) {
|
|
677
|
+
// Detach instead of destroy so ads can survive DOM recycling.
|
|
678
|
+
withInternal(() => detachWrap(kindClass, wrap));
|
|
679
|
+
}
|
|
577
680
|
});
|
|
578
681
|
}
|
|
579
682
|
|
|
683
|
+
function reapDetached(kindClass) {
|
|
684
|
+
// Soft-prune very old detached nodes.
|
|
685
|
+
try {
|
|
686
|
+
const m = st.detached.get(kindClass);
|
|
687
|
+
if (!m || !m.size) return;
|
|
688
|
+
for (const [pos, wrap] of m.entries()) {
|
|
689
|
+
if (!wrap) { m.delete(pos); continue; }
|
|
690
|
+
if (wrap.isConnected) { m.delete(pos); continue; }
|
|
691
|
+
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
692
|
+
if (created && (now() - created) > 15 * 60 * 1000) {
|
|
693
|
+
m.delete(pos);
|
|
694
|
+
try { dropWrap(wrap); } catch (e) {}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch (e) {}
|
|
698
|
+
}
|
|
699
|
+
|
|
580
700
|
function injectWraps(kindClass, items, interval, showFirst, poolKey, cursorKey) {
|
|
581
701
|
if (!items.length) return 0;
|
|
582
702
|
const { map, targets } = computeTargets(items, interval, showFirst);
|
|
583
703
|
const max = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
584
704
|
let n = 0;
|
|
585
705
|
|
|
706
|
+
const detachedMap = st.detached.get(kindClass);
|
|
707
|
+
|
|
586
708
|
for (const pos of targets) {
|
|
587
709
|
if (n >= max) break;
|
|
588
710
|
const el = map.get(pos);
|
|
@@ -597,6 +719,27 @@
|
|
|
597
719
|
if (isAdjacentWrap(el)) continue;
|
|
598
720
|
if (findWrapAt(kindClass, pos)) continue;
|
|
599
721
|
|
|
722
|
+
// If we previously had a wrap at this exact position but NodeBB recycled
|
|
723
|
+
// the DOM, re-attach the cached wrap instead of creating a new one.
|
|
724
|
+
if (detachedMap && detachedMap.has(pos)) {
|
|
725
|
+
const cached = detachedMap.get(pos);
|
|
726
|
+
detachedMap.delete(pos);
|
|
727
|
+
if (cached) {
|
|
728
|
+
withInternal(() => el.insertAdjacentElement('afterend', cached));
|
|
729
|
+
// Re-observe placeholder and trigger show only if the cached wrap is empty.
|
|
730
|
+
try {
|
|
731
|
+
const ph = cached.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
732
|
+
const id = ph ? parseInt(ph.getAttribute('data-ezoic-id') || (ph.id || '').split('-').pop(), 10) : 0;
|
|
733
|
+
if (id > 0) {
|
|
734
|
+
observePh(id);
|
|
735
|
+
if (!isFilled(cached)) enqueueShow(id);
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {}
|
|
738
|
+
n++;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
600
743
|
const id = pickId(poolKey, cursorKey);
|
|
601
744
|
if (!id) break;
|
|
602
745
|
|
|
@@ -684,6 +827,7 @@
|
|
|
684
827
|
const items = getItems('topic');
|
|
685
828
|
const set = new Set(items);
|
|
686
829
|
removeOrphanWraps('ezoic-ad-message', set);
|
|
830
|
+
reapDetached('ezoic-ad-message');
|
|
687
831
|
inserted += injectWraps('ezoic-ad-message', items,
|
|
688
832
|
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
689
833
|
normBool(cfg.showFirstMessageAd), 'post', 'post');
|
|
@@ -692,6 +836,7 @@
|
|
|
692
836
|
const items = getItems('categoryTopics');
|
|
693
837
|
const set = new Set(items);
|
|
694
838
|
removeOrphanWraps('ezoic-ad-between', set);
|
|
839
|
+
reapDetached('ezoic-ad-between');
|
|
695
840
|
inserted += injectWraps('ezoic-ad-between', items,
|
|
696
841
|
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
697
842
|
normBool(cfg.showFirstTopicAd), 'topic', 'topic');
|
|
@@ -701,6 +846,7 @@
|
|
|
701
846
|
const items = getItems('categories');
|
|
702
847
|
const set = new Set(items);
|
|
703
848
|
removeOrphanWraps('ezoic-ad-categories', set);
|
|
849
|
+
reapDetached('ezoic-ad-categories');
|
|
704
850
|
inserted += injectWraps('ezoic-ad-categories', items,
|
|
705
851
|
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
706
852
|
normBool(cfg.showFirstCategoryAd), 'category', 'category');
|
|
@@ -792,6 +938,13 @@
|
|
|
792
938
|
function cleanup() {
|
|
793
939
|
blockedUntil = now() + 1200;
|
|
794
940
|
try { document.querySelectorAll('.' + WRAP_CLASS).forEach(dropWrap); } catch (e) {}
|
|
941
|
+
try {
|
|
942
|
+
for (const m of st.detached.values()) {
|
|
943
|
+
for (const w of m.values()) try { dropWrap(w); } catch (e) {}
|
|
944
|
+
m.clear();
|
|
945
|
+
}
|
|
946
|
+
st.detached.clear();
|
|
947
|
+
} catch (e) {}
|
|
795
948
|
st.cfg = null;
|
|
796
949
|
st.pools = { topic: [], post: [], category: [] };
|
|
797
950
|
st.cursors = { topic: 0, post: 0, category: 0 };
|