nodebb-plugin-ezoic-infinite 1.6.43 → 1.6.44

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +127 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.43",
3
+ "version": "1.6.44",
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
@@ -1646,6 +1646,129 @@ function buildOrdinalMap(items) {
1646
1646
  });
1647
1647
  }
1648
1648
 
1649
+ // --- Empty ad "poke" helper ---
1650
+ // Some GPT / safeframe renders can stay visually blank until a *real* scroll happens.
1651
+ // We still avoid showAds()/refresh() (can hurt fill), but we mimic the user "micro scroll".
1652
+ var ezEmptySeen = Object.create(null);
1653
+ var ezEmptyPokeTimer = Object.create(null);
1654
+ var ezEmptyAttempts = Object.create(null);
1655
+
1656
+ function inViewportLoose(el) {
1657
+ try {
1658
+ if (!el || !el.getBoundingClientRect) return false;
1659
+ var r = el.getBoundingClientRect();
1660
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1661
+ // within 1.5 viewports from top/bottom
1662
+ return r.bottom > -vh * 0.5 && r.top < vh * 1.5;
1663
+ } catch (e) {
1664
+ return false;
1665
+ }
1666
+ }
1667
+
1668
+ function hasAnyIframe(divEl) {
1669
+ try {
1670
+ if (!divEl) return false;
1671
+ return !!divEl.querySelector('iframe');
1672
+ } catch (e) {
1673
+ return false;
1674
+ }
1675
+ }
1676
+
1677
+ function microScrollNudge() {
1678
+ try {
1679
+ // Only if the page can actually scroll
1680
+ var maxY = (document.documentElement && document.documentElement.scrollHeight) ? document.documentElement.scrollHeight : 0;
1681
+ if (maxY <= (window.innerHeight || 0) + 2) return;
1682
+
1683
+ var x = window.scrollX || 0;
1684
+ var y = window.scrollY || 0;
1685
+ // Nudge by 1px and restore on the next frame.
1686
+ window.scrollTo(x, y + 1);
1687
+ requestAnimationFrame(function(){
1688
+ try { window.scrollTo(x, y); } catch (e) {}
1689
+ });
1690
+ } catch (e) {}
1691
+ }
1692
+
1693
+ function pokeReflowForDivId(divId) {
1694
+ try {
1695
+ var el = document.getElementById(divId);
1696
+ if (!el) return;
1697
+ if (!inViewportLoose(el)) return;
1698
+
1699
+ // If the slot already has an iframe, don't keep poking.
1700
+ if (hasAnyIframe(el)) return;
1701
+
1702
+ // Force a tiny reflow/paint on the closest wrapper
1703
+ var wrap = el.closest ? (el.closest('.ez-fixed-wrap') || el.parentElement) : el.parentElement;
1704
+ if (!wrap) wrap = el;
1705
+
1706
+ // 1) Force layout
1707
+ try { void wrap.offsetHeight; } catch (e) {}
1708
+
1709
+ // 2) Toggle visibility for one frame (forces paint)
1710
+ var prevVis = wrap.style.visibility;
1711
+ wrap.style.visibility = 'hidden';
1712
+ requestAnimationFrame(function(){
1713
+ wrap.style.visibility = prevVis || '';
1714
+ // 3) Fire synthetic events (cheap)
1715
+ try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1716
+ try { window.dispatchEvent(new Event('resize')); } catch (e) {}
1717
+
1718
+ // 4) If the slot was "empty", a real scroll often makes it fill immediately.
1719
+ microScrollNudge();
1720
+ });
1721
+ } catch (e) {}
1722
+ }
1723
+
1724
+ function schedulePoke(divId) {
1725
+ try {
1726
+ if (!divId) return;
1727
+ var now = Date.now();
1728
+ // Cooldown per slot to avoid loops
1729
+ if (ezEmptySeen[divId] && now - ezEmptySeen[divId] < 15000) return;
1730
+ // New window: reset attempts
1731
+ ezEmptyAttempts[divId] = 0;
1732
+ ezEmptySeen[divId] = now;
1733
+
1734
+ // Cap attempts per slot in a short window
1735
+ ezEmptyAttempts[divId] = (ezEmptyAttempts[divId] || 0) + 1;
1736
+ if (ezEmptyAttempts[divId] > 3) return;
1737
+
1738
+ if (ezEmptyPokeTimer[divId]) return;
1739
+ // Multi-poke: fast + a couple of retries (covers slow GPT / lazy paint)
1740
+ ezEmptyPokeTimer[divId] = setTimeout(function(){
1741
+ ezEmptyPokeTimer[divId] = null;
1742
+ pokeReflowForDivId(divId);
1743
+ setTimeout(function(){ pokeReflowForDivId(divId); }, 350);
1744
+ setTimeout(function(){ pokeReflowForDivId(divId); }, 1200);
1745
+ }, 80);
1746
+ } catch (e) {}
1747
+ }
1748
+
1749
+ function initGptEmptyPoke() {
1750
+ try {
1751
+ if (!window.googletag || !googletag.cmd || !googletag.pubads) return;
1752
+ googletag.cmd.push(function(){
1753
+ try {
1754
+ var pub = googletag.pubads();
1755
+ if (!pub || !pub.addEventListener) return;
1756
+ pub.addEventListener('slotRenderEnded', function(e){
1757
+ try {
1758
+ if (!e) return;
1759
+ // divId is the GPT container id for the slot (e.g. div-gpt-ad-...)
1760
+ var divId = e.slot && e.slot.getSlotElementId ? e.slot.getSlotElementId() : (e.slotElementId || '');
1761
+ if (e.isEmpty) {
1762
+ if (divId) console.log('[EZ EMPTY]', divId);
1763
+ schedulePoke(divId);
1764
+ }
1765
+ } catch (err) {}
1766
+ });
1767
+ } catch (err2) {}
1768
+ });
1769
+ } catch (e) {}
1770
+ }
1771
+
1649
1772
  function init() {
1650
1773
  schedule('init');
1651
1774
 
@@ -1685,6 +1808,10 @@ function buildOrdinalMap(items) {
1685
1808
  });
1686
1809
  } catch (e) {}
1687
1810
  }
1811
+
1812
+ // Listen for GPT empty renders and apply a cheap reflow "poke".
1813
+ // This is intentionally non-invasive (no showAds/refresh) to preserve fill.
1814
+ initGptEmptyPoke();
1688
1815
  }
1689
1816
 
1690
1817
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);