nodebb-plugin-ezoic-infinite 1.4.90 → 1.4.91

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.90",
3
+ "version": "1.4.91",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -11,52 +11,33 @@
11
11
 
12
12
  const WRAP_CLASS = 'ezoic-ad';
13
13
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
14
-
15
- const MAX_INSERTS_PER_RUN = 2;
14
+ const MAX_INSERTS_PER_RUN = 3;
16
15
 
17
16
  const state = {
18
17
  pageKey: null,
19
18
  cfg: null,
20
19
  cfgPromise: null,
21
-
22
20
  poolTopics: [],
23
21
  poolPosts: [],
24
22
  poolCategories: [],
25
-
26
23
  usedTopics: new Set(),
27
24
  usedPosts: new Set(),
28
25
  usedCategories: new Set(),
29
-
30
- // wrappers currently on page (FIFO for recycling)
31
- liveBetween: [],
32
- liveMessage: [],
33
- liveCategory: [],
34
- usedCategories: new Set(),
35
-
36
26
  lastShowById: new Map(),
37
- pendingById: new Set(),
38
- retryById: new Map(),
39
- retryTimer: null,
40
- retryQueue: [],
41
- retryQueueSet: new Set(),
42
- retryQueueRunning: false,
43
- badIds: new Set(),
44
- definedIds: new Set(),
45
-
27
+ canShowAds: false,
46
28
  scheduled: false,
47
29
  timer: null,
48
-
49
30
  obs: null,
50
- attempts: 0,
51
31
  };
52
32
 
33
+ const sessionDefinedIds = new Set();
34
+
53
35
  function normalizeBool(v) {
54
36
  return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
55
37
  }
56
38
 
57
39
  function uniqInts(lines) {
58
- const out = [];
59
- const seen = new Set();
40
+ const out = [], seen = new Set();
60
41
  for (const v of lines) {
61
42
  const n = parseInt(v, 10);
62
43
  if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
@@ -89,10 +70,6 @@
89
70
  if (/^\/topic\//.test(p)) return 'topic';
90
71
  if (/^\/category\//.test(p)) return 'categoryTopics';
91
72
  if (p === '/' || /^\/categories/.test(p)) return 'categories';
92
- // fallback by DOM
93
- if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
94
- if (document.querySelector(SELECTORS.postItem)) return 'topic';
95
- if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
96
73
  return 'other';
97
74
  }
98
75
 
@@ -100,120 +77,67 @@
100
77
  return Array.from(document.querySelectorAll(SELECTORS.topicItem));
101
78
  }
102
79
 
80
+ function getPostContainers() {
81
+ return Array.from(document.querySelectorAll(SELECTORS.postItem));
82
+ }
83
+
103
84
  function getCategoryItems() {
104
85
  return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
105
86
  }
106
87
 
107
- function getPostContainers() {
108
- const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
109
- return nodes.filter((el) => {
110
- if (!el || !el.isConnected) return false;
111
- if (!el.querySelector('[component="post/content"]')) return false;
112
- const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
113
- if (parentPost && parentPost !== el) return false;
114
- if (el.getAttribute('component') === 'post/parent') return false;
115
- return true;
116
- });
117
- }
88
+ async function fetchConfig() {
89
+ if (state.cfg) return state.cfg;
90
+ if (state.cfgPromise) return state.cfgPromise;
118
91
 
92
+ state.cfgPromise = (async () => {
93
+ try {
94
+ const res = await fetch('/api/ezoic-infinite/config');
95
+ if (!res.ok) return null;
96
+ const cfg = await res.json();
97
+ state.cfg = cfg;
98
+ return cfg;
99
+ } catch (e) {
100
+ return null;
101
+ }
102
+ })();
119
103
 
120
- function safeRect(el) {
121
- try { return el.getBoundingClientRect(); } catch (e) { return null; }
104
+ return state.cfgPromise;
105
+ }
106
+
107
+ function initPools(cfg) {
108
+ if (!state.poolTopics.length) state.poolTopics = parsePool(cfg.poolTopics);
109
+ if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.poolPosts);
110
+ if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.poolCategories);
122
111
  }
123
112
 
124
113
  function destroyPlaceholderIds(ids) {
125
114
  if (!ids || !ids.length) return;
126
- // Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
127
- const filtered = ids.filter((id) => {
128
- try { return state.definedIds && state.definedIds.has(id); } catch (e) { return true; }
129
- });
115
+ const filtered = ids.filter(id => Number.isFinite(id) && id > 0);
130
116
  if (!filtered.length) return;
131
117
 
132
- const call = () => {
133
- try {
134
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
135
- window.ezstandalone.destroyPlaceholders(filtered);
136
- }
137
- } catch (e) {}
138
- };
139
- try {
140
- window.ezstandalone = window.ezstandalone || {};
141
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
142
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
143
- else window.ezstandalone.cmd.push(call);
144
- } catch (e) {}
145
- }
146
- };
147
118
  try {
148
119
  window.ezstandalone = window.ezstandalone || {};
149
120
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
150
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
151
- else window.ezstandalone.cmd.push(call);
152
- } catch (e) {}
153
- }
121
+
122
+ const call = () => {
123
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
124
+ window.ezstandalone.destroyPlaceholders(filtered);
125
+ }
126
+ };
154
127
 
