nodebb-plugin-ezoic-infinite 1.4.70 → 1.4.72

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.70",
3
+ "version": "1.4.72",
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,464 +1,840 @@
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;
3
10
 
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
- */
11
+ const sessionDefinedIds = new Set();
11
12
 
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
- };
13
+ const insertingIds = new Set(), state = {
14
+ pageKey: null,
15
+ cfg: null,
16
+ cfgPromise: null,
18
17
 
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(),
42
- };
18
+ poolTopics: [],
19
+ poolPosts: [],
20
+ poolCategories: [],
21
+
22
+ usedTopics: new Set(),
23
+ usedPosts: new Set(),
24
+ usedCategories: new Set(),
43
25
 
44
- function now() { return Date.now(); }
26
+ lastShowById: new Map(),
27
+ pendingById: new Set(),
28
+ definedIds: new Set(),
29
+
30
+ scheduled: false,
31
+ timer: null,
32
+
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
+ }
45
58
 
46
59
  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 || '';
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;
55
68
  }
56
69
 
57
70
  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;
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';
86
80
  }
87
81
 
88
- async function fetchConfig() {
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);
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) {}
169
189
  }
170
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) {}
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
+ }
319
+ }
320
+
321
+
171
322
  function forcePlaceholderAutoHeight(wrap, id) {
172
- const ph = wrap && wrap.querySelector ? wrap.querySelector(`#${PH_PREFIX}${id}`) : null;
323
+ try {
324
+ if (!wrap || !id) return;
325
+ const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
173
326
  if (!ph) return;
327
+ // Neutraliser les hauteurs réservées (inline ou CSS) qui créent un espace après la pub
174
328
  ph.style.setProperty('height', 'auto', 'important');
175
329
  ph.style.setProperty('min-height', '0px', 'important');
330
+ // Eviter le gap baseline sous les iframes/ins
331
+ wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => {
332
+ try { n.style.setProperty('display', 'block', 'important'); } catch (e) {}
333
+ });
176
334
  requestAnimationFrame(() => {
177
- ph.style.setProperty('height', 'auto', 'important');
178
- ph.style.setProperty('min-height', '0px', 'important');
335
+ try {
336
+ ph.style.setProperty('height', 'auto', 'important');
337
+ ph.style.setProperty('min-height', '0px', 'important');
338
+ } catch (e) {}
179
339
  });
340
+ } catch (e) {}
180
341
  }
181
342
 
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');
343
+ function markFilled(wrap, id) {
344
+ try {
345
+ if (!wrap) return;
346
+ if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
347
+ wrap.setAttribute('data-ezoic-filled', '1');
348
+ try { forcePlaceholderAutoHeight(wrap, id); } catch (e) {}
349
+ } catch (e) {}
350
+ }
186
351
 
187
- const id = parseInt(wrap.dataset.ezoicId, 10);
188
- if (Number.isFinite(id) && id > 0) forcePlaceholderAutoHeight(wrap, id);
352
+ function isWrapMarkedFilled(wrap) {
353
+ try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
189
354
  }
190
355
 
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) {}
356
+ function attachFillObserver(wrap, id) {
357
+ try {
358
+ const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
359
+ if (!ph) return;
360
+ // Already filled?
361
+ if (ph.childNodes && ph.childNodes.length > 0) {
362
+ markFilled(wrap, id); // Afficher wrapper
363
+ sessionDefinedIds.add(id);
364
+ return;
365
+ }
366
+ const obs = new MutationObserver(() => {
367
+ if (ph.childNodes && ph.childNodes.length > 0) {
368
+ markFilled(wrap, id); // CRITIQUE: Afficher wrapper maintenant
369
+ try { sessionDefinedIds.add(id); } catch (e) {}
370
+ try { obs.disconnect(); } catch (e) {}
198
371
  }
372
+ });
373
+ obs.observe(ph, { childList: true, subtree: true });
374
+ wrap.__ezoicFillObs = obs;
375
+ } catch (e) {}
376
+ }
377
+
378
+ function isPlaceholderFilled(id) {
379
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
380
+ if (!ph || !ph.isConnected) return false;
199
381
 
200
- function showEzoicAd(id) {
201
- if (!id || id <= 0) return;
382
+ const wrap = ph.parentElement;
383
+ if (wrap && isWrapMarkedFilled(wrap)) return true;
202
384
 
203
- const last = state.lastShowById.get(id) || 0;
204
- if (now() - last < SHOW_DEBOUNCE_MS) return;
205
- state.lastShowById.set(id, now());
385
+ const filled = !!(ph.childNodes && ph.childNodes.length > 0);
386
+ if (filled) {
387
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
388
+ try { markFilled(wrap, id); } catch (e) {}
389
+ }
390
+ return filled;
391
+ }
206
392
 
207
- // Capture current page key so queued calls don't fire on the next ajaxify page.
208
- const expectedPageKey = state.pageKey;
393
+ let batchShowAdsTimer = null;
394
+ const pendingShowAdsIds = new Set();
209
395
 
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
- };
396
+ function scheduleShowAdsBatch(id) {
397
+ if (!id) return;
226
398
 
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) {}
399
+ if (sessionDefinedIds.has(id)) {
400
+ try {
401
+ destroyPlaceholderIds([id]);
402
+ sessionDefinedIds.delete(id);
403
+ } catch (e) {}
233
404
  }
