nodebb-plugin-ezoic-infinite 1.5.73 → 1.5.75

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.73",
3
+ "version": "1.5.75",
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
@@ -44,37 +44,100 @@ function isFilledNode(node) {
44
44
  return !!(node && node.querySelector && node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
45
45
  }
46
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.
47
+ // Ezoic injects inline `min-height:400px !important` on one or more nested wrappers.
48
+ // If the creative is 250px, this leaves ~150px empty space. Because it's inline+important,
49
+ // CSS alone cannot fix it reliably we must rewrite the inline style.
50
50
  function tightenEzoicMinHeight(wrap) {
51
51
  try {
52
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
53
 
58
- const iframes = outer.querySelectorAll('iframe');
54
+ const iframes = wrap.querySelectorAll('iframe');
59
55
  if (!iframes || !iframes.length) return;
60
56
 
61
- let h = 0;
57
+ // Find the closest "big" ezoic container that carries the 400px min-height.
58
+ const firstIframe = iframes[0];
59
+ let refNode = null;
60
+ let p = firstIframe && firstIframe.parentElement;
61
+ while (p && p !== wrap) {
62
+ if (p.classList && p.classList.contains('ezoic-ad')) {
63
+ const st = (p.getAttribute('style') || '').toLowerCase();
64
+ if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
65
+ refNode = p;
66
+ break;
67
+ }
68
+ }
69
+ p = p.parentElement;
70
+ }
71
+ if (!refNode) {
72
+ refNode = wrap.querySelector('.ezoic-ad-adaptive') || wrap.querySelector('.ezoic-ad') || wrap;
73
+ }
74
+
75
+ let refTop = 0;
76
+ try { refTop = refNode.getBoundingClientRect().top; } catch (e) { refTop = 0; }
77
+
78
+ // Compute the rendered height needed inside refNode (visible iframes only).
79
+ let maxBottom = 0;
62
80
  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);
81
+ if (!f || !f.getBoundingClientRect) return;
82
+ const rect = f.getBoundingClientRect();
83
+ if (rect.width <= 1 || rect.height <= 1) return;
84
+ const bottom = rect.bottom - refTop;
85
+ maxBottom = Math.max(maxBottom, bottom);
66
86
  });
67
- if (!h) return;
68
87
 
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) {}
88
+ // Fallback to attr/offset if layout metrics are not available.
89
+ if (!maxBottom) {
90
+ iframes.forEach((f) => {
91
+ const ah = parseInt(f.getAttribute('height') || '0', 10);
92
+ const oh = f.offsetHeight || 0;
93
+ maxBottom = Math.max(maxBottom, ah, oh);
94
+ });
95
+ }
96
+ if (!maxBottom) return;
97
+
98
+ // Account for the Ezoic badge (reportline) which is absolutely positioned.
99
+ const report = refNode.querySelector('.reportline');
100
+ if (report && report.offsetHeight) {
101
+ maxBottom += report.offsetHeight;
102
+ }
103
+
104
+ const h = Math.max(1, Math.ceil(maxBottom));
105
+
106
+ const tightenNode = (node) => {
107
+ if (!node || !node.style) return;
108
+ try { node.style.setProperty('min-height', h + 'px', 'important'); } catch (e) { node.style.minHeight = h + 'px'; }
109
+ try { node.style.setProperty('height', 'auto', 'important'); } catch (e) {}
110
+ try { node.style.setProperty('line-height', '0', 'important'); } catch (e) {}
111
+ };
112
+
113
+ // Tighten refNode and any ancestor ezoic-ad nodes with the problematic min-height.
114
+ let cur = refNode;
115
+ while (cur && cur !== wrap) {
116
+ if (cur.classList && cur.classList.contains('ezoic-ad')) {
117
+ const st = (cur.getAttribute('style') || '').toLowerCase();
118
+ if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
119
+ tightenNode(cur);
120
+ }
121
+ }
122
+ cur = cur.parentElement;
123
+ }
124
+ tightenNode(refNode);
125
+
126
+ // Tighten any nested wrappers that also have the 400px min-height inline.
127
+ refNode.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive').forEach((n) => {
128
+ const st = (n.getAttribute('style') || '').toLowerCase();
129
+ if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
130
+ tightenNode(n);
131
+ }
132
+ });
72
133
 
73
134
  // Mobile friendliness: avoid giant fixed widths causing overflow/reflow.
74
135
  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) {}
136
+ [refNode].forEach((n) => {
137
+ try { n.style.setProperty('width', '100%', 'important'); } catch (e) {}
138
+ try { n.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
139
+ try { n.style.setProperty('min-width', '0', 'important'); } catch (e) {}
140
+ });
78
141
  }
79
142
  } catch (e) {}