155
- function getRecyclable(liveArr) {
156
- const margin = 1800;
157
- for (let i = 0; i < liveArr.length; i++) {
158
- const entry = liveArr[i];
159
- if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
160
- const r = safeRect(entry.wrap);
161
- if (r && r.bottom < -margin) {
162
- liveArr.splice(i, 1);
163
- return entry;
128
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
129
+ call();
130
+ } else {
131
+ window.ezstandalone.cmd.push(call);
164
132
  }
165
- }
166
- return null;
167
- }
168
-
169
- function moveWrapAfter(wrap, target, kindClass, afterPos) {
170
- try {
171
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
172
- wrap.setAttribute('data-ezoic-after', String(afterPos));
173
- target.insertAdjacentElement('afterend', wrap);
174
- return true;
175
- } catch (e) {
176
- return false;
177
- }
178
- }
179
133
 
180
- function pickId(pool, liveArr) {
181
- if (pool.length) return { id: pool.shift(), recycled: null };
182
- const recycled = getRecyclable(liveArr);
183
- if (recycled) return { id: recycled.id, recycled };
184
- return { id: null, recycled: null };
185
- }
186
-
187
-
188
- function resetPlaceholderInWrap(wrap, id) {
189
- try {
190
- if (!wrap) return;
191
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
192
- try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
193
- const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
194
- if (old) old.remove();
195
- // Remove any leftover markup inside wrapper
196
- wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
197
- const ph = document.createElement('div');
198
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
199
- wrap.appendChild(ph);
134
+ // Recyclage: libérer après 100ms
135
+ setTimeout(() => {
136
+ filtered.forEach(id => sessionDefinedIds.delete(id));
137
+ }, 100);
200
138
  } catch (e) {}
201
139
  }
202
140
 
