nodebb-plugin-ezoic-infinite 1.5.39 → 1.5.41

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.39",
3
+ "version": "1.5.41",
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
@@ -135,6 +135,7 @@ function mutationHasRelevantAddedNodes(mutations) {
135
135
 
136
136
  // observers / schedulers
137
137
  domObs: null,
138
+ tightenObs: null,
138
139
  io: null,
139
140
  runQueued: false,
140
141
 
@@ -292,6 +293,124 @@ function mutationHasRelevantAddedNodes(mutations) {
292
293
  }
293
294
  }
294
295
 
296
+ // ---------- Ezoic min-height tightening (lightweight) ----------
297
+ // Some Ezoic placements reserve a large min-height (e.g. 400px) even when
298
+ // the rendered iframe/container is smaller (often 250px). That creates a
299
+ // visible empty gap and can make creatives appear to "slide" inside the slot.
300
+ // We correct ONLY those cases, without scroll listeners or full-page rescans.
301
+
302
+ function getRenderedAdHeight(adSpan) {
303
+ try {
304
+ const c = adSpan.querySelector('div[id$="__container__"]');
305
+ if (c && c.offsetHeight) return c.offsetHeight;
306
+ const f = adSpan.querySelector('iframe');
307
+ if (!f) return 0;
308
+ const attr = parseInt(f.getAttribute('height') || '', 10);
309
+ if (Number.isFinite(attr) && attr > 0) return attr;
310
+ if (f.offsetHeight) return f.offsetHeight;
311
+ } catch (e) {}
312
+ return 0;
313
+ }
314
+
315
+ function tightenMinHeight(adSpan) {
316
+ try {
317
+ if (!adSpan || adSpan.nodeType !== 1) return;
318
+ if (adSpan.tagName !== 'SPAN') return;
319
+ if (!adSpan.classList || !adSpan.classList.contains('ezoic-ad')) return;
320
+
321
+ // Some Ezoic templates apply sticky/fixed positioning inside the ad slot
322
+ // (e.g. .ezads-sticky-intradiv) which can make the creative appear to
323
+ // "slide" within an oversized container. Neutralize it inside the slot.
324
+ try {
325
+ const sticky = adSpan.querySelectorAll('.ezads-sticky-intradiv');
326
+ sticky.forEach((el) => {
327
+ el.style.setProperty('position', 'static', 'important');
328
+ el.style.setProperty('top', 'auto', 'important');
329
+ el.style.setProperty('bottom', 'auto', 'important');
330
+ });
331
+
332
+ // Safety net: any descendant that ends up sticky/fixed via inline style
333
+ // (rare, but causes "floating" creatives).
334
+ const positioned = adSpan.querySelectorAll('[style*="position: sticky"], [style*="position:sticky"], [style*="position: fixed"], [style*="position:fixed"]');
335
+ positioned.forEach((el) => {
336
+ el.style.setProperty('position', 'static', 'important');
337
+ el.style.setProperty('top', 'auto', 'important');
338
+ el.style.setProperty('bottom', 'auto', 'important');
339
+ });
340
+ } catch (e) {}
341
+
342
+ const mhStr = adSpan.style && adSpan.style.minHeight ? String(adSpan.style.minHeight) : '';
343
+ const mh = mhStr ? parseInt(mhStr, 10) : 0;
344
+ if (!mh || mh < 350) return; // only fix the "400px"-style reservations
345
+
346
+ const h = getRenderedAdHeight(adSpan);
347
+ if (!h || h <= 0) return;
348
+ if (h >= mh) return;
349
+
350
+ adSpan.style.setProperty('min-height', `${h}px`, 'important');
351
+ } catch (e) {}
352
+ }
353
+
354
+ function closestEzoicAdSpan(node) {
355
+ try {
356
+ if (!node || node.nodeType !== 1) return null;
357
+ const el = /** @type {Element} */ (node);
358
+ if (el.tagName === 'SPAN' && el.classList && el.classList.contains('ezoic-ad')) return el;
359
+ if (el.closest) return el.closest('span.ezoic-ad');
360
+ } catch (e) {}
361
+ return null;
362
+ }
363
+
364
+ function ensureTightenObserver() {
365
+ if (state.tightenObs) return;
366
+
367
+ let raf = 0;
368
+ const pending = new Set();
369
+ const schedule = (adSpan) => {
370
+ if (!adSpan) return;
371
+ pending.add(adSpan);
372
+ if (raf) return;
373
+ raf = requestAnimationFrame(() => {
374
+ raf = 0;
375
+ for (const el of pending) tightenMinHeight(el);
376
+ pending.clear();
377
+ });
378
+ };
379
+
380
+ state.tightenObs = new MutationObserver((mutations) => {
381
+ try {
382
+ for (const m of mutations) {
383
+ if (m.type === 'attributes') {
384
+ const ad = closestEzoicAdSpan(m.target);
385
+ if (ad) schedule(ad);
386
+ continue;
387
+ }
388
+ if (!m.addedNodes || !m.addedNodes.length) continue;
389
+ for (const n of m.addedNodes) {
390
+ const ad = closestEzoicAdSpan(n);
391
+ if (ad) schedule(ad);
392
+ if (n && n.nodeType === 1 && n.querySelectorAll) {
393
+ n.querySelectorAll('span.ezoic-ad').forEach(schedule);
394
+ }
395
+ }
396
+ }
397
+ } catch (e) {}
398
+ });
399
+
400
+ try {
401
+ state.tightenObs.observe(document.documentElement, {
402
+ subtree: true,
403
+ childList: true,
404
+ attributes: true,
405
+ attributeFilter: ['style', 'class', 'data-load-complete', 'height'],
406
+ });
407
+ } catch (e) {}
408
+
409
+ try {
410
+ document.querySelectorAll('span.ezoic-ad[style*="min-height"]').forEach(tightenMinHeight);
411
+ } catch (e) {}
412
+ }
413
+
295
414
  const RECYCLE_COOLDOWN_MS = 1500;
