nodebb-plugin-ezoic-infinite 1.4.68 → 1.4.70

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.70",
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,464 @@
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
-
18
- poolTopics: [],
19
- poolPosts: [],
20
- poolCategories: [],
21
-
22
- usedTopics: new Set(),
23
- usedPosts: new Set(),
24
- usedCategories: new Set(),
25
-
26
- lastShowById: new Map(),
27
- pendingById: new Set(),
28
- definedIds: new Set(),
29
3
 
30
- scheduled: false,
31
- timer: null,
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
+ */
32
11
 
33
- obs: null,
34
- activeTimeouts: new Set(),
35
- lastScrollRun: 0, };
36
-
37
- function normalizeBool(v) {
38
- return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
39
- }
40
-
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
- }
52
-
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
- }
58
-
59
- 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;
68
- }
69
-
70
- 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);
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"]',
124
17
  };
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
18
 
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 };
210
- }
211
-
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) {}
19
+ const CLASS_WRAP = 'ezoic-ad';
20
+ const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
21
+
22
+ const MAX_INSERTS_PER_TICK = 3;
23
+ // Ezoic can be late to render on ajaxified pages; keep wrappers around a bit longer
24
+ // (they are hidden until filled anyway, so no visual blank space).
25
+ const FILL_TIMEOUT_MS = 8000;
26
+ const RECYCLE_MARGIN_PX = 600;
27
+ const SHOW_DEBOUNCE_MS = 500;
28
+
29
+ const state = {
30
+ pageKey: null,
31
+ cfg: null,
32
+ cfgPromise: null,
33
+
34
+ pools: { topics: [], categories: [], messages: [] },
35
+ live: { topics: [], categories: [], messages: [] },
36
+
37
+ scheduled: false,
38
+ lastRunAt: 0,
39
+ timeouts: new Set(),
40
+ observer: null,
41
+ lastShowById: new Map(),
309
42
  };