203
- function isAdjacentAd(target) {
204
- if (!target || !target.nextElementSibling) return false;
205
- const next = target.nextElementSibling;
206
- if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
207
- return false;
208
- }
209
-
210
- function isPrevAd(target) {
211
- if (!target || !target.previousElementSibling) return false;
212
- const prev = target.previousElementSibling;
213
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
214
- return false;
215
- }
216
-
217
141
  function buildWrap(id, kindClass, afterPos) {
218
142
  const wrap = document.createElement('div');
219
143
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
@@ -233,404 +157,150 @@
233
157
  function insertAfter(target, id, kindClass, afterPos) {
234
158
  if (!target || !target.insertAdjacentElement) return null;
235
159
  if (findWrap(kindClass, afterPos)) return null;
236
- const wrap = buildWrap(id, kindClass, afterPos);
237
- target.insertAdjacentElement('afterend', wrap);
238
- attachFillObserver(wrap, id);
239
- return wrap;
240
- }
241
-
242
- function destroyUsedPlaceholders() {
243
- const ids = [];
244
- try {
245
- state.usedTopics.forEach((id) => ids.push(id));
246
- state.usedPosts.forEach((id) => ids.push(id));
247
- state.usedCategories && state.usedCategories.forEach((id) => ids.push(id));
248
- } catch (e) {}
249
- destroyPlaceholderIds(ids);
250
- }
251
- } catch (e) {}
252
- };
253
-
254
- try {
255
- window.ezstandalone = window.ezstandalone || {};
256
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
257
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
258
- else window.ezstandalone.cmd.push(call);
259
- } catch (e) {}
260
- }
261
160
 
262
- function patchShowAds() {
263
- // Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
264
161
  try {
265
- window.ezstandalone = window.ezstandalone || {};
266
- const ez = window.ezstandalone;
267
- if (ez.__nodebbEzoicPatched) return;
268
- if (typeof ez.showAds !== 'function') return;
269
-
270
- ez.__nodebbEzoicPatched = true;
271
- const orig = ez.showAds;
272
-
273
- ez.showAds = function (arg) {
274
- if (Array.isArray(arg)) {
275
- const seen = new Set();
276
- for (const v of arg) {
277
- const id = parseInt(v, 10);
278
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
279
- seen.add(id);
280
- try { orig.call(ez, id); } catch (e) {}
281
- }
282
- return;
283
- }
284
- return orig.apply(ez, arguments);
285
- };
286
- } catch (e) {}
287
- }
288
-
289
-
290
-
291
- function markFilled(wrap) {
292
- try {
293
- if (!wrap) return;
294
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
295
- try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
296
- wrap.setAttribute('data-ezoic-filled', '1');
297
- } catch (e) {}
298
- }
299
-
300
- function isWrapMarkedFilled(wrap) {
301
- try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
302
- }
303
-
304
- function attachFillObserver(wrap, id) {
305
- try {
306
- const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
307
- if (!ph) return;
308
- // Already filled?
309
- if (ph.childNodes && ph.childNodes.length > 0) {
310
- markFilled(wrap);
311
- state.definedIds && state.definedIds.add(id);
312
- return;
313
- }
314
- const obs = new MutationObserver(() => {
315
- if (ph.childNodes && ph.childNodes.length > 0) {
316
- markFilled(wrap);
317
- try { state.definedIds && state.definedIds.add(id); } catch (e) {}
318
- try { obs.disconnect(); } catch (e) {}
319
- }
320
- });
321
- obs.observe(ph, { childList: true, subtree: true });
322
- // Keep a weak reference on the wrapper so we can disconnect on recycle/remove
323
- wrap.__ezoicFillObs = obs;
324
- } catch (e) {}
325
- }
326
-
327
- function isPlaceholderFilled(id) {
328
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
329
- if (!ph || !ph.isConnected) return false;
330
-
331
- const wrap = ph.parentElement;
332
- if (wrap && isWrapMarkedFilled(wrap)) return true;
333
-
334
- const filled = !!(ph.childNodes && ph.childNodes.length > 0);
335
- if (filled) {
336
- try { state.definedIds && state.definedIds.add(id); } catch (e) {}
337
- try { markFilled(wrap); } catch (e) {}
162
+ const wrap = buildWrap(id, kindClass, afterPos);
163
+ target.insertAdjacentElement('afterend', wrap);
164
+ return wrap;
165
+ } catch (e) {
166
+ return null;
338
167
  }
339
- return filled;
340
- }
341
- return filled;
342
168
  }
343
169
 
