nodebb-plugin-ezoic-infinite 1.4.94 → 1.4.95
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 +55 -56
- package/public/style.css +3 -10
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
}, WRAP_CLASS = 'ezoic-ad';
|
|
9
9
|
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
|
|
10
10
|
|
|
11
|
+
// Nécessaire pour savoir si on doit appeler destroyPlaceholders avant recyclage.
|
|
11
12
|
const sessionDefinedIds = new Set();
|
|
12
13
|
|
|
13
14
|
const insertingIds = new Set(), state = {
|
|
@@ -105,7 +106,9 @@
|
|
|
105
106
|
|
|
106
107
|
function destroyPlaceholderIds(ids) {
|
|
107
108
|
if (!ids || !ids.length) return;
|
|
109
|
+
// Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
|
|
108
110
|
const filtered = ids.filter((id) => {
|
|
111
|
+
// Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
|
|
109
112
|
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
110
113
|
});
|
|
111
114
|
if (!filtered.length) return;
|
|
@@ -116,11 +119,6 @@
|
|
|
116
119
|
window.ezstandalone.destroyPlaceholders(filtered);
|
|
117
120
|
}
|
|
118
121
|
} catch (e) {}
|
|
119
|
-
|
|
120
|
-
// Recyclage: libérer IDs après 100ms
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
123
|
-
}, 100);
|
|
124
122
|
};
|
|
125
123
|
try {
|
|
126
124
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -128,50 +126,10 @@
|
|
|
128
126
|
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
129
127
|
else window.ezstandalone.cmd.push(call);
|
|
130
128
|
} catch (e) {}
|
|
131
|
-
|
|
132
|
-
// Recyclage: libérer IDs après 100ms
|
|
133
|
-
setTimeout(() => {
|
|
134
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
135
|
-
}, 100);
|
|
136
129
|
}
|
|
137
130
|
|
|
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
131
|
|
|
159
|
-
//
|
|
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
|
-
|
|
132
|
+
// Nettoyer les wrappers vides (sans pub) pour éviter espaces verticaux
|
|
175
133
|
function cleanupEmptyWrappers() {
|
|
176
134
|
try {
|
|
177
135
|
document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
|
|
@@ -182,7 +140,7 @@
|
|
|
182
140
|
if (ph.children.length === 0) {
|
|
183
141
|
wrapper.remove();
|
|
184
142
|
}
|
|
185
|
-
},
|
|
143
|
+
}, 3000);
|
|
186
144
|
}
|
|
187
145
|
});
|
|
188
146
|
} catch (e) {}
|
|
@@ -259,8 +217,10 @@
|
|
|
259
217
|
if (findWrap(kindClass, afterPos)) return null;
|
|
260
218
|
|
|
261
219
|
// CRITICAL: Double-lock pour éviter race conditions sur les doublons
|
|
220
|
+
// 1. Vérifier qu'aucun autre thread n'est en train d'insérer cet ID
|
|
262
221
|
if (insertingIds.has(id)) return null;
|
|
263
222
|
|
|
223
|
+
// 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
|
|
264
224
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
265
225
|
if (existingPh && existingPh.isConnected) return null;
|
|
266
226
|
|
|
@@ -273,6 +233,7 @@
|
|
|
273
233
|
attachFillObserver(wrap, id);
|
|
274
234
|
return wrap;
|
|
275
235
|
} finally {
|
|
236
|
+
// Libérer le lock après 100ms (le temps que le DOM soit stable)
|
|
276
237
|
setTimeout(() => insertingIds.delete(id), 50);
|
|
277
238
|
}
|
|
278
239
|
}
|
|
@@ -283,6 +244,8 @@
|
|
|
283
244
|
}
|
|
284
245
|
|
|
285
246
|
function patchShowAds() {
|
|
247
|
+
// Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
|
|
248
|
+
// Also ensures the patch is applied even if Ezoic loads after our script.
|
|
286
249
|
const applyPatch = () => {
|
|
287
250
|
try {
|
|
288
251
|
window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
|
|
@@ -309,6 +272,7 @@
|
|
|
309
272
|
};
|
|
310
273
|
|
|
311
274
|
applyPatch();
|
|
275
|
+
// Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
|
|
312
276
|
if (!window.__nodebbEzoicPatched) {
|
|
313
277
|
try {
|
|
314
278
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -321,6 +285,7 @@
|
|
|
321
285
|
function markFilled(wrap) {
|
|
322
286
|
try {
|
|
323
287
|
if (!wrap) return;
|
|
288
|
+
// Disconnect the fill observer first (no need to remove+re-add the attribute)
|
|
324
289
|
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
325
290
|
wrap.setAttribute('data-ezoic-filled', '1');
|
|
326
291
|
} catch (e) {}
|
|
@@ -336,18 +301,19 @@
|
|
|
336
301
|
if (!ph) return;
|
|
337
302
|
// Already filled?
|
|
338
303
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
339
|
-
markFilled(wrap);
|
|
340
|
-
sessionDefinedIds.add(id);
|
|
304
|
+
markFilled(wrap);
|
|
305
|
+
state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
|
|
341
306
|
return;
|
|
342
307
|
}
|
|
343
308
|
const obs = new MutationObserver(() => {
|
|
344
309
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
345
|
-
markFilled(wrap);
|
|
346
|
-
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
310
|
+
markFilled(wrap);
|
|
311
|
+
try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
|
|
347
312
|
try { obs.disconnect(); } catch (e) {}
|
|
348
313
|
}
|
|
349
314
|
});
|
|
350
315
|
obs.observe(ph, { childList: true, subtree: true });
|
|
316
|
+
// Keep a weak reference on the wrapper so we can disconnect on recycle/remove
|
|
351
317
|
wrap.__ezoicFillObs = obs;
|
|
352
318
|
} catch (e) {}
|
|
353
319
|
}
|
|
@@ -367,12 +333,15 @@
|
|
|
367
333
|
return filled;
|
|
368
334
|
}
|
|
369
335
|
|
|
336
|
+
// Appeler showAds() en batch selon recommandations Ezoic
|
|
337
|
+
// Au lieu de showAds(id1), showAds(id2)... faire showAds(id1, id2, id3...)
|
|
370
338
|
let batchShowAdsTimer = null;
|
|
371
339
|
const pendingShowAdsIds = new Set();
|
|
372
340
|
|
|
373
341
|
function scheduleShowAdsBatch(id) {
|
|
374
342
|
if (!id) return;
|
|
375
343
|
|
|
344
|
+
// CRITIQUE: Si cet ID a déjà été défini (sessionDefinedIds), le détruire d'abord
|
|
376
345
|
if (sessionDefinedIds.has(id)) {
|
|
377
346
|
try {
|
|
378
347
|
destroyPlaceholderIds([id]);
|
|
@@ -387,6 +356,7 @@
|
|
|
387
356
|
// Ajouter à la batch
|
|
388
357
|
pendingShowAdsIds.add(id);
|
|
389
358
|
|
|
359
|
+
// Debounce: attendre 100ms pour collecter tous les IDs
|
|
390
360
|
clearTimeout(batchShowAdsTimer);
|
|
391
361
|
batchShowAdsTimer = setTimeout(() => {
|
|
392
362
|
if (pendingShowAdsIds.size === 0) return;
|
|
@@ -410,11 +380,6 @@
|
|
|
410
380
|
}
|
|
411
381
|
});
|
|
412
382
|
} catch (e) {}
|
|
413
|
-
|
|
414
|
-
// CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
|
|
415
|
-
setTimeout(() => {
|
|
416
|
-
cleanupInvisibleEzoicElements();
|
|
417
|
-
}, 800); // 1.5s pour laisser Ezoic charger
|
|
418
383
|
}, 100);
|
|
419
384
|
}
|
|
420
385
|
|
|
@@ -441,15 +406,19 @@
|
|
|
441
406
|
const startPageKey = state.pageKey;
|
|
442
407
|
let attempts = 0;
|
|
443
408
|
(function waitForPh() {
|
|
409
|
+
// Abort if the user navigated away since this showAds was scheduled
|
|
444
410
|
if (state.pageKey !== startPageKey) return;
|
|
411
|
+
// Abort if another concurrent call is already handling this id
|
|
445
412
|
if (state.pendingById.has(id)) return;
|
|
446
413
|
|
|
447
414
|
attempts += 1;
|
|
448
415
|
const el = document.getElementById(phId);
|
|
449
416
|
if (el && el.isConnected) {
|
|
417
|
+
// CRITIQUE: Vérifier que le placeholder est VISIBLE
|
|
450
418
|
|
|
451
419
|
// Si on arrive ici, soit visible, soit timeout
|
|
452
420
|
|
|
421
|
+
// Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
|
|
453
422
|
if (doCall()) {
|
|
454
423
|
state.pendingById.delete(id);
|
|
455
424
|
return;
|
|
@@ -515,6 +484,7 @@
|
|
|
515
484
|
const el = items[afterPos - 1];
|
|
516
485
|
if (!el || !el.isConnected) continue;
|
|
517
486
|
|
|
487
|
+
// Prevent adjacent ads (DOM-based, robust against virtualization)
|
|
518
488
|
if (isAdjacentAd(el) || isPrevAd(el)) {
|
|
519
489
|
continue;
|
|
520
490
|
}
|
|
@@ -531,14 +501,17 @@
|
|
|
531
501
|
|
|
532
502
|
let wrap = null;
|
|
533
503
|
if (pick.recycled && pick.recycled.wrap) {
|
|
504
|
+
// Only destroy if Ezoic has actually defined this placeholder before
|
|
534
505
|
if (sessionDefinedIds.has(id)) {
|
|
535
506
|
destroyPlaceholderIds([id]);
|
|
536
507
|
}
|
|
508
|
+
// Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
|
|
537
509
|
const oldWrap = pick.recycled.wrap;
|
|
538
510
|
try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
|
|
539
511
|
try { oldWrap && oldWrap.remove(); } catch (e) {}
|
|
540
512
|
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
541
513
|
if (!wrap) continue;
|
|
514
|
+
// Attendre que le wrapper soit dans le DOM puis appeler showAds
|
|
542
515
|
setTimeout(() => {
|
|
543
516
|
callShowAdsWhenReady(id);
|
|
544
517
|
}, 50);
|
|
@@ -552,10 +525,12 @@
|
|
|
552
525
|
}
|
|
553
526
|
|
|
554
527
|
liveArr.push({ id, wrap });
|
|
528
|
+
// If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
|
|
555
529
|
if (wrap && (
|
|
556
530
|
(wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
|
|
557
531
|
)) {
|
|
558
532
|
try { wrap.remove(); } catch (e) {}
|
|
533
|
+
// Put id back if it was newly consumed (not recycled)
|
|
559
534
|
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
560
535
|
try { kindPool.unshift(id); } catch (e) {}
|
|
561
536
|
usedSet.delete(id);
|
|
@@ -572,6 +547,7 @@
|
|
|
572
547
|
for (let i = 0; i < ads.length; i++) {
|
|
573
548
|
const ad = ads[i], prev = ad.previousElementSibling;
|
|
574
549
|
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
550
|
+
// Supprimer le wrapper adjacent au lieu de le cacher
|
|
575
551
|
try {
|
|
576
552
|
const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
577
553
|
if (ph) {
|
|
@@ -592,6 +568,8 @@
|
|
|
592
568
|
function cleanup() {
|
|
593
569
|
destroyUsedPlaceholders();
|
|
594
570
|
|
|
571
|
+
// CRITIQUE: Supprimer TOUS les wrappers .ezoic-ad du DOM
|
|
572
|
+
// Sinon ils restent et deviennent "unused" sur la nouvelle page
|
|
595
573
|
document.querySelectorAll('.ezoic-ad').forEach(el => {
|
|
596
574
|
try { el.remove(); } catch (e) {}
|
|
597
575
|
});
|
|
@@ -607,14 +585,23 @@
|
|
|
607
585
|
state.usedPosts.clear();
|
|
608
586
|
state.usedCategories.clear();
|
|
609
587
|
state.lastShowById.clear();
|
|
588
|
+
// CRITIQUE: Vider pendingById pour annuler tous les showAds en cours
|
|
589
|
+
// Sinon Ezoic essaie d'accéder aux placeholders pendant que NodeBB vide le DOM
|
|
610
590
|
state.pendingById.clear();
|
|
611
591
|
state.definedIds.clear();
|
|
612
592
|
|
|
593
|
+
// NE PAS supprimer les wrappers Ezoic ici - ils seront supprimés naturellement
|
|
594
|
+
// quand NodeBB vide le DOM lors de la navigation ajaxify
|
|
595
|
+
// Les supprimer manuellement cause des problèmes avec l'état interne d'Ezoic
|
|
596
|
+
|
|
597
|
+
// CRITIQUE: Annuler TOUS les timeouts en cours pour éviter que les anciens
|
|
598
|
+
// showAds() continuent à s'exécuter après la navigation
|
|
613
599
|
state.activeTimeouts.forEach(id => {
|
|
614
600
|
try { clearTimeout(id); } catch (e) {}
|
|
615
601
|
});
|
|
616
602
|
state.activeTimeouts.clear();
|
|
617
603
|
|
|
604
|
+
// Vider aussi pendingById pour annuler les showAds en attente
|
|
618
605
|
state.pendingById.clear();
|
|
619
606
|
|
|
620
607
|
if (state.obs) { state.obs.disconnect(); state.obs = null; }
|
|
@@ -631,6 +618,7 @@
|
|
|
631
618
|
}
|
|
632
619
|
|
|
633
620
|
async function runCore() {
|
|
621
|
+
// Attendre que canInsert soit true (protection race condition navigation)
|
|
634
622
|
if (!state.canShowAds) {
|
|
635
623
|
return;
|
|
636
624
|
}
|
|
@@ -673,6 +661,7 @@
|
|
|
673
661
|
|
|
674
662
|
enforceNoAdjacentAds();
|
|
675
663
|
|
|
664
|
+
// If nothing inserted and list isn't in DOM yet (first click), retry a bit
|
|
676
665
|
let count = 0;
|
|
677
666
|
if (kind === 'topic') count = getPostContainers().length;
|
|
678
667
|
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
@@ -684,9 +673,12 @@
|
|
|
684
673
|
}
|
|
685
674
|
|
|
686
675
|
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
676
|
+
// Plus d'insertions possibles ce cycle, continuer immédiatement
|
|
687
677
|
setTimeout(arguments[0], 50);
|
|
688
678
|
} else if (inserted === 0 && count > 0) {
|
|
689
679
|
// Pool épuisé ou recyclage pas encore disponible.
|
|
680
|
+
// Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
|
|
681
|
+
// le temps de défiler hors écran et devenir recyclables.
|
|
690
682
|
if (state.poolWaitAttempts < 8) {
|
|
691
683
|
state.poolWaitAttempts += 1;
|
|
692
684
|
setTimeout(arguments[0], 50);
|
|
@@ -720,11 +712,15 @@
|
|
|
720
712
|
state.pageKey = getPageKey();
|
|
721
713
|
ensureObserver();
|
|
722
714
|
|
|
715
|
+
// CRITIQUE: Attendre 300ms avant de permettre l'insertion de nouveaux placeholders
|
|
716
|
+
// pour laisser les anciens showAds() (en cours) se terminer ou échouer proprement
|
|
717
|
+
// Sinon race condition: NodeBB vide le DOM pendant que Ezoic essaie d'accéder aux placeholders
|
|
723
718
|
state.canShowAds = true;
|
|
724
719
|
});
|
|
725
720
|
|
|
726
721
|
$(window).on('action:category.loaded.ezoicInfinite', () => {
|
|
727
722
|
ensureObserver();
|
|
723
|
+
// category.loaded = infinite scroll, Ezoic déjà chargé normalement
|
|
728
724
|
waitForContentThenRun();
|
|
729
725
|
});
|
|
730
726
|
$(window).on('action:topics.loaded.ezoicInfinite', () => {
|
|
@@ -754,6 +750,7 @@
|
|
|
754
750
|
window.requestAnimationFrame(() => {
|
|
755
751
|
ticking = false;
|
|
756
752
|
enforceNoAdjacentAds();
|
|
753
|
+
// Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
|
|
757
754
|
const now = Date.now();
|
|
758
755
|
if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
|
|
759
756
|
state.lastScrollRun = now;
|
|
@@ -763,6 +760,7 @@
|
|
|
763
760
|
}, { passive: true });
|
|
764
761
|
}
|
|
765
762
|
|
|
763
|
+
// Fonction qui attend que la page ait assez de contenu avant d'insérer les pubs
|
|
766
764
|
function waitForContentThenRun() {
|
|
767
765
|
const MIN_WORDS = 250;
|
|
768
766
|
let attempts = 0;
|
|
@@ -793,6 +791,7 @@
|
|
|
793
791
|
})();
|
|
794
792
|
}
|
|
795
793
|
|
|
794
|
+
// Fonction qui attend que Ezoic soit vraiment chargé
|
|
796
795
|
function waitForEzoicThenRun() {
|
|
797
796
|
let attempts = 0;
|
|
798
797
|
const maxAttempts = 50; // 50 × 200ms = 10s max
|
package/public/style.css
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
.ezoic-ad
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
margin: 0 !important;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.ezoic-ad * {
|
|
8
|
-
margin: 0 !important;
|
|
9
|
-
padding: 0 !important;
|
|
10
|
-
}
|
|
1
|
+
.ezoic-ad{height:auto !important; padding:0 !important; margin: 0.25rem 0;}
|
|
2
|
+
.ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
|
|
3
|
+
.ezoic-ad .ezoic-ad-inner > div{padding:0;margin:0;}
|