310
43
 
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
- }
319
- }
320
-
321
- 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
- }
332
-
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
- }
354
-
355
- function isPlaceholderFilled(id) {
356
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
357
- if (!ph || !ph.isConnected) return false;
358
-
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) {}
366
- }
367
- return filled;
368
- }
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) {}
381
- }
382
-
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
- }
420
-
421
- function callShowAdsWhenReady(id) {
422
- if (!id) return;
423
-
424
- const now = Date.now(), last = state.lastShowById.get(id) || 0;
425
- if (now - last < 3500) return;
426
-
427
- const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
428
- try {
429
- window.ezstandalone = window.ezstandalone || {};
430
- if (typeof window.ezstandalone.showAds === 'function') {
431
-
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
- }
44
+ function now() { return Date.now(); }
457
45
 
46
+ function getPageKey() {
47
+ try {
48
+ const ax = window.ajaxify;
49
+ if (ax && ax.data) {
50
+ if (ax.data.tid) return `topic:${ax.data.tid}`;
51
+ if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
52
+ }
53
+ } catch (e) {}
54
+ return window.location.pathname || '';
458
55
  }
459
56
 
460
- if (attempts < 100) {
461
- const timeoutId = setTimeout(waitForPh, 50);
462
- state.activeTimeouts.add(timeoutId);
463
- }
464
- })();
57
+ function getKind() {
58
+ const p = window.location.pathname || '';
59
+ if (/^\/topic\//.test(p)) return 'topic';
60
+ if (/^\/category\//.test(p)) return 'categoryTopics';
61
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
62
+
63
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
64
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
65
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
66
+ return 'other';
67
+ }
68
+
69
+ function parseIdPool(raw) {
70
+ if (!raw) return [];
71
+ const tokens = String(raw)
72
+ .split(/[\s,;]+/)
73
+ .map(s => s.trim())
74
+ .filter(Boolean);
75
+
76
+ const out = [];
77
+ const seen = new Set();
78
+ for (const t of tokens) {
79
+ const n = parseInt(t, 10);
80
+ if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
81
+ seen.add(n);
82
+ out.push(n);
83
+ }
84
+ }
85
+ return out;
465
86
  }
466
87
 
467
88
  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
- })();
487
-
488
- try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
489
- }
490
-
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);
89
+ if (state.cfg) return state.cfg;
90
+ if (state.cfgPromise) return state.cfgPromise;
91
+
92
+ state.cfgPromise = fetch('/api/plugins/ezoic-infinite/config', {
93
+ credentials: 'same-origin',
94
+ headers: { 'Accept': 'application/json' },
95
+ })
96
+ .then(r => (r && r.ok ? r.json() : null))
97
+ .catch(() => null)
98
+ .then((cfg) => {
99
+ state.cfg = cfg || { excluded: true };
100
+ state.pools.topics = parseIdPool(state.cfg.placeholderIds);
101
+ state.pools.categories = parseIdPool(state.cfg.categoryPlaceholderIds);
102
+ state.pools.messages = parseIdPool(state.cfg.messagePlaceholderIds);
103
+ return state.cfg;
104
+ })
105
+ .finally(() => { state.cfgPromise = null; });
106
+
107
+ return state.cfgPromise;
108
+ }
109
+
110
+ function rect(el) {
111
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
112
+ }
113
+
114
+ function takeRecyclable(kind) {
115
+ const arr = state.live[kind];
116
+ for (let i = 0; i < arr.length; i++) {
117
+ const entry = arr[i];
118
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) {
119
+ arr.splice(i, 1); i--; continue;
120
+ }
121
+ const r = rect(entry.wrap);
122
+ if (r && r.bottom < -RECYCLE_MARGIN_PX) {
123
+ arr.splice(i, 1);
124
+ return entry;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function pickId(kind) {
131
+ const pool = state.pools[kind];
132
+ if (pool && pool.length) return { id: pool.shift(), recycled: null };
133
+ const recycled = takeRecyclable(kind);
134
+ if (recycled) return { id: recycled.id, recycled };
135
+ return { id: null, recycled: null };
136
+ }
137
+
138
+ function buildWrap(kind, afterIndex, id) {
139
+ const wrap = document.createElement('div');
140
+ wrap.className = `${CLASS_WRAP} ${CLASS_WRAP}--${kind}`;
141
+ wrap.dataset.ezoicKind = kind;
142
+ wrap.dataset.ezoicAfter = String(afterIndex);
143
+ wrap.dataset.ezoicId = String(id);
144
+
145
+ const ph = document.createElement('div');
146
+ ph.id = `${PH_PREFIX}${id}`;
147
+ wrap.appendChild(ph);
148
+
149
+ return wrap;
150
+ }
151
+
152
+ function findWrap(kind, afterIndex) {
153
+ return document.querySelector(`.${CLASS_WRAP}[data-ezoic-kind="${kind}"][data-ezoic-after="${afterIndex}"]`);
154
+ }
155
+
156
+ function resetWrapPlaceholder(wrap, id) {
157
+ if (!wrap) return;
158
+ if (wrap.__ezoicObs) {
159
+ try { wrap.__ezoicObs.disconnect(); } catch (e) {}
160
+ wrap.__ezoicObs = null;
161
+ }
162
+ wrap.removeAttribute('data-ezoic-filled');
163
+ wrap.dataset.ezoicId = String(id);
164
+
165
+ while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
166
+ const ph = document.createElement('div');
167
+ ph.id = `${PH_PREFIX}${id}`;
168
+ wrap.appendChild(ph);
169
+ }
170
+
171
+ function forcePlaceholderAutoHeight(wrap, id) {
172
+ const ph = wrap && wrap.querySelector ? wrap.querySelector(`#${PH_PREFIX}${id}`) : null;
173
+ if (!ph) return;
174
+ ph.style.setProperty('height', 'auto', 'important');
175
+ ph.style.setProperty('min-height', '0px', 'important');
176
+ requestAnimationFrame(() => {
177
+ ph.style.setProperty('height', 'auto', 'important');
178
+ ph.style.setProperty('min-height', '0px', 'important');
179
+ });
495
180
  }
496
181
 
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);
182
+ function markFilled(wrap) {
183
+ if (!wrap || !wrap.isConnected) return;
184
+ if (wrap.getAttribute('data-ezoic-filled') === '1') return;
185
+ wrap.setAttribute('data-ezoic-filled', '1');
186
+
187
+ const id = parseInt(wrap.dataset.ezoicId, 10);
188
+ if (Number.isFinite(id) && id > 0) forcePlaceholderAutoHeight(wrap, id);
189
+ }
190
+
191
+ function removeWrap(wrap) {
192
+ if (!wrap) return;
193
+ if (wrap.__ezoicObs) {
194
+ try { wrap.__ezoicObs.disconnect(); } catch (e) {}
195
+ wrap.__ezoicObs = null;
196
+ }
197
+ try { wrap.remove(); } catch (e) {}
198
+ }
199
+
200
+ function showEzoicAd(id) {
201
+ if (!id || id <= 0) return;
202
+
203
+ const last = state.lastShowById.get(id) || 0;
204
+ if (now() - last < SHOW_DEBOUNCE_MS) return;
205
+ state.lastShowById.set(id, now());
206
+
207
+ // Capture current page key so queued calls don't fire on the next ajaxify page.
208
+ const expectedPageKey = state.pageKey;
209
+
210
+ const safeCall = () => {
211
+ try {
212
+ // Only attempt if we're still on the same page and the placeholder exists *now*.
213
+ if (expectedPageKey !== state.pageKey) return;
214
+ const el = document.getElementById(`${PH_PREFIX}${id}`);
215
+ if (!el) return;
216
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
217
+ // Let the DOM settle one frame (helps with ajaxify/morphdom timing)
218
+ requestAnimationFrame(() => {
219
+ if (expectedPageKey !== state.pageKey) return;
220
+ if (!document.getElementById(`${PH_PREFIX}${id}`)) return;
221
+ window.ezstandalone.showAds(id);
222
+ });
223
+ }
224
+ } catch (e) {}
225
+ };
226
+
227
+ try {
228
+ window.ezstandalone = window.ezstandalone || {};
229
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
230
+ if (typeof window.ezstandalone.showAds === 'function') safeCall();
231
+ else window.ezstandalone.cmd.push(safeCall);
232
+ } catch (e) {}
233
+ }
234
+
235
+ function observeFill(wrap, id) {
236
+ const ph = wrap.querySelector(`#${PH_PREFIX}${id}`);
237
+ if (!ph) return;
238
+
239
+ // immediate
240
+ if (ph.querySelector('iframe, ins, img') || ph.childElementCount > 0) {
241
+ markFilled(wrap);
242
+ return;
243
+ }
244
+
245
+ const obs = new MutationObserver(() => {
246
+ if (!wrap.isConnected) return;
247
+ const hasCreative = !!ph.querySelector('iframe, ins, img');
248
+ const r = rect(ph);
249
+ if (hasCreative || (r && r.height > 20)) {
250
+ markFilled(wrap);
251
+ try { obs.disconnect(); } catch (e) {}
252
+ wrap.__ezoicObs = null;
253
+ }
254
+ });
255
+
256
+ wrap.__ezoicObs = obs;
257
+ try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (e) {}
258
+ }
259
+
260
+ function scheduleRemovalIfUnfilled(wrap, kind, id) {
261
+ const t = setTimeout(() => {
262
+ state.timeouts.delete(t);
263
+ if (!wrap || !wrap.isConnected) return;
264
+ if (wrap.getAttribute('data-ezoic-filled') === '1') return;
265
+ removeWrap(wrap);
266
+ if (state.pools[kind] && typeof id === 'number' && id > 0) state.pools[kind].push(id);
267
+ }, FILL_TIMEOUT_MS);
268
+ state.timeouts.add(t);
269
+ }
270
+
271
+ function insertAfter(target, wrap) {
272
+ if (!target || !wrap) return false;
273
+ const next = target.nextElementSibling;
274
+ if (next && next.classList && next.classList.contains(CLASS_WRAP)) return false;
275
+ try {
276
+ target.insertAdjacentElement('afterend', wrap);
277
+ return true;
278
+ } catch (e) {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ function computeAfterIndexes(count, interval, showFirst) {
284
+ const out = [];
285
+ const step = Math.max(1, parseInt(interval, 10) || 1);
286
+ const start = showFirst ? 1 : step;
287
+ for (let i = start; i < count; i += step) out.push(i);
288
+ return out;
289
+ }
290
+
291
+ function runCategoryTopics(cfg) {
292
+ if (!cfg.enableBetweenAds) return;
293
+
294
+ const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
295
+ if (!items.length) return;
296
+
297
+ const after = computeAfterIndexes(items.length, cfg.intervalPosts || 6, !!cfg.showFirstTopicAd);
298
+ let inserts = 0;
299
+
300
+ for (const afterIndex of after) {
301
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
302
+ const anchor = items[afterIndex - 1];
303
+ if (!anchor) continue;
304
+ if (findWrap('topics', afterIndex)) continue;
305
+
306
+ const { id, recycled } = pickId('topics');
307
+ if (!id) break;
308
+
309
+ const wrap = recycled ? recycled.wrap : buildWrap('topics', afterIndex, id);
310
+ if (recycled) resetWrapPlaceholder(wrap, id);
311
+
312
+ if (!insertAfter(anchor, wrap)) { state.pools.topics.push(id); continue; }
313
+
314
+ state.live.topics.push({ id, wrap });
315
+ observeFill(wrap, id);
316
+ scheduleRemovalIfUnfilled(wrap, 'topics', id);
317
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
318
+ inserts++;
319
+ }
505
320
  }
506
321
 
507
- function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
508
- if (!items.length) return 0;
509
- const targets = computeTargets(items.length, interval, showFirst);
322
+ function runCategories(cfg) {
323
+ if (!cfg.enableCategoryAds) return;
510
324
 
511
- let inserted = 0;
512
- for (const afterPos of targets) {
513
- if (inserted >= MAX_INSERTS_PER_RUN) break;
325
+ const items = Array.from(document.querySelectorAll(SELECTORS.categoryItem));
326
+ if (!items.length) return;
514
327
 
515
- const el = items[afterPos - 1];
516
- if (!el || !el.isConnected) continue;
328
+ const after = computeAfterIndexes(items.length, cfg.intervalCategories || 4, !!cfg.showFirstCategoryAd);
329
+ let inserts = 0;
517
330
 
518
- if (isAdjacentAd(el) || isPrevAd(el)) {
519
- continue;
520
- }
331
+ for (const afterIndex of after) {
332
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
333
+ const anchor = items[afterIndex - 1];
334
+ if (!anchor) continue;
335
+ if (findWrap('categories', afterIndex)) continue;
521
336
 
522
- // Prevent back-to-back at load
523
- const prevWrap = findWrap(kindClass, afterPos - 1);
524
- if (prevWrap) continue;
337
+ const { id, recycled } = pickId('categories');
338
+ if (!id) break;
525
339
 
526
- if (findWrap(kindClass, afterPos)) continue;
527
-
528
- const pick = pickId(kindPool, []);
529
- const id = pick.id;
530
- if (!id) break;
531
-
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
- }
340
+ const wrap = recycled ? recycled.wrap : buildWrap('categories', afterIndex, id);
341
+ if (recycled) resetWrapPlaceholder(wrap, id);
553
342
 
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
- }
343
+ if (!insertAfter(anchor, wrap)) { state.pools.categories.push(id); continue; }
569
344
 
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
- }
345
+ state.live.categories.push({ id, wrap });
346
+ observeFill(wrap, id);
347
+ scheduleRemovalIfUnfilled(wrap, 'categories', id);
348
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
349
+ inserts++;
350
+ }
585
351
  }
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
352
 