344
- function scheduleRefill(delay = 350) {
345
- clearTimeout(state.retryTimer);
346
- state.retryTimer = setTimeout(refillUnfilled, delay);
170
+ function pickId(pool) {
171
+ if (!pool || !pool.length) return null;
172
+ return pool.shift();
347
173
  }
348
174
 
349
- function enqueueRetry(id) {
175
+ function callShowAds(id) {
350
176
  if (!id) return;
351
- if (state.badIds && state.badIds.has(id)) return;
352
- if (state.retryQueueSet.has(id)) return;
353
- state.retryQueueSet.add(id);
354
- state.retryQueue.push(id);
355
- processRetryQueue();
356
- }
357
-
358
- function processRetryQueue() {
359
- if (state.retryQueueRunning) return;
360
- state.retryQueueRunning = true;
361
-
362
- const step = () => {
363
- const id = state.retryQueue.shift();
364
- if (!id) {
365
- state.retryQueueRunning = false;
366
- state.badIds = new Set();
367
- state.definedIds = new Set();
368
- return;
369
- }
370
- state.retryQueueSet.delete(id);
371
- // If this id was previously attempted and still empty, force a full reset before re-requesting.
372
- const attempts = (state.retryById.get(id) || 0);
373
- const phNow = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
374
- const wrapNow = phNow && phNow.parentElement;
375
- // If Ezoic already defined this id earlier but the placeholder is empty now, we must destroy+reset before re-showAds.
376
- if (wrapNow && wrapNow.isConnected && state.definedIds && state.definedIds.has(id) && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow)) {
377
- destroyPlaceholderIds([id]);
378
- resetPlaceholderInWrap(wrapNow, id);
379
- }
380
- // If this id was previously attempted and still empty, force a full reset before re-requesting.
381
- if (attempts > 0 && wrapNow && wrapNow.isConnected && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow)) {
382
- destroyPlaceholderIds([id]);
383
- resetPlaceholderInWrap(wrapNow, id);
384
- }
385
- callShowAdsWhenReady(id);
386
- setTimeout(step, 1100);
387
- };
388
-
389
- step();
390
- }
391
-
392
-
393
- function refillUnfilled() {
394
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
395
- let scheduledAny = false;
396
-
397
- for (const wrap of wraps) {
398
- const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
399
- if (!ph) continue;
400
- const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
401
- if (!Number.isFinite(id) || id <= 0) continue;
402
-
403
- if (isPlaceholderFilled(id)) {
404
- state.retryById.delete(id);
405
- continue;
406
- }
407
- // If wrapper was marked filled, don't try to refill even if placeholder temporarily appears empty.
408
- if (isWrapMarkedFilled(wrap)) {
409
- state.retryById.delete(id);
410
- continue;
411
- }
412
-
413
- const tries = (state.retryById.get(id) || 0);
414
- if (tries >= 8) { state.badIds && state.badIds.add(id); continue; }
415
-
416
- const r = safeRect(wrap);
417
- if (r && (r.top > window.innerHeight + 1200 || r.bottom < -1200)) continue;
418
-
419
- state.retryById.set(id, tries + 1);
420
- enqueueRetry(id);
421
- scheduledAny = true;
422
- }
423
-
424
- if (scheduledAny) scheduleRefill(700);
425
- }
426
-
427
- function callShowAdsWhenReady(id) {
428
- if (!id) return;
429
-
430
- const now = Date.now();
431
- const last = state.lastShowById.get(id) || 0;
432
- if (now - last < 3500) return;
433
-
434
- const phId = `${PLACEHOLDER_PREFIX}${id}`;
435
-
436
- const doCall = () => {
437
- try {
438
- window.ezstandalone = window.ezstandalone || {};
177
+
178
+ try {
179
+ window.ezstandalone = window.ezstandalone || {};
180
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
181
+
182
+ window.ezstandalone.cmd.push(function() {
439
183
  if (typeof window.ezstandalone.showAds === 'function') {
440
- state.lastShowById.set(id, Date.now());
441
184
  window.ezstandalone.showAds(id);
442
- return true;
185
+ sessionDefinedIds.add(id);
443
186
  }
444
- } catch (e) {}
445
- return false;
446
- };
447
-
448
- let attempts = 0;
449
- (function waitForPh() {
450
- attempts += 1;
451
- const el = document.getElementById(phId);
452
- if (el && el.isConnected) {
453
- if (doCall()) return;
454
-
455
- if (state.pendingById.has(id)) return;
456
- state.pendingById.add(id);
457
-
458
- window.ezstandalone = window.ezstandalone || {};
459
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
460
- window.ezstandalone.cmd.push(() => {
461
- try {
462
- if (typeof window.ezstandalone.showAds === 'function') {
463
- state.pendingById.delete(id);
464
- state.lastShowById.set(id, Date.now());
465
- window.ezstandalone.showAds(id);
466
- }
467
- } catch (e) {}
468
- });
469
-
470
- let tries = 0;
471
- (function tick() {
472
- tries += 1;
473
- if (doCall() || tries >= 5) {
474
- if (tries >= 5) state.pendingById.delete(id);
475
- return;
476
- }
477
- setTimeout(tick, 700);
478
- })();
479
- return;
480
- }
481
-
482
- if (attempts < 50) setTimeout(waitForPh, 50);
483
- })();
187
+ });
188
+ } catch (e) {}
484
189
  }
