nodebb-plugin-ezoic-infinite 1.5.61 → 1.5.62

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.61",
3
+ "version": "1.5.62",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -4,136 +4,12 @@
4
4
  // NodeBB client context
5
5
  const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
6
6
 
7
- const WRAP_CLASS = 'ezoic-ad';
7
+ // IMPORTANT: must NOT collide with Ezoic's own markup (they use `.ezoic-ad`).
8
+ // If we reuse that class, cleanup/pruning can delete real ads and cause
9
+ // "placeholder does not exist" spam + broken 1/X insertion.
10
+ const WRAP_CLASS = 'nodebb-ezoic-wrap';
8
11
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
9
12
 
10
-
11
- // Offscreen pool to keep placeholder elements alive across ajaxify/navigation.
12
- // This prevents Ezoic from trying to define ids that are not currently injected,
13
- // and eliminates "HTML element with id ... does not exist" noise.
14
- const POOL_ID = 'ezoic-placeholder-pool';
15
-
16
- function isInPool(el) {
17
- try { return !!(el && el.closest && el.closest('#' + POOL_ID)); } catch (e) { return false; }
18
- }
19
-
20
- function isPlaceholderInUse(ph) {
21
- // In use = connected AND not parked in our offscreen pool.
22
- try { return !!(ph && ph.isConnected && !isInPool(ph)); } catch (e) { return false; }
23
- }
24
-
25
- function ensurePool() {
26
- let pool = document.getElementById(POOL_ID);
27
- if (pool) {
28
- // In rare cases (aggressive SPA navigation), the pool may get detached.
29
- if (!pool.isConnected) {
30
- try { document.body.appendChild(pool); } catch (e) {}
31
- }
32
- return pool;
33
- }
34
- pool = document.createElement('div');
35
- pool.id = POOL_ID;
36
- pool.style.position = 'absolute';
37
- pool.style.left = '-99999px';
38
- pool.style.top = '0';
39
- pool.style.width = '1px';
40
- pool.style.height = '1px';
41
- pool.style.overflow = 'hidden';
42
- pool.setAttribute('aria-hidden', 'true');
43
- // Attach early (documentElement exists before body), so placeholders are always connected.
44
- try { document.body.appendChild(pool); } catch (e) {}
45
- // If body exists later, move it under body (purely cosmetic).
46
- try {
47
- if (document.body && pool.parentNode !== document.body) {
48
- document.body.appendChild(pool);
49
- }
50
- } catch (e) {}
51
- return pool;
52
- }
53
-
54
- // Create placeholder divs for all configured ids upfront.
55
- // Ezoic sometimes attempts to initialize/refresh a range of ids even if we
56
- // haven't injected them yet; keeping them in the offscreen pool prevents
57
- // "HTML element ... does not exist" spam.
58
- function primePool(ids) {
59
- try {
60
- if (!ids || !ids.length) return;
61
- const pool = ensurePool();
62
- for (const v of ids) {
63
- const id = parseInt(v, 10);
64
- if (!Number.isFinite(id) || id <= 0) continue;
65
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
66
- let ph = document.getElementById(domId);
67
- if (!ph) {
68
- ph = document.createElement('div');
69
- ph.id = domId;
70
- ph.setAttribute('data-ezoic-id', String(id));
71
- pool.appendChild(ph);
72
- } else if (!ph.isConnected) {
73
- pool.appendChild(ph);
74
- }
75
- }
76
- } catch (e) {}
77
- }
78
-
79
-
80
- // Prime a continuous id range (inclusive) into the offscreen pool.
81
- // Ezoic sometimes references ids outside our configured list (e.g. +N); a small buffer prevents "does not exist" spam.
82
- function primePoolRange(minId, maxId) {
83
- try {
84
- minId = parseInt(minId, 10);
85
- maxId = parseInt(maxId, 10);
86
- if (!Number.isFinite(minId) || !Number.isFinite(maxId)) return;
87
- if (minId <= 0 || maxId <= 0) return;
88
- if (maxId < minId) { const t = minId; minId = maxId; maxId = t; }
89
- const span = Math.min(400, Math.max(0, maxId - minId)); // cap
90
- const pool = ensurePool();
91
- for (let id = minId; id <= minId + span; id += 1) {
92
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
93
- let ph = document.getElementById(domId);
94
- if (!ph) {
95
- ph = document.createElement('div');
96
- ph.id = domId;
97
- ph.setAttribute('data-ezoic-id', String(id));
98
- pool.appendChild(ph);
99
- } else if (!ph.isConnected) {
100
- pool.appendChild(ph);
101
- }
102
- }
103
- } catch (e) {}
104
- }
105
-
106
-
107
- function acquirePlaceholder(id) {
108
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
109
- let ph = document.getElementById(domId);
110
- if (!ph) {
111
- ph = document.createElement('div');
112
- ph.id = domId;
113
- ph.setAttribute('data-ezoic-id', String(id));
114
- ensurePool().appendChild(ph);
115
- }
116
- // Note: appendChild will automatically move the node, no manual detach (avoids race gaps).
117
- // Clear request/defined flags when reusing
118
- try {
119
- if (ph.dataset) {
120
- ph.dataset.ezRequested = '0';
121
- ph.dataset.ezDefined = '0';
122
- ph.dataset.ezActive = '0';
123
- }
124
- } catch (e) {}
125
- return ph;
126
- }
127
-
128
- function parkPlaceholderFromWrap(wrap) {
129
- try {
130
- const ph = wrap && wrap.querySelector ? wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`) : null;
131
- if (!ph) return;
132
- try { if (state && state.io) state.io.unobserve(ph); } catch (e) {}
133
- try { if (ph.dataset) ph.dataset.ezActive = '0'; } catch (e) {}
134
- ensurePool().appendChild(ph);
135
- } catch (e) {}
136
- }
137
13
  // Insert at most N ads per run to keep the UI smooth on infinite scroll
138
14
  const MAX_INSERTS_PER_RUN = 3;
139
15
 
@@ -346,107 +222,34 @@ function parkPlaceholderFromWrap(wrap) {
346
222
  } catch (e) {}
347
223
  }
348
224
 
349
-
350
225
  // Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
351
- // Ezoic can (re)define ezstandalone.showAds over time; we hook assignments to keep the guard.
352
226
  function patchShowAds() {
353
- function wrapShowAds(ez, orig) {
354
- if (orig && orig.__nodebbWrapped) return orig;
355
-
356
- const wrapped = function (...args) {
357
- if (isBlocked()) return;
358
-
359
- let ids = [];
360
- if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
361
- else ids = args;
362
-
363
- const seen = new Set();
364
- for (const v of ids) {
365
- const id = parseInt(v, 10);
366
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
367
-
368
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
369
- let ph = document.getElementById(domId);
370
- if (!ph || !ph.isConnected) {
371
- // If Ezoic (or another script) tries to show an id we haven't injected yet,
372
- // create the placeholder in the offscreen pool so it exists, but don't load.
373
- try {
374
- ph = document.createElement('div');
375
- ph.id = domId;
376
- ph.setAttribute('data-ezoic-id', String(id));
377
- if (ph.dataset) ph.dataset.ezActive = '0';
378
- ensurePool().appendChild(ph);
379
- } catch (e) {}
380
- continue;
381
- }
382
-
383
- // Only allow loads for placeholders actively injected into the page (not parked in the pool)
384
- // and currently marked active.
385
- if (!isPlaceholderInUse(ph)) continue;
386
- if (ph.dataset && ph.dataset.ezActive !== '1') continue;
387
-
388
- // Prevent repeated "define" attempts on the same placeholder while it remains in DOM.
389
- if (ph.dataset && (ph.dataset.ezRequested === '1' || ph.dataset.ezDefined === '1')) continue;
390
-
391
- seen.add(id);
392
- try {
393
- if (ph.dataset) ph.dataset.ezRequested = '1';
394
- orig.call(ez, id);
395
- if (ph.dataset) {
396
- ph.dataset.ezDefined = '1';
397
- ph.dataset.ezRequested = '0';
398
- }
399
- } catch (e) {
400
- try { if (ph.dataset) ph.dataset.ezRequested = '0'; } catch (_) {}
401
- }
402
- }
403
- };
404
- try { wrapped.__nodebbWrapped = true; } catch (e) {}
405
- return wrapped;
406
- }
407
-
408
- function ensureHook() {
227
+ const applyPatch = () => {
409
228
  try {
410
229
  window.ezstandalone = window.ezstandalone || {};
411
230
  const ez = window.ezstandalone;
231
+ if (window.__nodebbEzoicPatched) return;
232
+ if (typeof ez.showAds !== 'function') return;
412
233
 
413
- // Hook future assignments to showAds so we keep our wrapper even if Ezoic overwrites it.
414
- if (!ez.__nodebbShowAdsHooked) {
415
- ez.__nodebbShowAdsHooked = true;
416
- let _showAds = ez.showAds;
417
-
418
- Object.defineProperty(ez, 'showAds', {
419
- configurable: true,
420
- enumerable: true,
421
- get() {
422
- return _showAds;
423
- },
424
- set(fn) {
425
- _showAds = (typeof fn === 'function') ? wrapShowAds(ez, fn) : fn;
426
- },
427
- });
428
-
429
- // If showAds already exists, wrap it immediately
430
- if (typeof _showAds === 'function') {
431
- _showAds = wrapShowAds(ez, _showAds);
432
- }
433
- } else if (typeof ez.showAds === 'function' && !ez.showAds.__nodebbWrapped) {
434
- // In case defineProperty wasn't possible (rare), re-wrap
435
- ez.showAds = wrapShowAds(ez, ez.showAds);
436
- }
437
- } catch (e) {}
438
- }
439
-
440
- ensureHook();
234
+ window.__nodebbEzoicPatched = true;
235
+ const orig = ez.showAds;
441
236
 
442
- // Also attempt via cmd queue
443
- try {
444
- window.ezstandalone = window.ezstandalone || {};
445
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
446
- window.ezstandalone.cmd.push(ensureHook);
447
- } catch (e) {}
448
- }
237
+ ez.showAds = function (...args) {
238
+ if (isBlocked()) return;
449
239
 
240
+ let ids = [];
241
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
242
+ else ids = args;
243
+
244
+ const seen = new Set();
245
+ for (const v of ids) {
246
+ const id = parseInt(v, 10);
247
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
248
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
249
+ if (!ph || !ph.isConnected) continue;
250
+ seen.add(id);
251
+ try { orig.call(ez, id); } catch (e) {}
252
+ }
450
253
  };
451
254
  } catch (e) {}
452
255
  };
@@ -497,65 +300,22 @@ function withInternalDomChange(fn) {
497
300
  }
498
301
 
499
302
  function initPools(cfg) {
500
- if (!cfg) return;
501
- if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
502
- if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
503
- if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
504
-
505
- // Keep placeholders alive even before they're injected.
506
- // Ezoic can reference ids slightly outside our configured lists (often sequential),
507
- // so we prime a small buffered range to prevent "does not exist" spam.
508
- const primeBuffered = (ids) => {
509
- try {
510
- if (!ids || !ids.length) return;
511
- primePool(ids);
512
-
513
- let min = Infinity, max = -Infinity;
514
- for (const v of ids) {
515
- const n = parseInt(v, 10);
516
- if (!Number.isFinite(n) || n <= 0) continue;
517
- if (n < min) min = n;
518
- if (n > max) max = n;
519
- }
520
- if (!Number.isFinite(min) || !Number.isFinite(max)) return;
521
-
522
- // small buffer in both directions; range prime is capped internally
523
- primePoolRange(Math.max(1, min - 5), max + 120);
524
- } catch (e) {}
525
- };
526
-
527
- primeBuffered(state.allTopics);
528
- primeBuffered(state.allPosts);
529
- primeBuffered(state.allCategories);
530
- }
531
-
303
+ if (!cfg) return;
304
+ if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
305
+ if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
306
+ if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
307
+ }
532
308
 
533
309
  // ---------- insertion primitives ----------
534
310
 
535
311
  function isAdjacentAd(target) {
536
- if (!target) return false;
537
- const isAdEl = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
538
- // Look a few siblings away, skipping spacer elements inserted by NodeBB/themes
539
- let el = target;
540
- for (let i = 0; i < 3; i += 1) {
541
- el = el.nextElementSibling;
542
- if (!el) break;
543
- if (isAdEl(el)) return true;
544
- // Skip elements that are effectively spacers/dividers
545
- const h = el.getBoundingClientRect ? el.getBoundingClientRect().height : 0;
546
- if (h > 8) break;
547
- }
548
- el = target;
549
- for (let i = 0; i < 3; i += 1) {
550
- el = el.previousElementSibling;
551
- if (!el) break;
552
- if (isAdEl(el)) return true;
553
- const h = el.getBoundingClientRect ? el.getBoundingClientRect().height : 0;
554
- if (h > 8) break;
312
+ if (!target) return false;
313
+ const next = target.nextElementSibling;
314
+ if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
315
+ const prev = target.previousElementSibling;
316
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
317
+ return false;
555
318
  }
556
- return false;
557
- }
558
-
559
319
 
560
320
 
561
321
  function getWrapIdFromWrap(wrap) {
@@ -570,20 +330,9 @@ function withInternalDomChange(fn) {
570
330
 
571
331
  function safeDestroyById(id) {
572
332
  try {
573
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
574
- const ph = document.getElementById(domId);
575
-
576
- // If the element is already gone, do NOT call destroyPlaceholders (Ezoic will log "does not exist").
577
- if (!ph || !ph.isConnected) return;
578
-
579
- if (ph.dataset) {
580
- delete ph.dataset.ezDefined;
581
- delete ph.dataset.ezRequested;
582
- }
583
-
584
333
  const ez = window.ezstandalone;
585
334
  if (ez && typeof ez.destroyPlaceholders === 'function') {
586
- ez.destroyPlaceholders([domId]);
335
+ ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
587
336
  }
588
337
  } catch (e) {}
589
338
  }
@@ -608,7 +357,6 @@ function withInternalDomChange(fn) {
608
357
  withInternalDomChange(() => {
609
358
  try {
610
359
  if (id) safeDestroyById(id);
611
- parkPlaceholderFromWrap(wrap);
612
360
  wrap.remove();
613
361
  } catch (e) {}
614
362
  });
@@ -635,54 +383,42 @@ function withInternalDomChange(fn) {
635
383
  }, 3500);
636
384
  }
637
385
 
638
- function buildWrap(id, kindClass, afterPos, afterPid) {
639
- const wrap = document.createElement('div');
640
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
641
- wrap.setAttribute('data-ezoic-after', String(afterPos));
642
- if (afterPid) wrap.setAttribute('data-ezoic-after-pid', String(afterPid));
643
- wrap.setAttribute('data-ezoic-wrapid', String(id));
644
- wrap.style.width = '100%';
386
+ function buildWrap(id, kindClass, afterPos) {
387
+ const wrap = document.createElement('div');
388
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
389
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
390
+ wrap.setAttribute('data-ezoic-wrapid', String(id));
391
+ wrap.style.width = '100%';
645
392
 
646
- const ph = acquirePlaceholder(id);
647
- try { if (ph.dataset) ph.dataset.ezActive = '1'; } catch (e) {}
648
- wrap.appendChild(ph);
393
+ const ph = document.createElement('div');
394
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
395
+ ph.setAttribute('data-ezoic-id', String(id));
396
+ wrap.appendChild(ph);
649
397
 
650
- return wrap;
651
- }
398
+ return wrap;
399
+ }
652
400
 
401
+ function findWrap(kindClass, afterPos) {
402
+ return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
403
+ }
653
404
 
654
- function findWrap(kindClass, afterPos, afterPid) {
655
- try {
656
- if (afterPid) {
657
- const w = document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after-pid="${afterPid}"]`);
658
- if (w) return w;
659
- }
660
- } catch (e) {}
661
- return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
662
- }
405
+ function insertAfter(target, id, kindClass, afterPos) {
406
+ if (!target || !target.insertAdjacentElement) return null;
407
+ if (findWrap(kindClass, afterPos)) return null;
408
+ if (insertingIds.has(id)) return null;
663
409
 
410
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
411
+ if (existingPh && existingPh.isConnected) return null;
664
412
 
665
- function insertAfter(target, id, kindClass, afterPos) {
666
- if (!target || !target.insertAdjacentElement) return null;
667
- const afterPid = target.getAttribute ? target.getAttribute('data-pid') : null;
668
- if (findWrap(kindClass, afterPos, afterPid)) return null;
669
- if (insertingIds.has(id)) return null;
670
-
671
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
672
- if (existingPh && isPlaceholderInUse(existingPh)) return null;
673
-
674
- insertingIds.add(id);
675
- try {
676
- const wrap = buildWrap(id, kindClass, afterPos, afterPid);
677
- target.insertAdjacentElement('afterend', wrap);
678
- // Mark post as processed anchor to stabilize 1/X across infinite scroll reflows
679
- try { if (afterPid) target.setAttribute('data-ezoic-anchored', '1'); } catch (e) {}
680
- return wrap;
681
- } finally {
682
- insertingIds.delete(id);
413
+ insertingIds.add(id);
414
+ try {
415
+ const wrap = buildWrap(id, kindClass, afterPos);
416
+ target.insertAdjacentElement('afterend', wrap);
417
+ return wrap;
418
+ } finally {
419
+ insertingIds.delete(id);
420
+ }
683
421
  }
684
- }
685
-
686
422
 
687
423
  function pickIdFromAll(allIds, cursorKey) {
688
424
  const n = allIds.length;
@@ -695,7 +431,7 @@ function buildWrap(id, kindClass, afterPos, afterPid) {
695
431
 
696
432
  const id = allIds[idx];
697
433
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
698
- if (ph && isPlaceholderInUse(ph)) continue;
434
+ if (ph && ph.isConnected) continue;
699
435
 
700
436
  return id;
701
437
  }
@@ -723,7 +459,6 @@ function buildWrap(id, kindClass, afterPos, afterPid) {
723
459
  if (ph && state.io) state.io.unobserve(ph);
724
460
  } catch (e) {}
725
461
 
726
- parkPlaceholderFromWrap(victim);
727
462
  victim.remove();
728
463
  return true;
729
464
  } catch (e) {
@@ -760,11 +495,9 @@ function drainQueue() {
760
495
  }
761
496
  }
762
497
 
763
-
764
498
  function startShow(id) {
765
- if (!id) return;
499
+ if (!id || isBlocked()) return;
766
500
 
767
- // Reserve an inflight slot, but ALWAYS release on any early exit.
768
501
  state.inflight++;
769
502
  let released = false;
770
503
  const release = () => {
@@ -778,41 +511,30 @@ function startShow(id) {
778
511
 
779
512
  requestAnimationFrame(() => {
780
513
  try {
781
- if (isBlocked()) { clearTimeout(hardTimer); release(); return; }
514
+ if (isBlocked()) return;
782
515
 
783
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
784
- const phNow = document.getElementById(domId);
785
- if (!phNow || !phNow.isConnected) { clearTimeout(hardTimer); release(); return; }
516
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
517
+ if (!ph || !ph.isConnected) return;
786
518
 
787
519
  const now2 = Date.now();
788
520
  const last2 = state.lastShowById.get(id) || 0;
789
- if (now2 - last2 < 900) { clearTimeout(hardTimer); release(); return; }
521
+ if (now2 - last2 < 900) return;
790
522
  state.lastShowById.set(id, now2);
791
523
 
792
524
  window.ezstandalone = window.ezstandalone || {};
793
525
  const ez = window.ezstandalone;
794
526
 
795
527
  const doShow = () => {
796
- try {
797
- if (isBlocked()) return;
798
-
799
- // Re-check at execution time (ez.cmd can run later).
800
- const ph = document.getElementById(domId);
801
- if (!ph || !ph.isConnected) return;
802
-
803
- // If this id was used before, destroy safely using full DOM id.
804
- if (state.usedOnce && state.usedOnce.has(id)) {
805
- safeDestroyById(id);
806
- }
807
-
808
- // showAds is patched to ignore missing placeholders and repeated defines.
809
- ez.showAds(id);
810
-
811
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
812
- } catch (e) {}
813
- finally {
814
- setTimeout(() => { try { clearTimeout(hardTimer); } catch (_) {} release(); }, 650);
815
- }
528
+ // Do NOT call destroyPlaceholders here.
529
+ // In ajaxify + infinite scroll flows, Ezoic can be in the middle of a refresh cycle.
530
+ // Calling destroy on active placeholders is a common source of:
531
+ // - "HTML element ... does not exist"
532
+ // - "Placeholder Id ... already been defined"
533
+ // Prefer a straight showAds; Ezoic will refresh as needed.
534
+ try { ez.showAds(id); } catch (e) {}
535
+ try { markEmptyWrapper(id); } catch (e) {}
536
+
537
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
816
538
  };
817
539
 
818
540
  if (Array.isArray(ez.cmd)) {
@@ -820,15 +542,13 @@ function startShow(id) {
820
542
  } else {
821
543
  doShow();
822
544
  }
823
- } catch (e) {
824
- try { clearTimeout(hardTimer); } catch (_) {}
825
- release();
545
+ } finally {
546
+ // If we returned early, hardTimer will release.
826
547
  }
827
548
  });
828
549
  }
829
550
 
830
551
 
831
-
832
552
  // ---------- preload / above-the-fold ----------
833
553
 
834
554
  function ensurePreloadObserver() {
@@ -935,25 +655,6 @@ function startShow(id) {
935
655
  inserted += 1;
936
656
  }
937
657
 
938
- // Safety: if DOM churn results in two consecutive ad wrappers, remove the latter.
939
- // (This can happen when NodeBB inserts/removes spacer elements around the anchor.)
940
- try {
941
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
942
- for (const w of wraps) {
943
- const prev = w.previousElementSibling;
944
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
945
- withInternalDomChange(() => {
946
- try {
947
- const id = getWrapIdFromWrap(w);
948
- if (id) safeDestroyById(id);
949
- parkPlaceholderFromWrap(w);
950
- w.remove();
951
- } catch (e) {}
952
- });
953
- }
954
- }
955
- } catch (e) {}
956
-
957
658
  return inserted;
958
659
  }
959
660
 
@@ -1089,7 +790,6 @@ function startShow(id) {
1089
790
  // remove all wrappers
1090
791
  try {
1091
792
  document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
1092
- try { parkPlaceholderFromWrap(el); } catch (e) {}
1093
793
  try { el.remove(); } catch (e) {}
1094
794
  });
1095
795
  } catch (e) {}
package/public/style.css CHANGED
@@ -1,5 +1,8 @@
1
- /* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
2
- .ezoic-ad {
1
+ /*
2
+ Keep our NodeBB-inserted wrappers CLS-safe.
3
+ NOTE: must not rely on `.ezoic-ad` because Ezoic uses that class internally.
4
+ */
5
+ .nodebb-ezoic-wrap {
3
6
  display: block;
4
7
  width: 100%;
5
8
  margin: 0 !important;
@@ -7,22 +10,22 @@
7
10
  overflow: hidden;
8
11
  }
9
12
 
10
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
13
+ .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
11
14
  margin: 0 !important;
12
15
  padding: 0 !important;
13
16
  min-height: 1px; /* keeps placeholder measurable for IO */
14
17
  }
15
18
 
16
- /* Ezoic sometimes wraps in extra spans/divs with margins */
17
- .ezoic-ad span.ezoic-ad,
18
- .ezoic-ad .ezoic-ad {
19
+ /* If Ezoic wraps inside our wrapper, keep it tight */
20
+ .nodebb-ezoic-wrap span.ezoic-ad,
21
+ .nodebb-ezoic-wrap .ezoic-ad {
19
22
  margin: 0 !important;
20
23
  padding: 0 !important;
21
24
  }
22
25
 
23
26
 
24
27
  /* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
25
- .ezoic-ad.is-empty {
28
+ .nodebb-ezoic-wrap.is-empty {
26
29
  display: block !important;
27
30
  margin: 0 !important;
28
31
  padding: 0 !important;
@@ -31,12 +34,19 @@
31
34
  overflow: hidden !important;
32
35
  }
33
36
 
34
- .ezoic-ad {
37
+ .nodebb-ezoic-wrap {
35
38
  min-height: 0 !important;
36
39
  }
37
40
 
38
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
41
+ .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
39
42
  min-height: 0 !important;
40
43
  }
41
44
 
42
- #ezoic-placeholder-pool{display:block;}
45
+ /*
46
+ Optional: also neutralize spacing on native Ezoic `.ezoic-ad` blocks.
47
+ (Keeps your previous "CSS very good" behavior.)
48
+ */
49
+ .ezoic-ad {
50
+ margin: 0 !important;
51
+ padding: 0 !important;
52
+ }