599
- state.pageKey = getPageKey();
600
- state.cfg = null;
601
- state.cfgPromise = null;
353
+ function runMessageAds(cfg) {
354
+ if (!cfg.enableMessageAds) return;
602
355
 
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();
356
+ const posts = Array.from(document.querySelectorAll(SELECTORS.postItem))
357
+ .filter(p => p && p.isConnected && p.querySelector(SELECTORS.postContent));
358
+ if (!posts.length) return;
612
359
 
613
- state.activeTimeouts.forEach(id => {
614
- try { clearTimeout(id); } catch (e) {}
615
- });
616
- state.activeTimeouts.clear();
360
+ const after = computeAfterIndexes(posts.length, cfg.messageIntervalPosts || 3, !!cfg.showFirstMessageAd);
361
+ let inserts = 0;
617
362
 
618
- state.pendingById.clear();
363
+ for (const afterIndex of after) {
364
+ if (inserts >= MAX_INSERTS_PER_TICK) break;
365
+ const anchor = posts[afterIndex - 1];
366
+ if (!anchor) continue;
367
+ if (findWrap('messages', afterIndex)) continue;
619
368
 
620
- if (state.obs) { state.obs.disconnect(); state.obs = null; }
369
+ const { id, recycled } = pickId('messages');
370
+ if (!id) break;
621
371
 
622
- state.scheduled = false;
623
- clearTimeout(state.timer);
624
- state.timer = null;
625
- }
626
-
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
- }
632
-
633
- async function runCore() {
634
- if (!state.canShowAds) {
635
- return;
636
- }
637
-
638
- patchShowAds();
372
+ const wrap = recycled ? recycled.wrap : buildWrap('messages', afterIndex, id);
373
+ wrap.classList.add('ezoic-ad--message');
374
+ if (recycled) resetWrapPlaceholder(wrap, id);
639
375
 
640
- const cfg = await fetchConfig();
641
- if (!cfg || cfg.excluded) return;
376
+ if (!insertAfter(anchor, wrap)) { state.pools.messages.push(id); continue; }
642
377
 
643
- initPools(cfg);
644
-
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
- }
378
+ state.live.messages.push({ id, wrap });
379
+ observeFill(wrap, id);
380
+ scheduleRemovalIfUnfilled(wrap, 'messages', id);
381
+ if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
382
+ inserts++;
383
+ }
672
384
  }