485
190
 
486
- function nextId(pool) {
487
- // backward compatible: the injector passes the pool array
488
- if (Array.isArray(pool) && pool.length) return pool.shift();
489
- return null;
490
- }
191
+ function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
192
+ if (!items || items.length === 0) return 0;
491
193
 
492
- async function fetchConfig() {
493
- if (state.cfg) return state.cfg;
494
- if (state.cfgPromise) return state.cfgPromise;
495
-
496
- state.cfgPromise = (async () => {
497
- try {
498
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
499
- if (!res.ok) return null;
500
- state.cfg = await res.json();
501
- return state.cfg;
502
- } catch (e) {
503
- return null;
504
- } finally {
505
- state.cfgPromise = null;
506
- }
507
- })();
508
-
509
- return state.cfgPromise;
510
- }
511
-
512
- function initPools(cfg) {
513
- if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
514
- if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
515
- if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
516
- }
194
+ let inserted = 0;
195
+ const targets = [];
517
196
 
518
- function computeTargets(count, interval, showFirst) {
519
- const out = [];
520
- if (count <= 0) return out;
521
- if (showFirst) out.push(1);
522
- for (let i = 1; i <= count; i++) {
523
- if (i % interval === 0) out.push(i);
197
+ for (let i = 0; i < items.length; i++) {
198
+ const afterPos = i + 1;
199
+ if (afterPos === 1 && !showFirst) continue;
200
+ if (afterPos % interval !== (showFirst ? 1 : 0)) continue;
201
+ targets.push(afterPos);
524
202
  }
525
- return Array.from(new Set(out)).sort((a, b) => a - b);
526
- }
527
203
 
528
- function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
529
- if (!items.length) return 0;
530
- const targets = computeTargets(items.length, interval, showFirst);
531
-
532
- let inserted = 0;
533
204
  for (const afterPos of targets) {
534
205
  if (inserted >= MAX_INSERTS_PER_RUN) break;
535
206
 
536
207
  const el = items[afterPos - 1];
537
208
  if (!el || !el.isConnected) continue;
538
-
539
- // Prevent adjacent ads (DOM-based, robust against virtualization)
540
- if (isAdjacentAd(el) || isPrevAd(el)) {
541
- continue;
542
- }
543
-
544
- // Prevent back-to-back at load
545
- const prevWrap = findWrap(kindClass, afterPos - 1);
546
- if (prevWrap) continue;
547
-
548
209
  if (findWrap(kindClass, afterPos)) continue;
549
210
 
550
- const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
551
- : (kindClass === 'ezoic-ad-message') ? state.liveMessage
552
- : state.liveCategory;
553
-
554
- const pick = pickId(kindPool, liveArr);
555
- const id = pick.id;
211
+ const id = pickId(pool);
556
212
  if (!id) break;
557
213
 
558
- let wrap = null;
559
- if (pick.recycled && pick.recycled.wrap) {
560
- // Only destroy if Ezoic has actually defined this placeholder before
561
- if (state.definedIds && state.definedIds.has(id)) {
562
- destroyPlaceholderIds([id]);
563
- }
564
- // Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
565
- const oldWrap = pick.recycled.wrap;
566
- try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
567
- try { oldWrap && oldWrap.remove(); } catch (e) {}
568
- wrap = insertAfter(el, id, kindClass, afterPos);
569
- if (!wrap) continue;
570
- setTimeout(() => { enqueueRetry(id); }, 450);
571
- } else {
572
- usedSet.add(id);
573
- wrap = insertAfter(el, id, kindClass, afterPos);
574
- if (!wrap) continue;
575
- }
214
+ const wrap = insertAfter(el, id, kindClass, afterPos);
215
+ if (!wrap) continue;
576
216
 
577
- liveArr.push({ id, wrap });
578
- // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
579
- if (wrap && (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))) {
580
- try { wrap.remove(); } catch (e) {}
581
- // Put id back if it was newly consumed (not recycled)
582
- if (!(pick.recycled && pick.recycled.wrap)) {
583
- try { kindPool.unshift(id); } catch (e) {}
584
- try { usedSet.delete(id); } catch (e) {}
585
- }
586
- inserted -= 0; // no-op
587
- continue;
588
- }
589
- if (!(pick.recycled && pick.recycled.wrap)) {
590
- callShowAdsWhenReady(id);
591
- }
217
+ usedSet.add(id);
218
+ callShowAds(id);
592
219
  inserted += 1;
593
220
  }
