nodebb-plugin-ezoic-infinite 1.5.60 → 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.60",
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,108 +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.documentElement || 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.documentElement || 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
- function acquirePlaceholder(id) {
80
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
81
- let ph = document.getElementById(domId);
82
- if (!ph) {
83
- ph = document.createElement('div');
84
- ph.id = domId;
85
- ph.setAttribute('data-ezoic-id', String(id));
86
- ensurePool().appendChild(ph);
87
- }
88
- // Note: appendChild will automatically move the node, no manual detach (avoids race gaps).
89
- // Clear request/defined flags when reusing
90
- try {
91
- if (ph.dataset) {
92
- ph.dataset.ezRequested = '0';
93
- ph.dataset.ezDefined = '0';
94
- ph.dataset.ezActive = '0';
95
- }
96
- } catch (e) {}
97
- return ph;
98
- }
99
-
100
- function parkPlaceholderFromWrap(wrap) {
101
- try {
102
- const ph = wrap && wrap.querySelector ? wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`) : null;
103
- if (!ph) return;
104
- try { if (state && state.io) state.io.unobserve(ph); } catch (e) {}
105
- try { if (ph.dataset) ph.dataset.ezActive = '0'; } catch (e) {}
106
- ensurePool().appendChild(ph);
107
- } catch (e) {}
108
- }
109
13
  // Insert at most N ads per run to keep the UI smooth on infinite scroll
110
14
  const MAX_INSERTS_PER_RUN = 3;
111
15
 
@@ -318,107 +222,34 @@ function parkPlaceholderFromWrap(wrap) {
318
222
  } catch (e) {}
319
223
  }
320
224
 
321
-
322
225
  // Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
323
- // Ezoic can (re)define ezstandalone.showAds over time; we hook assignments to keep the guard.
324
226
  function patchShowAds() {
325
- function wrapShowAds(ez, orig) {
326
- if (orig && orig.__nodebbWrapped) return orig;
327
-
328
- const wrapped = function (...args) {
329
- if (isBlocked()) return;
330
-
331
- let ids = [];
332
- if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
333
- else ids = args;
334
-
335
- const seen = new Set();
336
- for (const v of ids) {
337
- const id = parseInt(v, 10);
338
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
339
-
340
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
341
- let ph = document.getElementById(domId);
342
- if (!ph || !ph.isConnected) {
343
- // If Ezoic (or another script) tries to show an id we haven't injected yet,
344
- // create the placeholder in the offscreen pool so it exists, but don't load.
345
- try {
346
- ph = document.createElement('div');
347
- ph.id = domId;
348
- ph.setAttribute('data-ezoic-id', String(id));
349
- if (ph.dataset) ph.dataset.ezActive = '0';
350
- ensurePool().appendChild(ph);
351
- } catch (e) {}
352
- continue;
353
- }
354
-
355
- // Only allow loads for placeholders actively injected into the page (not parked in the pool)
356
- // and currently marked active.
357
- if (!isPlaceholderInUse(ph)) continue;
358
- if (ph.dataset && ph.dataset.ezActive !== '1') continue;
359
-
360
- // Prevent repeated "define" attempts on the same placeholder while it remains in DOM.
361
- if (ph.dataset && (ph.dataset.ezRequested === '1' || ph.dataset.ezDefined === '1')) continue;
362
-
363
- seen.add(id);
364
- try {
365
- if (ph.dataset) ph.dataset.ezRequested = '1';
366
- orig.call(ez, id);
367
- if (ph.dataset) {
368
- ph.dataset.ezDefined = '1';
369
- ph.dataset.ezRequested = '0';
370
- }
371
- } catch (e) {
372
- try { if (ph.dataset) ph.dataset.ezRequested = '0'; } catch (_) {}
373
- }
374
- }
375
- };
376
- try { wrapped.__nodebbWrapped = true; } catch (e) {}
377
- return wrapped;
378
- }
379
-
380
- function ensureHook() {
227
+ const applyPatch = () => {
381
228
  try {
382
229
  window.ezstandalone = window.ezstandalone || {};
383
230
  const ez = window.ezstandalone;
231
+ if (window.__nodebbEzoicPatched) return;
232
+ if (typeof ez.showAds !== 'function') return;
384
233
 
385
- // Hook future assignments to showAds so we keep our wrapper even if Ezoic overwrites it.
386
- if (!ez.__nodebbShowAdsHooked) {
387
- ez.__nodebbShowAdsHooked = true;
388
- let _showAds = ez.showAds;
389
-
390
- Object.defineProperty(ez, 'showAds', {
391
- configurable: true,
392
- enumerable: true,
393
- get() {
394
- return _showAds;
395
- },
396
- set(fn) {
397
- _showAds = (typeof fn === 'function') ? wrapShowAds(ez, fn) : fn;
398
- },
399
- });
400
-
401
- // If showAds already exists, wrap it immediately
402
- if (typeof _showAds === 'function') {
403
- _showAds = wrapShowAds(ez, _showAds);
404
- }
405
- } else if (typeof ez.showAds === 'function' && !ez.showAds.__nodebbWrapped) {
406
- // In case defineProperty wasn't possible (rare), re-wrap
407
- ez.showAds = wrapShowAds(ez, ez.showAds);
408
- }
409
- } catch (e) {}
410
- }
411
-
412
- ensureHook();
234
+ window.__nodebbEzoicPatched = true;
235
+ const orig = ez.showAds;
413
236
 
414
- // Also attempt via cmd queue
415
- try {
416
- window.ezstandalone = window.ezstandalone || {};
417
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
418
- window.ezstandalone.cmd.push(ensureHook);
419
- } catch (e) {}
420
- }
237
+ ez.showAds = function (...args) {
238
+ if (isBlocked()) return;
421
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
+ }
422
253
  };
423
254
  } catch (e) {}
424
255
  };
@@ -473,12 +304,6 @@ function withInternalDomChange(fn) {
473
304
  if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
474
305
  if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
475
306
  if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
476
-
477
- // Keep placeholders alive even before they're injected.
478
- // This avoids Ezoic trying to touch ids that we haven't inserted yet.
479
- primePool(state.allTopics);
480
- primePool(state.allPosts);
481
- primePool(state.allCategories);
482
307
  }
483
308
 
484
309
  // ---------- insertion primitives ----------
@@ -505,20 +330,9 @@ function withInternalDomChange(fn) {
505
330
 
506
331
  function safeDestroyById(id) {
507
332
  try {
508
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
509
- const ph = document.getElementById(domId);
510
-
511
- // If the element is already gone, do NOT call destroyPlaceholders (Ezoic will log "does not exist").
512
- if (!ph || !ph.isConnected) return;
513
-
514
- if (ph.dataset) {
515
- delete ph.dataset.ezDefined;
516
- delete ph.dataset.ezRequested;
517
- }
518
-
519
333
  const ez = window.ezstandalone;
520
334
  if (ez && typeof ez.destroyPlaceholders === 'function') {
521
- ez.destroyPlaceholders([domId]);
335
+ ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
522
336
  }
523
337
  } catch (e) {}
524
338
  }
@@ -543,7 +357,6 @@ function withInternalDomChange(fn) {
543
357
  withInternalDomChange(() => {
544
358
  try {
545
359
  if (id) safeDestroyById(id);
546
- parkPlaceholderFromWrap(wrap);
547
360
  wrap.remove();
548
361
  } catch (e) {}
549
362
  });
@@ -577,8 +390,9 @@ function buildWrap(id, kindClass, afterPos) {
577
390
  wrap.setAttribute('data-ezoic-wrapid', String(id));
578
391
  wrap.style.width = '100%';
579
392
 
580
- const ph = acquirePlaceholder(id);
581
- try { if (ph.dataset) ph.dataset.ezActive = '1'; } catch (e) {}
393
+ const ph = document.createElement('div');
394
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
395
+ ph.setAttribute('data-ezoic-id', String(id));
582
396
  wrap.appendChild(ph);
583
397
 
584
398
  return wrap;
@@ -594,7 +408,7 @@ function buildWrap(id, kindClass, afterPos) {
594
408
  if (insertingIds.has(id)) return null;
595
409
 
596
410
  const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
597
- if (existingPh && isPlaceholderInUse(existingPh)) return null;
411
+ if (existingPh && existingPh.isConnected) return null;
598
412
 
599
413
  insertingIds.add(id);
600
414
  try {
@@ -617,7 +431,7 @@ function buildWrap(id, kindClass, afterPos) {
617
431
 
618
432
  const id = allIds[idx];
619
433
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
620
- if (ph && isPlaceholderInUse(ph)) continue;
434
+ if (ph && ph.isConnected) continue;
621
435
 
622
436
  return id;
623
437
  }
@@ -645,7 +459,6 @@ function buildWrap(id, kindClass, afterPos) {
645
459
  if (ph && state.io) state.io.unobserve(ph);
646
460
  } catch (e) {}
647
461
 
648
- parkPlaceholderFromWrap(victim);
649
462
  victim.remove();
650
463
  return true;
651
464
  } catch (e) {
@@ -682,11 +495,9 @@ function drainQueue() {
682
495
  }
683
496
  }
684
497
 
685
-
686
498
  function startShow(id) {
687
- if (!id) return;
499
+ if (!id || isBlocked()) return;
688
500
 
689
- // Reserve an inflight slot, but ALWAYS release on any early exit.
690
501
  state.inflight++;
691
502
  let released = false;
692
503
  const release = () => {
@@ -700,41 +511,30 @@ function startShow(id) {
700
511
 
701
512
  requestAnimationFrame(() => {
702
513
  try {
703
- if (isBlocked()) { clearTimeout(hardTimer); release(); return; }
514
+ if (isBlocked()) return;
704
515
 
705
- const domId = `${PLACEHOLDER_PREFIX}${id}`;
706
- const phNow = document.getElementById(domId);
707
- if (!phNow || !phNow.isConnected) { clearTimeout(hardTimer); release(); return; }
516
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
517
+ if (!ph || !ph.isConnected) return;
708
518
 
709
519
  const now2 = Date.now();
710
520
  const last2 = state.lastShowById.get(id) || 0;
711
- if (now2 - last2 < 900) { clearTimeout(hardTimer); release(); return; }
521
+ if (now2 - last2 < 900) return;
712
522
  state.lastShowById.set(id, now2);
713
523
 
714
524
  window.ezstandalone = window.ezstandalone || {};
715
525
  const ez = window.ezstandalone;
716
526
 
717
527
  const doShow = () => {
718
- try {
719
- if (isBlocked()) return;
720
-
721
- // Re-check at execution time (ez.cmd can run later).
722
- const ph = document.getElementById(domId);
723
- if (!ph || !ph.isConnected) return;
724
-
725
- // If this id was used before, destroy safely using full DOM id.
726
- if (state.usedOnce && state.usedOnce.has(id)) {
727
- safeDestroyById(id);
728
- }
729
-
730
- // showAds is patched to ignore missing placeholders and repeated defines.
731
- ez.showAds(id);
732
-
733
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
734
- } catch (e) {}
735
- finally {
736
- setTimeout(() => { try { clearTimeout(hardTimer); } catch (_) {} release(); }, 650);
737
- }
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);
738
538
  };
739
539
 
740
540
  if (Array.isArray(ez.cmd)) {
@@ -742,15 +542,13 @@ function startShow(id) {
742
542
  } else {
743
543
  doShow();
744
544
  }
745
- } catch (e) {
746
- try { clearTimeout(hardTimer); } catch (_) {}
747
- release();
545
+ } finally {
546
+ // If we returned early, hardTimer will release.
748
547
  }
749
548
  });
750
549
  }
751
550
 
752
551
 
753
-
754
552
  // ---------- preload / above-the-fold ----------
755
553
 
756
554
  function ensurePreloadObserver() {
@@ -857,25 +655,6 @@ function startShow(id) {
857
655
  inserted += 1;
858
656
  }
859
657
 
860
- // Safety: if DOM churn results in two consecutive ad wrappers, remove the latter.
861
- // (This can happen when NodeBB inserts/removes spacer elements around the anchor.)
862
- try {
863
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
864
- for (const w of wraps) {
865
- const prev = w.previousElementSibling;
866
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
867
- withInternalDomChange(() => {
868
- try {
869
- const id = getWrapIdFromWrap(w);
870
- if (id) safeDestroyById(id);
871
- parkPlaceholderFromWrap(w);
872
- w.remove();
873
- } catch (e) {}
874
- });
875
- }
876
- }
877
- } catch (e) {}
878
-
879
658
  return inserted;
880
659
  }
881
660
 
@@ -1011,7 +790,6 @@ function startShow(id) {
1011
790
  // remove all wrappers
1012
791
  try {
1013
792
  document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
1014
- try { parkPlaceholderFromWrap(el); } catch (e) {}
1015
793
  try { el.remove(); } catch (e) {}
1016
794
  });
1017
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
+ }