nodebb-plugin-ezoic-infinite 1.5.61 → 1.5.63

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.63",
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;
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;
547
318
  }
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;
555
- }
556
- return false;
557
- }
558
-
559
319
 
560
320
 
561
321
  function getWrapIdFromWrap(wrap) {
@@ -569,23 +329,12 @@ function withInternalDomChange(fn) {
569
329
  }
570
330
 
571
331
  function safeDestroyById(id) {
572
- 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
- const ez = window.ezstandalone;
585
- if (ez && typeof ez.destroyPlaceholders === 'function') {
586
- ez.destroyPlaceholders([domId]);
587
- }
588
- } catch (e) {}
332
+ // IMPORTANT:
333
+ // Do NOT call ez.destroyPlaceholders here.
334
+ // In NodeBB ajaxify/infinite-scroll flows, Ezoic can be mid-refresh.
335
+ // Destroy calls can create churn, reduce fill, and generate "does not exist" spam.
336
+ // We only remove our wrapper; Ezoic manages slot lifecycle.
337
+ return;
589
338
  }
590
339
 
591
340
  function pruneOrphanWraps(kindClass, items) {
@@ -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,53 @@ 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}`);
664
411
 
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);
683
- }
684
- }
412
+ insertingIds.add(id);
413
+ try {
414
+ const wrap = buildWrap(id, kindClass, afterPos);
415
+ target.insertAdjacentElement('afterend', wrap);
685
416
 
417
+ // If a placeholder with this id already exists elsewhere (some Ezoic flows
418
+ // pre-create placeholders), move it into our wrapper instead of aborting.
419
+ // replaceChild moves the node atomically (no detach window).
420
+ if (existingPh && existingPh !== wrap.firstElementChild) {
421
+ try {
422
+ existingPh.setAttribute('data-ezoic-id', String(id));
423
+ wrap.replaceChild(existingPh, wrap.firstElementChild);
424
+ } catch (e) {
425
+ // Keep the new placeholder if replace fails.
426
+ }
427
+ }
428
+ return wrap;
429
+ } finally {
430
+ insertingIds.delete(id);
431
+ }
432
+ }
686
433
 
687
434
  function pickIdFromAll(allIds, cursorKey) {
688
435
  const n = allIds.length;
@@ -695,7 +442,7 @@ function buildWrap(id, kindClass, afterPos, afterPid) {
695
442
 
696
443
  const id = allIds[idx];
697
444
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
698
- if (ph && isPlaceholderInUse(ph)) continue;
445
+ if (ph && ph.isConnected) continue;
699
446
 
700
447
  return id;
701
448
  }
@@ -723,7 +470,6 @@ function buildWrap(id, kindClass, afterPos, afterPid) {
723
470
  if (ph && state.io) state.io.unobserve(ph);
724
471
  } catch (e) {}
725
472
 
726
- parkPlaceholderFromWrap(victim);
727
473
  victim.remove();
728
474
  return true;
729
475
  } catch (e) {
@@ -760,11 +506,9 @@ function drainQueue() {
760
506
  }
761
507
  }
762
508
 
763
-
764
509
  function startShow(id) {
765
- if (!id) return;
510
+ if (!id || isBlocked()) return;
766
511
 
767
- // Reserve an inflight slot, but ALWAYS release on any early exit.
768
512
  state.inflight++;
769
513
  let released = false;
770
514
  const release = () => {
@@ -778,41 +522,30 @@ function startShow(id) {
778
522
 
779
523
  requestAnimationFrame(() => {
780
524
  try {
781
- if (isBlocked()) { clearTimeout(hardTimer); release(); return; }
525
+ if (isBlocked()) return;
782
526
 
783
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
784
- const phNow = document.getElementById(domId);
785
- if (!phNow || !phNow.isConnected) { clearTimeout(hardTimer); release(); return; }
527
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
528
+ if (!ph || !ph.isConnected) return;
786
529
 
787
530
  const now2 = Date.now();
788
531
  const last2 = state.lastShowById.get(id) || 0;
789
- if (now2 - last2 < 900) { clearTimeout(hardTimer); release(); return; }
532
+ if (now2 - last2 < 900) return;
790
533
  state.lastShowById.set(id, now2);
791
534
 
792
535
  window.ezstandalone = window.ezstandalone || {};
793
536
  const ez = window.ezstandalone;
794
537
 
795
538
  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
- }
539
+ // Do NOT call destroyPlaceholders here.
540
+ // In ajaxify + infinite scroll flows, Ezoic can be in the middle of a refresh cycle.
541
+ // Calling destroy on active placeholders is a common source of:
542
+ // - "HTML element ... does not exist"
543
+ // - "Placeholder Id ... already been defined"
544
+ // Prefer a straight showAds; Ezoic will refresh as needed.
545
+ try { ez.showAds(id); } catch (e) {}
546
+ try { markEmptyWrapper(id); } catch (e) {}
547
+
548
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
816
549
  };
817
550
 
818
551
  if (Array.isArray(ez.cmd)) {
@@ -820,15 +553,13 @@ function startShow(id) {
820
553
  } else {
821
554
  doShow();
822
555
  }
823
- } catch (e) {
824
- try { clearTimeout(hardTimer); } catch (_) {}
825
- release();
556
+ } finally {
557
+ // If we returned early, hardTimer will release.
826
558
  }
827
559
  });
828
560
  }
829
561
 
830
562
 
831
-
832
563
  // ---------- preload / above-the-fold ----------
833
564
 
834
565
  function ensurePreloadObserver() {
@@ -935,25 +666,6 @@ function startShow(id) {
935
666
  inserted += 1;
936
667
  }
937
668
 
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
669
  return inserted;
958
670
  }
959
671
 
@@ -1089,7 +801,6 @@ function startShow(id) {
1089
801
  // remove all wrappers
1090
802
  try {
1091
803
  document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
1092
- try { parkPlaceholderFromWrap(el); } catch (e) {}
1093
804
  try { el.remove(); } catch (e) {}
1094
805
  });
1095
806
  } 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
+ }