221
+
594
222
  return inserted;
595
223
  }
596
224
 
597
- function enforceNoAdjacentAds() {
598
- const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
599
- for (let i = 0; i < ads.length; i++) {
600
- const ad = ads[i];
601
- const prev = ad.previousElementSibling;
602
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) ad.style.display = 'none';
603
- else ad.style.display = '';
225
+ async function runCore() {
226
+ if (!state.canShowAds) return;
227
+
228
+ const cfg = await fetchConfig();
229
+ if (!cfg || cfg.excluded) return;
230
+
231
+ initPools(cfg);
232
+
233
+ const kind = getKind();
234
+ let inserted = 0;
235
+
236
+ if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
237
+ inserted = injectBetween(
238
+ 'ezoic-ad-message',
239
+ getPostContainers(),
240
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
241
+ normalizeBool(cfg.showFirstMessageAd),
242
+ state.poolPosts,
243
+ state.usedPosts
244
+ );
245
+ } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
246
+ inserted = injectBetween(
247
+ 'ezoic-ad-between',
248
+ getTopicItems(),
249
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
250
+ normalizeBool(cfg.showFirstTopicAd),
251
+ state.poolTopics,
252
+ state.usedTopics
253
+ );
254
+ } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
255
+ inserted = injectBetween(
256
+ 'ezoic-ad-category',
257
+ getCategoryItems(),
258
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 6),
259
+ normalizeBool(cfg.showFirstCategoryAd),
260
+ state.poolCategories,
261
+ state.usedCategories
262
+ );
604
263
  }
605
264
  }
606
265
 
