nodebb-plugin-ezoic-infinite 1.5.50 → 1.5.51

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.50",
3
+ "version": "1.5.51",
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
@@ -96,53 +96,11 @@
96
96
 
97
97
  const insertingIds = new Set();
98
98
 
99
- // ---------- lightweight "fade-in" for ad iframes ----------
100
- // This reduces the perception of "flashing" when a slot appears empty then fills.
101
- // We avoid scroll listeners and only react to DOM insertions.
102
- const _faded = new WeakSet();
103
- function _fadeInIframe(iframe) {
104
- try {
105
- if (!iframe || _faded.has(iframe)) return;
106
- _faded.add(iframe);
107
- iframe.style.opacity = '0';
108
- iframe.style.transition = 'opacity 140ms ease';
109
- // Next frame: show
110
- requestAnimationFrame(() => {
111
- try { iframe.style.opacity = '1'; } catch (e) {}
112
- });
113
- } catch (e) {}
114
- }
115
-
116
- const _adFillObserver = new MutationObserver((muts) => {
117
- try {
118
- for (const m of muts) {
119
- if (m.addedNodes && m.addedNodes.length) {
120
- for (const n of m.addedNodes) {
121
- if (!n || n.nodeType !== 1) continue;
122
- if (n.tagName === 'IFRAME') {
123
- const w = n.closest && n.closest(`.${WRAP_CLASS}`);
124
- if (w) _fadeInIframe(n);
125
- continue;
126
- }
127
- // If a subtree is added, look for iframes inside ad wrappers only.
128
- const ifs = n.querySelectorAll ? n.querySelectorAll(`.${WRAP_CLASS} iframe`) : null;
129
- if (ifs && ifs.length) ifs.forEach(_fadeInIframe);
130
- }
131
- }
132
- }
133
- } catch (e) {}
134
- });
135
-
136
- try {
137
- _adFillObserver.observe(document.documentElement, { subtree: true, childList: true });
138
- } catch (e) {}
139
-
140
-
141
99
 
