nodebb-plugin-ezoic-infinite 1.4.68 → 1.4.69

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.4.68",
3
+ "version": "1.4.69",
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
@@ -1,828 +1,450 @@
1
1
  (function () {
2
2
  'use strict';
3
- const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
4
- const SELECTORS = {
5
- topicItem: 'li[component="category/topic"]',
6
- postItem: '[component="post"][data-pid]',
7
- categoryItem: 'li[component="categories/category"]',
8
- }, WRAP_CLASS = 'ezoic-ad';
9
- const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
10
-
11
- const sessionDefinedIds = new Set();
12
-
13
- const insertingIds = new Set(), state = {
14
- pageKey: null,
15
- cfg: null,
16
- cfgPromise: null,
17
3
 
18
- poolTopics: [],
19
- poolPosts: [],
20
- poolCategories: [],
4
+ /**
5
+ * NodeBB + Ezoic infinite ad injector
6
+ * Production-ready refactor:
7
+ * - No blank space: wrappers hidden until filled, removed on timeout
8
+ * - Bounded work per tick + throttled MutationObserver
9
+ * - Clean ajaxify navigation handling
10
+ */
21
11
 
22
- usedTopics: new Set(),
23
- usedPosts: new Set(),
24
- usedCategories: new Set(),
12
+ const SELECTORS = {
13
+ topicItem: 'li[component="category/topic"]',
14
+ categoryItem: 'li[component="categories/category"]',
15
+ postItem: '[component="post"][data-pid]',
16
+ postContent: '[component="post/content"]',
17
+ };
25
18
 
26
- lastShowById: new Map(),
27
- pendingById: new Set(),
28
- definedIds: new Set(),
19
+ const CLASS_WRAP = 'ezoic-ad';
20
+ const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
29
21
 
30
- scheduled: false,
31
- timer: null,
22
+ const MAX_INSERTS_PER_TICK = 3;
23
+ const FILL_TIMEOUT_MS = 3500;
24
+ const RECYCLE_MARGIN_PX = 600;
25
+ const SHOW_DEBOUNCE_MS = 500;
32
26
 
33
- obs: null,
34
- activeTimeouts: new Set(),
35
- lastScrollRun: 0, };
27
+ const state = {
28
+ pageKey: null,
29
+ cfg: null,
30
+ cfgPromise: null,
36
31
 
37
- function normalizeBool(v) {
38
- return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
39
- }
32
+ pools: { topics: [], categories: [], messages: [] },
33
+ live: { topics: [], categories: [], messages: [] },
40
34
 
41
- function uniqInts(lines) {
42
- const out = [], seen = new Set();
43
- for (const v of lines) {
44
- const n = parseInt(v, 10);
45
- if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
46
- seen.add(n);
47
- out.push(n);
48
- }
49
- }
50
- return out;
51
- }
35
+ scheduled: false,
36
+ lastRunAt: 0,
37
+ timeouts: new Set(),
38
+ observer: null,
39
+ lastShowById: new Map(),
40
+ };
52
41
 
53
- function parsePool(raw) {
54
- if (!raw) return [];
55
- const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
56
- return uniqInts(lines);
57
- }
42
+ function now() { return Date.now(); }
58
43
 
59
44
  function getPageKey() {
60
- try {
61
- const ax = window.ajaxify;
62
- if (ax && ax.data) {
63
- if (ax.data.tid) return `topic:${ax.data.tid}`;
64
- if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
65
- }
66
- } catch (e) {}
67
- return window.location.pathname;
45
+ try {
46
+ const ax = window.ajaxify;
47
+ if (ax && ax.data) {
48
+ if (ax.data.tid) return `topic:${ax.data.tid}`;
49
+ if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
50
+ }
51
+ } catch (e) {}
52
+ return window.location.pathname || '';
68
53
  }
69
54
 
70
55
  function getKind() {
71
- const p = window.location.pathname || '';
72
- if (/^\/topic\//.test(p)) return 'topic';
73
- if (/^\/category\//.test(p)) return 'categoryTopics';
74
- if (p === '/' || /^\/categories/.test(p)) return 'categories';
75
- // fallback by DOM
76
- if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
77
- if (document.querySelector(SELECTORS.postItem)) return 'topic';
78
- if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
79
- return 'other';
80
- }
81
-
82
- function getTopicItems() {
83
- return Array.from(document.querySelectorAll(SELECTORS.topicItem));
84
- }
85
-
86
- function getCategoryItems() {
87
- return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
88
- }
89
-
90
- function getPostContainers() {
91
- const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
92
- return nodes.filter((el) => {
93
- if (!el || !el.isConnected) return false;
94
- if (!el.querySelector('[component="post/content"]')) return false;
95
- const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
96
- if (parentPost && parentPost !== el) return false;
97
- if (el.getAttribute('component') === 'post/parent') return false;
98
- return true;
99
- });
100
- }
101
-
102
- function safeRect(el) {
103
- try { return el.getBoundingClientRect(); } catch (e) { return null; }
104
- }
105
-
106
- function destroyPlaceholderIds(ids) {
107
- if (!ids || !ids.length) return;
108
- const filtered = ids.filter((id) => {
109
- try { return sessionDefinedIds.has(id); } catch (e) { return true; }
110
- });
111
- if (!filtered.length) return;
112
-
113
- const call = () => {
114
- try {
115
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
116
- window.ezstandalone.destroyPlaceholders(filtered);
117
- }
118
- } catch (e) {}
119
-
120
- // Recyclage: libérer IDs après 100ms
121
- setTimeout(() => {
122
- filtered.forEach(id => sessionDefinedIds.delete(id));
123
- }, 100);
124
- };
125
- try {
126
- window.ezstandalone = window.ezstandalone || {};
127
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
128
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
129
- else window.ezstandalone.cmd.push(call);
130
- } catch (e) {}
131
-
132
- // Recyclage: libérer IDs après 100ms
133
- setTimeout(() => {
134
- filtered.forEach(id => sessionDefinedIds.delete(id));
135
- }, 100);
136
- }
137
-
138
- // Nettoyer éléments Ezoic invisibles qui créent espace vertical
139
- function cleanupInvisibleEzoicElements() {
140
- try {
141
- document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
142
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
143
- if (!ph) return;
144
-
145
- // Supprimer TOUS les éléments après le placeholder rempli
146
- // qui créent de l'espace vertical
147
- let found = false;
148
- Array.from(wrapper.children).forEach(child => {
149
- if (child === ph || child.contains(ph)) {
150
- found = true;
151
- return;
152
- }
153
-
154
- // Si élément APRÈS le placeholder
155
- if (found) {
156
- const rect = child.getBoundingClientRect();
157
- const computed = window.getComputedStyle(child);
158
-
159
- // Supprimer si:
160
- // 1. Height > 0 mais pas de texte/image visible
161
- // 2. Ou opacity: 0
162
- // 3. Ou visibility: hidden
163
- const hasContent = child.textContent.trim().length > 0 ||
164
- child.querySelector('img, iframe, video');
165
-
166
- if (!hasContent || computed.opacity === '0' || computed.visibility === 'hidden') {
167
- child.remove();
168
- }
169
- }
170
- });
171
- });
172
- } catch (e) {}
173
- }
174
-
175
- function cleanupEmptyWrappers() {
176
- try {
177
- document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
178
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
179
- if (ph && ph.children.length === 0) {
180
- // Placeholder vide après 3s = pub non chargée
181
- setTimeout(() => {
182
- if (ph.children.length === 0) {
183
- wrapper.remove();
184
- }
185
- }, 1500);
186
- }
187
- });
188
- } catch (e) {}
189
- }
190
-
191
- function getRecyclable(liveArr) {
192
- const margin = 600;
193
- for (let i = 0; i < liveArr.length; i++) {
194
- const entry = liveArr[i];
195
- if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
196
- const r = safeRect(entry.wrap);
197
- if (r && r.bottom < -margin) {
198
- liveArr.splice(i, 1);
199
- return entry;
200
- }
201
- }
202
- return null;
203
- }
204
-
205
- function pickId(pool, liveArr) {
206
- if (pool.length) return { id: pool.shift(), recycled: null };
207
- const recycled = getRecyclable(liveArr);
208
- if (recycled) return { id: recycled.id, recycled };
209
- return { id: null, recycled: null };
56
+ const p = window.location.pathname || '';
57
+ if (/^\/topic\//.test(p)) return 'topic';
58
+ if (/^\/category\//.test(p)) return 'categoryTopics';
59
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
60
+
61
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
62
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
63
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
64
+ return 'other';
65
+ }
66
+
67
+ function parseIdPool(raw) {
68
+ if (!raw) return [];
69
+ const tokens = String(raw)
70
+ .split(/[\s,;]+/)
71
+ .map(s => s.trim())
72
+ .filter(Boolean);
73
+
74
+ const out = [];
75
+ const seen = new Set();
76
+ for (const t of tokens) {
77
+ const n = parseInt(t, 10);
78
+ if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
79
+ seen.add(n);
80
+ out.push(n);
81
+ }
82
+ }
83
+ return out;
210
84
  }
211
85
 
212
- function resetPlaceholderInWrap(wrap, id) {
213
- try {
214
- if (!wrap) return;
215
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
216
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
217
- const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
218
- if (old) old.remove();
219
- // Remove any leftover markup inside wrapper
220
- wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
221
- const ph = document.createElement('div');
222
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
223
- wrap.appendChild(ph);
224
- } catch (e) {}
225
- }
226
-
227
- function isAdjacentAd(target) {
228
- if (!target || !target.nextElementSibling) return false;
229
- const next = target.nextElementSibling;
230
- if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
231
- return false;
232
- }
233
-
234
- function isPrevAd(target) {
235
- if (!target || !target.previousElementSibling) return false;
236
- const prev = target.previousElementSibling;
237
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
238
- return false;
239
- }
240
-
241
- function buildWrap(id, kindClass, afterPos) {
242
- const wrap = document.createElement('div');
243
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
244
- wrap.setAttribute('data-ezoic-after', String(afterPos));
245
- wrap.style.width = '100%';
246
-
247
- const ph = document.createElement('div');
248
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
249
- wrap.appendChild(ph);
250
- return wrap;
251
- }
252
-
253
- function findWrap(kindClass, afterPos) {
254
- return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
255
- }
256
-
257
- function insertAfter(target, id, kindClass, afterPos) {
258
- if (!target || !target.insertAdjacentElement) return null;
259
- if (findWrap(kindClass, afterPos)) return null;
260
-
261
- // CRITICAL: Double-lock pour éviter race conditions sur les doublons
262
- if (insertingIds.has(id)) return null;
263
-
264
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
265
- if (existingPh && existingPh.isConnected) return null;
266
-
267
- // Acquérir le lock
268
- insertingIds.add(id);
269
-
270
- try {
271
- const wrap = buildWrap(id, kindClass, afterPos);
272
- target.insertAdjacentElement('afterend', wrap);
273
- attachFillObserver(wrap, id);
274
- return wrap;
275
- } finally {
276
- setTimeout(() => insertingIds.delete(id), 50);
277
- }
278
- }
279
-
280
- function destroyUsedPlaceholders() {
281
- const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
282
- if (ids.length) destroyPlaceholderIds(ids);
283
- }
284
-
285
- function patchShowAds() {
286
- const applyPatch = () => {
287
- try {
288
- window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
289
- if (window.__nodebbEzoicPatched) return;
290
- if (typeof ez.showAds !== 'function') return;
291
-
292
- window.__nodebbEzoicPatched = true;
293
- const orig = ez.showAds;
294
-
295
- ez.showAds = function (arg) {
296
- if (Array.isArray(arg)) {
297
- const seen = new Set();
298
- for (const v of arg) {
299
- const id = parseInt(v, 10);
300
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
301
- seen.add(id);
302
- try { orig.call(ez, id); } catch (e) {}
303
- }
304
- return;
305
- }
306
- return orig.apply(ez, arguments);
307
- };
308
- } catch (e) {}
309
- };
310
-
311
- applyPatch();
312
- if (!window.__nodebbEzoicPatched) {
313
- try {
314
- window.ezstandalone = window.ezstandalone || {};
315
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
316
- window.ezstandalone.cmd.push(applyPatch);
317
- } catch (e) {}
318
- }
86
+ async function fetchConfig() {
87
+ if (state.cfg) return state.cfg;
88
+ if (state.cfgPromise) return state.cfgPromise;
89
+
90
+ state.cfgPromise = fetch('/api/plugins/ezoic-infinite/config', {
91
+ credentials: 'same-origin',
92
+ headers: { 'Accept': 'application/json' },
93
+ })
94
+ .then(r => (r && r.ok ? r.json() : null))
95
+ .catch(() => null)
96
+ .then((cfg) => {
97
+ state.cfg = cfg || { excluded: true };
98
+ state.pools.topics = parseIdPool(state.cfg.placeholderIds);
99
+ state.pools.categories = parseIdPool(state.cfg.categoryPlaceholderIds);
100
+ state.pools.messages = parseIdPool(state.cfg.messagePlaceholderIds);
101
+ return state.cfg;
102
+ })
103
+ .finally(() => { state.cfgPromise = null; });
104
+
105
+ return state.cfgPromise;
106
+ }
107
+
108
+ function rect(el) {
109
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
110
+ }
111
+
112
+ function takeRecyclable(kind) {
113
+ const arr = state.live[kind];
114
+ for (let i = 0; i < arr.length; i++) {
115
+ const entry = arr[i];
116
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) {
117
+ arr.splice(i, 1); i--; continue;
118
+ }
119
+ const r = rect(entry.wrap);
120
+ if (r && r.bottom < -RECYCLE_MARGIN_PX) {
121
+ arr.splice(i, 1);
122
+ return entry;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function pickId(kind) {
129
+ const pool = state.pools[kind];
130
+ if (pool && pool.length) return { id: pool.shift(), recycled: null };
131
+ const recycled = takeRecyclable(kind);
132
+ if (recycled) return { id: recycled.id, recycled };
133
+ return { id: null, recycled: null };
134
+ }
135
+
136
+ function buildWrap(kind, afterIndex, id) {
137
+ const wrap = document.createElement('div');
138
+ wrap.className = `${CLASS_WRAP} ${CLASS_WRAP}--${kind}`;
139
+ wrap.dataset.ezoicKind = kind;
140
+ wrap.dataset.ezoicAfter = String(afterIndex);
141
+ wrap.dataset.ezoicId = String(id);
142
+
143
+ const ph = document.createElement('div');
144
+ ph.id = `${PH_PREFIX}${id}`;
145
+ wrap.appendChild(ph);
146
+
147
+ return wrap;
148
+ }
149
+
150
+ function findWrap(kind, afterIndex) {
151
+ return document.querySelector(`.${CLASS_WRAP}[data-ezoic-kind="${kind}"][data-ezoic-after="${afterIndex}"]`);
152
+ }
153
+
154
+ function resetWrapPlaceholder(wrap, id) {
155
+ if (!wrap) return;
156
+ if (wrap.__ezoicObs) {
157
+ try { wrap.__ezoicObs.disconnect(); } catch (e) {}
158
+ wrap.__ezoicObs = null;
159
+ }
160
+ wrap.removeAttribute('data-ezoic-filled');
161
+ wrap.dataset.ezoicId = String(id);
162
+
163
+ while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
164
+ const ph = document.createElement('div');
165
+ ph.id = `${PH_PREFIX}${id}`;
166
+ wrap.appendChild(ph);
167
+ }
168
+
169
+ function forcePlaceholderAutoHeight(wrap, id) {
170
+ const ph = wrap && wrap.querySelector ? wrap.querySelector(`#${PH_PREFIX}${id}`) : null;
171
+ if (!ph) return;
172
+ ph.style.setProperty('height', 'auto', 'important');
173
+ ph.style.setProperty('min-height', '0px', 'important');
174
+ requestAnimationFrame(() => {
175
+ ph.style.setProperty('height', 'auto', 'important');
176
+ ph.style.setProperty('min-height', '0px', 'important');
177
+ });
319
178
  }
320
179
 
321
180
  function markFilled(wrap) {
322
- try {
323
- if (!wrap) return;
324
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
325
- wrap.setAttribute('data-ezoic-filled', '1');
326
- } catch (e) {}
327
- }
328
-
329
- function isWrapMarkedFilled(wrap) {
330
- try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
331
- }
181
+ if (!wrap || !wrap.isConnected) return;
182
+ if (wrap.getAttribute('data-ezoic-filled') === '1') return;
183
+ wrap.setAttribute('data-ezoic-filled', '1');
184
+
185
+ const id = parseInt(wrap.dataset.ezoicId, 10);
186
+ if (Number.isFinite(id) && id > 0) forcePlaceholderAutoHeight(wrap, id);
187
+ }
188
+
189
+ function removeWrap(wrap) {
190
+ if (!wrap) return;
191
+ if (wrap.__ezoicObs) {
192
+ try { wrap.__ezoicObs.disconnect(); } catch (e) {}
193
+ wrap.__ezoicObs = null;
194
+ }
195
+ try { wrap.remove(); } catch (e) {}
196
+ }
197
+
198
+ function showEzoicAd(id) {
199
+ if (!id || id <= 0) return;
200
+
201
+ const last = state.lastShowById.get(id) || 0;
202
+ if (now() - last < SHOW_DEBOUNCE_MS) return;
203
+ state.lastShowById.set(id, now());
204
+
205
+ const call = () => {
206
+ try {
207
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
208
+ window.ezstandalone.showAds(id);
209
+ }
210
+ } catch (e) {}
211
+ };
212
+
213
+ try {
214
+ window.ezstandalone = window.ezstandalone || {};
215
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
216
+ if (typeof window.ezstandalone.showAds === 'function') call();
217
+ else window.ezstandalone.cmd.push(call);
218
+ } catch (e) {}
219
+ }
220
+
221
+ function observeFill(wrap, id) {
222
+ const ph = wrap.querySelector(`#${PH_PREFIX}${id}`);
223
+ if (!ph) return;
224
+
225
+ // immediate
226
+ if (ph.querySelector('iframe, ins, img') || ph.childElementCount > 0) {
227
+ markFilled(wrap);
228
+ return;
229
+ }
332
230
 
333
- function attachFillObserver(wrap, id) {
334
- try {
335
- const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
336
- if (!ph) return;
337
- // Already filled?
338
- if (ph.childNodes && ph.childNodes.length > 0) {
339
- markFilled(wrap); // Afficher wrapper
340
- sessionDefinedIds.add(id);
341
- return;
342
- }
343
- const obs = new MutationObserver(() => {
344
- if (ph.childNodes && ph.childNodes.length > 0) {
345
- markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
346
- try { sessionDefinedIds.add(id); } catch (e) {}
347
- try { obs.disconnect(); } catch (e) {}
348
- }
349
- });
350
- obs.observe(ph, { childList: true, subtree: true });
351
- wrap.__ezoicFillObs = obs;
352
- } catch (e) {}
353
- }
231
+ const obs = new MutationObserver(() => {
232
+ if (!wrap.isConnected) return;
233
+ const hasCreative = !!ph.querySelector('iframe, ins, img');
234
+ const r = rect(ph);
235
+ if (hasCreative || (r && r.height > 20)) {
236
+ markFilled(wrap);
237
+ try { obs.disconnect(); } catch (e) {}
238
+ wrap.__ezoicObs = null;
239
+ }
240
+ });
354
241
 
355
- function isPlaceholderFilled(id) {
356
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
357
- if (!ph || !ph.isConnected) return false;
242
+ wrap.__ezoicObs = obs;
243
+ try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (e) {}
244
+ }
358
245
 
359
- const wrap = ph.parentElement;
360
- if (wrap && isWrapMarkedFilled(wrap)) return true;
361
-
362
- const filled = !!(ph.childNodes && ph.childNodes.length > 0);
363
- if (filled) {
364
- try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
365
- try { markFilled(wrap); } catch (e) {}
246
+ function scheduleRemovalIfUnfilled(wrap, kind, id) {
247
+ const t = setTimeout(() => {
248
+ state.timeouts.delete(t);
249
+ if (!wrap || !wrap.isConnected) return;
250
+ if (wrap.getAttribute('data-ezoic-filled') === '1') return;
251
+ removeWrap(wrap);
252
+ if (state.pools[kind] && typeof id === 'number' && id > 0) state.pools[kind].push(id);
253
+ }, FILL_TIMEOUT_MS);
254
+ state.timeouts.add(t);
366
255
  }
367
- return filled;
256
+
257
+ function insertAfter(target, wrap) {
258
+ if (!target || !wrap) return false;
259
+ const next = target.nextElementSibling;
260
+ if (next && next.classList && next.classList.contains(CLASS_WRAP)) return false;
261
+ try {
262
+ target.insertAdjacentElement('afterend', wrap);
263
+ return true;
264
+ } catch (e) {
265
+ return false;
266
+ }
368
267
  }
369
-
370
- let batchShowAdsTimer = null;
371
- const pendingShowAdsIds = new Set();
372
-
373
- function scheduleShowAdsBatch(id) {
374
- if (!id) return;
375
-
376
- if (sessionDefinedIds.has(id)) {
377
- try {
378
- destroyPlaceholderIds([id]);
379
- sessionDefinedIds.delete(id);
380
- } catch (e) {}
268
+
269
+ function computeAfterIndexes(count, interval, showFirst) {
270
+ const out = [];
271
+ const step = Math.max(1, parseInt(interval, 10) || 1);
272
+ const start = showFirst ? 1 : step;
273
+ for (let i = start; i < count; i += step) out.push(i);
274
+ return out;
381
275
  }
382
276
 
383
- // Throttle: ne pas rappeler le même ID trop vite
384
- const now = Date.now(), last = state.lastShowById.get(id) || 0;
385
- if (now - last < 3500) return;
386
-
387
- // Ajouter à la batch
388
- pendingShowAdsIds.add(id);
389
-
390
- clearTimeout(batchShowAdsTimer);
391
- batchShowAdsTimer = setTimeout(() => {
392
- if (pendingShowAdsIds.size === 0) return;
393
-
394
- const idsArray = Array.from(pendingShowAdsIds);
395
- pendingShowAdsIds.clear();
396
-
397
- // Appeler showAds avec TOUS les IDs en une fois
398
- try {
399
- window.ezstandalone = window.ezstandalone || {};
400
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
401
- window.ezstandalone.cmd.push(function() {
402
- if (typeof window.ezstandalone.showAds === 'function') {
403
- // Appel batch: showAds(id1, id2, id3...)
404
- window.ezstandalone.showAds(...idsArray);
405
- // Tracker tous les IDs
406
- idsArray.forEach(id => {
407
- state.lastShowById.set(id, Date.now());
408
- sessionDefinedIds.add(id);
409
- });
410
- }
411
- });
412
- } catch (e) {}
413
-
414
- // CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
415
- setTimeout(() => {
416
- cleanupInvisibleEzoicElements();
417
- }, 800); // 1.5s pour laisser Ezoic charger
418
- }, 100);
419
- }
277
+ function runCategoryTopics(cfg) {
278
+ if (!cfg.enableBetweenAds) return;
420
279
 
421
- function callShowAdsWhenReady(id) {
422
- if (!id) return;
280
+ const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
281
+ if (!items.length) return;
423
282
 
424
- const now = Date.now(), last = state.lastShowById.get(id) || 0;
425
- if (now - last < 3500) return;
283
+ const after = computeAfterIndexes(items.length, cfg.intervalPosts || 6, !!cfg.showFirstTopicAd);
284
+ let inserts = 0;
426
285
 
427
- const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
428
- try {
429
- window.ezstandalone = window.ezstandalone || {};
430
- if (typeof window.ezstandalone.showAds === 'function') {
286
+ for (const afterIndex of after) {
287
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
288
+ const anchor = items[afterIndex - 1];
289
+ if (!anchor) continue;
290
+ if (findWrap('topics', afterIndex)) continue;
431
291
 
432
- state.lastShowById.set(id, Date.now());
433
- window.ezstandalone.showAds(id);
434
- sessionDefinedIds.add(id);
435
- return true;
436
- }
437
- } catch (e) {}
438
- return false;
439
- };
440
-
441
- const startPageKey = state.pageKey;
442
- let attempts = 0;
443
- (function waitForPh() {
444
- if (state.pageKey !== startPageKey) return;
445
- if (state.pendingById.has(id)) return;
446
-
447
- attempts += 1;
448
- const el = document.getElementById(phId);
449
- if (el && el.isConnected) {
450
-
451
- // Si on arrive ici, soit visible, soit timeout
452
-
453
- if (doCall()) {
454
- state.pendingById.delete(id);
455
- return;
456
- }
457
-
458
- }
459
-
460
- if (attempts < 100) {
461
- const timeoutId = setTimeout(waitForPh, 50);
462
- state.activeTimeouts.add(timeoutId);
463
- }
464
- })();
465
- }
466
-
467
- async function fetchConfig() {
468
- if (state.cfg) return state.cfg;
469
- if (state.cfgPromise) return state.cfgPromise;
470
-
471
- state.cfgPromise = (async () => {
472
- const MAX_TRIES = 3;
473
- let delay = 800;
474
- for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
475
- try {
476
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
477
- if (res.ok) {
478
- state.cfg = await res.json();
479
- return state.cfg;
480
- }
481
- } catch (e) {}
482
- if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
483
- delay *= 2;
484
- }
485
- return null;
486
- })();
292
+ const { id, recycled } = pickId('topics');
293
+ if (!id) break;
487
294
 
488
- try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
489
- }
295
+ const wrap = recycled ? recycled.wrap : buildWrap('topics', afterIndex, id);
296
+ if (recycled) resetWrapPlaceholder(wrap, id);
490
297
 
491
- function initPools(cfg) {
492
- if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
493
- if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
494
- if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
495
- }
298
+ if (!insertAfter(anchor, wrap)) { state.pools.topics.push(id); continue; }
496
299
 
497
- function computeTargets(count, interval, showFirst) {
498
- const out = [];
499
- if (count <= 0) return out;
500
- if (showFirst) out.push(1);
501
- for (let i = 1; i <= count; i++) {
502
- if (i % interval === 0) out.push(i);
503
- }
504
- return Array.from(new Set(out)).sort((a, b) => a - b);
300
+ state.live.topics.push({ id, wrap });
301
+ observeFill(wrap, id);
302
+ scheduleRemovalIfUnfilled(wrap, 'topics', id);
303
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
304
+ inserts++;
305
+ }
505
306
  }
506
307
 
507
- function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
508
- if (!items.length) return 0;
509
- const targets = computeTargets(items.length, interval, showFirst);
510
-
511
- let inserted = 0;
512
- for (const afterPos of targets) {
513
- if (inserted >= MAX_INSERTS_PER_RUN) break;
514
-
515
- const el = items[afterPos - 1];
516
- if (!el || !el.isConnected) continue;
308
+ function runCategories(cfg) {
309
+ if (!cfg.enableCategoryAds) return;
517
310
 
518
- if (isAdjacentAd(el) || isPrevAd(el)) {
519
- continue;
520
- }
311
+ const items = Array.from(document.querySelectorAll(SELECTORS.categoryItem));
312
+ if (!items.length) return;
521
313
 
522
- // Prevent back-to-back at load
523
- const prevWrap = findWrap(kindClass, afterPos - 1);
524
- if (prevWrap) continue;
314
+ const after = computeAfterIndexes(items.length, cfg.intervalCategories || 4, !!cfg.showFirstCategoryAd);
315
+ let inserts = 0;
525
316
 
526
- if (findWrap(kindClass, afterPos)) continue;
317
+ for (const afterIndex of after) {
318
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
319
+ const anchor = items[afterIndex - 1];
320
+ if (!anchor) continue;
321
+ if (findWrap('categories', afterIndex)) continue;
527
322
 
528
- const pick = pickId(kindPool, []);
529
- const id = pick.id;
530
- if (!id) break;
323
+ const { id, recycled } = pickId('categories');
324
+ if (!id) break;
531
325
 
532
- let wrap = null;
533
- if (pick.recycled && pick.recycled.wrap) {
534
- if (sessionDefinedIds.has(id)) {
535
- destroyPlaceholderIds([id]);
536
- }
537
- const oldWrap = pick.recycled.wrap;
538
- try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
539
- try { oldWrap && oldWrap.remove(); } catch (e) {}
540
- wrap = insertAfter(el, id, kindClass, afterPos);
541
- if (!wrap) continue;
542
- setTimeout(() => {
543
- callShowAdsWhenReady(id);
544
- }, 50);
545
- } else {
546
- usedSet.add(id);
547
- wrap = insertAfter(el, id, kindClass, afterPos);
548
- if (!wrap) continue;
549
- // Micro-délai pour laisser le DOM se synchroniser
550
- // Appel immédiat au lieu de 10ms delay
551
- callShowAdsWhenReady(id);
552
- }
326
+ const wrap = recycled ? recycled.wrap : buildWrap('categories', afterIndex, id);
327
+ if (recycled) resetWrapPlaceholder(wrap, id);
553
328
 
554
- liveArr.push({ id, wrap });
555
- if (wrap && (
556
- (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
557
- )) {
558
- try { wrap.remove(); } catch (e) {}
559
- if (!(pick.recycled && pick.recycled.wrap)) {
560
- try { kindPool.unshift(id); } catch (e) {}
561
- usedSet.delete(id);
562
- }
563
- continue;
564
- }
565
- inserted += 1;
566
- }
567
- return inserted;
568
- }
329
+ if (!insertAfter(anchor, wrap)) { state.pools.categories.push(id); continue; }
569
330
 
570
- function enforceNoAdjacentAds() {
571
- const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
572
- for (let i = 0; i < ads.length; i++) {
573
- const ad = ads[i], prev = ad.previousElementSibling;
574
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
575
- try {
576
- const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
577
- if (ph) {
578
- const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
579
- if (Number.isFinite(id) && id > 0) {
580
- // Détruire le placeholder si Ezoic l'a déjà défini
581
- if (sessionDefinedIds.has(id)) {
582
- destroyPlaceholderIds([id]);
583
- }
584
- }
331
+ state.live.categories.push({ id, wrap });
332
+ observeFill(wrap, id);
333
+ scheduleRemovalIfUnfilled(wrap, 'categories', id);
334
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
335
+ inserts++;
336
+ }
585
337
  }
586
- ad.remove();
587
- } catch (e) {}
588
- }
589
- }
590
- }
591
-
592
- function cleanup() {
593
- destroyUsedPlaceholders();
594
-
595
- document.querySelectorAll('.ezoic-ad').forEach(el => {
596
- try { el.remove(); } catch (e) {}
597
- });
598
338
 
599
- state.pageKey = getPageKey();
600
- state.cfg = null;
601
- state.cfgPromise = null;
339
+ function runMessageAds(cfg) {
340
+ if (!cfg.enableMessageAds) return;
602
341
 
603
- state.poolTopics = [];
604
- state.poolPosts = [];
605
- state.poolCategories = [];
606
- state.usedTopics.clear();
607
- state.usedPosts.clear();
608
- state.usedCategories.clear();
609
- state.lastShowById.clear();
610
- state.pendingById.clear();
611
- state.definedIds.clear();
342
+ const posts = Array.from(document.querySelectorAll(SELECTORS.postItem))
343
+ .filter(p => p && p.isConnected && p.querySelector(SELECTORS.postContent));
344
+ if (!posts.length) return;
612
345
 
613
- state.activeTimeouts.forEach(id => {
614
- try { clearTimeout(id); } catch (e) {}
615
- });
616
- state.activeTimeouts.clear();
346
+ const after = computeAfterIndexes(posts.length, cfg.messageIntervalPosts || 3, !!cfg.showFirstMessageAd);
347
+ let inserts = 0;
617
348
 
618
- state.pendingById.clear();
349
+ for (const afterIndex of after) {
350
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
351
+ const anchor = posts[afterIndex - 1];
352
+ if (!anchor) continue;
353
+ if (findWrap('messages', afterIndex)) continue;
619
354
 
620
- if (state.obs) { state.obs.disconnect(); state.obs = null; }
355
+ const { id, recycled } = pickId('messages');
356
+ if (!id) break;
621
357
 
622
- state.scheduled = false;
623
- clearTimeout(state.timer);
624
- state.timer = null;
625
- }
358
+ const wrap = recycled ? recycled.wrap : buildWrap('messages', afterIndex, id);
359
+ wrap.classList.add('ezoic-ad--message');
360
+ if (recycled) resetWrapPlaceholder(wrap, id);
626
361
 
627
- function ensureObserver() {
628
- if (state.obs) return;
629
- state.obs = new MutationObserver(() => scheduleRun('mutation'));
630
- try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
631
- }
362
+ if (!insertAfter(anchor, wrap)) { state.pools.messages.push(id); continue; }
632
363
 
633
- async function runCore() {
634
- if (!state.canShowAds) {
635
- return;
364
+ state.live.messages.push({ id, wrap });
365
+ observeFill(wrap, id);
366
+ scheduleRemovalIfUnfilled(wrap, 'messages', id);
367
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
368
+ inserts++;
369
+ }
636
370
  }
637
371
 
638
- patchShowAds();
639
-
640
- const cfg = await fetchConfig();
641
- if (!cfg || cfg.excluded) return;
372
+ function cleanupForNavigation() {
373
+ for (const t of state.timeouts) clearTimeout(t);
374
+ state.timeouts.clear();
642
375
 
643
- initPools(cfg);
376
+ if (state.observer) {
377
+ try { state.observer.disconnect(); } catch (e) {}
378
+ state.observer = null;
379
+ }
644
380
 
645
- const kind = getKind();
646
- let inserted = 0;
647
-
648
- if (kind === 'topic') {
649
- if (normalizeBool(cfg.enableMessageAds)) {
650
- inserted = injectBetween('ezoic-ad-message', getPostContainers(),
651
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
652
- normalizeBool(cfg.showFirstMessageAd),
653
- state.poolPosts,
654
- state.usedPosts);
655
- }
656
- } else if (kind === 'categoryTopics') {
657
- if (normalizeBool(cfg.enableBetweenAds)) {
658
- inserted = injectBetween('ezoic-ad-between', getTopicItems(),
659
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
660
- normalizeBool(cfg.showFirstTopicAd),
661
- state.poolTopics,
662
- state.usedTopics);
663
- }
664
- } else if (kind === 'categories') {
665
- if (normalizeBool(cfg.enableCategoryAds)) {
666
- inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
667
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
668
- normalizeBool(cfg.showFirstCategoryAd),
669
- state.poolCategories,
670
- state.usedCategories);
671
- }
672
- }
673
-
674
- enforceNoAdjacentAds();
675
-
676
- let count = 0;
677
- if (kind === 'topic') count = getPostContainers().length;
678
- else if (kind === 'categoryTopics') count = getTopicItems().length;
679
- else if (kind === 'categories') count = getCategoryItems().length;
680
-
681
- if (count === 0 && 0 < 25) {
682
- setTimeout(arguments[0], 50);
683
- return;
684
- }
685
-
686
- if (inserted >= MAX_INSERTS_PER_RUN) {
687
- setTimeout(arguments[0], 50);
688
- } else if (inserted === 0 && count > 0) {
689
- // Pool épuisé ou recyclage pas encore disponible.
690
- if (state.poolWaitAttempts < 8) {
691
- state.poolWaitAttempts += 1;
692
- setTimeout(arguments[0], 50);
693
- } else {
694
- }
695
- } else if (inserted > 0) {
696
- }
381
+ state.live.topics = [];
382
+ state.live.categories = [];
383
+ state.live.messages = [];
384
+ state.lastShowById.clear();
697
385
  }
698
386
 
699
387
  function scheduleRun() {
700
- if (state.scheduled) return;
701
- state.scheduled = true;
702
-
703
- clearTimeout(state.timer);
704
- state.timer = setTimeout(() => {
705
- state.scheduled = false;
706
- const pk = getPageKey();
707
- if (state.pageKey && pk !== state.pageKey) return;
708
- runCore().catch(() => {});
709
- }, 50);
710
- }
711
-
712
- function bind() {
713
- if (!$) return;
714
-
715
- $(window).off('.ezoicInfinite');
716
-
717
- $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
718
-
719
- $(window).on('action:ajaxify.end.ezoicInfinite', () => {
720
- state.pageKey = getPageKey();
721
- ensureObserver();
722
-
723
- state.canShowAds = true;
724
- });
725
-
726
- $(window).on('action:category.loaded.ezoicInfinite', () => {
727
- ensureObserver();
728
- waitForContentThenRun();
729
- });
730
- $(window).on('action:topics.loaded.ezoicInfinite', () => {
731
- ensureObserver();
732
- waitForContentThenRun();
733
- });
734
-
735
- $(window).on('action:topic.loaded.ezoicInfinite', () => {
736
- ensureObserver();
737
- waitForContentThenRun();
738
- });
739
-
740
- $(window).on('action:posts.loaded.ezoicInfinite', () => {
741
- ensureObserver();
742
- // posts.loaded = infinite scroll
743
- waitForContentThenRun();
744
- });
745
- }
746
-
747
- function bindScroll() {
748
- if (state.lastScrollRun > 0) return;
749
- state.lastScrollRun = Date.now();
750
- let ticking = false;
751
- window.addEventListener('scroll', () => {
752
- if (ticking) return;
753
- ticking = true;
754
- window.requestAnimationFrame(() => {
755
- ticking = false;
756
- enforceNoAdjacentAds();
757
- const now = Date.now();
758
- if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
759
- state.lastScrollRun = now;
760
- scheduleRun();
761
- }
762
- });
763
- }, { passive: true });
764
- }
765
-
766
- function waitForContentThenRun() {
767
- const MIN_WORDS = 250;
768
- let attempts = 0;
769
- const maxAttempts = 20; // 20 × 200ms = 4s max
770
-
771
- (function check() {
772
- attempts++;
773
-
774
- // Compter les mots sur la page
775
- const text = document.body.innerText || '';
776
- const wordCount = text.split(/\s+/).filter(Boolean).length;
777
-
778
- if (wordCount >= MIN_WORDS) {
779
- // Assez de contenu → lancer l'insertion
780
- scheduleRun();
781
- return;
782
- }
783
-
784
- // Pas assez de contenu
785
- if (attempts >= maxAttempts) {
786
- // Timeout après 4s → tenter quand même
787
- scheduleRun();
788
- return;
789
- }
790
-
791
- // Réessayer dans 200ms
792
- setTimeout(check, 50);
793
- })();
794
- }
795
-
796
- function waitForEzoicThenRun() {
797
- let attempts = 0;
798
- const maxAttempts = 50; // 50 × 200ms = 10s max
799
-
800
- (function check() {
801
- attempts++;
802
- // Vérifier si Ezoic est chargé
803
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
804
- // Ezoic est prêt → lancer l'insertion
805
- scheduleRun();
806
- waitForContentThenRun();
807
- return;
808
- }
809
- // Ezoic pas encore prêt
810
- if (attempts >= maxAttempts) {
811
- // Tenter quand même
812
- scheduleRun();
813
- return;
814
- }
815
- // Réessayer dans 200ms
816
- setTimeout(check, 50);
817
- })();
388
+ if (state.scheduled) return;
389
+ state.scheduled = true;
390
+ requestAnimationFrame(() => {
391
+ state.scheduled = false;
392
+ run().catch(() => {});
393
+ });
394
+ }
395
+
396
+ async function run() {
397
+ const pk = getPageKey();
398
+ if (pk !== state.pageKey) {
399
+ state.pageKey = pk;
400
+ cleanupForNavigation();
401
+ state.cfg = null;
402
+ state.cfgPromise = null;
403
+ }
404
+
405
+ const cfg = await fetchConfig();
406
+ if (!cfg || cfg.excluded) return;
407
+
408
+ const kind = getKind();
409
+ if (kind === 'categoryTopics') runCategoryTopics(cfg);
410
+ if (kind === 'categories') runCategories(cfg);
411
+ if (kind === 'topic') runMessageAds(cfg);
412
+
413
+ // remove obvious empty spacer nodes injected by networks
414
+ try {
415
+ document.querySelectorAll(`.${CLASS_WRAP} > div:empty`).forEach(n => n.remove());
416
+ } catch (e) {}
417
+ }
418
+
419
+ function start() {
420
+ scheduleRun();
421
+
422
+ state.observer = new MutationObserver(() => {
423
+ const t = now();
424
+ if (t - state.lastRunAt < 200) return;
425
+ state.lastRunAt = t;
426
+ scheduleRun();
427
+ });
428
+
429
+ try { state.observer.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
430
+
431
+ // NodeBB ajaxify hook
432
+ if (window.jQuery && window.jQuery(window).on) {
433
+ window.jQuery(window).on('action:ajaxify.end', () => {
434
+ state.pageKey = null;
435
+ scheduleRun();
436
+ });
437
+ } else {
438
+ window.addEventListener('popstate', () => {
439
+ state.pageKey = null;
440
+ scheduleRun();
441
+ });
442
+ }
443
+ }
444
+
445
+ if (document.readyState === 'loading') {
446
+ document.addEventListener('DOMContentLoaded', start);
447
+ } else {
448
+ start();
818
449
  }
819
-
820
- cleanup();
821
- bind();
822
- bindScroll();
823
- ensureObserver();
824
- state.pageKey = getPageKey();
825
-
826
- // Attendre que Ezoic soit chargé avant d'insérer
827
- waitForEzoicThenRun();
828
- })();
450
+ })();
package/public/style.css CHANGED
@@ -1,10 +1,41 @@
1
+ /*
2
+ Ezoic Infinite Ads
3
+ - Wrappers are hidden until ad is detected as filled (data-ezoic-filled="1").
4
+ - Prevent reserved heights/margins creating blank space under creatives.
5
+ */
6
+
1
7
  .ezoic-ad {
2
- height: auto !important;
3
- padding: 0 !important;
4
- margin: 0 !important;
8
+ display: none;
9
+ width: 100%;
10
+ clear: both;
11
+ margin: 0;
12
+ padding: 0;
5
13
  }
6
14
 
7
- .ezoic-ad * {
15
+ .ezoic-ad[data-ezoic-filled="1"] {
16
+ display: block;
17
+ }
18
+
19
+ /* Do not let the placeholder reserve fixed height */
20
+ .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
21
+ height: auto !important;
22
+ min-height: 0 !important;
8
23
  margin: 0 !important;
9
24
  padding: 0 !important;
10
- }
25
+ }
26
+
27
+ /* Avoid baseline gaps under iframes/ins */
28
+ .ezoic-ad iframe,
29
+ .ezoic-ad ins {
30
+ display: block !important;
31
+ }
32
+
33
+ /* Remove empty spacer divs that can appear after injection */
34
+ .ezoic-ad > div:empty {
35
+ display: none !important;
36
+ }
37
+
38
+ /* Optional: message-style ad spacing (looks nicer between posts) */
39
+ .ezoic-ad--message[data-ezoic-filled="1"] {
40
+ margin: 0.75rem 0;
41
+ }