nodebb-plugin-ezoic-infinite 1.7.23 → 1.7.24

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 +59 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.23",
3
+ "version": "1.7.24",
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,5 +1,5 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v29
2
+ * NodeBB Ezoic Infinite Ads — client.js v30
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
@@ -30,6 +30,14 @@
30
30
  * v28 decluster supprimé. pruneOrphans supprimé (v27). Wraps persistants sur session.
31
31
  *
32
32
  * v29 Fix ancrage topics : data-index → data-tid.
33
+ *
34
+ * v30 Fix adjacentWrap : ne compte plus les wraps orphelins (ancre hors DOM).
35
+ * Quand NodeBB virtualise et retire des topics du DOM, les wraps restent
36
+ * en place (div dans le ul). adjacentWrap(el) retournait true sur ces
37
+ * wraps orphelins → injection bloquée sur les topics suivants.
38
+ * Fix : adjacentWrap vérifie que le wrap voisin a son ancre dans le DOM.
39
+ * recycleOrphanId() : quand le pool est épuisé, recycle les wraps orphelins
40
+ * non remplis qui sont loin au-dessus du viewport.
33
41
  * data-index = position relative dans le batch NodeBB, pas un ID stable.
34
42
  * Lors du scroll infini, les nouveaux batches démarrent à data-index=0,
35
43
  * ce qui créait des collisions de clés d'ancrage → mauvaise déduplication
@@ -184,11 +192,23 @@
184
192
  const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
185
193
  const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
186
194
 
195
+ function wrapIsLive(wrap) {
196
+ if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
197
+ const key = wrap.getAttribute(A_ANCHOR);
198
+ if (!key) return false;
199
+ const colonIdx = key.indexOf(':');
200
+ const klass = key.slice(0, colonIdx);
201
+ const anchorId = key.slice(colonIdx + 1);
202
+ const cfg = KIND[klass];
203
+ if (!cfg) return false;
204
+ try {
205
+ const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
206
+ return !!(found?.isConnected);
207
+ } catch (_) { return false; }
208
+ }
209
+
187
210
  function adjacentWrap(el) {
188
- return !!(
189
- el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
190
- el.previousElementSibling?.classList?.contains(WRAP_CLASS)
191
- );
211
+ return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
192
212
  }
193
213
 
194
214
  // ── Ancres stables ─────────────────────────────────────────────────────────
@@ -230,6 +250,38 @@
230
250
  return null;
231
251
  }
232
252
 
253
+ function recycleOrphanId(klass) {
254
+ // Quand le pool est épuisé : cherche un wrap orphelin (ancre hors DOM, non rempli)
255
+ // loin au-dessus du viewport et libère son ID.
256
+ const vh = window.innerHeight || 800;
257
+ const threshold = -vh * 3;
258
+ let best = null, bestBottom = Infinity;
259
+ document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
260
+ if (wrap.getAttribute(A_CREATED) === null) return;
261
+ if (isFilled(wrap)) return;
262
+ const key = wrap.getAttribute(A_ANCHOR);
263
+ if (!key) return;
264
+ const colonIdx = key.indexOf(':');
265
+ const anchorId = key.slice(colonIdx + 1);
266
+ const cfg = KIND[klass];
267
+ if (!cfg) return;
268
+ try {
269
+ const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
270
+ if (found?.isConnected) return; // ancre encore dans le DOM, pas orphelin
271
+ } catch (_) { return; }
272
+ try {
273
+ const rect = wrap.getBoundingClientRect();
274
+ if (rect.bottom > threshold) return;
275
+ if (rect.bottom < bestBottom) { bestBottom = rect.bottom; best = wrap; }
276
+ } catch (_) {}
277
+ });
278
+ if (!best) return null;
279
+ const id = parseInt(best.getAttribute(A_WRAPID), 10);
280
+ if (!Number.isFinite(id)) return null;
281
+ mutate(() => dropWrap(best));
282
+ return id;
283
+ }
284
+
233
285
  // ── Wraps DOM ──────────────────────────────────────────────────────────────
234
286
 
235
287
  function makeWrap(id, klass, key) {
@@ -315,8 +367,8 @@
315
367
  const key = anchorKey(klass, el);
316
368
  if (findWrap(key)) continue;
317
369
 
318
- const id = pickId(poolKey);
319
- if (!id) continue; // pool épuisé : tous les ids sont montés, on passe au suivant
370
+ let id = pickId(poolKey);
371
+ if (!id) { id = recycleOrphanId(klass); if (!id) continue; }
320
372
 
321
373
  const w = insertAfter(el, id, klass, key);
322
374
  if (w) { observePh(id); inserted++; }