673
385
 
674
- enforceNoAdjacentAds();
386
+ function cleanupForNavigation() {
387
+ for (const t of state.timeouts) clearTimeout(t);
388
+ state.timeouts.clear();
675
389
 
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;
390
+ if (state.observer) {
391
+ try { state.observer.disconnect(); } catch (e) {}
392
+ state.observer = null;
393
+ }
680
394
 
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
- }
395
+ state.live.topics = [];
396
+ state.live.categories = [];
397
+ state.live.messages = [];
398
+ state.lastShowById.clear();
697
399
  }
698
400
 
699
401
  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
- })();
402
+ if (state.scheduled) return;
403
+ state.scheduled = true;
404
+ requestAnimationFrame(() => {
405
+ state.scheduled = false;
406
+ run().catch(() => {});
407
+ });
408
+ }
409
+
410
+ async function run() {
411
+ const pk = getPageKey();
412
+ if (pk !== state.pageKey) {
413
+ state.pageKey = pk;
414
+ cleanupForNavigation();
415
+ state.cfg = null;
416
+ state.cfgPromise = null;
417
+ }
418
+
419
+ const cfg = await fetchConfig();
420
+ if (!cfg || cfg.excluded) return;
421
+
422
+ const kind = getKind();
423
+ if (kind === 'categoryTopics') runCategoryTopics(cfg);
424
+ if (kind === 'categories') runCategories(cfg);
425
+ if (kind === 'topic') runMessageAds(cfg);
426
+
427
+ // remove obvious empty spacer nodes injected by networks
428
+ try {
429
+ document.querySelectorAll(`.${CLASS_WRAP} > div:empty`).forEach(n => n.remove());
430
+ } catch (e) {}
431
+ }
432
+
433
+ function start() {
434
+ scheduleRun();
435
+
436
+ state.observer = new MutationObserver(() => {
437
+ const t = now();
438
+ if (t - state.lastRunAt < 200) return;
439
+ state.lastRunAt = t;
440
+ scheduleRun();
441
+ });
442
+
443
+ try { state.observer.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
444
+
445
+ // NodeBB ajaxify hook
446
+ if (window.jQuery && window.jQuery(window).on) {
447
+ window.jQuery(window).on('action:ajaxify.end', () => {
448
+ state.pageKey = null;
449
+ scheduleRun();
450
+ });
451
+ } else {
452
+ window.addEventListener('popstate', () => {
453
+ state.pageKey = null;
454
+ scheduleRun();
455
+ });
456
+ }
457
+ }
458
+
459
+ if (document.readyState === 'loading') {
460
+ document.addEventListener('DOMContentLoaded', start);
461
+ } else {
462
+ start();
818
463
  }
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
- })();
464
+ })();
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
+ }