nodebb-plugin-ezoic-infinite 1.4.70 → 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.70",
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,464 +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-';
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);
82
+ function getTopicItems() {
83
+ return Array.from(document.querySelectorAll(SELECTORS.topicItem));
84
+ }
164
85
 
165
- while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
166
- const ph = document.createElement('div');
167
- ph.id = `${PH_PREFIX}${id}`;
168
- wrap.appendChild(ph);
86
+ function getCategoryItems() {
87
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
169
88
  }
170
89
 
171
- function forcePlaceholderAutoHeight(wrap, id) {
172
- const ph = wrap && wrap.querySelector ? wrap.querySelector(`#${PH_PREFIX}${id}`) : null;
90
+ function getPostContainers() {
91
+ const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
92
+ return nodes.filter((el) => {
93
+ if (!el || !el.isConnected) return false;
94
+ if (!el.querySelector('[component="post/content"]')) return false;
95
+ const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
96
+ if (parentPost && parentPost !== el) return false;
97
+ if (el.getAttribute('component') === 'post/parent') return false;
98
+ return true;
99
+ });
100
+ }
101
+
102
+ function safeRect(el) {
103
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
104
+ }
105
+
106
+ function destroyPlaceholderIds(ids) {
107
+ if (!ids || !ids.length) return;
108
+ const filtered = ids.filter((id) => {
109
+ try { return sessionDefinedIds.has(id); } catch (e) { return true; }
110
+ });
111
+ if (!filtered.length) return;
112
+
113
+ const call = () => {
114
+ try {
115
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
116
+ window.ezstandalone.destroyPlaceholders(filtered);
117
+ }
118
+ } catch (e) {}
119
+
120
+ // Recyclage: libérer IDs après 100ms
121
+ setTimeout(() => {
122
+ filtered.forEach(id => sessionDefinedIds.delete(id));
123
+ }, 100);
124
+ };
125
+ try {
126
+ window.ezstandalone = window.ezstandalone || {};
127
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
128
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
129
+ else window.ezstandalone.cmd.push(call);
130
+ } catch (e) {}
131
+
132
+ // Recyclage: libérer IDs après 100ms
133
+ setTimeout(() => {
134
+ filtered.forEach(id => sessionDefinedIds.delete(id));
135
+ }, 100);
136
+ }
137
+
138
+ // Nettoyer éléments Ezoic invisibles qui créent espace vertical
139
+ function cleanupInvisibleEzoicElements() {
140
+ try {
141
+ document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
142
+ const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
143
+ if (!ph) return;
144
+
145
+ // Supprimer TOUS les éléments après le placeholder rempli
146
+ // qui créent de l'espace vertical
147
+ let found = false;
148
+ Array.from(wrapper.children).forEach(child => {
149
+ if (child === ph || child.contains(ph)) {
150
+ found = true;
151
+ return;
152
+ }
153
+
154
+ // Si élément APRÈS le placeholder
155
+ if (found) {
156
+ const rect = child.getBoundingClientRect();
157
+ const computed = window.getComputedStyle(child);
158
+
159
+ // Supprimer si:
160
+ // 1. Height > 0 mais pas de texte/image visible
161
+ // 2. Ou opacity: 0
162
+ // 3. Ou visibility: hidden
163
+ const hasContent = child.textContent.trim().length > 0 ||
164
+ child.querySelector('img, iframe, video');
165
+
166
+ if (!hasContent || computed.opacity === '0' || computed.visibility === 'hidden') {
167
+ child.remove();
168
+ }
169
+ }
170
+ });
171
+ });
172
+ } catch (e) {}
173
+ }
174
+
175
+ function cleanupEmptyWrappers() {
176
+ try {
177
+ document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
178
+ const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
179
+ if (ph && ph.children.length === 0) {
180
+ // Placeholder vide après 3s = pub non chargée
181
+ setTimeout(() => {
182
+ if (ph.children.length === 0) {
183
+ wrapper.remove();
184
+ }
185
+ }, 1500);
186
+ }
187
+ });
188
+ } catch (e) {}
189
+ }
190
+
191
+ function getRecyclable(liveArr) {
192
+ const margin = 600;
193
+ for (let i = 0; i < liveArr.length; i++) {
194
+ const entry = liveArr[i];
195
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
196
+ const r = safeRect(entry.wrap);
197
+ if (r && r.bottom < -margin) {
198
+ liveArr.splice(i, 1);
199
+ return entry;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+
205
+ function pickId(pool, liveArr) {
206
+ if (pool.length) return { id: pool.shift(), recycled: null };
207
+ const recycled = getRecyclable(liveArr);
208
+ if (recycled) return { id: recycled.id, recycled };
209
+ return { id: null, recycled: null };
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}`);
173
298
  if (!ph) return;
174
299
  ph.style.setProperty('height', 'auto', 'important');
175
300
  ph.style.setProperty('min-height', '0px', 'important');
176
301
  requestAnimationFrame(() => {
177
- ph.style.setProperty('height', 'auto', 'important');
178
- 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) {}
179
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;
180
330
  }
331
+ return orig.apply(ez, arguments);
332
+ };
333
+ } catch (e) {}
334
+ };
181
335
 
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');
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
+ }
186
345
 
187
- const id = parseInt(wrap.dataset.ezoicId, 10);
188
- 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) {}
189
353
  }
190
354
 
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) {}
355
+ function isWrapMarkedFilled(wrap) {
356
+ try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
198
357
  }
199
358
 
200
- function showEzoicAd(id) {
201
- 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
+ }
202
380
 
203
- const last = state.lastShowById.get(id) || 0;
204
- if (now() - last < SHOW_DEBOUNCE_MS) return;
205
- state.lastShowById.set(id, now());
381
+ function isPlaceholderFilled(id) {
382
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
383
+ if (!ph || !ph.isConnected) return false;
206
384
 
207
- // Capture current page key so queued calls don't fire on the next ajaxify page.
208
- const expectedPageKey = state.pageKey;
385
+ const wrap = ph.parentElement;
386
+ if (wrap && isWrapMarkedFilled(wrap)) return true;
209
387
 
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
- };
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;
394
+ }
226
395
 
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) {}
396
+ let batchShowAdsTimer = null;
397
+ const pendingShowAdsIds = new Set();
398
+
399
+ function scheduleShowAdsBatch(id) {
400
+ if (!id) return;
401
+
402
+ if (sessionDefinedIds.has(id)) {
403
+ try {
404
+ destroyPlaceholderIds([id]);
405
+ sessionDefinedIds.delete(id);
406
+ } catch (e) {}
233
407
  }
234
408
 
235
- function observeFill(wrap, id) {
236
- const ph = wrap.querySelector(`#${PH_PREFIX}${id}`);
237
- if (!ph) return;
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
+ });
436
+ }
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);
445
+ }
238
446
 
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
- });
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') {
255
457
 