266
+ function scheduleRun() {
267
+ if (state.scheduled) return;
268
+ state.scheduled = true;
269
+
270
+ clearTimeout(state.timer);
271
+ state.timer = setTimeout(() => {
272
+ state.scheduled = false;
273
+ const pk = getPageKey();
274
+ if (state.pageKey && pk !== state.pageKey) return;
275
+ runCore().catch(() => {});
276
+ }, 50);
277
+ }
278
+
607
279
  function cleanup() {
608
- destroyUsedPlaceholders();
280
+ const allIds = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
281
+ if (allIds.length) destroyPlaceholderIds(allIds);
282
+
283
+ document.querySelectorAll('.ezoic-ad').forEach(el => {
284
+ try { el.remove(); } catch (e) {}
285
+ });
609
286
 
610
287
  state.pageKey = getPageKey();
611
288
  state.cfg = null;
612
289
  state.cfgPromise = null;
613
-
614
290
  state.poolTopics = [];
615
291
  state.poolPosts = [];
616
292
  state.poolCategories = [];
617
- state.poolCategories = [];
618
293
  state.usedTopics.clear();
619
294
  state.usedPosts.clear();
620
- state.usedCategories && state.usedCategories.clear();
621
- state.liveBetween = [];
622
- state.liveMessage = [];
623
- state.liveCategory = [];
624
295
  state.usedCategories.clear();
296
+ state.lastShowById.clear();
297
+ sessionDefinedIds.clear();
625
298
 
626
- state.lastShowById = new Map();
627
- state.pendingById = new Set();
628
-
629
- state.attempts = 0;
630
-
631
- document.querySelectorAll(`.${WRAP_CLASS}`).forEach(el => el.remove());
299
+ if (state.obs) {
300
+ state.obs.disconnect();
301
+ state.obs = null;
302
+ }
632
303
 
633
- if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; }
634
304
  state.scheduled = false;
635
305
  clearTimeout(state.timer);
636
306
  state.timer = null;
@@ -638,139 +308,64 @@
638
308
 
639
309
  function ensureObserver() {
640
310
  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
- setTimeout(() => { if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; } }, 15000);
311
+ state.obs = new MutationObserver(() => scheduleRun());
312
+ try {
313
+ state.obs.observe(document.body, { childList: true, subtree: true });
314
+ } catch (e) {}
644
315
  }
645
316
 
