nodebb-plugin-ezoic-infinite 1.7.89 → 1.7.90

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +46 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.89",
3
+ "version": "1.7.90",
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
@@ -132,6 +132,8 @@
132
132
  burstDeadline: 0,
133
133
  burstCount: 0,
134
134
  lastBurstTs: 0,
135
+ lastScrollY: 0,
136
+ scrollDir: 1, // 1=down, -1=up
135
137
  };
136
138
 
137
139
  let blockedUntil = 0;
@@ -282,28 +284,6 @@
282
284
  return (w?.isConnected) ? w : null;
283
285
  }
284
286
 
285
- /**
286
- * Libère les ids de wraps supprimés du DOM (virtualisation NodeBB / rerender).
287
- * Sans ce sweep, mountedIds peut conserver des ids fantômes → pool épuisé
288
- * après long scroll alors qu'aucun wrap recyclable n'existe encore en DOM.
289
- */
290
- function sweepDeadWraps() {
291
- for (const [key, w] of S.wrapByKey.entries()) {
292
- if (w?.isConnected) continue;
293
- const id = parseInt(w?.getAttribute?.(A_WRAPID), 10);
294
- if (Number.isFinite(id)) {
295
- S.mountedIds.delete(id);
296
- S.lastShow.delete(id);
297
- S.pendingSet.delete(id);
298
- }
299
- S.wrapByKey.delete(key);
300
- }
301
- if (S.pending.length) {
302
- S.pending = S.pending.filter(id => !S.pendingSet.has(id) || document.getElementById(`${PH_PREFIX}${id}`)?.isConnected);
303
- S.pendingSet = new Set(S.pending);
304
- }
305
- }
306
-
307
287
  // ── Pool ───────────────────────────────────────────────────────────────────
308
288
 