256
- wrap.__ezoicObs = obs;
257
- try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (e) {}
458
+ state.lastShowById.set(id, Date.now());
459
+ window.ezstandalone.showAds(id);
460
+ sessionDefinedIds.add(id);
461
+ return true;
258
462
  }
463
+ } catch (e) {}
464
+ return false;
465
+ };
466
+
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;
259
472
 
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);
473
+ attempts += 1;
474
+ const el = document.getElementById(phId);
475
+ if (el && el.isConnected) {
476
+
477
+ // Si on arrive ici, soit visible, soit timeout
478
+
479
+ if (doCall()) {
480
+ state.pendingById.delete(id);
481
+ return;
269
482
  }
270
483
 
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
484
  }
282
485
 
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;
486
+ if (attempts < 100) {
487
+ const timeoutId = setTimeout(waitForPh, 50);
488
+ state.activeTimeouts.add(timeoutId);
489
+ }
490
+ })();
289
491
  }
290
492
 
291
- function runCategoryTopics(cfg) {
292
- if (!cfg.enableBetweenAds) return;
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
+ })();
513
+
514
+ try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
515
+ }
516
+
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
+ }
522
+
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
+ }
293
532
 
294
- const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
295
- if (!items.length) return;
533
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
534
+ if (!items.length) return 0;
535
+ const targets = computeTargets(items.length, interval, showFirst);
296
536
 
297
- const after = computeAfterIndexes(items.length, cfg.intervalPosts || 6, !!cfg.showFirstTopicAd);
298
- let inserts = 0;
537
+ let inserted = 0;
538
+ for (const afterPos of targets) {
539
+ if (inserted >= MAX_INSERTS_PER_RUN) break;
299
540
 
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;
541
+ const el = items[afterPos - 1];
542
+ if (!el || !el.isConnected) continue;
305
543
 
306
- const { id, recycled } = pickId('topics');
307
- if (!id) break;
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;
308
553
 
309
- const wrap = recycled ? recycled.wrap : buildWrap('topics', afterIndex, id);
310
- if (recycled) resetWrapPlaceholder(wrap, id);
554
+ const pick = pickId(kindPool, []);
555
+ const id = pick.id;
556
+ if (!id) break;
311
557
 
312
- if (!insertAfter(anchor, wrap)) { state.pools.topics.push(id); continue; }
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
+ }
313
595
 
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
- }
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
+ }
320
616
  }
321
617
 