80
143
  }
@@ -82,19 +145,88 @@ function tightenEzoicMinHeight(wrap) {
82
145
  function watchWrapForFill(wrap) {
83
146
  try {
84
147
  if (!wrap || wrap.__ezFillObs) return;
85
- const obs = new MutationObserver(() => {
148
+
149
+ // Ezoic can (re)apply inline styles after fill; keep tightening for a short window.
150
+ const start = now();
151
+ const tightenBurst = () => {
152
+ try { tightenEzoicMinHeight(wrap); } catch (e) {}
153
+ if (now() - start < 6000) {
154
+ setTimeout(tightenBurst, 350);
155
+ }
156
+ };
157
+
158
+ const obs = new MutationObserver((muts) => {
159
+ // If anything that looks like ad content appears, treat as filled.
86
160
  if (isFilledNode(wrap)) {
87
161
  wrap.classList.remove('is-empty');
88
- tightenEzoicMinHeight(wrap);
162
+ tightenBurst();
163
+ }
164
+
165
+ // If Ezoic changes inline style on descendants (min-height:400!important), tighten again.
166
+ for (const m of muts) {
167
+ if (m.type === 'attributes' && m.attributeName === 'style') {
168
+ try { tightenEzoicMinHeight(wrap); } catch (e) {}
169
+ break;
170
+ }
171
+ }
172
+
173
+ // Disconnect only after the burst window to avoid missing late style rewrites.
174
+ if (now() - start > 7000) {
89
175
  try { obs.disconnect(); } catch (e) {}
90
176
  wrap.__ezFillObs = null;
91
177
  }
92
178
  });
93
- obs.observe(wrap, { childList: true, subtree: true });
179
+
180
+ obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
94
181
  wrap.__ezFillObs = obs;
95
182
  } catch (e) {}
96
183
  }
97
184
 
185
+ // Global safety net: sometimes Ezoic swaps nodes in ways that bypass our per-wrap observers.
186
+ // When we see an Ezoic container with min-height:400!important inside posts/topics, shrink it.
187
+ function globalGapFixInit() {
188
+ try {
189
+ if (window.__nodebbEzoicGapFix) return;
190
+ window.__nodebbEzoicGapFix = true;
191
+
192
+ const inPostArea = (el) => {
193
+ try {
194
+ return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
195
+ } catch (e) { return false; }
196
+ };
197
+
198
+ const maybeFix = (root) => {
199
+ if (!root || !root.querySelectorAll) return;
200
+ const nodes = root.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]');
201
+ nodes.forEach((n) => {
202
+ const st = (n.getAttribute('style') || '').toLowerCase();
203
+ if (!st.includes('min-height:400')) return;
204
+ if (!inPostArea(n)) return;
205
+ try {
206
+ const tmpWrap = n.closest('.' + WRAP_CLASS) || n.parentElement;
207
+ tightenEzoicMinHeight(tmpWrap || n);
208
+ } catch (e) {}
209
+ });
210
+ };
211
+
212
+ requestAnimationFrame(() => maybeFix(document));
213
+
214
+ const obs = new MutationObserver((muts) => {
215
+ for (const m of muts) {
216
+ const t = m.target;
217
+ if (m.type === 'attributes') {
218
+ maybeFix(t && t.nodeType === 1 ? t : t && t.parentElement);
219
+ } else if (m.addedNodes && m.addedNodes.length) {
220
+ m.addedNodes.forEach((n) => {
221
+ if (n && n.nodeType === 1) maybeFix(n);
222
+ });
223
+ }
224
+ }
225
+ });
226
+ obs.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
227
+ } catch (e) {}
228
+ }
229
+
98
230
  // ---------------- state ----------------
99
231
 
100
232
  const state = {
@@ -1024,6 +1156,7 @@ function watchWrapForFill(wrap) {
1024
1156
 
1025
1157
  warmUpNetwork();
1026
1158
  patchShowAds();
1159
+ globalGapFixInit();
1027
1160
  ensurePreloadObserver();
1028
1161
  ensureDomObserver();
1029
1162
 
package/public/style.css CHANGED
@@ -71,3 +71,11 @@
71
71
 
72
72
  /* Ensure Ezoic reportline doesn't affect layout */
73
73
  .nodebb-ezoic-wrap .reportline{position:absolute!important;}
74
+
75
+ /* Ezoic sometimes injects `position: sticky` inside placements. In long NodeBB topics,
76
+ this can create "gliding" and sudden disappear/reappear effects while scrolling.
77
+ We neutralize sticky positioning *inside our injected wrappers* only. */
78
+ .nodebb-ezoic-wrap .ezads-sticky-intradiv {
79
+ position: static !important;
80
+ top: auto !important;
81
+ }