nodebb-plugin-ezoic-infinite 1.8.13 → 1.8.15
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/library.js +74 -78
- package/package.json +2 -2
- package/plugin.json +26 -10
- package/public/client.js +366 -656
- package/public/style.css +15 -9
package/public/client.js
CHANGED
|
@@ -1,96 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v20)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Correctifs critiques vs v19
|
|
5
|
+
* ───────────────────────────
|
|
6
|
+
* [BUG FATAL] Pubs catégories disparaissent au premier scroll sur mobile
|
|
7
|
+
* pruneOrphans cherchait l'ancre via `[data-index]`, mais les éléments
|
|
8
|
+
* `li[component="categories/category"]` ont `data-cid`, pas `data-index`.
|
|
9
|
+
* → anchorEl toujours null → suppression à chaque runCore() → disparition.
|
|
10
|
+
* Fix : table KIND_META qui mappe chaque kindClass vers son attribut d'ancre
|
|
11
|
+
* stable (data-pid pour posts, data-index pour topics, data-cid pour catégories).
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* [BUG] decluster : break trop large stoppait tout quand UN wrap était en grâce
|
|
14
|
+
* Fix : on skip uniquement le wrap courant, pas toute la boucle.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* IO fixe (une instance, jamais recréée).
|
|
14
|
-
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
16
|
+
* [BUG] injectBetween : `break` sur pool épuisé empêchait d'observer les wraps
|
|
17
|
+
* existants sur les items suivants. Fix : `continue` au lieu de `break`.
|
|
15
18
|
*
|
|
16
|
-
*
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
17
21
|
*
|
|
18
|
-
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
19
24
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* pruneOrphansBetween réactivé uniquement pour topics de catégorie :
|
|
27
|
-
* – NodeBB NE virtualise PAS les topics dans une liste de catégorie,
|
|
28
|
-
* les ancres (data-index) restent en DOM → prune safe et nécessaire
|
|
29
|
-
* pour éviter l'empilement après scroll long.
|
|
30
|
-
* – Toujours désactivé pour les posts : NodeBB virtualise les posts
|
|
31
|
-
* hors-viewport → faux-orphelins → bug réinjection en haut.
|
|
32
|
-
*
|
|
33
|
-
* v34 moveDistantWrap — voir v38.
|
|
34
|
-
*
|
|
35
|
-
* v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
|
|
36
|
-
* après login — filter:middleware.renderHeader re-évalue l'exclusion au
|
|
37
|
-
* rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
|
|
38
|
-
*
|
|
39
|
-
* v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
|
|
40
|
-
*
|
|
41
|
-
* v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
|
|
42
|
-
*
|
|
43
|
-
* v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
|
|
44
|
-
*
|
|
45
|
-
* v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
|
|
46
|
-
* Séquence : destroy → 300ms → define → 300ms → displayMore.
|
|
47
|
-
* Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
|
|
48
|
-
*
|
|
49
|
-
* v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
|
|
50
|
-
* sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
|
|
51
|
-
* déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
|
|
52
|
-
* break propre dans injectBetween. muteConsole : ajout warnings refresh.
|
|
53
|
-
*
|
|
54
|
-
* v36 Optimisations chemin critique (scroll → injectBetween) :
|
|
55
|
-
* – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
|
|
56
|
-
* sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
|
|
57
|
-
* dropWrap et cleanup.
|
|
58
|
-
* – wrapIsLive allégé : pour les voisins immédiats on vérifie les
|
|
59
|
-
* attributs du nœud lui-même sans querySelector global.
|
|
60
|
-
* – MutationObserver : matches() vérifié avant querySelector() pour
|
|
61
|
-
* court-circuiter les sous-arbres entiers ajoutés par NodeBB.
|
|
62
|
-
*
|
|
63
|
-
* v35 Revue complète prod-ready :
|
|
64
|
-
* – initPools protégé contre ré-initialisation inutile (S.poolsReady).
|
|
65
|
-
* – muteConsole élargit à "No valid placeholders for loadMore".
|
|
66
|
-
* – Commentaires et historique nettoyés.
|
|
25
|
+
* Nettoyage
|
|
26
|
+
* ─────────
|
|
27
|
+
* - Suppression du scroll boost (complexité sans gain mesurable côté SPA statique)
|
|
28
|
+
* - MAX_INFLIGHT unique desktop/mobile (inutile de différencier)
|
|
29
|
+
* - getAnchorKey/getGlobalOrdinal fusionnés en helpers cohérents
|
|
30
|
+
* - Commentaires internes allégés (code auto-documenté)
|
|
67
31
|
*/
|
|
68
|
-
(function
|
|
32
|
+
(function () {
|
|
69
33
|
'use strict';
|
|
70
34
|
|
|
71
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
72
36
|
|
|
73
|
-
const WRAP_CLASS
|
|
74
|
-
const PH_PREFIX
|
|
75
|
-
const A_ANCHOR
|
|
76
|
-
const A_WRAPID
|
|
77
|
-
const A_CREATED
|
|
78
|
-
const A_SHOWN
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const SHOW_THROTTLE_MS
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
|
|
90
|
-
const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
|
|
91
|
-
const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
|
|
92
|
-
|
|
93
|
-
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
37
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
38
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
39
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
40
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic (number string)
|
|
41
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création (ms)
|
|
42
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds (ms)
|
|
43
|
+
|
|
44
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
45
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds où l'on ne decluster pas
|
|
46
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
47
|
+
const MAX_INSERTS_PER_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
94
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
95
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
96
55
|
|
|
@@ -101,49 +60,42 @@
|
|
|
101
60
|
};
|
|
102
61
|
|
|
103
62
|
/**
|
|
104
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
105
64
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
65
|
+
* L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
|
|
66
|
+
* quelle que soit la page ou la virtualisation :
|
|
67
|
+
* posts → data-pid (id du message, unique et permanent)
|
|
68
|
+
* topics → data-index (position 0-based dans la liste, fourni par NodeBB)
|
|
69
|
+
* catégories → data-cid (id de la catégorie, unique et permanent)
|
|
70
|
+
* ← C'était le bug v19 : on cherchait data-index ici
|
|
112
71
|
*/
|
|
113
72
|
const KIND = {
|
|
114
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
115
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
116
|
-
'ezoic-ad-categories': { sel: SEL.category,
|
|
73
|
+
'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid' },
|
|
74
|
+
'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index' },
|
|
75
|
+
'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid' },
|
|
117
76
|
};
|
|
118
77
|
|
|
119
|
-
// ── État
|
|
78
|
+
// ── État ───────────────────────────────────────────────────────────────────
|
|
120
79
|
|
|
121
80
|
const S = {
|
|
122
|
-
pageKey:
|
|
123
|
-
cfg:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
141
|
-
ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
|
|
142
|
-
ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
|
|
143
|
-
scrollDir: 1, // 1=bas, -1=haut
|
|
144
|
-
scrollSpeed: 0, // px/s approx (EMA)
|
|
145
|
-
lastScrollY: 0,
|
|
146
|
-
lastScrollTs: 0,
|
|
81
|
+
pageKey: null,
|
|
82
|
+
cfg: null,
|
|
83
|
+
poolSig: null,
|
|
84
|
+
|
|
85
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
86
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
87
|
+
mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
|
|
88
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
89
|
+
|
|
90
|
+
io: null,
|
|
91
|
+
ioMargin: null,
|
|
92
|
+
domObs: null,
|
|
93
|
+
mutGuard: 0, // compteur internalMutation
|
|
94
|
+
|
|
95
|
+
inflight: 0,
|
|
96
|
+
pending: [],
|
|
97
|
+
pendingSet: new Set(),
|
|
98
|
+
|
|
147
99
|
runQueued: false,
|
|
148
100
|
burstActive: false,
|
|
149
101
|
burstDeadline: 0,
|
|
@@ -152,127 +104,13 @@
|
|
|
152
104
|
};
|
|
153
105
|
|
|
154
106
|
let blockedUntil = 0;
|
|
155
|
-
|
|
156
|
-
const ts
|
|
157
|
-
const isBlocked = () => ts() < blockedUntil;
|
|
158
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
159
|
-
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
160
|
-
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
161
|
-
|
|
162
|
-
function healFalseEmpty(root = document) {
|
|
163
|
-
try {
|
|
164
|
-
const list = [];
|
|
165
|
-
if (root instanceof Element && root.classList?.contains(WRAP_CLASS)) list.push(root);
|
|
166
|
-
const found = root.querySelectorAll ? root.querySelectorAll(`.${WRAP_CLASS}.is-empty`) : [];
|
|
167
|
-
for (const w of found) list.push(w);
|
|
168
|
-
for (const w of list) {
|
|
169
|
-
if (!w?.classList?.contains('is-empty')) continue;
|
|
170
|
-
if (isFilled(w)) w.classList.remove('is-empty');
|
|
171
|
-
}
|
|
172
|
-
} catch (_) {}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function phEl(id) {
|
|
176
|
-
return document.getElementById(`${PH_PREFIX}${id}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function hasSinglePlaceholder(id) {
|
|
180
|
-
try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function canShowPlaceholderId(id, now = ts()) {
|
|
184
|
-
const n = parseInt(id, 10);
|
|
185
|
-
if (!Number.isFinite(n) || n <= 0) return false;
|
|
186
|
-
if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
|
|
187
|
-
const ph = phEl(n);
|
|
188
|
-
if (!ph?.isConnected || isFilled(ph)) return false;
|
|
189
|
-
if (!hasSinglePlaceholder(n)) return false;
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function queueSweepDeadWraps() {
|
|
194
|
-
if (S.sweepQueued) return;
|
|
195
|
-
S.sweepQueued = true;
|
|
196
|
-
requestAnimationFrame(() => {
|
|
197
|
-
S.sweepQueued = false;
|
|
198
|
-
sweepDeadWraps();
|
|
199
|
-
healFalseEmpty();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function getDynamicShowBatchMax() {
|
|
204
|
-
const speed = S.scrollSpeed || 0;
|
|
205
|
-
const pend = S.pending.length;
|
|
206
|
-
// Scroll très rapide => petits batches (réduit le churn/unused)
|
|
207
|
-
if (speed > 2600) return 2;
|
|
208
|
-
if (speed > 1400) return 3;
|
|
209
|
-
// Peu de candidats => flush plus vite, inutile d'attendre 4
|
|
210
|
-
if (pend <= 1) return 1;
|
|
211
|
-
if (pend <= 3) return 2;
|
|
212
|
-
// Par défaut compromis dynamique
|
|
213
|
-
return 3;
|
|
214
|
-
}
|
|
107
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
108
|
+
const ts = () => Date.now();
|
|
215
109
|
|
|
216
110
|
function mutate(fn) {
|
|
217
111
|
S.mutGuard++;
|
|
218
112
|
try { fn(); } finally { S.mutGuard--; }
|
|
219
113
|
}
|
|
220
|
-
function scheduleDestroyFlush() {
|
|
221
|
-
if (S.destroyBatchTimer) return;
|
|
222
|
-
S.destroyBatchTimer = setTimeout(() => {
|
|
223
|
-
S.destroyBatchTimer = 0;
|
|
224
|
-
flushDestroyBatch();
|
|
225
|
-
}, DESTROY_FLUSH_MS);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function flushDestroyBatch() {
|
|
229
|
-
if (!S.destroyPending.length) return;
|
|
230
|
-
const ids = [];
|
|
231
|
-
while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
|
|
232
|
-
const id = S.destroyPending.shift();
|
|
233
|
-
S.destroyPendingSet.delete(id);
|
|
234
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
235
|
-
ids.push(id);
|
|
236
|
-
}
|
|
237
|
-
if (ids.length) {
|
|
238
|
-
try {
|
|
239
|
-
const ez = window.ezstandalone;
|
|
240
|
-
const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
|
|
241
|
-
try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
|
|
242
|
-
} catch (_) {}
|
|
243
|
-
}
|
|
244
|
-
if (S.destroyPending.length) scheduleDestroyFlush();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function destroyEzoicId(id) {
|
|
248
|
-
if (!Number.isFinite(id) || id <= 0) return;
|
|
249
|
-
if (!S.ezActiveIds.has(id)) return;
|
|
250
|
-
S.ezActiveIds.delete(id);
|
|
251
|
-
if (!S.destroyPendingSet.has(id)) {
|
|
252
|
-
S.destroyPending.push(id);
|
|
253
|
-
S.destroyPendingSet.add(id);
|
|
254
|
-
}
|
|
255
|
-
scheduleDestroyFlush();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function destroyBeforeReuse(ids) {
|
|
259
|
-
const out = [];
|
|
260
|
-
const toDestroy = [];
|
|
261
|
-
const seen = new Set();
|
|
262
|
-
for (const raw of (ids || [])) {
|
|
263
|
-
const id = parseInt(raw, 10);
|
|
264
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
265
|
-
seen.add(id);
|
|
266
|
-
out.push(id);
|
|
267
|
-
if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
|
|
268
|
-
}
|
|
269
|
-
if (toDestroy.length) {
|
|
270
|
-
try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
|
|
271
|
-
for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
|
|
272
|
-
}
|
|
273
|
-
return out;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
114
|
|
|
277
115
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
278
116
|
|
|
@@ -285,22 +123,44 @@ function destroyBeforeReuse(ids) {
|
|
|
285
123
|
return S.cfg;
|
|
286
124
|
}
|
|
287
125
|
|
|
126
|
+
function initPools(cfg) {
|
|
127
|
+
// (Perf) Ne reparse les pools que si les strings ont changé.
|
|
128
|
+
const sig = `${cfg.placeholderIds || ''}§${cfg.messagePlaceholderIds || ''}§${cfg.categoryPlaceholderIds || ''}`;
|
|
129
|
+
if (S.poolSig === sig) return;
|
|
130
|
+
S.poolSig = sig;
|
|
131
|
+
|
|
132
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
133
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
134
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
135
|
+
|
|
136
|
+
// Réinitialise les curseurs si une pool change
|
|
137
|
+
S.cursors.topics = 0;
|
|
138
|
+
S.cursors.posts = 0;
|
|
139
|
+
S.cursors.categories = 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
288
142
|
function parseIds(raw) {
|
|
143
|
+
// Accepte : un ID par ligne, ou séparés par virgules/espaces (ACP le mentionne).
|
|
289
144
|
const out = [], seen = new Set();
|
|
290
|
-
|
|
145
|
+
const parts = String(raw || '')
|
|
146
|
+
.replace(/\r/g, '\n')
|
|
147
|
+
.split(/[\n\s,]+/)
|
|
148
|
+
.map(s => s.trim())
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
|
|
151
|
+
for (const v of parts) {
|
|
291
152
|
const n = parseInt(v, 10);
|
|
292
153
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
293
154
|
}
|
|
294
155
|
return out;
|
|
295
156
|
}
|
|
296
157
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
158
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
159
|
+
|
|
160
|
+
const isFilled = (n) =>
|
|
161
|
+
!!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
162
|
+
|
|
163
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
304
164
|
|
|
305
165
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
306
166
|
|
|
@@ -324,13 +184,13 @@ function destroyBeforeReuse(ids) {
|
|
|
324
184
|
return 'other';
|
|
325
185
|
}
|
|
326
186
|
|
|
327
|
-
// ──
|
|
187
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
328
188
|
|
|
329
189
|
function getPosts() {
|
|
330
190
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
331
191
|
if (!el.isConnected) return false;
|
|
332
192
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
333
|
-
const p = el.parentElement?.closest(
|
|
193
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
334
194
|
if (p && p !== el) return false;
|
|
335
195
|
return el.getAttribute('component') !== 'post/parent';
|
|
336
196
|
});
|
|
@@ -339,87 +199,53 @@ function destroyBeforeReuse(ids) {
|
|
|
339
199
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
340
200
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
341
201
|
|
|
342
|
-
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
346
|
-
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
347
|
-
*/
|
|
348
|
-
function wrapIsLive(wrap) {
|
|
349
|
-
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
350
|
-
const key = wrap.getAttribute(A_ANCHOR);
|
|
351
|
-
if (!key) return false;
|
|
352
|
-
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
353
|
-
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
354
|
-
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
355
|
-
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
356
|
-
const colonIdx = key.indexOf(':');
|
|
357
|
-
const klass = key.slice(0, colonIdx);
|
|
358
|
-
const anchorId = key.slice(colonIdx + 1);
|
|
359
|
-
const cfg = KIND[klass];
|
|
360
|
-
if (!cfg) return false;
|
|
361
|
-
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
362
|
-
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
363
|
-
const parent = wrap.parentElement;
|
|
364
|
-
if (parent) {
|
|
365
|
-
for (const sib of parent.children) {
|
|
366
|
-
if (sib === wrap) continue;
|
|
367
|
-
try {
|
|
368
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
369
|
-
return sib.isConnected;
|
|
370
|
-
}
|
|
371
|
-
} catch (_) {}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// Dernier recours : querySelector global
|
|
375
|
-
try {
|
|
376
|
-
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
377
|
-
return !!(found?.isConnected);
|
|
378
|
-
} catch (_) { return false; }
|
|
379
|
-
}
|
|
380
|
-
|
|
381
202
|
function adjacentWrap(el) {
|
|
382
|
-
return
|
|
203
|
+
return !!(
|
|
204
|
+
el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
205
|
+
el.previousElementSibling?.classList?.contains(WRAP_CLASS)
|
|
206
|
+
);
|
|
383
207
|
}
|
|
384
208
|
|
|
385
|
-
// ── Ancres stables
|
|
209
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
386
210
|
|
|
387
211
|
/**
|
|
388
|
-
* Retourne
|
|
389
|
-
*
|
|
212
|
+
* Retourne l'identifiant stable de l'élément selon son kindClass.
|
|
213
|
+
* Utilise l'attribut défini dans KIND (data-pid, data-index, data-cid).
|
|
214
|
+
* Fallback positionnel si l'attribut est absent.
|
|
390
215
|
*/
|
|
391
|
-
function stableId(
|
|
392
|
-
const attr = KIND[
|
|
216
|
+
function stableId(kindClass, el) {
|
|
217
|
+
const attr = KIND[kindClass]?.anchorAttr;
|
|
393
218
|
if (attr) {
|
|
394
219
|
const v = el.getAttribute(attr);
|
|
395
220
|
if (v !== null && v !== '') return v;
|
|
396
221
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
222
|
+
// Fallback : position dans le parent
|
|
223
|
+
try {
|
|
224
|
+
let i = 0;
|
|
225
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
226
|
+
if (s === el) return `i${i}`;
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
} catch (_) {}
|
|
402
230
|
return 'i0';
|
|
403
231
|
}
|
|
404
232
|
|
|
405
|
-
const
|
|
233
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
406
234
|
|
|
407
|
-
function findWrap(
|
|
408
|
-
|
|
409
|
-
|
|
235
|
+
function findWrap(anchorKey) {
|
|
236
|
+
try {
|
|
237
|
+
return document.querySelector(
|
|
238
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
239
|
+
);
|
|
240
|
+
} catch (_) { return null; }
|
|
410
241
|
}
|
|
411
242
|
|
|
412
243
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
413
244
|
|
|
414
|
-
/**
|
|
415
|
-
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
416
|
-
* ou null si tous les ids sont montés.
|
|
417
|
-
*/
|
|
418
245
|
function pickId(poolKey) {
|
|
419
246
|
const pool = S.pools[poolKey];
|
|
420
|
-
if (!pool.length) return null;
|
|
421
247
|
for (let t = 0; t < pool.length; t++) {
|
|
422
|
-
const i
|
|
248
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
423
249
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
424
250
|
const id = pool[i];
|
|
425
251
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -427,112 +253,7 @@ function destroyBeforeReuse(ids) {
|
|
|
427
253
|
return null;
|
|
428
254
|
}
|
|
429
255
|
|
|
430
|
-
|
|
431
|
-
// NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
|
|
432
|
-
// On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
|
|
433
|
-
for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
|
|
434
|
-
if (wrap?.isConnected) continue;
|
|
435
|
-
S.wrapByKey.delete(key);
|
|
436
|
-
const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
|
|
437
|
-
if (Number.isFinite(id)) {
|
|
438
|
-
S.mountedIds.delete(id);
|
|
439
|
-
S.pendingSet.delete(id);
|
|
440
|
-
S.lastShow.delete(id);
|
|
441
|
-
S.ezActiveIds.delete(id);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
if (S.pending.length) {
|
|
445
|
-
S.pending = S.pending.filter(id => S.pendingSet.has(id));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
451
|
-
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
452
|
-
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
453
|
-
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
454
|
-
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
455
|
-
*/
|
|
456
|
-
function recycleAndMove(klass, targetEl, newKey) {
|
|
457
|
-
const ez = window.ezstandalone;
|
|
458
|
-
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
459
|
-
typeof ez?.define !== 'function' ||
|
|
460
|
-
typeof ez?.displayMore !== 'function') return null;
|
|
461
|
-
|
|
462
|
-
const vh = window.innerHeight || 800;
|
|
463
|
-
const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
|
|
464
|
-
const farAbove = -vh;
|
|
465
|
-
const farBelow = vh * 2;
|
|
466
|
-
|
|
467
|
-
let bestPrefEmpty = null, bestPrefMetric = Infinity;
|
|
468
|
-
let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
|
|
469
|
-
let bestAnyEmpty = null, bestAnyMetric = Infinity;
|
|
470
|
-
let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
|
|
471
|
-
|
|
472
|
-
for (const wrap of S.wrapByKey.values()) {
|
|
473
|
-
if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
|
|
474
|
-
try {
|
|
475
|
-
const rect = wrap.getBoundingClientRect();
|
|
476
|
-
const isAbove = rect.bottom <= farAbove;
|
|
477
|
-
const isBelow = rect.top >= farBelow;
|
|
478
|
-
const anyFar = isAbove || isBelow;
|
|
479
|
-
if (!anyFar) continue;
|
|
480
|
-
|
|
481
|
-
const qualifies = preferAbove ? isAbove : isBelow;
|
|
482
|
-
const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
|
|
483
|
-
const filled = isFilled(wrap);
|
|
484
|
-
|
|
485
|
-
if (qualifies) {
|
|
486
|
-
if (!filled) {
|
|
487
|
-
if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
|
|
488
|
-
} else {
|
|
489
|
-
if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (!filled) {
|
|
493
|
-
if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
|
|
494
|
-
} else {
|
|
495
|
-
if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
|
|
496
|
-
}
|
|
497
|
-
} catch (_) {}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
|
|
501
|
-
if (!best) return null;
|
|
502
|
-
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
503
|
-
if (!Number.isFinite(id)) return null;
|
|
504
|
-
|
|
505
|
-
const oldKey = best.getAttribute(A_ANCHOR);
|
|
506
|
-
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
507
|
-
mutate(() => {
|
|
508
|
-
best.setAttribute(A_ANCHOR, newKey);
|
|
509
|
-
best.setAttribute(A_CREATED, String(ts()));
|
|
510
|
-
best.setAttribute(A_SHOWN, '0');
|
|
511
|
-
best.classList.remove('is-empty');
|
|
512
|
-
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
513
|
-
if (ph) ph.innerHTML = '';
|
|
514
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
515
|
-
});
|
|
516
|
-
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
517
|
-
S.wrapByKey.set(newKey, best);
|
|
518
|
-
|
|
519
|
-
const doDestroy = () => {
|
|
520
|
-
if (S.ezShownSinceDestroy.has(id)) {
|
|
521
|
-
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
522
|
-
S.ezShownSinceDestroy.delete(id);
|
|
523
|
-
}
|
|
524
|
-
S.ezActiveIds.delete(id);
|
|
525
|
-
setTimeout(doDefine, 330);
|
|
526
|
-
};
|
|
527
|
-
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
528
|
-
const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
|
|
529
|
-
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
530
|
-
|
|
531
|
-
return { id, wrap: best };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
256
|
+
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
536
257
|
|
|
537
258
|
function makeWrap(id, klass, key) {
|
|
538
259
|
const w = document.createElement('div');
|
|
@@ -540,7 +261,6 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
540
261
|
w.setAttribute(A_ANCHOR, key);
|
|
541
262
|
w.setAttribute(A_WRAPID, String(id));
|
|
542
263
|
w.setAttribute(A_CREATED, String(ts()));
|
|
543
|
-
w.setAttribute(A_SHOWN, '0');
|
|
544
264
|
w.style.cssText = 'width:100%;display:block;';
|
|
545
265
|
const ph = document.createElement('div');
|
|
546
266
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -550,79 +270,115 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
550
270
|
}
|
|
551
271
|
|
|
552
272
|
function insertAfter(el, id, klass, key) {
|
|
553
|
-
if (!el?.insertAdjacentElement)
|
|
554
|
-
if (findWrap(key))
|
|
555
|
-
if (S.mountedIds.has(id))
|
|
273
|
+
if (!el?.insertAdjacentElement) return null;
|
|
274
|
+
if (findWrap(key)) return null; // ancre déjà présente
|
|
275
|
+
if (S.mountedIds.has(id)) return null; // id déjà monté
|
|
556
276
|
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
557
277
|
const w = makeWrap(id, klass, key);
|
|
558
278
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
559
279
|
S.mountedIds.add(id);
|
|
560
|
-
S.wrapByKey.set(key, w);
|
|
561
280
|
return w;
|
|
562
281
|
}
|
|
563
282
|
|
|
564
283
|
function dropWrap(w) {
|
|
565
284
|
try {
|
|
566
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
567
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
568
285
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
569
|
-
if (Number.isFinite(id))
|
|
570
|
-
|
|
571
|
-
|
|
286
|
+
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
287
|
+
// IMPORTANT : ne passer unobserve que si c'est un vrai Element.
|
|
288
|
+
// unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
|
|
289
|
+
// "parameter 1 is not of type Element" sur le prochain observe).
|
|
290
|
+
try {
|
|
291
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
292
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
293
|
+
} catch (_) {}
|
|
572
294
|
w.remove();
|
|
573
295
|
} catch (_) {}
|
|
574
296
|
}
|
|
575
297
|
|
|
576
|
-
// ── Prune
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
298
|
+
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
302
|
+
*
|
|
303
|
+
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
304
|
+
* Exemples :
|
|
305
|
+
* ezoic-ad-message → cherche [data-pid="123"]
|
|
306
|
+
* ezoic-ad-between → cherche [data-index="5"]
|
|
307
|
+
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
308
|
+
*
|
|
309
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
310
|
+
*/
|
|
311
|
+
function pruneOrphans(klass) {
|
|
312
|
+
const meta = KIND[klass];
|
|
313
|
+
if (!meta) return;
|
|
314
|
+
|
|
315
|
+
// (Perf) Construire 1 set d'ancres présentes, au lieu de querySelector par wrap.
|
|
316
|
+
const present = new Set();
|
|
317
|
+
try {
|
|
318
|
+
document.querySelectorAll(meta.sel).forEach(el => {
|
|
319
|
+
const v = el.getAttribute(meta.anchorAttr);
|
|
320
|
+
if (v !== null && v !== '') present.add(String(v));
|
|
321
|
+
});
|
|
322
|
+
} catch (_) {}
|
|
594
323
|
|
|
595
324
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
596
|
-
|
|
597
|
-
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
325
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
598
326
|
|
|
599
327
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
600
|
-
const sid = key.slice(klass.length + 1); // après "
|
|
328
|
+
const sid = key.slice(klass.length + 1); // après "kindClass:"
|
|
601
329
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
602
330
|
|
|
603
|
-
|
|
604
|
-
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
331
|
+
if (!present.has(String(sid))) mutate(() => dropWrap(w));
|
|
605
332
|
});
|
|
606
333
|
}
|
|
334
|
+
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
338
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
339
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
340
|
+
*/
|
|
341
|
+
function decluster(klass) {
|
|
342
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
343
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
344
|
+
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
345
|
+
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
346
|
+
|
|
347
|
+
let prev = w.previousElementSibling, steps = 0;
|
|
348
|
+
while (prev && steps++ < 3) {
|
|
349
|
+
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
350
|
+
|
|
351
|
+
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
352
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
353
|
+
|
|
354
|
+
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
355
|
+
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
607
360
|
|
|
608
361
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
609
362
|
|
|
610
363
|
/**
|
|
611
|
-
* Ordinal 0-based pour le calcul de l'intervalle
|
|
612
|
-
*
|
|
364
|
+
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
365
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
366
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
613
367
|
*/
|
|
614
368
|
function ordinal(klass, el) {
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
369
|
+
const di = el.getAttribute('data-index');
|
|
370
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
371
|
+
// Fallback positionnel
|
|
372
|
+
try {
|
|
373
|
+
const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
|
|
374
|
+
if (tag) {
|
|
375
|
+
let i = 0;
|
|
376
|
+
for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
|
|
377
|
+
if (n === el) return i;
|
|
378
|
+
i++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (_) {}
|
|
626
382
|
return 0;
|
|
627
383
|
}
|
|
628
384
|
|
|
@@ -631,26 +387,23 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
631
387
|
let inserted = 0;
|
|
632
388
|
|
|
633
389
|
for (const el of items) {
|
|
634
|
-
if (inserted >=
|
|
390
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
635
391
|
if (!el?.isConnected) continue;
|
|
636
392
|
|
|
637
|
-
const ord
|
|
638
|
-
|
|
393
|
+
const ord = ordinal(klass, el);
|
|
394
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
395
|
+
if (!isTarget) continue;
|
|
396
|
+
|
|
639
397
|
if (adjacentWrap(el)) continue;
|
|
640
398
|
|
|
641
|
-
const key =
|
|
642
|
-
if (findWrap(key)) continue;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (!id)
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} else {
|
|
650
|
-
const recycled = recycleAndMove(klass, el, key);
|
|
651
|
-
if (!recycled) break;
|
|
652
|
-
inserted++;
|
|
653
|
-
}
|
|
399
|
+
const key = makeAnchorKey(klass, el);
|
|
400
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
401
|
+
|
|
402
|
+
const id = pickId(poolKey);
|
|
403
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
404
|
+
|
|
405
|
+
const w = insertAfter(el, id, klass, key);
|
|
406
|
+
if (w) { observePh(id); inserted++; }
|
|
654
407
|
}
|
|
655
408
|
return inserted;
|
|
656
409
|
}
|
|
@@ -658,7 +411,14 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
658
411
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
659
412
|
|
|
660
413
|
function getIO() {
|
|
661
|
-
|
|
414
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
415
|
+
if (S.io && S.ioMargin === margin) return S.io;
|
|
416
|
+
// Si la marge doit changer (resize/orientation), on recrée l'observer.
|
|
417
|
+
if (S.io && S.ioMargin !== margin) {
|
|
418
|
+
try { S.io.disconnect(); } catch (_) {}
|
|
419
|
+
S.io = null;
|
|
420
|
+
}
|
|
421
|
+
S.ioMargin = margin;
|
|
662
422
|
try {
|
|
663
423
|
S.io = new IntersectionObserver(entries => {
|
|
664
424
|
for (const e of entries) {
|
|
@@ -667,119 +427,86 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
667
427
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
668
428
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
669
429
|
}
|
|
670
|
-
}, { root: null, rootMargin:
|
|
430
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
671
431
|
} catch (_) { S.io = null; }
|
|
672
432
|
return S.io;
|
|
673
433
|
}
|
|
674
434
|
|
|
675
435
|
function observePh(id) {
|
|
676
|
-
const ph =
|
|
436
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
677
437
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
678
|
-
// Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
|
|
679
|
-
try {
|
|
680
|
-
if (!ph?.isConnected) return;
|
|
681
|
-
const rect = ph.getBoundingClientRect();
|
|
682
|
-
const vh = window.innerHeight || 800;
|
|
683
|
-
const preload = isMobile() ? 1400 : 1000;
|
|
684
|
-
if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
|
|
685
|
-
} catch (_) {}
|
|
686
438
|
}
|
|
687
439
|
|
|
688
|
-
function enqueueShow(id) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
function scheduleDrainQueue() {
|
|
698
|
-
if (isBlocked()) return;
|
|
699
|
-
if (S.showBatchTimer) return;
|
|
700
|
-
S.showBatchTimer = setTimeout(() => {
|
|
701
|
-
S.showBatchTimer = 0;
|
|
702
|
-
drainQueue();
|
|
703
|
-
}, BATCH_FLUSH_MS);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function drainQueue() {
|
|
707
|
-
if (isBlocked()) return;
|
|
708
|
-
const free = Math.max(0, MAX_INFLIGHT - S.inflight);
|
|
709
|
-
if (!free || !S.pending.length) return;
|
|
710
|
-
|
|
711
|
-
const picked = [];
|
|
712
|
-
const seen = new Set();
|
|
713
|
-
const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
|
|
714
|
-
while (S.pending.length && picked.length < batchCap) {
|
|
715
|
-
const id = S.pending.shift();
|
|
716
|
-
S.pendingSet.delete(id);
|
|
717
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
718
|
-
if (!phEl(id)?.isConnected) continue;
|
|
719
|
-
seen.add(id);
|
|
720
|
-
picked.push(id);
|
|
440
|
+
function enqueueShow(id) {
|
|
441
|
+
if (!id || isBlocked()) return;
|
|
442
|
+
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
443
|
+
if (S.inflight >= MAX_INFLIGHT) {
|
|
444
|
+
if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
startShow(id);
|
|
721
448
|
}
|
|
722
|
-
if (picked.length) startShowBatch(picked);
|
|
723
|
-
if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function startShowBatch(ids) {
|
|
727
|
-
if (!ids?.length || isBlocked()) return;
|
|
728
|
-
const reserve = ids.length;
|
|
729
|
-
S.inflight += reserve;
|
|
730
|
-
|
|
731
|
-
let done = false;
|
|
732
|
-
const release = () => {
|
|
733
|
-
if (done) return;
|
|
734
|
-
done = true;
|
|
735
|
-
S.inflight = Math.max(0, S.inflight - reserve);
|
|
736
|
-
drainQueue();
|
|
737
|
-
};
|
|
738
|
-
const timer = setTimeout(release, SHOW_FAILSAFE_MS);
|
|
739
449
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
450
|
+
function drainQueue() {
|
|
451
|
+
if (isBlocked()) return;
|
|
452
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
453
|
+
const id = S.pending.shift();
|
|
454
|
+
S.pendingSet.delete(id);
|
|
455
|
+
startShow(id);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
743
458
|
|
|
744
|
-
|
|
745
|
-
|
|
459
|
+
function startShow(id) {
|
|
460
|
+
if (!id || isBlocked()) return;
|
|
461
|
+
S.inflight++;
|
|
462
|
+
let done = false;
|
|
463
|
+
const release = () => {
|
|
464
|
+
if (done) return;
|
|
465
|
+
done = true;
|
|
466
|
+
S.inflight = Math.max(0, S.inflight - 1);
|
|
467
|
+
drainQueue();
|
|
468
|
+
};
|
|
469
|
+
const timer = setTimeout(release, 7000);
|
|
746
470
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
const ph =
|
|
751
|
-
if (!
|
|
471
|
+
requestAnimationFrame(() => {
|
|
472
|
+
try {
|
|
473
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
474
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
475
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
752
476
|
|
|
477
|
+
const t = ts();
|
|
478
|
+
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
753
479
|
S.lastShow.set(id, t);
|
|
754
|
-
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
755
|
-
valid.push(id);
|
|
756
|
-
}
|
|
757
480
|
|
|
758
|
-
|
|
481
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
482
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
759
483
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
};
|
|
772
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
773
|
-
} catch (_) { clearTimeout(timer); release(); }
|
|
774
|
-
});
|
|
775
|
-
}
|
|
484
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
485
|
+
const ez = window.ezstandalone;
|
|
486
|
+
const doShow = () => {
|
|
487
|
+
try { ez.showAds(id); } catch (_) {}
|
|
488
|
+
scheduleEmptyCheck(id, t);
|
|
489
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
490
|
+
};
|
|
491
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
492
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
493
|
+
});
|
|
494
|
+
}
|
|
776
495
|
|
|
496
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
497
|
+
setTimeout(() => {
|
|
498
|
+
try {
|
|
499
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
500
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
501
|
+
if (!wrap || !ph?.isConnected) return;
|
|
502
|
+
// Un show plus récent → ne pas toucher
|
|
503
|
+
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
504
|
+
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
505
|
+
} catch (_) {}
|
|
506
|
+
}, EMPTY_CHECK_MS);
|
|
507
|
+
}
|
|
777
508
|
|
|
778
509
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
779
|
-
//
|
|
780
|
-
// Intercepte ez.showAds() pour :
|
|
781
|
-
// – ignorer les appels pendant blockedUntil
|
|
782
|
-
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
783
510
|
|
|
784
511
|
function patchShowAds() {
|
|
785
512
|
const apply = () => {
|
|
@@ -791,21 +518,14 @@ function startShowBatch(ids) {
|
|
|
791
518
|
const orig = ez.showAds.bind(ez);
|
|
792
519
|
ez.showAds = function (...args) {
|
|
793
520
|
if (isBlocked()) return;
|
|
794
|
-
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
795
|
-
const valid = [];
|
|
521
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
796
522
|
const seen = new Set();
|
|
797
523
|
for (const v of ids) {
|
|
798
524
|
const id = parseInt(v, 10);
|
|
799
525
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
800
|
-
if (!
|
|
526
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
|
|
801
527
|
seen.add(id);
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
if (!valid.length) return;
|
|
805
|
-
try { orig(...valid); } catch (_) {
|
|
806
|
-
for (const id of valid) {
|
|
807
|
-
try { orig(id); } catch (_) {}
|
|
808
|
-
}
|
|
528
|
+
try { orig(id); } catch (_) {}
|
|
809
529
|
}
|
|
810
530
|
};
|
|
811
531
|
} catch (_) {}
|
|
@@ -817,12 +537,11 @@ function startShowBatch(ids) {
|
|
|
817
537
|
}
|
|
818
538
|
}
|
|
819
539
|
|
|
820
|
-
// ── Core
|
|
540
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
821
541
|
|
|
822
542
|
async function runCore() {
|
|
823
543
|
if (isBlocked()) return 0;
|
|
824
544
|
patchShowAds();
|
|
825
|
-
sweepDeadWraps();
|
|
826
545
|
|
|
827
546
|
const cfg = await fetchConfig();
|
|
828
547
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -833,30 +552,30 @@ function startShowBatch(ids) {
|
|
|
833
552
|
|
|
834
553
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
835
554
|
if (!normBool(cfgEnable)) return 0;
|
|
555
|
+
const items = getItems();
|
|
836
556
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
837
|
-
|
|
557
|
+
pruneOrphans(klass);
|
|
558
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
559
|
+
if (n) decluster(klass);
|
|
560
|
+
return n;
|
|
838
561
|
};
|
|
839
562
|
|
|
840
563
|
if (kind === 'topic') return exec(
|
|
841
564
|
'ezoic-ad-message', getPosts,
|
|
842
565
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
843
566
|
);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
850
|
-
);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return exec(
|
|
567
|
+
if (kind === 'categoryTopics') return exec(
|
|
568
|
+
'ezoic-ad-between', getTopics,
|
|
569
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
570
|
+
);
|
|
571
|
+
if (kind === 'categories') return exec(
|
|
854
572
|
'ezoic-ad-categories', getCategories,
|
|
855
573
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
856
574
|
);
|
|
575
|
+
return 0;
|
|
857
576
|
}
|
|
858
577
|
|
|
859
|
-
// ── Scheduler
|
|
578
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
860
579
|
|
|
861
580
|
function scheduleRun(cb) {
|
|
862
581
|
if (S.runQueued) return;
|
|
@@ -874,8 +593,10 @@ function startShowBatch(ids) {
|
|
|
874
593
|
if (isBlocked()) return;
|
|
875
594
|
const t = ts();
|
|
876
595
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
877
|
-
S.lastBurstTs
|
|
878
|
-
|
|
596
|
+
S.lastBurstTs = t;
|
|
597
|
+
|
|
598
|
+
const pk = pageKey();
|
|
599
|
+
S.pageKey = pk;
|
|
879
600
|
S.burstDeadline = t + 2000;
|
|
880
601
|
|
|
881
602
|
if (S.burstActive) return;
|
|
@@ -883,69 +604,49 @@ function startShowBatch(ids) {
|
|
|
883
604
|
S.burstCount = 0;
|
|
884
605
|
|
|
885
606
|
const step = () => {
|
|
886
|
-
if (pageKey() !==
|
|
607
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
887
608
|
S.burstActive = false; return;
|
|
888
609
|
}
|
|
889
610
|
S.burstCount++;
|
|
890
611
|
scheduleRun(n => {
|
|
891
612
|
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
892
|
-
setTimeout(step, n > 0 ?
|
|
613
|
+
setTimeout(step, n > 0 ? 150 : 300);
|
|
893
614
|
});
|
|
894
615
|
};
|
|
895
616
|
step();
|
|
896
617
|
}
|
|
897
618
|
|
|
898
|
-
// ── Cleanup navigation
|
|
619
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
899
620
|
|
|
900
621
|
function cleanup() {
|
|
901
622
|
blockedUntil = ts() + 1500;
|
|
902
623
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
903
624
|
S.cfg = null;
|
|
904
|
-
S.
|
|
625
|
+
S.poolSig = null;
|
|
905
626
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
906
627
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
907
628
|
S.mountedIds.clear();
|
|
908
629
|
S.lastShow.clear();
|
|
909
|
-
S.wrapByKey.clear();
|
|
910
|
-
S.ezActiveIds.clear();
|
|
911
|
-
S.ezShownSinceDestroy.clear();
|
|
912
630
|
S.inflight = 0;
|
|
913
631
|
S.pending = [];
|
|
914
632
|
S.pendingSet.clear();
|
|
915
|
-
if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
|
|
916
|
-
if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
|
|
917
|
-
S.destroyPending = [];
|
|
918
|
-
S.destroyPendingSet.clear();
|
|
919
633
|
S.burstActive = false;
|
|
920
634
|
S.runQueued = false;
|
|
921
|
-
S.sweepQueued = false;
|
|
922
|
-
S.scrollSpeed = 0;
|
|
923
|
-
S.lastScrollY = 0;
|
|
924
|
-
S.lastScrollTs = 0;
|
|
925
635
|
}
|
|
926
636
|
|
|
927
|
-
// ──
|
|
637
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
928
638
|
|
|
929
639
|
function ensureDomObserver() {
|
|
930
640
|
if (S.domObs) return;
|
|
931
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
932
641
|
S.domObs = new MutationObserver(muts => {
|
|
933
642
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
934
643
|
for (const m of muts) {
|
|
935
|
-
|
|
936
|
-
for (const n of m.removedNodes) {
|
|
937
|
-
if (n.nodeType !== 1) continue;
|
|
938
|
-
if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || (n.querySelector && n.querySelector(`.${WRAP_CLASS}`))) {
|
|
939
|
-
sawWrapRemoval = true;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
if (sawWrapRemoval) queueSweepDeadWraps();
|
|
644
|
+
if (!m.addedNodes?.length) continue;
|
|
943
645
|
for (const n of m.addedNodes) {
|
|
944
646
|
if (n.nodeType !== 1) continue;
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
647
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
648
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
649
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
949
650
|
requestBurst(); return;
|
|
950
651
|
}
|
|
951
652
|
}
|
|
@@ -959,14 +660,7 @@ function startShowBatch(ids) {
|
|
|
959
660
|
function muteConsole() {
|
|
960
661
|
if (window.__nbbEzMuted) return;
|
|
961
662
|
window.__nbbEzMuted = true;
|
|
962
|
-
const MUTED = [
|
|
963
|
-
'[EzoicAds JS]: Placeholder Id',
|
|
964
|
-
'No valid placeholders for loadMore',
|
|
965
|
-
'cannot call refresh on the same page',
|
|
966
|
-
'no placeholders are currently defined in Refresh',
|
|
967
|
-
'Debugger iframe already exists',
|
|
968
|
-
`with id ${PH_PREFIX}`,
|
|
969
|
-
];
|
|
663
|
+
const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
|
|
970
664
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
971
665
|
const orig = console[m];
|
|
972
666
|
if (typeof orig !== 'function') continue;
|
|
@@ -978,18 +672,29 @@ function startShowBatch(ids) {
|
|
|
978
672
|
}
|
|
979
673
|
|
|
980
674
|
function ensureTcfLocator() {
|
|
675
|
+
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
676
|
+
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
677
|
+
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
678
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
679
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
680
|
+
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
981
681
|
try {
|
|
982
682
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
683
|
+
|
|
983
684
|
const inject = () => {
|
|
984
685
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
985
686
|
const f = document.createElement('iframe');
|
|
986
687
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
987
688
|
(document.body || document.documentElement).appendChild(f);
|
|
988
689
|
};
|
|
690
|
+
|
|
989
691
|
inject();
|
|
692
|
+
|
|
693
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
990
694
|
if (!window.__nbbTcfObs) {
|
|
991
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
992
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
695
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
696
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
697
|
+
{ childList: true, subtree: true });
|
|
993
698
|
}
|
|
994
699
|
} catch (_) {}
|
|
995
700
|
}
|
|
@@ -999,10 +704,10 @@ function startShowBatch(ids) {
|
|
|
999
704
|
const head = document.head;
|
|
1000
705
|
if (!head) return;
|
|
1001
706
|
for (const [rel, href, cors] of [
|
|
1002
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
1003
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
1004
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
1005
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true
|
|
707
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
708
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
709
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
710
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
1006
711
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1007
712
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1008
713
|
]) {
|
|
@@ -1016,7 +721,7 @@ function startShowBatch(ids) {
|
|
|
1016
721
|
}
|
|
1017
722
|
}
|
|
1018
723
|
|
|
1019
|
-
// ── Bindings
|
|
724
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
1020
725
|
|
|
1021
726
|
function bindNodeBB() {
|
|
1022
727
|
const $ = window.jQuery;
|
|
@@ -1027,16 +732,19 @@ function startShowBatch(ids) {
|
|
|
1027
732
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
1028
733
|
S.pageKey = pageKey();
|
|
1029
734
|
blockedUntil = 0;
|
|
1030
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
1031
|
-
|
|
735
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
736
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
1032
737
|
});
|
|
1033
738
|
|
|
1034
|
-
const
|
|
1035
|
-
'action:ajaxify.contentLoaded',
|
|
739
|
+
const BURST_EVENTS = [
|
|
740
|
+
'action:ajaxify.contentLoaded',
|
|
741
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
1036
742
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
1037
743
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1038
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
1039
744
|
|
|
745
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
746
|
+
|
|
747
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
1040
748
|
try {
|
|
1041
749
|
require(['hooks'], hooks => {
|
|
1042
750
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -1048,24 +756,25 @@ function startShowBatch(ids) {
|
|
|
1048
756
|
} catch (_) {}
|
|
1049
757
|
}
|
|
1050
758
|
|
|
759
|
+
|
|
760
|
+
function bindResize() {
|
|
761
|
+
let t = null;
|
|
762
|
+
window.addEventListener('resize', () => {
|
|
763
|
+
clearTimeout(t);
|
|
764
|
+
t = setTimeout(() => {
|
|
765
|
+
try { getIO(); } catch (_) {}
|
|
766
|
+
// Ré-observer les placeholders existants (si IO recréé)
|
|
767
|
+
try {
|
|
768
|
+
document.querySelectorAll(`.${WRAP_CLASS} [id^="${PH_PREFIX}"]`).forEach(ph => {
|
|
769
|
+
if (ph instanceof Element) { try { S.io?.observe(ph); } catch (_) {} }
|
|
770
|
+
});
|
|
771
|
+
} catch (_) {}
|
|
772
|
+
}, 200);
|
|
773
|
+
}, { passive: true });
|
|
774
|
+
}
|
|
1051
775
|
function bindScroll() {
|
|
1052
776
|
let ticking = false;
|
|
1053
|
-
try {
|
|
1054
|
-
S.lastScrollY = window.scrollY || window.pageYOffset || 0;
|
|
1055
|
-
S.lastScrollTs = ts();
|
|
1056
|
-
} catch (_) {}
|
|
1057
777
|
window.addEventListener('scroll', () => {
|
|
1058
|
-
try {
|
|
1059
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
1060
|
-
const t = ts();
|
|
1061
|
-
const dy = y - (S.lastScrollY || 0);
|
|
1062
|
-
const dt = Math.max(1, t - (S.lastScrollTs || t));
|
|
1063
|
-
if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
|
|
1064
|
-
const inst = Math.abs(dy) * 1000 / dt;
|
|
1065
|
-
S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
|
|
1066
|
-
S.lastScrollY = y;
|
|
1067
|
-
S.lastScrollTs = t;
|
|
1068
|
-
} catch (_) {}
|
|
1069
778
|
if (ticking) return;
|
|
1070
779
|
ticking = true;
|
|
1071
780
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
@@ -1082,6 +791,7 @@ function startShowBatch(ids) {
|
|
|
1082
791
|
getIO();
|
|
1083
792
|
ensureDomObserver();
|
|
1084
793
|
bindNodeBB();
|
|
794
|
+
bindResize();
|
|
1085
795
|
bindScroll();
|
|
1086
796
|
blockedUntil = 0;
|
|
1087
797
|
requestBurst();
|