296
415
 
297
416
  function kindKeyFromClass(kindClass) {
@@ -853,6 +972,9 @@ function startShow(id) {
853
972
  // reset perf profile cache
854
973
  state.perfProfile = null;
855
974
 
975
+ // tighten observer is global; keep it across ajaxify navigation but ensure it exists
976
+ // (do not disconnect here to avoid missing late style rewrites during transitions)
977
+
856
978
  // reset state
857
979
  state.cfg = null;
858
980
  state.allTopics = [];
@@ -899,6 +1021,7 @@ function startShow(id) {
899
1021
 
900
1022
  warmUpNetwork();
901
1023
  patchShowAds();
1024
+ ensureTightenObserver();
902
1025
  ensurePreloadObserver();
903
1026
  ensureDomObserver();
904
1027
 
@@ -921,11 +1044,30 @@ function startShow(id) {
921
1044
  state.pageKey = getPageKey();
922
1045
  warmUpNetwork();
923
1046
  patchShowAds();
1047
+ ensureTightenObserver();
924
1048
  ensurePreloadObserver();
925
1049
  ensureDomObserver();
926
-
927
1050
  bindNodeBB();
928
1051
 
1052
+ // Lightweight scroll kick: NodeBB infinite scroll can keep many nodes and only append occasionally.
1053
+ // Without a scroll trigger, we might not inject new placeholders until another DOM mutation occurs.
1054
+ // This is throttled and only triggers near the bottom to keep CPU usage minimal.
1055
+ state.lastScrollKick = 0;
1056
+ window.addEventListener('scroll', () => {
1057
+ const now = Date.now();
1058
+ if (now - state.lastScrollKick < 250) return;
1059
+ state.lastScrollKick = now;
1060
+
1061
+ // Only kick when user is approaching the end of currently rendered content
1062
+ const doc = document.documentElement;
1063
+ const scrollTop = window.pageYOffset || doc.scrollTop || 0;
1064
+ const viewportH = window.innerHeight || doc.clientHeight || 0;
1065
+ const fullH = Math.max(doc.scrollHeight, document.body ? document.body.scrollHeight : 0);
1066
+ if (scrollTop + viewportH > fullH - 2000) {
1067
+ if (!isBlocked()) scheduleRun();
1068
+ }
1069
+ }, { passive: true });
1070
+
929
1071
  // First paint: try hero + run
930
1072
  blockedUntil = 0;
931
1073
  insertHeroAdEarly().catch(() => {});
package/public/style.css CHANGED
@@ -6,4 +6,6 @@
6
6
  .ezoic-ad {
7
7
  display: block;
8
8
  width: 100%;
9
+ clear: both;
10
+ position: relative;
9
11
  }