nodebb-plugin-ezoic-infinite 1.6.99 → 1.7.0
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 +262 -233
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js (
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v19)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* Correctifs v19 vs v18 :
|
|
5
|
+
*
|
|
6
|
+
* [BUG 1] Pubs regroupées en haut après scroll up/down
|
|
7
|
+
* Cause : pruneOrphans gardait les wraps "filled" même quand leur ancre DOM
|
|
8
|
+
* avait disparu (post virtualisé). Ces wraps sans parent flottaient et
|
|
9
|
+
* NodeBB les réordonnait arbitrairement.
|
|
10
|
+
* Fix : un wrap dont l'ancre est absente du DOM EST supprimé, rempli ou non.
|
|
11
|
+
* Exception : si l'ancre est simplement hors-viewport mais still connected
|
|
12
|
+
* (NodeBB ne virtualise pas toujours le DOM), on la conserve.
|
|
13
|
+
*
|
|
14
|
+
* [BUG 2] Pub qui apparaît puis disparaît
|
|
15
|
+
* Cause : decluster() supprimait un wrap "vide" pendant la fenêtre de fill
|
|
16
|
+
* async d'Ezoic. Le guard TTL de 90s était calculé depuis la création,
|
|
17
|
+
* mais le show() peut avoir été appelé bien après la création.
|
|
18
|
+
* Fix : on ajoute data-ezoic-shown (timestamp du show). decluster ne touche
|
|
19
|
+
* pas un wrap dont le show date de moins de FILL_GRACE_MS (20s).
|
|
20
|
+
*
|
|
21
|
+
* [BUG 3] Intervalle 1/x non respecté sur infinite scroll
|
|
22
|
+
* Cause : computeTargetIndices utilisait l'index dans le tableau courant
|
|
23
|
+
* (items[0..N]), qui recommence à 0 à chaque batch de posts chargés.
|
|
24
|
+
* Fix : on utilise l'ordinal GLOBAL du post (data-index fourni par NodeBB,
|
|
25
|
+
* ou data-pid comme fallback numérique). L'intervalle est appliqué sur cet
|
|
26
|
+
* ordinal global → pub tous les X posts absolus, quel que soit le batch.
|
|
18
27
|
*/
|
|
19
28
|
(function () {
|
|
20
29
|
'use strict';
|
|
21
30
|
|
|
22
31
|
// ─── Constants ────────────────────────────────────────────────────────────
|
|
23
|
-
const WRAP_CLASS
|
|
24
|
-
const PLACEHOLDER_PREFIX
|
|
25
|
-
const ANCHOR_ATTR
|
|
26
|
-
const WRAPID_ATTR
|
|
27
|
-
const CREATED_ATTR
|
|
32
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
33
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
34
|
+
const ANCHOR_ATTR = 'data-ezoic-anchor'; // "kindClass:globalOrdinal"
|
|
35
|
+
const WRAPID_ATTR = 'data-ezoic-wrapid'; // ezoic placeholder id
|
|
36
|
+
const CREATED_ATTR = 'data-ezoic-created'; // timestamp création
|
|
37
|
+
const SHOWN_ATTR = 'data-ezoic-shown'; // timestamp dernier showAds
|
|
28
38
|
|
|
29
39
|
const MAX_INSERTS_PER_RUN = 6;
|
|
30
|
-
|
|
31
|
-
const
|
|
40
|
+
// Après un showAds(), ne pas decluster pendant ce délai (fill async Ezoic)
|
|
41
|
+
const FILL_GRACE_MS = 20_000;
|
|
42
|
+
// Collapse is-empty seulement après ce délai post-show
|
|
43
|
+
const EMPTY_CHECK_DELAY = 18_000;
|
|
32
44
|
|
|
33
45
|
const PRELOAD_MARGIN = {
|
|
34
|
-
desktop:
|
|
35
|
-
mobile:
|
|
36
|
-
desktopBoosted:'4500px 0px 4500px 0px',
|
|
37
|
-
mobileBoosted:
|
|
46
|
+
desktop: '2000px 0px 2000px 0px',
|
|
47
|
+
mobile: '3000px 0px 3000px 0px',
|
|
48
|
+
desktopBoosted: '4500px 0px 4500px 0px',
|
|
49
|
+
mobileBoosted: '4500px 0px 4500px 0px',
|
|
38
50
|
};
|
|
39
|
-
const BOOST_DURATION_MS
|
|
40
|
-
const BOOST_SPEED_PX_PER_MS
|
|
41
|
-
const MAX_INFLIGHT_DESKTOP
|
|
42
|
-
const MAX_INFLIGHT_MOBILE
|
|
43
|
-
const SHOW_THROTTLE_MS
|
|
51
|
+
const BOOST_DURATION_MS = 2500;
|
|
52
|
+
const BOOST_SPEED_PX_PER_MS = 2.2;
|
|
53
|
+
const MAX_INFLIGHT_DESKTOP = 4;
|
|
54
|
+
const MAX_INFLIGHT_MOBILE = 3;
|
|
55
|
+
const SHOW_THROTTLE_MS = 900;
|
|
44
56
|
|
|
45
57
|
const SELECTORS = {
|
|
46
58
|
topicItem: 'li[component="category/topic"]',
|
|
@@ -49,62 +61,47 @@
|
|
|
49
61
|
};
|
|
50
62
|
|
|
51
63
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
52
|
-
const now
|
|
64
|
+
const now = () => Date.now();
|
|
53
65
|
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
54
66
|
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
67
|
+
const isFilledNode = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
55
68
|
|
|
56
69
|
function uniqInts(raw) {
|
|
57
70
|
const out = [], seen = new Set();
|
|
58
|
-
for (const v of String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
71
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
59
72
|
const n = parseInt(v, 10);
|
|
60
73
|
if (Number.isFinite(n) && n > 0 && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
61
74
|
}
|
|
62
75
|
return out;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
function isFilledNode(node) {
|
|
66
|
-
return !!(node?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
78
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
70
79
|
const state = {
|
|
71
80
|
pageKey: null,
|
|
72
81
|
cfg: null,
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
83
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
84
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
77
85
|
|
|
78
|
-
//
|
|
86
|
+
// IDs Ezoic actuellement montés dans le DOM (Set<number>)
|
|
79
87
|
mountedIds: new Set(),
|
|
80
88
|
|
|
81
|
-
// Throttle par id
|
|
82
89
|
lastShowById: new Map(),
|
|
83
90
|
|
|
84
|
-
// Observers
|
|
85
91
|
domObs: null,
|
|
86
|
-
io: null,
|
|
87
|
-
ioMargin: null,
|
|
92
|
+
io: null, ioMargin: null,
|
|
88
93
|
|
|
89
|
-
// Guard contre nos propres mutations
|
|
90
94
|
internalMutation: 0,
|
|
91
95
|
|
|
92
|
-
// File de show
|
|
93
96
|
inflight: 0,
|
|
94
|
-
pending: [],
|
|
95
|
-
pendingSet: new Set(),
|
|
97
|
+
pending: [], pendingSet: new Set(),
|
|
96
98
|
|
|
97
|
-
// Scroll boost
|
|
98
99
|
scrollBoostUntil: 0,
|
|
99
|
-
lastScrollY: 0,
|
|
100
|
-
lastScrollTs: 0,
|
|
100
|
+
lastScrollY: 0, lastScrollTs: 0,
|
|
101
101
|
|
|
102
|
-
// Scheduler
|
|
103
102
|
runQueued: false,
|
|
104
|
-
burstActive: false,
|
|
105
|
-
|
|
106
|
-
burstCount: 0,
|
|
107
|
-
lastBurstReqTs: 0,
|
|
103
|
+
burstActive: false, burstDeadline: 0,
|
|
104
|
+
burstCount: 0, lastBurstReqTs: 0,
|
|
108
105
|
};
|
|
109
106
|
|
|
110
107
|
let blockedUntil = 0;
|
|
@@ -129,10 +126,10 @@
|
|
|
129
126
|
|
|
130
127
|
function initPools(cfg) {
|
|
131
128
|
if (!cfg) return;
|
|
132
|
-
|
|
133
|
-
state.pools.
|
|
134
|
-
state.pools.
|
|
135
|
-
|
|
129
|
+
// Réinitialise à chaque page (cleanup() remet les curseurs à 0)
|
|
130
|
+
state.pools.topics = uniqInts(cfg.placeholderIds);
|
|
131
|
+
state.pools.posts = uniqInts(cfg.messagePlaceholderIds);
|
|
132
|
+
state.pools.categories = uniqInts(cfg.categoryPlaceholderIds);
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
// ─── Page / Kind ──────────────────────────────────────────────────────────
|
|
@@ -147,9 +144,9 @@
|
|
|
147
144
|
|
|
148
145
|
function getKind() {
|
|
149
146
|
const p = location.pathname;
|
|
150
|
-
if (/^\/topic\//.test(p))
|
|
151
|
-
if (/^\/category\//.test(p))
|
|
152
|
-
if (p === '/' || /^\/categories/.test(p))
|
|
147
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
148
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
149
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
153
150
|
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
154
151
|
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
155
152
|
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
@@ -161,60 +158,84 @@
|
|
|
161
158
|
return Array.from(document.querySelectorAll(SELECTORS.postItem)).filter(el => {
|
|
162
159
|
if (!el.isConnected) return false;
|
|
163
160
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
161
|
+
const parent = el.parentElement?.closest('[component="post"][data-pid]');
|
|
162
|
+
if (parent && parent !== el) return false;
|
|
166
163
|
if (el.getAttribute('component') === 'post/parent') return false;
|
|
167
164
|
return true;
|
|
168
165
|
});
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
function getTopicItems() { return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
168
|
+
function getTopicItems() { return Array.from(document.querySelectorAll(SELECTORS.topicItem)); }
|
|
172
169
|
function getCategoryItems() { return Array.from(document.querySelectorAll(SELECTORS.categoryItem)); }
|
|
173
170
|
|
|
171
|
+
function hasAdjacentWrap(el) {
|
|
172
|
+
return !!(el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
173
|
+
el.previousElementSibling?.classList?.contains(WRAP_CLASS));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Ordinal global (BUG FIX #3) ──────────────────────────────────────────
|
|
174
177
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
+
* Retourne l'ordinal ABSOLU d'un élément dans la page complète (pas le batch).
|
|
179
|
+
*
|
|
180
|
+
* Pour les posts (topic) : NodeBB expose data-index (0-based) sur chaque
|
|
181
|
+
* [component="post"]. On l'utilise directement.
|
|
182
|
+
*
|
|
183
|
+
* Pour les topics (liste catégorie) : idem, data-index sur le <li>.
|
|
184
|
+
*
|
|
185
|
+
* Fallback : on parcourt le DOM pour compter la position réelle de l'élément
|
|
186
|
+
* parmi ses frères de même type.
|
|
178
187
|
*/
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
return `${kindClass}:${id}`;
|
|
184
|
-
}
|
|
188
|
+
function getGlobalOrdinal(el, selector) {
|
|
189
|
+
// 1. data-index (NodeBB 3+/4+) — 0-based → on retourne 1-based
|
|
190
|
+
const di = el.getAttribute('data-index');
|
|
191
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10); // déjà 0-based, on le garde
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
// 2. Compter dans le DOM parmi les frères du même type
|
|
194
|
+
try {
|
|
195
|
+
const all = el.parentElement?.querySelectorAll?.(':scope > ' + selector.split('[')[0]);
|
|
196
|
+
if (all) {
|
|
197
|
+
let i = 0;
|
|
198
|
+
for (const node of all) {
|
|
199
|
+
if (node === el) return i;
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (_) {}
|
|
189
204
|
|
|
190
|
-
|
|
191
|
-
const next = el.nextElementSibling;
|
|
192
|
-
if (next?.classList?.contains(WRAP_CLASS)) return true;
|
|
193
|
-
const prev = el.previousElementSibling;
|
|
194
|
-
if (prev?.classList?.contains(WRAP_CLASS)) return true;
|
|
195
|
-
return false;
|
|
205
|
+
return 0;
|
|
196
206
|
}
|
|
197
207
|
|
|
198
|
-
// ─── ID pool / rotation ───────────────────────────────────────────────────
|
|
199
208
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
209
|
+
* Clé d'ancre unique et stable pour un élément donné.
|
|
210
|
+
* Format : "kindClass:globalOrdinal"
|
|
211
|
+
* → Identique au scroll up/down, identique entre batches.
|
|
202
212
|
*/
|
|
213
|
+
function getAnchorKey(kindClass, el, selector) {
|
|
214
|
+
const ord = getGlobalOrdinal(el, selector);
|
|
215
|
+
return `${kindClass}:${ord}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function findWrapByAnchor(anchorKey) {
|
|
219
|
+
// CSS.escape pour les : dans la clé
|
|
220
|
+
try {
|
|
221
|
+
return document.querySelector(`.${WRAP_CLASS}[${ANCHOR_ATTR}="${anchorKey.replace(/"/g, '\\"')}"]`);
|
|
222
|
+
} catch (_) { return null; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Pool rotation ────────────────────────────────────────────────────────
|
|
203
226
|
function pickId(poolKey) {
|
|
204
227
|
const pool = state.pools[poolKey];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const idx = state.cursors[poolKey] % n;
|
|
210
|
-
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % n;
|
|
228
|
+
if (!pool.length) return null;
|
|
229
|
+
for (let tries = 0; tries < pool.length; tries++) {
|
|
230
|
+
const idx = state.cursors[poolKey] % pool.length;
|
|
231
|
+
state.cursors[poolKey] = (state.cursors[poolKey] + 1) % pool.length;
|
|
211
232
|
const id = pool[idx];
|
|
212
233
|
if (!state.mountedIds.has(id)) return id;
|
|
213
234
|
}
|
|
214
|
-
return null;
|
|
235
|
+
return null;
|
|
215
236
|
}
|
|
216
237
|
|
|
217
|
-
// ─── Wrap
|
|
238
|
+
// ─── Wrap build / insert / remove ─────────────────────────────────────────
|
|
218
239
|
function buildWrap(id, kindClass, anchorKey) {
|
|
219
240
|
const wrap = document.createElement('div');
|
|
220
241
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
@@ -227,20 +248,14 @@
|
|
|
227
248
|
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
228
249
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
229
250
|
wrap.appendChild(ph);
|
|
230
|
-
|
|
231
251
|
return wrap;
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
function insertWrapAfter(el, id, kindClass, anchorKey) {
|
|
235
255
|
if (!el?.insertAdjacentElement) return null;
|
|
236
|
-
if (findWrapByAnchor(anchorKey))
|
|
237
|
-
if (state.mountedIds.has(id))
|
|
238
|
-
|
|
239
|
-
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
240
|
-
if (existingPh?.isConnected) {
|
|
241
|
-
// Cet id a déjà un placeholder dans le DOM → on ne peut pas le dupliquer
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
256
|
+
if (findWrapByAnchor(anchorKey)) return null; // déjà inséré
|
|
257
|
+
if (state.mountedIds.has(id)) return null; // id déjà monté
|
|
258
|
+
if (document.getElementById(`${PLACEHOLDER_PREFIX}${id}`)?.isConnected) return null;
|
|
244
259
|
|
|
245
260
|
const wrap = buildWrap(id, kindClass, anchorKey);
|
|
246
261
|
withInternalMutation(() => el.insertAdjacentElement('afterend', wrap));
|
|
@@ -252,119 +267,123 @@
|
|
|
252
267
|
try {
|
|
253
268
|
const id = parseInt(wrap.getAttribute(WRAPID_ATTR), 10);
|
|
254
269
|
if (Number.isFinite(id)) state.mountedIds.delete(id);
|
|
270
|
+
try { state.io?.unobserve(wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`)); } catch (_) {}
|
|
255
271
|
wrap.remove();
|
|
256
272
|
} catch (_) {}
|
|
257
273
|
}
|
|
258
274
|
|
|
259
|
-
// ─── Prune
|
|
275
|
+
// ─── Prune (BUG FIX #1) ───────────────────────────────────────────────────
|
|
260
276
|
/**
|
|
261
|
-
* Supprime les wraps dont l'
|
|
262
|
-
*
|
|
277
|
+
* Supprime les wraps dont l'ancre DOM n'est plus connectée.
|
|
278
|
+
*
|
|
279
|
+
* Règle simple et sans exception "filled" :
|
|
280
|
+
* - Si l'élément ancre est présent et connecté → wrap OK, rien à faire.
|
|
281
|
+
* - Si l'élément ancre est absent (virtualisé/retiré) → wrap supprimé,
|
|
282
|
+
* qu'il soit rempli ou non. Cela libère l'ID pour réutilisation.
|
|
283
|
+
*
|
|
284
|
+
* On ne touche PAS aux wraps fraîchement créés (< 5s) pour laisser le
|
|
285
|
+
* temps à NodeBB de finir d'insérer les posts du batch.
|
|
263
286
|
*/
|
|
264
|
-
function pruneOrphans(kindClass) {
|
|
287
|
+
function pruneOrphans(kindClass, selector) {
|
|
265
288
|
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
266
|
-
let removed = 0;
|
|
267
289
|
|
|
268
290
|
wraps.forEach(wrap => {
|
|
269
|
-
// Ne jamais pruner un wrap tout juste créé (fill lent côté Ezoic)
|
|
270
291
|
const created = parseInt(wrap.getAttribute(CREATED_ATTR) || '0', 10);
|
|
271
|
-
if (
|
|
292
|
+
if (now() - created < 5_000) return; // trop récent, on laisse
|
|
272
293
|
|
|
273
294
|
const anchorKey = wrap.getAttribute(ANCHOR_ATTR);
|
|
274
|
-
if (!anchorKey) {
|
|
275
|
-
withInternalMutation(() => removeWrap(wrap));
|
|
276
|
-
removed++;
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
295
|
+
if (!anchorKey) { withInternalMutation(() => removeWrap(wrap)); return; }
|
|
279
296
|
|
|
280
|
-
//
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
let anchorEl = null;
|
|
297
|
+
// Retrouver l'ordinal depuis la clé
|
|
298
|
+
const ordStr = anchorKey.split(':').slice(1).join(':');
|
|
299
|
+
const ord = parseInt(ordStr, 10);
|
|
284
300
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
?? document.querySelector(`${SELECTORS.categoryItem}[data-index="${CSS.escape(anchorId)}"]`);
|
|
290
|
-
}
|
|
301
|
+
// Chercher l'élément ancre par son ordinal global (data-index)
|
|
302
|
+
const anchorEl = isNaN(ord)
|
|
303
|
+
? null
|
|
304
|
+
: document.querySelector(`${selector.split('[')[0]}[data-index="${ord}"]`);
|
|
291
305
|
|
|
292
306
|
if (!anchorEl || !anchorEl.isConnected) {
|
|
293
|
-
// Ancre disparue →
|
|
294
|
-
|
|
295
|
-
withInternalMutation(() => removeWrap(wrap));
|
|
296
|
-
removed++;
|
|
297
|
-
}
|
|
307
|
+
// Ancre disparue → suppression inconditionnelle
|
|
308
|
+
withInternalMutation(() => removeWrap(wrap));
|
|
298
309
|
}
|
|
299
310
|
});
|
|
300
|
-
|
|
301
|
-
return removed;
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
// ─── Decluster (BUG FIX #2) ───────────────────────────────────────────────
|
|
304
314
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
315
|
+
* Supprime les doublons adjacents.
|
|
316
|
+
* Ne touche JAMAIS un wrap qui vient de recevoir un showAds() (fill async).
|
|
307
317
|
*/
|
|
308
318
|
function decluster(kindClass) {
|
|
309
319
|
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
310
|
-
let removed = 0;
|
|
311
320
|
|
|
312
321
|
for (const wrap of wraps) {
|
|
313
322
|
let prev = wrap.previousElementSibling;
|
|
314
323
|
let steps = 0;
|
|
315
324
|
while (prev && steps < 3) {
|
|
316
325
|
if (prev.classList?.contains(WRAP_CLASS)) {
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
326
|
+
const wShown = parseInt(wrap.getAttribute(SHOWN_ATTR) || '0', 10);
|
|
327
|
+
const pShown = parseInt(prev.getAttribute(SHOWN_ATTR) || '0', 10);
|
|
328
|
+
const wFilled = isFilledNode(wrap);
|
|
329
|
+
const pFilled = isFilledNode(prev);
|
|
330
|
+
|
|
331
|
+
// Ne jamais toucher un wrap en cours de fill (dans les FILL_GRACE_MS après show)
|
|
332
|
+
const wInGrace = wShown && (now() - wShown) < FILL_GRACE_MS;
|
|
333
|
+
const pInGrace = pShown && (now() - pShown) < FILL_GRACE_MS;
|
|
334
|
+
|
|
335
|
+
if (wInGrace || pInGrace) break; // les deux en grace → rien
|
|
322
336
|
|
|
323
|
-
if (!wFilled) {
|
|
337
|
+
if (!wFilled && !wInGrace) {
|
|
324
338
|
withInternalMutation(() => removeWrap(wrap));
|
|
325
|
-
|
|
326
|
-
} else if (!pFilled) {
|
|
339
|
+
} else if (!pFilled && !pInGrace) {
|
|
327
340
|
withInternalMutation(() => removeWrap(prev));
|
|
328
|
-
removed++;
|
|
329
341
|
}
|
|
330
|
-
// Si les deux sont remplis, laisser en place
|
|
331
342
|
break;
|
|
332
343
|
}
|
|
333
344
|
prev = prev.previousElementSibling;
|
|
334
345
|
steps++;
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
|
-
|
|
338
|
-
return removed;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ─── Injection ────────────────────────────────────────────────────────────
|
|
342
|
-
function computeTargetIndices(count, interval, showFirst) {
|
|
343
|
-
const targets = new Set();
|
|
344
|
-
if (showFirst && count > 0) targets.add(0);
|
|
345
|
-
for (let i = interval - 1; i < count; i += interval) targets.add(i);
|
|
346
|
-
return [...targets].sort((a, b) => a - b);
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
+
// ─── Injection (BUG FIX #3) ───────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* Calcule les positions cibles basées sur l'ordinal GLOBAL de chaque item.
|
|
353
|
+
*
|
|
354
|
+
* interval=3 → pub après les posts dont (globalOrdinal % interval === interval-1)
|
|
355
|
+
* c.-à-d. après les posts globaux 2, 5, 8, 11… (0-based)
|
|
356
|
+
*
|
|
357
|
+
* showFirst=true → aussi après le post global 0.
|
|
358
|
+
*
|
|
359
|
+
* Ce calcul est STABLE entre les batches : si les posts 0-19 sont en DOM,
|
|
360
|
+
* les cibles sont 2, 5, 8, 11, 14, 17. Si les posts 20-39 arrivent,
|
|
361
|
+
* les cibles deviennent 20 (si 20%3===2? non), 23, 26, 29, 32, 35, 38.
|
|
362
|
+
* Jamais de recalcul depuis 0.
|
|
363
|
+
*/
|
|
364
|
+
function injectBetween(kindClass, items, interval, showFirst, poolKey, selector) {
|
|
350
365
|
if (!items.length) return 0;
|
|
351
366
|
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
let inserted = 0;
|
|
367
|
+
const maxIns = MAX_INSERTS_PER_RUN + (isBoosted() ? 2 : 0);
|
|
368
|
+
let inserted = 0;
|
|
355
369
|
|
|
356
|
-
for (const
|
|
370
|
+
for (const el of items) {
|
|
357
371
|
if (inserted >= maxIns) break;
|
|
358
|
-
|
|
359
|
-
const el = items[idx];
|
|
360
372
|
if (!el?.isConnected) continue;
|
|
373
|
+
|
|
374
|
+
const ord = getGlobalOrdinal(el, selector);
|
|
375
|
+
|
|
376
|
+
// Est-ce une position cible ?
|
|
377
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
378
|
+
if (!isTarget) continue;
|
|
379
|
+
|
|
361
380
|
if (hasAdjacentWrap(el)) continue;
|
|
362
381
|
|
|
363
|
-
const anchorKey
|
|
364
|
-
if (findWrapByAnchor(anchorKey)) continue;
|
|
382
|
+
const anchorKey = `${kindClass}:${ord}`;
|
|
383
|
+
if (findWrapByAnchor(anchorKey)) continue;
|
|
365
384
|
|
|
366
385
|
const id = pickId(poolKey);
|
|
367
|
-
if (!id) break;
|
|
386
|
+
if (!id) break;
|
|
368
387
|
|
|
369
388
|
const wrap = insertWrapAfter(el, id, kindClass, anchorKey);
|
|
370
389
|
if (!wrap) continue;
|
|
@@ -405,7 +424,6 @@
|
|
|
405
424
|
state.ioMargin = margin;
|
|
406
425
|
} catch (_) { state.io = null; state.ioMargin = null; }
|
|
407
426
|
|
|
408
|
-
// Ré-observer les placeholders déjà dans le DOM
|
|
409
427
|
try {
|
|
410
428
|
document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`).forEach(n => {
|
|
411
429
|
try { state.io?.observe(n); } catch (_) {}
|
|
@@ -420,7 +438,6 @@
|
|
|
420
438
|
if (!ph?.isConnected) return;
|
|
421
439
|
try { state.io?.observe(ph); } catch (_) {}
|
|
422
440
|
|
|
423
|
-
// Si déjà proche du viewport → show immédiat
|
|
424
441
|
try {
|
|
425
442
|
const r = ph.getBoundingClientRect();
|
|
426
443
|
const screens = isBoosted() ? 6 : (isMobile() ? 4 : 2.5);
|
|
@@ -436,10 +453,7 @@
|
|
|
436
453
|
if (t - (state.lastShowById.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
437
454
|
|
|
438
455
|
if (state.inflight >= getMaxInflight()) {
|
|
439
|
-
if (!state.pendingSet.has(id)) {
|
|
440
|
-
state.pending.push(id);
|
|
441
|
-
state.pendingSet.add(id);
|
|
442
|
-
}
|
|
456
|
+
if (!state.pendingSet.has(id)) { state.pending.push(id); state.pendingSet.add(id); }
|
|
443
457
|
return;
|
|
444
458
|
}
|
|
445
459
|
startShow(id);
|
|
@@ -470,45 +484,60 @@
|
|
|
470
484
|
|
|
471
485
|
requestAnimationFrame(() => {
|
|
472
486
|
try {
|
|
473
|
-
if (isBlocked()) return release();
|
|
487
|
+
if (isBlocked()) { clearTimeout(timeout); return release(); }
|
|
488
|
+
|
|
474
489
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
475
|
-
if (!ph?.isConnected) return release();
|
|
490
|
+
if (!ph?.isConnected) { clearTimeout(timeout); return release(); }
|
|
476
491
|
if (isFilledNode(ph)) { clearTimeout(timeout); return release(); }
|
|
477
492
|
|
|
478
493
|
const t = now();
|
|
479
|
-
if (t - (state.lastShowById.get(id) ?? 0) < SHOW_THROTTLE_MS)
|
|
494
|
+
if (t - (state.lastShowById.get(id) ?? 0) < SHOW_THROTTLE_MS) {
|
|
495
|
+
clearTimeout(timeout); return release();
|
|
496
|
+
}
|
|
480
497
|
state.lastShowById.set(id, t);
|
|
481
498
|
|
|
499
|
+
// Marquer le timestamp du show sur le wrap (pour decluster grace period)
|
|
500
|
+
try {
|
|
501
|
+
const wrap = ph.closest?.(`.${WRAP_CLASS}`);
|
|
502
|
+
if (wrap) wrap.setAttribute(SHOWN_ATTR, String(t));
|
|
503
|
+
} catch (_) {}
|
|
504
|
+
|
|
482
505
|
window.ezstandalone = window.ezstandalone || {};
|
|
483
506
|
const ez = window.ezstandalone;
|
|
484
507
|
|
|
485
508
|
const doShow = () => {
|
|
486
509
|
try { ez.showAds(id); } catch (_) {}
|
|
487
|
-
scheduleEmptyCheck(id);
|
|
488
|
-
setTimeout(release, 650);
|
|
510
|
+
scheduleEmptyCheck(id, t);
|
|
511
|
+
setTimeout(() => { clearTimeout(timeout); release(); }, 650);
|
|
489
512
|
};
|
|
490
513
|
|
|
491
514
|
if (Array.isArray(ez.cmd)) ez.cmd.push(doShow);
|
|
492
515
|
else doShow();
|
|
493
|
-
}
|
|
516
|
+
} catch (_) { clearTimeout(timeout); release(); }
|
|
494
517
|
});
|
|
495
518
|
}
|
|
496
519
|
|
|
497
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Vérifie si le wrap est toujours vide après EMPTY_CHECK_DELAY.
|
|
522
|
+
* On compare avec le timestamp du show pour éviter de colapser
|
|
523
|
+
* un wrap qui aurait reçu un nouveau show entre-temps.
|
|
524
|
+
*/
|
|
525
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
498
526
|
setTimeout(() => {
|
|
499
527
|
try {
|
|
500
528
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
501
529
|
if (!ph?.isConnected) return;
|
|
502
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
530
|
+
const wrap = ph.closest?.(`.${WRAP_CLASS}`);
|
|
503
531
|
if (!wrap) return;
|
|
504
532
|
|
|
505
|
-
|
|
506
|
-
|
|
533
|
+
// Si un nouveau show a eu lieu après celui-ci, ne pas colapser
|
|
534
|
+
const lastShown = parseInt(wrap.getAttribute(SHOWN_ATTR) || '0', 10);
|
|
535
|
+
if (lastShown > showTs) return;
|
|
507
536
|
|
|
508
537
|
if (!isFilledNode(ph)) wrap.classList.add('is-empty');
|
|
509
538
|
else wrap.classList.remove('is-empty');
|
|
510
539
|
} catch (_) {}
|
|
511
|
-
},
|
|
540
|
+
}, EMPTY_CHECK_DELAY);
|
|
512
541
|
}
|
|
513
542
|
|
|
514
543
|
// ─── Patch Ezoic showAds ──────────────────────────────────────────────────
|
|
@@ -558,23 +587,35 @@
|
|
|
558
587
|
const kind = getKind();
|
|
559
588
|
let inserted = 0;
|
|
560
589
|
|
|
561
|
-
|
|
590
|
+
/**
|
|
591
|
+
* @param {string} kindClass
|
|
592
|
+
* @param {() => Element[]} getItems
|
|
593
|
+
* @param {string} selector - sélecteur CSS de base (pour ordinal fallback)
|
|
594
|
+
* @param {*} cfgEnable
|
|
595
|
+
* @param {number} cfgInterval
|
|
596
|
+
* @param {*} cfgShowFirst
|
|
597
|
+
* @param {string} poolKey
|
|
598
|
+
*/
|
|
599
|
+
const run = (kindClass, getItems, selector, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
562
600
|
if (!normBool(cfgEnable)) return 0;
|
|
563
|
-
const items
|
|
564
|
-
|
|
565
|
-
const
|
|
601
|
+
const items = getItems();
|
|
602
|
+
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
603
|
+
const first = normBool(cfgShowFirst);
|
|
604
|
+
|
|
605
|
+
pruneOrphans(kindClass, selector);
|
|
606
|
+
const n = injectBetween(kindClass, items, interval, first, poolKey, selector);
|
|
566
607
|
if (n) decluster(kindClass);
|
|
567
608
|
return n;
|
|
568
609
|
};
|
|
569
610
|
|
|
570
611
|
if (kind === 'topic') {
|
|
571
|
-
inserted += run('ezoic-ad-message', getPostContainers,
|
|
612
|
+
inserted += run('ezoic-ad-message', getPostContainers, SELECTORS.postItem,
|
|
572
613
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
573
614
|
} else if (kind === 'categoryTopics') {
|
|
574
|
-
inserted += run('ezoic-ad-between', getTopicItems,
|
|
615
|
+
inserted += run('ezoic-ad-between', getTopicItems, SELECTORS.topicItem,
|
|
575
616
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
576
617
|
} else if (kind === 'categories') {
|
|
577
|
-
inserted += run('ezoic-ad-categories', getCategoryItems,
|
|
618
|
+
inserted += run('ezoic-ad-categories', getCategoryItems, SELECTORS.categoryItem,
|
|
578
619
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
579
620
|
}
|
|
580
621
|
|
|
@@ -582,7 +623,7 @@
|
|
|
582
623
|
}
|
|
583
624
|
|
|
584
625
|
// ─── Scheduler / Burst ────────────────────────────────────────────────────
|
|
585
|
-
function scheduleRun(delayMs
|
|
626
|
+
function scheduleRun(delayMs, cb) {
|
|
586
627
|
if (state.runQueued) return;
|
|
587
628
|
state.runQueued = true;
|
|
588
629
|
|
|
@@ -606,7 +647,7 @@
|
|
|
606
647
|
state.lastBurstReqTs = t;
|
|
607
648
|
|
|
608
649
|
const pk = getPageKey();
|
|
609
|
-
state.pageKey
|
|
650
|
+
state.pageKey = pk;
|
|
610
651
|
state.burstDeadline = t + 1800;
|
|
611
652
|
|
|
612
653
|
if (state.burstActive) return;
|
|
@@ -614,10 +655,10 @@
|
|
|
614
655
|
state.burstCount = 0;
|
|
615
656
|
|
|
616
657
|
const step = () => {
|
|
617
|
-
if (getPageKey() !== pk)
|
|
618
|
-
if (isBlocked())
|
|
619
|
-
if (now() > state.burstDeadline)
|
|
620
|
-
if (state.burstCount >= 8)
|
|
658
|
+
if (getPageKey() !== pk) { state.burstActive = false; return; }
|
|
659
|
+
if (isBlocked()) { state.burstActive = false; return; }
|
|
660
|
+
if (now() > state.burstDeadline) { state.burstActive = false; return; }
|
|
661
|
+
if (state.burstCount >= 8) { state.burstActive = false; return; }
|
|
621
662
|
|
|
622
663
|
state.burstCount++;
|
|
623
664
|
scheduleRun(0, (n) => {
|
|
@@ -629,26 +670,22 @@
|
|
|
629
670
|
step();
|
|
630
671
|
}
|
|
631
672
|
|
|
632
|
-
// ─── Cleanup
|
|
673
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────
|
|
633
674
|
function cleanup() {
|
|
634
|
-
// Bloquer toute injection pendant la transition
|
|
635
675
|
blockedUntil = now() + 1500;
|
|
636
676
|
|
|
637
|
-
// Supprimer tous les wraps injectés → libère les IDs
|
|
638
677
|
withInternalMutation(() => {
|
|
639
678
|
document.querySelectorAll(`.${WRAP_CLASS}`).forEach(removeWrap);
|
|
640
679
|
});
|
|
641
680
|
|
|
642
|
-
|
|
643
|
-
state.
|
|
644
|
-
state.
|
|
645
|
-
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
681
|
+
state.cfg = null;
|
|
682
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
683
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
646
684
|
state.mountedIds.clear();
|
|
647
685
|
state.lastShowById.clear();
|
|
648
|
-
state.inflight
|
|
649
|
-
state.pending
|
|
686
|
+
state.inflight = 0;
|
|
687
|
+
state.pending = [];
|
|
650
688
|
state.pendingSet.clear();
|
|
651
|
-
|
|
652
689
|
state.burstActive = false;
|
|
653
690
|
state.runQueued = false;
|
|
654
691
|
}
|
|
@@ -659,11 +696,10 @@
|
|
|
659
696
|
if (!m.addedNodes?.length) continue;
|
|
660
697
|
for (const n of m.addedNodes) {
|
|
661
698
|
if (n.nodeType !== 1) continue;
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
) return true;
|
|
699
|
+
if (n.matches?.(SELECTORS.postItem) || n.querySelector?.(SELECTORS.postItem) ||
|
|
700
|
+
n.matches?.(SELECTORS.topicItem) || n.querySelector?.(SELECTORS.topicItem) ||
|
|
701
|
+
n.matches?.(SELECTORS.categoryItem) || n.querySelector?.(SELECTORS.categoryItem))
|
|
702
|
+
return true;
|
|
667
703
|
}
|
|
668
704
|
}
|
|
669
705
|
return false;
|
|
@@ -680,7 +716,7 @@
|
|
|
680
716
|
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
681
717
|
}
|
|
682
718
|
|
|
683
|
-
// ─── Utilities
|
|
719
|
+
// ─── Utilities ────────────────────────────────────────────────────────────
|
|
684
720
|
function muteNoisyConsole() {
|
|
685
721
|
if (window.__nodebbEzoicConsoleMuted) return;
|
|
686
722
|
window.__nodebbEzoicConsoleMuted = true;
|
|
@@ -704,9 +740,8 @@
|
|
|
704
740
|
try {
|
|
705
741
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
706
742
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
707
|
-
const f =
|
|
708
|
-
|
|
709
|
-
});
|
|
743
|
+
const f = document.createElement('iframe');
|
|
744
|
+
f.style.display = 'none'; f.id = '__tcfapiLocator'; f.name = '__tcfapiLocator';
|
|
710
745
|
(document.body || document.documentElement).appendChild(f);
|
|
711
746
|
} catch (_) {}
|
|
712
747
|
}
|
|
@@ -716,12 +751,12 @@
|
|
|
716
751
|
const head = document.head;
|
|
717
752
|
if (!head) return;
|
|
718
753
|
const links = [
|
|
719
|
-
['preconnect', 'https://g.ezoic.net',
|
|
720
|
-
['preconnect', 'https://go.ezoic.net',
|
|
721
|
-
['preconnect', 'https://securepubads.g.doubleclick.net',
|
|
722
|
-
['preconnect', 'https://pagead2.googlesyndication.com',
|
|
723
|
-
['dns-prefetch', 'https://g.ezoic.net',
|
|
724
|
-
['dns-prefetch', 'https://securepubads.g.doubleclick.net',
|
|
754
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
755
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
756
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
757
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
758
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
759
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
725
760
|
];
|
|
726
761
|
for (const [rel, href, cors] of links) {
|
|
727
762
|
const key = `${rel}|${href}`;
|
|
@@ -741,9 +776,7 @@
|
|
|
741
776
|
|
|
742
777
|
$(window).off('.ezoicInfinite');
|
|
743
778
|
|
|
744
|
-
$(window).on('action:ajaxify.start.ezoicInfinite',
|
|
745
|
-
cleanup();
|
|
746
|
-
});
|
|
779
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', cleanup);
|
|
747
780
|
|
|
748
781
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
749
782
|
state.pageKey = getPageKey();
|
|
@@ -768,7 +801,6 @@
|
|
|
768
801
|
|
|
769
802
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
770
803
|
|
|
771
|
-
// Hooks AMD (NodeBB 4.x)
|
|
772
804
|
try {
|
|
773
805
|
require(['hooks'], hooks => {
|
|
774
806
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -786,20 +818,17 @@
|
|
|
786
818
|
function bindScroll() {
|
|
787
819
|
let ticking = false;
|
|
788
820
|
window.addEventListener('scroll', () => {
|
|
789
|
-
// Scroll boost
|
|
790
821
|
try {
|
|
791
|
-
const t
|
|
792
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
822
|
+
const t = now(), y = window.scrollY || window.pageYOffset || 0;
|
|
793
823
|
if (state.lastScrollTs) {
|
|
794
824
|
const speed = Math.abs(y - state.lastScrollY) / Math.max(1, t - state.lastScrollTs);
|
|
795
825
|
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
796
|
-
const
|
|
826
|
+
const was = isBoosted();
|
|
797
827
|
state.scrollBoostUntil = Math.max(state.scrollBoostUntil, t + BOOST_DURATION_MS);
|
|
798
|
-
if (!
|
|
828
|
+
if (!was) ensurePreloadObserver();
|
|
799
829
|
}
|
|
800
830
|
}
|
|
801
|
-
state.lastScrollY
|
|
802
|
-
state.lastScrollTs = t;
|
|
831
|
+
state.lastScrollY = y; state.lastScrollTs = t;
|
|
803
832
|
} catch (_) {}
|
|
804
833
|
|
|
805
834
|
if (ticking) return;
|