nodebb-plugin-ezoic-infinite 1.7.23 → 1.7.25
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 +1 -1
- package/public/client.js +110 -18
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v31
|
|
3
3
|
*
|
|
4
4
|
* Historique des corrections majeures
|
|
5
5
|
* ────────────────────────────────────
|
|
@@ -30,6 +30,21 @@
|
|
|
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
|
+
* v31 pruneOrphans réactivé UNIQUEMENT pour ezoic-ad-between (topics de catégorie).
|
|
35
|
+
* NodeBB NE virtualise PAS les topics dans une liste de catégorie — les ancres
|
|
36
|
+
* restent dans le DOM entre les scrolls. pruneOrphans est donc safe ici et
|
|
37
|
+
* c'est lui qui empêche l'empilement des pubs en haut après un scroll long.
|
|
38
|
+
* Pour ezoic-ad-message (posts de topic), pruneOrphans reste désactivé car
|
|
39
|
+
* NodeBB virtualise les posts hors-viewport → faux-orphelins → bug réinjection.
|
|
40
|
+
*
|
|
41
|
+
* v30 Fix adjacentWrap : ne compte plus les wraps orphelins (ancre hors DOM).
|
|
42
|
+
* Quand NodeBB virtualise et retire des topics du DOM, les wraps restent
|
|
43
|
+
* en place (div dans le ul). adjacentWrap(el) retournait true sur ces
|
|
44
|
+
* wraps orphelins → injection bloquée sur les topics suivants.
|
|
45
|
+
* Fix : adjacentWrap vérifie que le wrap voisin a son ancre dans le DOM.
|
|
46
|
+
* recycleOrphanId() : quand le pool est épuisé, recycle les wraps orphelins
|
|
47
|
+
* non remplis qui sont loin au-dessus du viewport.
|
|
33
48
|
* data-index = position relative dans le batch NodeBB, pas un ID stable.
|
|
34
49
|
* Lors du scroll infini, les nouveaux batches démarrent à data-index=0,
|
|
35
50
|
* ce qui créait des collisions de clés d'ancrage → mauvaise déduplication
|
|
@@ -184,11 +199,23 @@
|
|
|
184
199
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
185
200
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
186
201
|
|
|
202
|
+
function wrapIsLive(wrap) {
|
|
203
|
+
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
204
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
205
|
+
if (!key) return false;
|
|
206
|
+
const colonIdx = key.indexOf(':');
|
|
207
|
+
const klass = key.slice(0, colonIdx);
|
|
208
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
209
|
+
const cfg = KIND[klass];
|
|
210
|
+
if (!cfg) return false;
|
|
211
|
+
try {
|
|
212
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
213
|
+
return !!(found?.isConnected);
|
|
214
|
+
} catch (_) { return false; }
|
|
215
|
+
}
|
|
216
|
+
|
|
187
217
|
function adjacentWrap(el) {
|
|
188
|
-
return
|
|
189
|
-
el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
190
|
-
el.previousElementSibling?.classList?.contains(WRAP_CLASS)
|
|
191
|
-
);
|
|
218
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
@@ -230,6 +257,38 @@
|
|
|
230
257
|
return null;
|
|
231
258
|
}
|
|
232
259
|
|
|
260
|
+
function recycleOrphanId(klass) {
|
|
261
|
+
// Quand le pool est épuisé : cherche un wrap orphelin (ancre hors DOM, non rempli)
|
|
262
|
+
// loin au-dessus du viewport et libère son ID.
|
|
263
|
+
const vh = window.innerHeight || 800;
|
|
264
|
+
const threshold = -vh * 3;
|
|
265
|
+
let best = null, bestBottom = Infinity;
|
|
266
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
267
|
+
if (wrap.getAttribute(A_CREATED) === null) return;
|
|
268
|
+
if (isFilled(wrap)) return;
|
|
269
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
270
|
+
if (!key) return;
|
|
271
|
+
const colonIdx = key.indexOf(':');
|
|
272
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
273
|
+
const cfg = KIND[klass];
|
|
274
|
+
if (!cfg) return;
|
|
275
|
+
try {
|
|
276
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
277
|
+
if (found?.isConnected) return; // ancre encore dans le DOM, pas orphelin
|
|
278
|
+
} catch (_) { return; }
|
|
279
|
+
try {
|
|
280
|
+
const rect = wrap.getBoundingClientRect();
|
|
281
|
+
if (rect.bottom > threshold) return;
|
|
282
|
+
if (rect.bottom < bestBottom) { bestBottom = rect.bottom; best = wrap; }
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
});
|
|
285
|
+
if (!best) return null;
|
|
286
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
287
|
+
if (!Number.isFinite(id)) return null;
|
|
288
|
+
mutate(() => dropWrap(best));
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
291
|
+
|
|
233
292
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
234
293
|
|
|
235
294
|
function makeWrap(id, klass, key) {
|
|
@@ -267,15 +326,45 @@
|
|
|
267
326
|
} catch (_) {}
|
|
268
327
|
}
|
|
269
328
|
|
|
270
|
-
// ── Prune
|
|
329
|
+
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
330
|
+
//
|
|
331
|
+
// pruneOrphans est réactivé UNIQUEMENT pour 'ezoic-ad-between'.
|
|
271
332
|
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
// ancres
|
|
333
|
+
// Pourquoi safe pour les topics ?
|
|
334
|
+
// NodeBB ne virtualise PAS la liste des topics dans une catégorie.
|
|
335
|
+
// Les <li component="category/topic"> restent dans le DOM pendant toute
|
|
336
|
+
// la session. Leurs ancres (data-tid) sont donc stables — un wrap orphelin
|
|
337
|
+
// signifie vraiment que le topic a été retiré (navigation, filtre, etc.).
|
|
276
338
|
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
339
|
+
// Pourquoi désactivé pour les posts ?
|
|
340
|
+
// NodeBB virtualise les posts hors-viewport : il retire les <li> du DOM
|
|
341
|
+
// puis les réinsère au scroll retour. pruneOrphans verait des ancres
|
|
342
|
+
// absentes → supprimerait les wraps → réinjection en haut au scroll retour.
|
|
343
|
+
//
|
|
344
|
+
// MIN_PRUNE_AGE_MS : délai de grâce après création (stabilisation du DOM).
|
|
345
|
+
|
|
346
|
+
const MIN_PRUNE_AGE_MS = 8_000;
|
|
347
|
+
|
|
348
|
+
function pruneOrphansBetween() {
|
|
349
|
+
const klass = 'ezoic-ad-between';
|
|
350
|
+
const cfg = KIND[klass];
|
|
351
|
+
|
|
352
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
353
|
+
// Délai de grâce : ne pas pruner un wrap trop récent
|
|
354
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
355
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return;
|
|
356
|
+
|
|
357
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
358
|
+
const sid = key.slice(klass.length + 1); // partie après "ezoic-ad-between:"
|
|
359
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
360
|
+
|
|
361
|
+
// Chercher l'ancre par data-tid
|
|
362
|
+
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
363
|
+
if (!anchorEl || !anchorEl.isConnected) {
|
|
364
|
+
mutate(() => dropWrap(w));
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
279
368
|
|
|
280
369
|
|
|
281
370
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
@@ -315,8 +404,8 @@
|
|
|
315
404
|
const key = anchorKey(klass, el);
|
|
316
405
|
if (findWrap(key)) continue;
|
|
317
406
|
|
|
318
|
-
|
|
319
|
-
if (!id)
|
|
407
|
+
let id = pickId(poolKey);
|
|
408
|
+
if (!id) { id = recycleOrphanId(klass); if (!id) continue; }
|
|
320
409
|
|
|
321
410
|
const w = insertAfter(el, id, klass, key);
|
|
322
411
|
if (w) { observePh(id); inserted++; }
|
|
@@ -468,10 +557,13 @@
|
|
|
468
557
|
'ezoic-ad-message', getPosts,
|
|
469
558
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
470
559
|
);
|
|
471
|
-
if (kind === 'categoryTopics')
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
560
|
+
if (kind === 'categoryTopics') {
|
|
561
|
+
pruneOrphansBetween(); // nettoie les wraps dont le topic a disparu du DOM
|
|
562
|
+
return exec(
|
|
563
|
+
'ezoic-ad-between', getTopics,
|
|
564
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
565
|
+
);
|
|
566
|
+
}
|
|
475
567
|
return exec(
|
|
476
568
|
'ezoic-ad-categories', getCategories,
|
|
477
569
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|