nodebb-plugin-ezoic-infinite 1.5.71 → 1.5.73

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.71",
3
+ "version": "1.5.73",
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
@@ -12,6 +12,11 @@
12
12
  // Smoothness caps
13
13
  const MAX_INSERTS_PER_RUN = 3;
14
14
 
15
+ // Keep empty (unfilled) wraps alive for a while. Topics/messages can fill late (auction/CMP).
16
+ // Pruning too early makes ads look like they "disappear" while scrolling.
17
+ // Keep empty wraps alive; mobile fills can be slow.
18
+ function keepEmptyWrapMs() { return isMobile() ? 120000 : 60000; }
19
+
15
20
  // Preload margins
16
21
  const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
17
22
  const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
@@ -33,7 +38,64 @@
33
38
  // Production build: debug disabled
34
39
  function dbg() {}
35
40
 
36
- // ---------------- state ----------------
41
+
42
+
43
+ function isFilledNode(node) {
44
+ return !!(node && node.querySelector && node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
45
+ }
46
+
47
+ // Ezoic often sets inline min-height (e.g. 400px) for adaptive placements.
48
+ // In NodeBB topics/messages this creates visible empty space under 250px creatives.
49
+ // We tighten the outermost Ezoic container to the actual creative height once it exists.
50
+ function tightenEzoicMinHeight(wrap) {
51
+ try {
52
+ if (!wrap || !wrap.querySelector) return;
53
+ const outer = wrap.querySelector('.ezoic-ad-adaptive') ||
54
+ wrap.querySelector('.ezoic-ad[style*="min-height"]') ||
55
+ wrap.querySelector('.ezoic-ad');
56
+ if (!outer) return;
57
+
58
+ const iframes = outer.querySelectorAll('iframe');
59
+ if (!iframes || !iframes.length) return;
60
+
61
+ let h = 0;
62
+ iframes.forEach((f) => {
63
+ const ah = parseInt(f.getAttribute('height') || '0', 10);
64
+ const oh = f.offsetHeight || 0;
65
+ h = Math.max(h, ah, oh);
66
+ });
67
+ if (!h) return;
68
+
69
+ // Override inline min-height with a newer inline important.
70
+ try { outer.style.setProperty('min-height', h + 'px', 'important'); } catch (e) { outer.style.minHeight = h + 'px'; }
71
+ try { outer.style.setProperty('height', 'auto', 'important'); } catch (e) {}
72
+
73
+ // Mobile friendliness: avoid giant fixed widths causing overflow/reflow.
74
+ if (isMobile()) {
75
+ try { outer.style.setProperty('width', '100%', 'important'); } catch (e) {}
76
+ try { outer.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
77
+ try { outer.style.setProperty('min-width', '0', 'important'); } catch (e) {}
78
+ }
79
+ } catch (e) {}
80
+ }
81
+
82
+ function watchWrapForFill(wrap) {
83
+ try {
84
+ if (!wrap || wrap.__ezFillObs) return;
85
+ const obs = new MutationObserver(() => {
86
+ if (isFilledNode(wrap)) {
87
+ wrap.classList.remove('is-empty');
88
+ tightenEzoicMinHeight(wrap);
89
+ try { obs.disconnect(); } catch (e) {}
90
+ wrap.__ezFillObs = null;
91
+ }
92
+ });
93
+ obs.observe(wrap, { childList: true, subtree: true });
94
+ wrap.__ezFillObs = obs;
95
+ } catch (e) {}
96
+ }
97
+
98
+ // ---------------- state ----------------
37
99
 
38
100
  const state = {
39
101
  pageKey: null,
@@ -207,6 +269,22 @@
207
269
  return el;
208
270
  }
209
271
 
272
+ function primePlaceholderPool(allIds) {
273
+ try {
274
+ if (!Array.isArray(allIds) || !allIds.length) return;
275
+ const pool = getPoolEl();
276
+ for (const id of allIds) {
277
+ if (!id) continue;
278
+ const domId = `${PLACEHOLDER_PREFIX}${id}`;
279
+ if (document.getElementById(domId)) continue;
280
+ const ph = document.createElement('div');
281
+ ph.id = domId;
282
+ ph.setAttribute('data-ezoic-id', String(id));
283
+ pool.appendChild(ph);
284
+ }
285
+ } catch (e) {}
286
+ }
287
+
210
288
  function isInPool(ph) {
211
289
  try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
212
290
  }
@@ -319,6 +397,13 @@
319
397
  if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
320
398
  if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
321
399
  if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
400
+
401
+ // Create placeholders up-front in an offscreen pool.
402
+ // Ezoic may attempt to define/show ids during load; if they don't exist yet,
403
+ // it can spam errors and sometimes short-circuit. Pooling keeps ids existing without layout.
404
+ primePlaceholderPool(state.allTopics);
405
+ primePlaceholderPool(state.allPosts);
406
+ primePlaceholderPool(state.allCategories);
322
407
  }
323
408
 
324
409
  // ---------------- insertion primitives ----------------
@@ -328,6 +413,7 @@
328
413
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
329
414
  wrap.setAttribute('data-ezoic-after', String(afterPos));
330
415
  wrap.setAttribute('data-ezoic-wrapid', String(id));
416
+ wrap.setAttribute('data-created', String(now()));
331
417
  wrap.style.width = '100%';
332
418
 
333
419
  if (createPlaceholder) {
@@ -386,6 +472,8 @@
386
472
  }
387
473
 
388
474
  function pruneOrphanWraps(kindClass, items) {
475
+ // On mobile topics/messages, keep wraps alive longer to avoid 'disappearing' ads.
476
+ if (kindClass === 'ezoic-ad-message' && isMobile()) return 0;
389
477
  if (!items || !items.length) return 0;
390
478
  const itemSet = new Set(items);
391
479
  const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
@@ -398,12 +486,12 @@
398
486
  const hasNearbyItem = (wrap) => {
399
487
  // NodeBB/skins can inject separators/spacers; be tolerant.
400
488
  let prev = wrap.previousElementSibling;
401
- for (let i = 0; i < 8 && prev; i++) {
489
+ for (let i = 0; i < 14 && prev; i++) {
402
490
  if (itemSet.has(prev)) return true;
403
491
  prev = prev.previousElementSibling;
404
492
  }
405
493
  let next = wrap.nextElementSibling;
406
- for (let i = 0; i < 8 && next; i++) {
494
+ for (let i = 0; i < 14 && next; i++) {
407
495
  if (itemSet.has(next)) return true;
408
496
  next = next.nextElementSibling;
409
497
  }
@@ -412,6 +500,13 @@
412
500
 
413
501
  wraps.forEach((wrap) => {
414
502
  if (isFilled(wrap)) return; // never prune filled ads
503
+
504
+ // Never prune a fresh wrap: it may fill late.
505
+ try {
506
+ const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
507
+ if (created && (now() - created) < keepEmptyWrapMs()) return;
508
+ } catch (e) {}
509
+
415
510
  if (hasNearbyItem(wrap)) return;
416
511
 
417
512
  withInternalDomChange(() => releaseWrapNode(wrap));
@@ -428,13 +523,32 @@
428
523
 
429
524
  const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
430
525
 
526
+ const isFilled = (wrap) => {
527
+ return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
528
+ };
529
+
530
+ const isFresh = (wrap) => {
531
+ try {
532
+ const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
533
+ return created && (now() - created) < keepEmptyWrapMs();
534
+ } catch (e) {
535
+ return false;
536
+ }
537
+ };
538
+
431
539
  let removed = 0;
432
540
  for (const w of wraps) {
433
541
  let prev = w.previousElementSibling;
434
542
  for (let i = 0; i < 3 && prev; i++) {
435
543
  if (isWrap(prev)) {
436
- withInternalDomChange(() => releaseWrapNode(w));
437
- removed++;
544
+ // Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
545
+ // Only decluster when at least one is filled, or when the newer one is stale.
546
+ const prevFilled = isFilled(prev);
547
+ const curFilled = isFilled(w);
548
+ if (prevFilled || curFilled || !isFresh(w)) {
549
+ withInternalDomChange(() => releaseWrapNode(w));
550
+ removed++;
551
+ }
438
552
  break;
439
553
  }
440
554
  prev = prev.previousElementSibling;
@@ -545,10 +659,23 @@
545
659
  if (!ph2 || !ph2.isConnected) return;
546
660
  const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
547
661
  if (!w2) return;
662
+
663
+ // Don't collapse "fresh" placements; slow auctions/CMP can fill late.
664
+ try {
665
+ const created = parseInt(w2.getAttribute('data-created') || '0', 10);
666
+ if (created && (now() - created) < keepEmptyWrapMs()) return;
667
+ } catch (e) {}
668
+
548
669
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
549
- if (!hasAd) w2.classList.add('is-empty');
670
+ if (!hasAd) {
671
+ w2.classList.add('is-empty');
672
+ watchWrapForFill(w2);
673
+ } else {
674
+ w2.classList.remove('is-empty');
675
+ tightenEzoicMinHeight(w2);
676
+ }
550
677
  } catch (e) {}
551
- }, 3500);
678
+ }, 15000);
552
679
  } catch (e) {}
553
680
  }
554
681
 
@@ -584,6 +711,15 @@
584
711
  const doShow = () => {
585
712
  try { ez.showAds(id); } catch (e) {}
586
713
  try { markEmptyWrapper(id); } catch (e) {}
714
+ try {
715
+ const phw = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
716
+ const ww = phw && phw.closest ? phw.closest(`.${WRAP_CLASS}`) : null;
717
+ if (ww) {
718
+ watchWrapForFill(ww);
719
+ setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 900);
720
+ setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 2200);
721
+ }
722
+ } catch (e) {}
587
723
  setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
588
724
  };
589
725
 
package/public/style.css CHANGED
@@ -68,3 +68,6 @@
68
68
  min-height: 1px !important; /* kill 400px gaps */
69
69
  height: auto !important;
70
70
  }
71
+
72
+ /* Ensure Ezoic reportline doesn't affect layout */
73
+ .nodebb-ezoic-wrap .reportline{position:absolute!important;}