nodebb-plugin-ezoic-infinite 1.4.69 → 1.4.71

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