646
- async function runCore() {
647
- patchShowAds();
648
-
649
- const cfg = await fetchConfig();
650
- if (!cfg || cfg.excluded) return;
651
-
652
- initPools(cfg);
653
-
317
+ function waitForContentThenRun() {
654
318
  const kind = getKind();
655
- let inserted = 0;
319
+ let selector = SELECTORS.postItem;
320
+ if (kind === 'categoryTopics') selector = SELECTORS.topicItem;
321
+ else if (kind === 'categories') selector = SELECTORS.categoryItem;
656
322
 
657
- if (kind === 'topic') {
658
- if (normalizeBool(cfg.enableMessageAds)) {
659
- inserted = injectBetween('ezoic-ad-message', getPostContainers(),
660
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
661
- normalizeBool(cfg.showFirstMessageAd),
662
- 'message',
663
- state.usedPosts);
664
- }
665
- } else if (kind === 'categoryTopics') {
666
- if (normalizeBool(cfg.enableBetweenAds)) {
667
- inserted = injectBetween('ezoic-ad-between', getTopicItems(),
668
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
669
- normalizeBool(cfg.showFirstTopicAd),
670
- 'between',
671
- state.usedTopics);
672
- }
673
- } else if (kind === 'categories') {
674
- if (normalizeBool(cfg.enableCategoryAds)) {
675
- inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
676
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
677
- normalizeBool(cfg.showFirstCategoryAd),
678
- 'categories',
679
- state.usedCategories);
323
+ const check = () => {
324
+ if (document.querySelector(selector)) {
325
+ scheduleRun();
326
+ } else {
327
+ setTimeout(check, 200);
680
328
  }
681
- }
682
-
683
- enforceNoAdjacentAds();
684
- scheduleRefill(250);
685
-
686
- // If nothing inserted and list isn't in DOM yet (first click), retry a bit
687
- let count = 0;
688
- if (kind === 'topic') count = getPostContainers().length;
689
- else if (kind === 'categoryTopics') count = getTopicItems().length;
690
- else if (kind === 'categories') count = getCategoryItems().length;
691
-
692
- if (count === 0 && state.attempts < 25) {
693
- state.attempts += 1;
694
- setTimeout(() => scheduleRun('await-items'), 120);
695
- return;
696
- }
697
-
698
- if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
699
- }
700
-
701
- function scheduleRun() {
702
- if (state.scheduled) return;
703
- state.scheduled = true;
704
-
705
- clearTimeout(state.timer);
706
- state.timer = setTimeout(() => {
707
- state.scheduled = false;
708
- const pk = getPageKey();
709
- if (state.pageKey && pk !== state.pageKey) return;
710
- runCore().catch(() => {});
711
- }, 80);
329
+ };
330
+ check();
712
331
  }
713
332
 
714
333
  function bind() {
715
334
  if (!$) return;
716
335
 
717
336
  $(window).off('.ezoicInfinite');
718
-
719
337
  $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
720
-
721
338
  $(window).on('action:ajaxify.end.ezoicInfinite', () => {
722
339
  state.pageKey = getPageKey();
723
340
  ensureObserver();
341
+ state.canShowAds = true;
724
342
  scheduleRun();
725
- setTimeout(scheduleRun, 200);
726
- setTimeout(scheduleRun, 700);
727
343
  });
728
344
 
729
345
  $(window).on('action:category.loaded.ezoicInfinite', () => {
730
346
  ensureObserver();
731
- scheduleRun();
732
- setTimeout(scheduleRun, 250);
347
+ waitForContentThenRun();
733
348
  });
734
349
 
735
350
  $(window).on('action:topics.loaded.ezoicInfinite', () => {
736
351
  ensureObserver();
737
- scheduleRun();
738
- setTimeout(scheduleRun, 150);
739
- });
740
-
741
- $(window).on('action:topic.loaded.ezoicInfinite', () => {
742
- ensureObserver();
743
- scheduleRun();
744
- setTimeout(scheduleRun, 200);
745
- });
746
-
747
- $(window).on('action:posts.loaded.ezoicInfinite', () => {
748
- ensureObserver();
749
- scheduleRun();
750
- setTimeout(scheduleRun, 150);
352
+ waitForContentThenRun();
751
353
  });
752
354
  }
753
355
 
754
- cleanup();
755
- bind();
756
- ensureObserver();
757
- state.pageKey = getPageKey();
758
- scheduleRun();
759
- setTimeout(scheduleRun, 250);
760
- })()
761
- function bindScroll() {
762
- if (state.__scrollBound) return;
763
- state.__scrollBound = true;
764
- let ticking = false;
765
- window.addEventListener('scroll', () => {
766
- if (ticking) return;
767
- ticking = true;
768
- window.requestAnimationFrame(() => {
769
- ticking = false;
770
- enforceNoAdjacentAds();
771
- scheduleRefill(200);
772
- });
773
- }, { passive: true });
356
+ function init() {
357
+ state.pageKey = getPageKey();
358
+ state.canShowAds = true;
359
+ bind();
360
+ ensureObserver();
361
+ waitForContentThenRun();
774
362
  }
775
363
 
776
- ;
364
+ if ($ && $(document).ready) {
365
+ $(document).ready(init);
366
+ } else if (document.readyState === 'loading') {
367
+ document.addEventListener('DOMContentLoaded', init);
368
+ } else {
369
+ init();
370
+ }
371
+ })();
@@ -1,13 +1,11 @@
1
1
  .ezoic-ad {
2
2
  padding: 0 !important;
3
3
  margin: 0 !important;
4
+ min-height: 0 !important;
5
+ min-width: 0 !important;
4
6
  }
5
7
 
6
- /* CRITIQUE: Forcer min-height et min-width à 0 */
7
- .ezoic-ad,
8
- .ezoic-ad *,
9
- span.ezoic-ad,
10
- span[class*="ezoic"] {
8
+ .ezoic-ad * {
11
9
  min-height: 0 !important;
12
10
  min-width: 0 !important;
13
11
  }