309
289
  /**
@@ -335,26 +315,49 @@
335
315
  typeof ez?.define !== 'function' ||
336
316
  typeof ez?.displayMore !== 'function') return null;
337
317
 
338
- const vh = window.innerHeight || 800;
339
- // Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
340
- // après pour neutraliser l'IO — plus de showAds parasite possible.
341
- const threshold = -vh;
342
- let bestEmpty = null, bestEmptyBottom = Infinity;
343
- let bestFilled = null, bestFilledBottom = Infinity;
318
+ const vh = window.innerHeight || 800;
319
+ const targetRect = targetEl?.getBoundingClientRect?.() || { top: vh, bottom: vh };
320
+
321
+ // Recyclage bidirectionnel :
322
+ // - scroll vers le bas -> recycle préférentiellement des wraps loin au-dessus
323
+ // - scroll vers le haut -> recycle préférentiellement des wraps loin en-dessous
324
+ // Fallback sur l'autre côté si aucun candidat.
325
+ const aboveThreshold = -vh; // wrap entièrement/suffisamment au-dessus
326
+ const belowThreshold = vh * 2; // wrap loin sous le viewport
327
+
328
+ let aboveEmpty = null, aboveEmptyBottom = Infinity;
329
+ let aboveFilled = null, aboveFilledBottom = Infinity;
330
+ let belowEmpty = null, belowEmptyTop = -Infinity;
331
+ let belowFilled = null, belowFilledTop = -Infinity;
344
332
 
345
333
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
346
334
  try {
335
+ if (!wrap?.isConnected) return;
347
336
  const rect = wrap.getBoundingClientRect();
348
- if (rect.bottom > threshold) return;
349
- if (!isFilled(wrap)) {
350
- if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
351
- } else {
352
- if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
337
+
338
+ if (rect.bottom < aboveThreshold) {
339
+ if (!isFilled(wrap)) {
340
+ if (rect.bottom < aboveEmptyBottom) { aboveEmptyBottom = rect.bottom; aboveEmpty = wrap; }
341
+ } else {
342
+ if (rect.bottom < aboveFilledBottom) { aboveFilledBottom = rect.bottom; aboveFilled = wrap; }
343
+ }
344
+ return;
345
+ }
346
+
347
+ if (rect.top > belowThreshold) {
348
+ if (!isFilled(wrap)) {
349
+ if (rect.top > belowEmptyTop) { belowEmptyTop = rect.top; belowEmpty = wrap; }
350
+ } else {
351
+ if (rect.top > belowFilledTop) { belowFilledTop = rect.top; belowFilled = wrap; }
352
+ }
353
353
  }
354
354
  } catch (_) {}
355
355
  });
356
356
 
357
- const best = bestEmpty ?? bestFilled;
357
+ const preferBelow = (S.scrollDir < 0) || (targetRect.top < vh * 0.5);
358
+ const pickAbove = () => aboveEmpty ?? aboveFilled;
359
+ const pickBelow = () => belowEmpty ?? belowFilled;
360
+ const best = preferBelow ? (pickBelow() ?? pickAbove()) : (pickAbove() ?? pickBelow());
358
361
  if (!best) return null;
359
362
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
360
363
  if (!Number.isFinite(id)) return null;
@@ -493,13 +496,7 @@
493
496
  const key = anchorKey(klass, el);
494
497
  if (findWrap(key)) continue;
495
498
 
496
- let id = pickId(poolKey);
497
- if (!id) {
498
- // Réessaie après sweep : des wraps ont pu être retirés du DOM (virtualisation)
499
- // sans passer par dropWrap, laissant des ids fantômes dans mountedIds.
500
- sweepDeadWraps();
501
- id = pickId(poolKey);
502
- }
499
+ const id = pickId(poolKey);
503
500
  if (id) {
504
501
  const w = insertAfter(el, id, klass, key);
505
502
  if (w) { observePh(id); inserted++; }
@@ -641,7 +638,6 @@
641
638
  async function runCore() {
642
639
  if (isBlocked()) return 0;
643
640
  patchShowAds();
644
- sweepDeadWraps();
645
641
 
646
642
  const cfg = await fetchConfig();
647
643
  if (!cfg || cfg.excluded) return 0;
@@ -740,26 +736,16 @@
740
736
  const allSel = [SEL.post, SEL.topic, SEL.category];
741
737
  S.domObs = new MutationObserver(muts => {
742
738
  if (S.mutGuard > 0 || isBlocked()) return;
743
- let sawRelevantAdd = false;
744
- let sawWrapRemoval = false;
745
739
  for (const m of muts) {
746
- for (const n of m.removedNodes) {
747
- if (n.nodeType !== 1) continue;
748
- if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || n.querySelector?.(`.${WRAP_CLASS}`)) {
749
- sawWrapRemoval = true;
750
- }
751
- }
752
740
  for (const n of m.addedNodes) {
753
741
  if (n.nodeType !== 1) continue;
754
742
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
755
743
  if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
756
744
  allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
757
- sawRelevantAdd = true;
745
+ requestBurst(); return;
758
746
  }
759
747
  }
760
748
  }
761
- if (sawWrapRemoval) sweepDeadWraps();
762
- if (sawRelevantAdd) requestBurst();
763
749
  });
764
750
  try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
765
751
  }
@@ -861,6 +847,12 @@
861
847
  function bindScroll() {
862
848
  let ticking = false;
863
849
  window.addEventListener('scroll', () => {
850
+ try {
851
+ const y = window.scrollY || window.pageYOffset || 0;
852
+ const dy = y - (S.lastScrollY || 0);
853
+ if (Math.abs(dy) > 2) S.scrollDir = dy > 0 ? 1 : -1;
854
+ S.lastScrollY = y;
855
+ } catch (_) {}
864
856
  if (ticking) return;
865
857
  ticking = true;
866
858
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
@@ -870,6 +862,7 @@
870
862
  // ── Boot ───────────────────────────────────────────────────────────────────
871
863
 
872
864
  S.pageKey = pageKey();
865
+ try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) { S.lastScrollY = 0; }
873
866
  muteConsole();
874
867
  ensureTcfLocator();
875
868
  warmNetwork();