142
100
  function markEmptyWrapper(id) {
143
101
  try {
144
102
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
145
- if (!ph || !ph.isConnected) return;
103
+ if (!ph || !ph.isConnected) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
146
104
  const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
147
105
  if (!wrap) return;
148
106
  // If still empty after a delay, collapse it.
@@ -247,18 +205,13 @@
247
205
  ['dns-prefetch', 'https://g.ezoic.net', false],
248
206
  ['preconnect', 'https://go.ezoic.net', true],
249
207
  ['dns-prefetch', 'https://go.ezoic.net', false],
250
-
251
- // Google ad stack (helps Safeframe/GPT warm up)
208
+ // Google Ads / Safeframe warm-up (helps cold-start latency)
252
209
  ['preconnect', 'https://pagead2.googlesyndication.com', true],
253
210
  ['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
254
211
  ['preconnect', 'https://securepubads.g.doubleclick.net', true],
255
212
  ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
256
213
  ['preconnect', 'https://tpc.googlesyndication.com', true],
257
214
  ['dns-prefetch', 'https://tpc.googlesyndication.com', false],
258
- ['preconnect', 'https://googleads.g.doubleclick.net', true],
259
- ['dns-prefetch', 'https://googleads.g.doubleclick.net', false],
260
- ['preconnect', 'https://static.doubleclick.net', true],
261
- ['dns-prefetch', 'https://static.doubleclick.net', false],
262
215
  ];
263
216
  for (const [rel, href, cors] of links) {
264
217
  const key = `${rel}|${href}`;
@@ -286,7 +239,7 @@
286
239
  const orig = ez.showAds;
287
240
 
288
241
  ez.showAds = function (...args) {
289
- if (isBlocked()) return;
242
+ if (isBlocked()) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
290
243
 
291
244
  let ids = [];
292
245
  if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
@@ -424,7 +377,7 @@ function withInternalDomChange(fn) {
424
377
  window.setTimeout(() => {
425
378
  try {
426
379
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
427
- if (!ph || !ph.isConnected) return;
380
+ if (!ph || !ph.isConnected) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
428
381
  const wrap = ph.closest(`.${WRAP_CLASS}`);
429
382
  if (!wrap) return;
430
383
  const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
@@ -439,7 +392,6 @@ function buildWrap(id, kindClass, afterPos) {
439
392
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
440
393
  wrap.setAttribute('data-ezoic-after', String(afterPos));
441
394
  wrap.setAttribute('data-ezoic-wrapid', String(id));
442
- wrap.setAttribute('data-ezoic-ts', String(Date.now()));
443
395
  wrap.style.width = '100%';
444
396
 
445
397
  const ph = document.createElement('div');
@@ -496,23 +448,14 @@ function buildWrap(id, kindClass, afterPos) {
496
448
  const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
497
449
  if (!wraps.length) return false;
498
450
 
499
- const now = Date.now();
500
-
501
- // Only recycle wraps that are clearly out of view AND old enough.
502
- // This avoids the "unstable" feeling where ads disappear when the user scrolls back a bit.
503
- const MIN_AGE_MS = 20000; // 20s
504
- const OFFSCREEN_PX = -5000; // far above viewport
505
-
451
+ // Prefer a wrap far above the viewport
506
452
  let victim = null;
507
453
  for (const w of wraps) {
508
454
  const r = w.getBoundingClientRect();
509
- const ts = parseInt(w.getAttribute('data-ezoic-ts') || '0', 10);
510
- const ageOk = !ts || (now - ts) >= MIN_AGE_MS;
511
- if (ageOk && r.bottom < OFFSCREEN_PX) { victim = w; break; }
455
+ if (r.bottom < -2000) { victim = w; break; }
512
456
  }
513
-
514
- // If nothing is eligible, do not recycle. We'll simply skip inserting new ads this run.
515
- if (!victim) return false;
457
+ // Otherwise remove the earliest one in the document
458
+ if (!victim) victim = wraps[0];
516
459
 
517
460
  // Unobserve placeholder if still observed
518
461
  try {
@@ -547,7 +490,7 @@ function buildWrap(id, kindClass, afterPos) {
547
490
  }
548
491
 
549
492
  function drainQueue() {
550
- if (isBlocked()) return;
493
+ if (isBlocked()) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
551
494
  const max = getMaxInflight();
552
495
  while (state.inflight < max && state.pending.length) {
553
496
  const id = state.pending.shift();
@@ -572,14 +515,14 @@ function startShow(id) {
572
515
 
573
516
  requestAnimationFrame(() => {
574
517
  try {
575
- if (isBlocked()) return;
518
+ if (isBlocked()) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
576
519
 
577
520
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
578
- if (!ph || !ph.isConnected) return;
521
+ if (!ph || !ph.isConnected) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
579
522
 
580
523
  const now2 = Date.now();
581
524
  const last2 = state.lastShowById.get(id) || 0;
582
- if (now2 - last2 < 900) return;
525
+ if (now2 - last2 < 900) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
583
526
  state.lastShowById.set(id, now2);
584
527
 
585
528
  window.ezstandalone = window.ezstandalone || {};
@@ -587,16 +530,35 @@ function startShow(id) {
587
530
 
588
531
  const doShow = () => {
589
532
  try {
533
+ // Re-check at execution time: ajaxify/infinite-scroll may have removed the placeholder.
534
+ const phNow = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
535
+ if (!phNow || !phNow.isConnected) {
536
+ try { clearTimeout(hardTimer); } catch (e) {}
537
+ release();
538
+ return;
539
+ }
540
+ } catch (e) {}
541
+
542
+ try {
543
+ // If we reused an id in this pageview, destroy first (use DOM id string form).
590
544
  if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
591
- try { ez.destroyPlaceholders(id); } catch (e) {}
545
+ try { ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]); } catch (e) {}
592
546
  }
593
547
  } catch (e) {}
594
548
 
595
- try { ez.showAds(id); } catch (e) {}
549
+ try {
550
+ // Call Ezoic. Our showAds patch will also filter if DOM disappeared.
551
+ ez.showAds(id);
552
+ } catch (e) {}
553
+
596
554
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
597
555
  try { markEmptyWrapper(id); } catch (e) {}
598
556
 
599
- setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
557
+ // Release budget quickly; rendering continues async in ad stack.
558
+ setTimeout(() => {
559
+ try { clearTimeout(hardTimer); } catch (e) {}
560
+ release();
561
+ }, 300);
600
562
  };
601
563
 
602
564
  if (Array.isArray(ez.cmd)) {
@@ -652,7 +614,7 @@ function startShow(id) {
652
614
 
653
615
  function observePlaceholder(id) {
654
616
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
655
- if (!ph || !ph.isConnected) return;
617
+ if (!ph || !ph.isConnected) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
656
618
  const io = ensurePreloadObserver();
657
619
  try { io && io.observe(ph); } catch (e) {}
658
620
 
@@ -912,7 +874,7 @@ function startShow(id) {
912
874
 
913
875
  // Infinite scroll / partial updates
914
876
  $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
915
- if (isBlocked()) return;
877
+ if (isBlocked()) { try { clearTimeout(hardTimer); } catch (e) {} release(); return; }
916
878
  scheduleRun();
917
879
  });
918
880
  }
package/public/style.css CHANGED
@@ -39,8 +39,15 @@
39
39
  min-height: 0 !important;
40
40
  }
41
41
 
42
- /* Avoid baseline gap under iframes (can look like extra space) */
42
+
43
+ /* Smooth fill: avoid harsh flash when safeframe/iframe arrives */
43
44
  .ezoic-ad iframe {
45
+ opacity: 0;
46
+ transition: opacity 140ms ease;
44
47
  display: block !important;
45
48
  vertical-align: top !important;
46
49
  }
50
+ .ezoic-ad iframe[data-load-complete="true"],
51
+ .ezoic-ad iframe[src] {
52
+ opacity: 1;
53
+ }