234
405
 
235
- function observeFill(wrap, id) {
236
- const ph = wrap.querySelector(`#${PH_PREFIX}${id}`);
237
- if (!ph) return;
406
+ // Throttle: ne pas rappeler le même ID trop vite
407
+ const now = Date.now(), last = state.lastShowById.get(id) || 0;
408
+ if (now - last < 3500) return;
409
+
410
+ // Ajouter à la batch
411
+ pendingShowAdsIds.add(id);
412
+
413
+ clearTimeout(batchShowAdsTimer);
414
+ batchShowAdsTimer = setTimeout(() => {
415
+ if (pendingShowAdsIds.size === 0) return;
416
+
417
+ const idsArray = Array.from(pendingShowAdsIds);
418
+ pendingShowAdsIds.clear();
419
+
420
+ // Appeler showAds avec TOUS les IDs en une fois
421
+ try {
422
+ window.ezstandalone = window.ezstandalone || {};
423
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
424
+ window.ezstandalone.cmd.push(function() {
425
+ if (typeof window.ezstandalone.showAds === 'function') {
426
+ // Appel batch: showAds(id1, id2, id3...)
427
+ const okIds = idsArray.filter(id => { const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`); return el && el.isConnected; });
428
+ if (okIds.length) { window.ezstandalone.showAds(...okIds); }
429
+ // Tracker tous les IDs
430
+ idsArray.forEach(id => {
431
+ state.lastShowById.set(id, Date.now());
432
+ sessionDefinedIds.add(id);
433
+ });
434
+ }
435
+ });
436
+ } catch (e) {}
437
+
438
+ // CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
439
+ setTimeout(() => {
440
+ cleanupInvisibleEzoicElements();
441
+ }, 800); // 1.5s pour laisser Ezoic charger
442
+ }, 100);
443
+ }
238
444
 
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
- });
445
+ function callShowAdsWhenReady(id) {
446
+ if (!id) return;
447
+
448
+ const now = Date.now(), last = state.lastShowById.get(id) || 0;
449
+ if (now - last < 3500) return;
450
+
451
+ const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
452
+ try {
453
+ window.ezstandalone = window.ezstandalone || {};
454
+ if (typeof window.ezstandalone.showAds === 'function') {
255
455
 
256
- wrap.__ezoicObs = obs;
257
- try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (e) {}
456
+ state.lastShowById.set(id, Date.now());
457
+ window.ezstandalone.showAds(id);
458
+ sessionDefinedIds.add(id);
459
+ return true;
258
460
  }
461
+ } catch (e) {}
462
+ return false;
463
+ };
464
+
465
+ const startPageKey = state.pageKey;
466
+ let attempts = 0;
467
+ (function waitForPh() {
468
+ if (state.pageKey !== startPageKey) return;
469
+ if (state.pendingById.has(id)) return;
470
+
471
+ attempts += 1;
472
+ const el = document.getElementById(phId);
473
+ if (el && el.isConnected) {
474
+
475
+ // Si on arrive ici, soit visible, soit timeout
259
476
 
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);
477
+ if (doCall()) {
478
+ state.pendingById.delete(id);
479
+ return;
269
480
  }
270
481
 
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
482
  }
282
483
 
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;
484
+ if (attempts < 100) {
485
+ const timeoutId = setTimeout(waitForPh, 50);
486
+ state.activeTimeouts.add(timeoutId);
487
+ }
488
+ })();
289
489
  }
290
490
 
291
- function runCategoryTopics(cfg) {
292
- if (!cfg.enableBetweenAds) return;
491
+ async function fetchConfig() {
492
+ if (state.cfg) return state.cfg;
493
+ if (state.cfgPromise) return state.cfgPromise;
494
+
495
+ state.cfgPromise = (async () => {
496
+ const MAX_TRIES = 3;
497
+ let delay = 800;
498
+ for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
499
+ try {
500
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
501
+ if (res.ok) {
502
+ state.cfg = await res.json();
503
+ return state.cfg;
504
+ }
505
+ } catch (e) {}
506
+ if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
507
+ delay *= 2;
508
+ }
509
+ return null;
510
+ })();
293
511
 
294
- const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
295
- if (!items.length) return;
512
+ try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
513
+ }
296
514
 
297
- const after = computeAfterIndexes(items.length, cfg.intervalPosts || 6, !!cfg.showFirstTopicAd);
298
- let inserts = 0;
515
+ function initPools(cfg) {
516
+ if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
517
+ if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
518
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
519
+ }
299
520
 
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;
521
+ function computeTargets(count, interval, showFirst) {
522
+ const out = [];
523
+ if (count <= 0) return out;
524
+ if (showFirst) out.push(1);
525
+ for (let i = 1; i <= count; i++) {
526
+ if (i % interval === 0) out.push(i);
527
+ }
528
+ return Array.from(new Set(out)).sort((a, b) => a - b);
529
+ }
305
530
 
306
- const { id, recycled } = pickId('topics');
307
- if (!id) break;
531
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
532
+ if (!items.length) return 0;
533
+ const targets = computeTargets(items.length, interval, showFirst);
308
534
 
309
- const wrap = recycled ? recycled.wrap : buildWrap('topics', afterIndex, id);
310
- if (recycled) resetWrapPlaceholder(wrap, id);
535
+ let inserted = 0;
536
+ for (const afterPos of targets) {
537
+ if (inserted >= MAX_INSERTS_PER_RUN) break;
311
538
 
312
- if (!insertAfter(anchor, wrap)) { state.pools.topics.push(id); continue; }
539
+ const el = items[afterPos - 1];
540
+ if (!el || !el.isConnected) continue;
313
541
 
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
- }
542
+ if (isAdjacentAd(el) || isPrevAd(el)) {
543
+ continue;
320
544
  }
321
545
 
322
- function runCategories(cfg) {
323
- if (!cfg.enableCategoryAds) return;
546
+ // Prevent back-to-back at load
547
+ const prevWrap = findWrap(kindClass, afterPos - 1);
548
+ if (prevWrap) continue;
324
549
 
325
- const items = Array.from(document.querySelectorAll(SELECTORS.categoryItem));
326
- if (!items.length) return;
550
+ if (findWrap(kindClass, afterPos)) continue;
327
551
 
328
- const after = computeAfterIndexes(items.length, cfg.intervalCategories || 4, !!cfg.showFirstCategoryAd);
329
- let inserts = 0;
552
+ const pick = pickId(kindPool, []);
553
+ const id = pick.id;
554
+ if (!id) break;
330
555
 
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;
556
+ let wrap = null;
557
+ if (pick.recycled && pick.recycled.wrap) {
558
+ if (sessionDefinedIds.has(id)) {
559
+ destroyPlaceholderIds([id]);
560
+ }
561
+ const oldWrap = pick.recycled.wrap;
562
+ try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
563
+ try { oldWrap && oldWrap.remove(); } catch (e) {}
564
+ wrap = insertAfter(el, id, kindClass, afterPos);
565
+ if (!wrap) continue;
566
+ setTimeout(() => {
567
+ callShowAdsWhenReady(id);
568
+ }, 50);
569
+ } else {
570
+ usedSet.add(id);
571
+ wrap = insertAfter(el, id, kindClass, afterPos);
572
+ if (!wrap) continue;
573
+ // Micro-délai pour laisser le DOM se synchroniser
574
+ // Appel immédiat au lieu de 10ms delay
575
+ callShowAdsWhenReady(id);
576
+ }
577
+ inserted += 1;
578
+ }
579
+ return inserted;
580
+ }
336
581
 
337
- const { id, recycled } = pickId('categories');
338
- if (!id) break;
582
+ function enforceNoAdjacentAds() {
583
+ const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
584
+ for (let i = 0; i < ads.length; i++) {
585
+ const ad = ads[i], prev = ad.previousElementSibling;
586
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
587
+ try {
588
+ const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
589
+ if (ph) {
590
+ const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
591
+ if (Number.isFinite(id) && id > 0) {
592
+ // Détruire le placeholder si Ezoic l'a déjà défini
593
+ if (sessionDefinedIds.has(id)) {
594
+ destroyPlaceholderIds([id]);
595
+ }
596
+ }
597
+ }
598
+ ad.remove();
599
+ } catch (e) {}
600
+ }
601
+ }
602
+ }
339
603
 
340
- const wrap = recycled ? recycled.wrap : buildWrap('categories', afterIndex, id);
341
- if (recycled) resetWrapPlaceholder(wrap, id);
604
+ function cleanup() {
605
+ destroyUsedPlaceholders();
342
606
 
343
- if (!insertAfter(anchor, wrap)) { state.pools.categories.push(id); continue; }
607
+ document.querySelectorAll('.ezoic-ad').forEach(el => {
608
+ try { el.remove(); } catch (e) {}
609
+ });
344
610
 
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
- }
351
- }
611
+ state.pageKey = getPageKey();
612
+ state.cfg = null;
613
+ state.cfgPromise = null;
614
+
615
+ state.poolTopics = [];
616
+ state.poolPosts = [];
617
+ state.poolCategories = [];
618
+ state.usedTopics.clear();
619
+ state.usedPosts.clear();
620
+ state.usedCategories.clear();
621
+ state.lastShowById.clear();
622
+ state.pendingById.clear();
623
+ state.definedIds.clear();
352
624
 
353
- function runMessageAds(cfg) {
354
- if (!cfg.enableMessageAds) return;
625
+ state.activeTimeouts.forEach(id => {
626
+ try { clearTimeout(id); } catch (e) {}
627
+ });
628
+ state.activeTimeouts.clear();
355
629
 
356
- const posts = Array.from(document.querySelectorAll(SELECTORS.postItem))
357
- .filter(p => p && p.isConnected && p.querySelector(SELECTORS.postContent));
358
- if (!posts.length) return;
630
+ state.pendingById.clear();
359
631
 
360
- const after = computeAfterIndexes(posts.length, cfg.messageIntervalPosts || 3, !!cfg.showFirstMessageAd);
361
- let inserts = 0;
632
+ if (state.obs) { state.obs.disconnect(); state.obs = null; }
633
+
634
+ state.scheduled = false;
635
+ clearTimeout(state.timer);
636
+ state.timer = null;
637
+ }
638
+
639
+ function ensureObserver() {
640
+ if (state.obs) return;
641
+ state.obs = new MutationObserver(() => scheduleRun('mutation'));
642
+ try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
643
+ }
644
+
645
+ async function runCore() {
646
+ if (!state.canShowAds) {
647
+ return;
648
+ }
362
649
 
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;
650
+ patchShowAds();
368
651
 
369
- const { id, recycled } = pickId('messages');
370
- if (!id) break;
652
+ const cfg = await fetchConfig();
653
+ if (!cfg || cfg.excluded) return;
371
654
 
372
- const wrap = recycled ? recycled.wrap : buildWrap('messages', afterIndex, id);
373
- wrap.classList.add('ezoic-ad--message');
374
- if (recycled) resetWrapPlaceholder(wrap, id);
655
+ initPools(cfg);
375
656
 
376
- if (!insertAfter(anchor, wrap)) { state.pools.messages.push(id); continue; }
657
+ const kind = getKind();
658
+ let inserted = 0;
377
659
 
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
- }
660
+ if (kind === 'topic') {
661
+ if (normalizeBool(cfg.enableMessageAds)) {
662
+ inserted = injectBetween('ezoic-ad-message', getPostContainers(),
663
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
664
+ normalizeBool(cfg.showFirstMessageAd),
665
+ state.poolPosts,
666
+ state.usedPosts);
667
+ }
668
+ } else if (kind === 'categoryTopics') {
669
+ if (normalizeBool(cfg.enableBetweenAds)) {
670
+ inserted = injectBetween('ezoic-ad-between', getTopicItems(),
671
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
672
+ normalizeBool(cfg.showFirstTopicAd),
673
+ state.poolTopics,
674
+ state.usedTopics);
675
+ }
676
+ } else if (kind === 'categories') {
677
+ if (normalizeBool(cfg.enableCategoryAds)) {
678
+ inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
679
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
680
+ normalizeBool(cfg.showFirstCategoryAd),
681
+ state.poolCategories,
682
+ state.usedCategories);
683
+ }
384
684
  }
385
685
 
386
- function cleanupForNavigation() {
387
- for (const t of state.timeouts) clearTimeout(t);
388
- state.timeouts.clear();
686
+ enforceNoAdjacentAds();
389
687
 
390
- if (state.observer) {
391
- try { state.observer.disconnect(); } catch (e) {}
392
- state.observer = null;
393
- }
688
+ let count = 0;
689
+ if (kind === 'topic') count = getPostContainers().length;
690
+ else if (kind === 'categoryTopics') count = getTopicItems().length;
691
+ else if (kind === 'categories') count = getCategoryItems().length;
394
692
 
395
- state.live.topics = [];
396
- state.live.categories = [];
397
- state.live.messages = [];
398
- state.lastShowById.clear();
693
+ if (count === 0 && 0 < 25) {
694
+ setTimeout(arguments[0], 50);
695
+ return;
696
+ }
697
+
698
+ if (inserted >= MAX_INSERTS_PER_RUN) {
699
+ setTimeout(arguments[0], 50);
700
+ } else if (inserted === 0 && count > 0) {
701
+ // Pool épuisé ou recyclage pas encore disponible.
702
+ if (state.poolWaitAttempts < 8) {
703
+ state.poolWaitAttempts += 1;
704
+ setTimeout(arguments[0], 50);
705
+ } else {
706
+ }
707
+ } else if (inserted > 0) {
708
+ }
399
709
  }
400
710
 
401
711
  function scheduleRun() {
402
- if (state.scheduled) return;
403
- state.scheduled = true;
404
- requestAnimationFrame(() => {
405
- state.scheduled = false;
406
- run().catch(() => {});
407
- });
712
+ if (state.scheduled) return;
713
+ state.scheduled = true;
714
+
715
+ clearTimeout(state.timer);
716
+ state.timer = setTimeout(() => {
717
+ state.scheduled = false;
718
+ const pk = getPageKey();
719
+ if (state.pageKey && pk !== state.pageKey) return;
720
+ runCore().catch(() => {});
721
+ }, 50);
408
722
  }
409
723
 
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
- }
724
+ function bind() {
725
+ if (!$) return;
726
+
727
+ $(window).off('.ezoicInfinite');
728
+
729
+ $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
730
+
731
+ $(window).on('action:ajaxify.end.ezoicInfinite', () => {
732
+ state.pageKey = getPageKey();
733
+ ensureObserver();
734
+
735
+ state.canShowAds = true;
736
+ });
418
737
 
419
- const cfg = await fetchConfig();
420
- if (!cfg || cfg.excluded) return;
738
+ $(window).on('action:category.loaded.ezoicInfinite', () => {
739
+ ensureObserver();
740
+ waitForContentThenRun();
741
+ });
742
+ $(window).on('action:topics.loaded.ezoicInfinite', () => {
743
+ ensureObserver();
744
+ waitForContentThenRun();
745
+ });
421
746
 
422
- const kind = getKind();
423
- if (kind === 'categoryTopics') runCategoryTopics(cfg);
424
- if (kind === 'categories') runCategories(cfg);
425
- if (kind === 'topic') runMessageAds(cfg);
747
+ $(window).on('action:topic.loaded.ezoicInfinite', () => {
748
+ ensureObserver();
749
+ waitForContentThenRun();
750
+ });
426
751
 
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) {}
752
+ $(window).on('action:posts.loaded.ezoicInfinite', () => {
753
+ ensureObserver();
754
+ // posts.loaded = infinite scroll
755
+ waitForContentThenRun();
756
+ });
431
757
  }
432
758
 
433
- function start() {
434
- scheduleRun();
759
+ function bindScroll() {
760
+ if (state.lastScrollRun > 0) return;
761
+ state.lastScrollRun = Date.now();
762
+ let ticking = false;
763
+ window.addEventListener('scroll', () => {
764
+ if (ticking) return;
765
+ ticking = true;
766
+ window.requestAnimationFrame(() => {
767
+ ticking = false;
768
+ enforceNoAdjacentAds();
769
+ const now = Date.now();
770
+ if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
771
+ state.lastScrollRun = now;
772
+ scheduleRun();
773
+ }
774
+ });
775
+ }, { passive: true });
776
+ }
435
777
 
436
- state.observer = new MutationObserver(() => {
437
- const t = now();
438
- if (t - state.lastRunAt < 200) return;
439
- state.lastRunAt = t;
440
- scheduleRun();
441
- });
778
+ function waitForContentThenRun() {
779
+ const MIN_WORDS = 250;
780
+ let attempts = 0;
781
+ const maxAttempts = 20; // 20 × 200ms = 4s max
782
+
783
+ (function check() {
784
+ attempts++;
442
785
 
443
- try { state.observer.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
786
+ // Compter les mots sur la page
787
+ const text = document.body.innerText || '';
788
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
444
789
 
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
- }
790
+ if (wordCount >= MIN_WORDS) {
791
+ // Assez de contenu → lancer l'insertion
792
+ scheduleRun();
793
+ return;
457
794
  }
458
795
 
459
- if (document.readyState === 'loading') {
460
- document.addEventListener('DOMContentLoaded', start);
461
- } else {
462
- start();
796
+ // Pas assez de contenu
797
+ if (attempts >= maxAttempts) {
798
+ // Timeout après 4s → tenter quand même
799
+ scheduleRun();
800
+ return;
801
+ }
802
+
803
+ // Réessayer dans 200ms
804
+ setTimeout(check, 50);
805
+ })();
806
+ }
807
+
808
+ function waitForEzoicThenRun() {
809
+ let attempts = 0;
810
+ const maxAttempts = 50; // 50 × 200ms = 10s max
811
+
812
+ (function check() {
813
+ attempts++;
814
+ // Vérifier si Ezoic est chargé
815
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
816
+ // Ezoic est prêt → lancer l'insertion
817
+ scheduleRun();
818
+ waitForContentThenRun();
819
+ return;
463
820
  }
464
- })();
821
+ // Ezoic pas encore prêt
822
+ if (attempts >= maxAttempts) {
823
+ // Tenter quand même
824
+ scheduleRun();
825
+ return;
826
+ }
827
+ // Réessayer dans 200ms
828
+ setTimeout(check, 50);
829
+ })();
830
+ }
831
+
832
+ cleanup();
833
+ bind();
834
+ bindScroll();
835
+ ensureObserver();
836
+ state.pageKey = getPageKey();
837
+
838
+ // Attendre que Ezoic soit chargé avant d'insérer
839
+ waitForEzoicThenRun();
840
+ })();
package/public/style.css CHANGED
@@ -1,41 +1,31 @@
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 {
8
- display: none;
9
- width: 100%;
10
- clear: both;
11
- margin: 0;
12
- padding: 0;
2
+ /* Wrapper: invisible tant que la pub n'est pas réellement insérée */
3
+ .ezoic-ad{
4
+ display:none;
5
+ width:100%;
6
+ height:auto !important;
7
+ padding:0 !important;
8
+ margin:0 !important;
13
9
  }
14
-
15
- .ezoic-ad[data-ezoic-filled="1"] {
16
- display: block;
10
+ .ezoic-ad[data-ezoic-filled="1"]{
11
+ display:block;
17
12
  }
18
13
 
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;
23
- margin: 0 !important;
24
- padding: 0 !important;
14
+ /* Le placeholder ne doit jamais réserver une hauteur fixe */
15
+ .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"]{
16
+ height:auto !important;
17
+ min-height:0 !important;
18
+ margin:0 !important;
19
+ padding:0 !important;
25
20
  }
26
21
 
27
- /* Avoid baseline gaps under iframes/ins */
22
+ /* Évite le gap baseline sous iframes/ins */
28
23
  .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;
24
+ .ezoic-ad ins{
25
+ display:block !important;
36
26
  }
37
27
 
38
- /* Optional: message-style ad spacing (looks nicer between posts) */
39
- .ezoic-ad--message[data-ezoic-filled="1"] {
40
- margin: 0.75rem 0;
28
+ /* Supprimer les spacers vides */
29
+ .ezoic-ad > div:empty{
30
+ display:none !important;
41
31
  }