322
- function runCategories(cfg) {
323
- if (!cfg.enableCategoryAds) return;
618
+ function cleanup() {
619
+ destroyUsedPlaceholders();
324
620
 
325
- const items = Array.from(document.querySelectorAll(SELECTORS.categoryItem));
326
- if (!items.length) return;
621
+ document.querySelectorAll('.ezoic-ad').forEach(el => {
622
+ try { el.remove(); } catch (e) {}
623
+ });
327
624
 
328
- const after = computeAfterIndexes(items.length, cfg.intervalCategories || 4, !!cfg.showFirstCategoryAd);
329
- let inserts = 0;
625
+ state.pageKey = getPageKey();
626
+ state.cfg = null;
627
+ state.cfgPromise = null;
330
628
 
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;
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();
336
638
 
337
- const { id, recycled } = pickId('categories');
338
- if (!id) break;
639
+ state.activeTimeouts.forEach(id => {
640
+ try { clearTimeout(id); } catch (e) {}
641
+ });
642
+ state.activeTimeouts.clear();
339
643
 
340
- const wrap = recycled ? recycled.wrap : buildWrap('categories', afterIndex, id);
341
- if (recycled) resetWrapPlaceholder(wrap, id);
644
+ state.pendingById.clear();
342
645
 
343
- if (!insertAfter(anchor, wrap)) { state.pools.categories.push(id); continue; }
646
+ if (state.obs) { state.obs.disconnect(); state.obs = null; }
344
647
 
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
- }
648
+ state.scheduled = false;
649
+ clearTimeout(state.timer);
650
+ state.timer = null;
351
651
  }
352
652
 
353
- function runMessageAds(cfg) {
354
- if (!cfg.enableMessageAds) return;
355
-
356
- const posts = Array.from(document.querySelectorAll(SELECTORS.postItem))
357
- .filter(p => p && p.isConnected && p.querySelector(SELECTORS.postContent));
358
- if (!posts.length) return;
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
+ }
359
658
 
360
- const after = computeAfterIndexes(posts.length, cfg.messageIntervalPosts || 3, !!cfg.showFirstMessageAd);
361
- let inserts = 0;
659
+ async function runCore() {
660
+ if (!state.canShowAds) {
661
+ return;
662
+ }
362
663
 
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;
664
+ patchShowAds();
368
665
 
369
- const { id, recycled } = pickId('messages');
370
- if (!id) break;
666
+ const cfg = await fetchConfig();
667
+ if (!cfg || cfg.excluded) return;
371
668
 
372
- const wrap = recycled ? recycled.wrap : buildWrap('messages', afterIndex, id);
373
- wrap.classList.add('ezoic-ad--message');
374
- if (recycled) resetWrapPlaceholder(wrap, id);
669
+ initPools(cfg);
375
670
 
376
- if (!insertAfter(anchor, wrap)) { state.pools.messages.push(id); continue; }
671
+ const kind = getKind();
672
+ let inserted = 0;
377
673
 
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
- }
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
+ }
384
698
  }
385
699
 
386
- function cleanupForNavigation() {
387
- for (const t of state.timeouts) clearTimeout(t);
388
- state.timeouts.clear();
700
+ enforceNoAdjacentAds();
389
701
 
390
- if (state.observer) {
391
- try { state.observer.disconnect(); } catch (e) {}
392
- state.observer = null;
393
- }
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;
394
706
 
395
- state.live.topics = [];
396
- state.live.categories = [];
397
- state.live.messages = [];
398
- 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
+ }
399
723
  }
400
724
 
401
725
  function scheduleRun() {
402
- if (state.scheduled) return;
403
- state.scheduled = true;
404
- requestAnimationFrame(() => {
405
- state.scheduled = false;
406
- run().catch(() => {});
407
- });
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);
408
736
  }
409
737
 
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
- }
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
+ });
418
751
 
419
- const cfg = await fetchConfig();
420
- 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
+ });
421
760
 
422
- const kind = getKind();
423
- if (kind === 'categoryTopics') runCategoryTopics(cfg);
424
- if (kind === 'categories') runCategories(cfg);
425
- if (kind === 'topic') runMessageAds(cfg);
761
+ $(window).on('action:topic.loaded.ezoicInfinite', () => {
762
+ ensureObserver();
763
+ waitForContentThenRun();
764
+ });
426
765
 
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) {}
766
+ $(window).on('action:posts.loaded.ezoicInfinite', () => {
767
+ ensureObserver();
768
+ // posts.loaded = infinite scroll
769
+ waitForContentThenRun();
770
+ });
431
771
  }
432
772
 
433
- function start() {
434
- 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
+ }
435
791
 
436
- state.observer = new MutationObserver(() => {
437
- const t = now();
438
- if (t - state.lastRunAt < 200) return;
439
- state.lastRunAt = t;
440
- scheduleRun();
441
- });
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++;
442
799
 
443
- 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;
444
803
 
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
- }
804
+ if (wordCount >= MIN_WORDS) {
805
+ // Assez de contenu → lancer l'insertion
806
+ scheduleRun();
807
+ return;
457
808
  }
458
809
 
459
- if (document.readyState === 'loading') {
460
- document.addEventListener('DOMContentLoaded', start);
461
- } else {
462
- 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;
463
834
  }
464
- })();
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
  }