nodebb-plugin-ezoic-infinite 1.8.21 → 1.8.23
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 +566 -922
package/public/client.js
CHANGED
|
@@ -1,328 +1,114 @@
|
|
|
1
|
-
(function
|
|
1
|
+
(function nbbEzoicInfiniteV22() {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
12
|
-
|
|
13
|
-
// Tunables (stables en prod)
|
|
14
|
-
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
15
|
-
const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
|
|
16
|
-
const MAX_INFLIGHT = 2; // ids max simultanés en vol (garde-fou)
|
|
17
|
-
const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
|
|
18
|
-
const SHOW_THROTTLE_MS = 500; // anti-spam showAds() par id (plus réactif)
|
|
19
|
-
const SHOW_RELEASE_MS = 300; // relâche inflight après showAds() batché
|
|
20
|
-
const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
|
|
21
|
-
const BATCH_FLUSH_MS = 30; // micro-buffer pour regrouper les ids proches
|
|
22
|
-
const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
|
|
23
|
-
const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
|
|
24
|
-
const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
|
|
25
|
-
const CLEANUP_GRACE_MS = 3_500; // délai mini avant cleanup d'un wrap candidat
|
|
26
|
-
const SPECIAL_GRACE_MS = 30_000; // délai allongé pour sticky/fixed/adhesion
|
|
27
|
-
const RECENT_WRAP_ACTIVITY_MS = 5_000; // protège un wrap récemment muté/rafraîchi
|
|
28
|
-
const VIEWPORT_BUFFER_DESKTOP = 500;
|
|
29
|
-
const VIEWPORT_BUFFER_MOBILE = 250;
|
|
30
|
-
|
|
31
|
-
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
32
|
-
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
33
|
-
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
4
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
5
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
6
|
+
const A_ANCHOR = 'data-ezoic-anchor';
|
|
7
|
+
const A_WRAPID = 'data-ezoic-wrapid';
|
|
8
|
+
const A_CREATED = 'data-ezoic-created';
|
|
9
|
+
const A_SHOWN = 'data-ezoic-shown';
|
|
10
|
+
const A_STATE = 'data-ezoic-state';
|
|
34
11
|
|
|
35
12
|
const SEL = {
|
|
36
|
-
post:
|
|
37
|
-
topic:
|
|
13
|
+
post: '[component="post"][data-pid]',
|
|
14
|
+
topic: 'li[component="category/topic"]',
|
|
38
15
|
category: 'li[component="categories/category"]',
|
|
39
16
|
};
|
|
40
|
-
const WRAP_SEL = `.${WRAP_CLASS}`;
|
|
41
|
-
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id]';
|
|
42
|
-
const CONTENT_SEL_LIST = [SEL.post, SEL.topic, SEL.category];
|
|
43
17
|
|
|
44
|
-
/**
|
|
45
|
-
* Table KIND — source de vérité par kindClass.
|
|
46
|
-
*
|
|
47
|
-
* sel sélecteur CSS complet des éléments cibles
|
|
48
|
-
* baseTag préfixe tag pour querySelector d'ancre
|
|
49
|
-
* (vide pour posts : le sélecteur commence par '[')
|
|
50
|
-
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
51
|
-
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
52
|
-
* null → fallback positionnel (catégories)
|
|
53
|
-
*/
|
|
54
18
|
const KIND = {
|
|
55
|
-
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
56
|
-
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
57
|
-
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
19
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index', pool: 'posts' },
|
|
20
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index', pool: 'topics' },
|
|
21
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null, pool: 'categories' },
|
|
58
22
|
};
|
|
59
23
|
|
|
60
|
-
|
|
24
|
+
const CFG = {
|
|
25
|
+
maxInsertsPerRun: 8,
|
|
26
|
+
maxShowInflight: 1,
|
|
27
|
+
maxShowBatch: 1,
|
|
28
|
+
showThrottleMs: 1500,
|
|
29
|
+
showCooldownMs: 15000,
|
|
30
|
+
showReleaseMs: 350,
|
|
31
|
+
showFailsafeMs: 8000,
|
|
32
|
+
batchFlushMs: 80,
|
|
33
|
+
runDebounceMs: 80,
|
|
34
|
+
orphanGraceMs: 12000,
|
|
35
|
+
retireGraceMs: 25000,
|
|
36
|
+
recentActivityMs: 6000,
|
|
37
|
+
viewportBufferDesktop: 700,
|
|
38
|
+
viewportBufferMobile: 350,
|
|
39
|
+
ioMarginDesktop: '1800px 0px 2200px 0px',
|
|
40
|
+
ioMarginMobile: '2400px 0px 2800px 0px',
|
|
41
|
+
maintenanceEveryMs: 1200,
|
|
42
|
+
minHeightRememberMin: 40,
|
|
43
|
+
};
|
|
61
44
|
|
|
62
45
|
const S = {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
destroyPending: [],
|
|
79
|
-
destroyPendingSet: new Set(),
|
|
80
|
-
sweepQueued: false,
|
|
81
|
-
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
82
|
-
ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
|
|
83
|
-
ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
|
|
84
|
-
wrapActivityAt: new Map(), // key/id -> ts activité DOM récente
|
|
46
|
+
cfg: null,
|
|
47
|
+
poolsReady: false,
|
|
48
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
49
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
50
|
+
pageKey: null,
|
|
51
|
+
blockedUntil: 0,
|
|
52
|
+
runTimer: 0,
|
|
53
|
+
maintenanceTimer: 0,
|
|
54
|
+
io: null,
|
|
55
|
+
domObs: null,
|
|
56
|
+
muting: 0,
|
|
57
|
+
mountedIds: new Set(),
|
|
58
|
+
wrapByKey: new Map(),
|
|
59
|
+
regById: new Map(),
|
|
60
|
+
lastShowById: new Map(),
|
|
85
61
|
lastWrapHeightByClass: new Map(),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
runQueued: false,
|
|
91
|
-
burstActive: false,
|
|
92
|
-
burstDeadline: 0,
|
|
93
|
-
burstCount: 0,
|
|
94
|
-
lastBurstTs: 0,
|
|
62
|
+
pendingShow: [],
|
|
63
|
+
pendingSet: new Set(),
|
|
64
|
+
showTimer: 0,
|
|
65
|
+
inflightShow: 0,
|
|
95
66
|
};
|
|
96
67
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
function viewportBufferPx() {
|
|
106
|
-
return isMobile() ? VIEWPORT_BUFFER_MOBILE : VIEWPORT_BUFFER_DESKTOP;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function textSig(el) {
|
|
110
|
-
if (!(el instanceof Element)) return '';
|
|
111
|
-
const parts = [el.id || '', String(el.className || ''), el.getAttribute?.('name') || ''];
|
|
112
|
-
try {
|
|
113
|
-
for (const a of ['data-google-query-id', 'data-google-container-id', 'data-slot', 'data-ad-slot']) {
|
|
114
|
-
const v = el.getAttribute?.(a);
|
|
115
|
-
if (v) parts.push(v);
|
|
116
|
-
}
|
|
117
|
-
} catch (_) {}
|
|
118
|
-
return parts.join(' ').toLowerCase();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isSpecialSlotLike(el) {
|
|
122
|
-
const sig = textSig(el);
|
|
123
|
-
return /adhesion|interstitial|anchor|sticky|outofpage/.test(sig);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function hasSpecialSlotMarkers(root) {
|
|
127
|
-
if (!(root instanceof Element)) return false;
|
|
128
|
-
if (isSpecialSlotLike(root)) return true;
|
|
129
|
-
try {
|
|
130
|
-
const nodes = root.querySelectorAll('[id],[class],[name],[data-slot],[data-ad-slot]');
|
|
131
|
-
for (const n of nodes) if (isSpecialSlotLike(n)) return true;
|
|
132
|
-
} catch (_) {}
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function hasFixedLikeNode(root, maxScan = 24) {
|
|
137
|
-
if (!(root instanceof Element)) return false;
|
|
138
|
-
const q = [root];
|
|
139
|
-
let seen = 0;
|
|
140
|
-
while (q.length && seen < maxScan) {
|
|
141
|
-
const n = q.shift();
|
|
142
|
-
seen++;
|
|
143
|
-
try {
|
|
144
|
-
const cs = window.getComputedStyle(n);
|
|
145
|
-
if (cs.position === 'fixed' || cs.position === 'sticky') return true;
|
|
146
|
-
} catch (_) {}
|
|
147
|
-
for (const c of n.children || []) q.push(c);
|
|
148
|
-
}
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function markWrapActivity(wrapOrId) {
|
|
153
|
-
const t = ts();
|
|
154
|
-
try {
|
|
155
|
-
if (wrapOrId instanceof Element) {
|
|
156
|
-
const key = wrapOrId.getAttribute(A_ANCHOR);
|
|
157
|
-
const id = parseInt(wrapOrId.getAttribute(A_WRAPID), 10);
|
|
158
|
-
if (key) S.wrapActivityAt.set(`k:${key}`, t);
|
|
159
|
-
if (Number.isFinite(id)) S.wrapActivityAt.set(`i:${id}`, t);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const id = parseInt(wrapOrId, 10);
|
|
163
|
-
if (Number.isFinite(id)) S.wrapActivityAt.set(`i:${id}`, t);
|
|
164
|
-
} catch (_) {}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function wrapRecentActivity(w) {
|
|
168
|
-
try {
|
|
169
|
-
const key = w?.getAttribute?.(A_ANCHOR);
|
|
170
|
-
const id = parseInt(w?.getAttribute?.(A_WRAPID), 10);
|
|
171
|
-
const t1 = key ? (S.wrapActivityAt.get(`k:${key}`) || 0) : 0;
|
|
172
|
-
const t2 = Number.isFinite(id) ? (S.wrapActivityAt.get(`i:${id}`) || 0) : 0;
|
|
173
|
-
const t3 = parseInt(w?.getAttribute?.(A_SHOWN) || '0', 10) || 0;
|
|
174
|
-
return (ts() - Math.max(t1, t2, t3)) < RECENT_WRAP_ACTIVITY_MS;
|
|
175
|
-
} catch (_) { return false; }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function wrapCleanupGraceMs(w) {
|
|
179
|
-
return (hasSpecialSlotMarkers(w) || hasFixedLikeNode(w)) ? SPECIAL_GRACE_MS : CLEANUP_GRACE_MS;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function wrapNearViewport(w) {
|
|
183
|
-
try {
|
|
184
|
-
const r = w.getBoundingClientRect();
|
|
185
|
-
const b = viewportBufferPx();
|
|
186
|
-
const vh = window.innerHeight || 800;
|
|
187
|
-
return r.bottom > -b && r.top < vh + b;
|
|
188
|
-
} catch (_) { return true; }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function rememberWrapHeight(w) {
|
|
192
|
-
try {
|
|
193
|
-
if (!(w instanceof Element)) return;
|
|
194
|
-
const klass = [...(w.classList || [])].find(c => c.startsWith('ezoic-ad-'));
|
|
195
|
-
if (!klass) return;
|
|
196
|
-
const h = Math.round(w.getBoundingClientRect().height || 0);
|
|
197
|
-
if (h >= 40) S.lastWrapHeightByClass.set(klass, h);
|
|
198
|
-
} catch (_) {}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function healFalseEmpty(root = document) {
|
|
202
|
-
try {
|
|
203
|
-
const list = [];
|
|
204
|
-
if (root instanceof Element && root.classList?.contains(WRAP_CLASS)) list.push(root);
|
|
205
|
-
const found = root.querySelectorAll ? root.querySelectorAll(`.${WRAP_CLASS}.is-empty`) : [];
|
|
206
|
-
for (const w of found) list.push(w);
|
|
207
|
-
for (const w of list) {
|
|
208
|
-
if (!w?.classList?.contains('is-empty')) continue;
|
|
209
|
-
if (isFilled(w)) w.classList.remove('is-empty');
|
|
210
|
-
}
|
|
211
|
-
} catch (_) {}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function phEl(id) {
|
|
215
|
-
return document.getElementById(`${PH_PREFIX}${id}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function hasSinglePlaceholder(id) {
|
|
219
|
-
try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function canShowPlaceholderId(id, now = ts()) {
|
|
223
|
-
const n = parseInt(id, 10);
|
|
224
|
-
if (!Number.isFinite(n) || n <= 0) return false;
|
|
225
|
-
if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
|
|
226
|
-
const ph = phEl(n);
|
|
227
|
-
if (!ph?.isConnected || isFilled(ph)) return false;
|
|
228
|
-
if (!hasSinglePlaceholder(n)) return false;
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function queueSweepDeadWraps() {
|
|
233
|
-
if (S.sweepQueued) return;
|
|
234
|
-
S.sweepQueued = true;
|
|
235
|
-
requestAnimationFrame(() => {
|
|
236
|
-
S.sweepQueued = false;
|
|
237
|
-
sweepDeadWraps();
|
|
238
|
-
healFalseEmpty();
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function getDynamicShowBatchMax() {
|
|
243
|
-
const speed = S.scrollSpeed || 0;
|
|
244
|
-
const pend = S.pending.length;
|
|
245
|
-
// Scroll très rapide => petits batches (réduit le churn/unused)
|
|
246
|
-
if (speed > 2600) return 2;
|
|
247
|
-
if (speed > 1400) return 3;
|
|
248
|
-
// Peu de candidats => flush plus vite, inutile d'attendre 4
|
|
249
|
-
if (pend <= 1) return 1;
|
|
250
|
-
if (pend <= 3) return 2;
|
|
251
|
-
// Par défaut compromis dynamique
|
|
252
|
-
return 3;
|
|
253
|
-
}
|
|
68
|
+
const now = () => Date.now();
|
|
69
|
+
const isBlocked = () => now() < S.blockedUntil;
|
|
70
|
+
const isMobile = () => {
|
|
71
|
+
try { return window.innerWidth < 768; } catch (_) { return false; }
|
|
72
|
+
};
|
|
73
|
+
const viewportBuffer = () => (isMobile() ? CFG.viewportBufferMobile : CFG.viewportBufferDesktop);
|
|
74
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
254
75
|
|
|
255
76
|
function mutate(fn) {
|
|
256
|
-
S.
|
|
257
|
-
try { fn(); } finally { S.
|
|
77
|
+
S.muting++;
|
|
78
|
+
try { return fn(); } finally { S.muting = Math.max(0, S.muting - 1); }
|
|
258
79
|
}
|
|
259
|
-
function scheduleDestroyFlush() {
|
|
260
|
-
if (S.destroyBatchTimer) return;
|
|
261
|
-
S.destroyBatchTimer = setTimeout(() => {
|
|
262
|
-
S.destroyBatchTimer = 0;
|
|
263
|
-
flushDestroyBatch();
|
|
264
|
-
}, DESTROY_FLUSH_MS);
|
|
265
|
-
}
|
|
266
80
|
|
|
267
|
-
function
|
|
268
|
-
if (!S.destroyPending.length) return;
|
|
269
|
-
const ids = [];
|
|
270
|
-
while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
|
|
271
|
-
const id = S.destroyPending.shift();
|
|
272
|
-
S.destroyPendingSet.delete(id);
|
|
273
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
274
|
-
ids.push(id);
|
|
275
|
-
}
|
|
276
|
-
if (ids.length) {
|
|
81
|
+
function pageKey() {
|
|
277
82
|
try {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
83
|
+
const d = window.ajaxify?.data;
|
|
84
|
+
if (d?.tid) return `t:${d.tid}`;
|
|
85
|
+
if (d?.cid) return `c:${d.cid}`;
|
|
281
86
|
} catch (_) {}
|
|
87
|
+
return location.pathname;
|
|
282
88
|
}
|
|
283
|
-
if (S.destroyPending.length) scheduleDestroyFlush();
|
|
284
|
-
}
|
|
285
89
|
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
90
|
+
function getKind() {
|
|
91
|
+
const p = location.pathname;
|
|
92
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
93
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
94
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
95
|
+
if (document.querySelector(SEL.post)) return 'topic';
|
|
96
|
+
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
97
|
+
if (document.querySelector(SEL.category)) return 'categories';
|
|
98
|
+
return 'other';
|
|
293
99
|
}
|
|
294
|
-
scheduleDestroyFlush();
|
|
295
|
-
}
|
|
296
100
|
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
out.push(id);
|
|
306
|
-
try {
|
|
307
|
-
const wrap = phEl(id)?.closest?.(WRAP_SEL) || null;
|
|
308
|
-
if (wrap && (wrapRecentActivity(wrap) || hasSpecialSlotMarkers(wrap) || hasFixedLikeNode(wrap))) {
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
} catch (_) {}
|
|
312
|
-
if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
|
|
313
|
-
}
|
|
314
|
-
if (toDestroy.length) {
|
|
315
|
-
try {
|
|
316
|
-
const ez = window.ezstandalone;
|
|
317
|
-
const run = () => { try { ez?.destroyPlaceholders?.(toDestroy); } catch (_) {} };
|
|
318
|
-
try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
|
|
319
|
-
} catch (_) {}
|
|
320
|
-
for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
|
|
101
|
+
function getPosts() {
|
|
102
|
+
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
103
|
+
if (!el.isConnected) return false;
|
|
104
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
105
|
+
const p = el.parentElement?.closest?.(SEL.post);
|
|
106
|
+
if (p && p !== el) return false;
|
|
107
|
+
return el.getAttribute('component') !== 'post/parent';
|
|
108
|
+
});
|
|
321
109
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// ── Config ─────────────────────────────────────────────────────────────────
|
|
110
|
+
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
111
|
+
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
326
112
|
|
|
327
113
|
async function fetchConfig() {
|
|
328
114
|
if (S.cfg) return S.cfg;
|
|
@@ -334,9 +120,10 @@ function destroyBeforeReuse(ids) {
|
|
|
334
120
|
}
|
|
335
121
|
|
|
336
122
|
function parseIds(raw) {
|
|
337
|
-
const out = []
|
|
338
|
-
|
|
339
|
-
|
|
123
|
+
const out = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const line of String(raw || '').split(/\r?\n/)) {
|
|
126
|
+
const n = parseInt(line.trim(), 10);
|
|
340
127
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
341
128
|
}
|
|
342
129
|
return out;
|
|
@@ -344,258 +131,147 @@ function destroyBeforeReuse(ids) {
|
|
|
344
131
|
|
|
345
132
|
function initPools(cfg) {
|
|
346
133
|
if (S.poolsReady) return;
|
|
347
|
-
S.pools
|
|
348
|
-
|
|
349
|
-
|
|
134
|
+
S.pools = {
|
|
135
|
+
topics: parseIds(cfg.placeholderIds),
|
|
136
|
+
posts: parseIds(cfg.messagePlaceholderIds),
|
|
137
|
+
categories: parseIds(cfg.categoryPlaceholderIds),
|
|
138
|
+
};
|
|
350
139
|
S.poolsReady = true;
|
|
351
140
|
}
|
|
352
141
|
|
|
353
|
-
|
|
142
|
+
function textSig(el) {
|
|
143
|
+
if (!(el instanceof Element)) return '';
|
|
144
|
+
const parts = [el.id || '', String(el.className || ''), el.getAttribute('name') || ''];
|
|
145
|
+
for (const a of ['data-slot', 'data-ad-slot', 'data-google-container-id']) {
|
|
146
|
+
try { const v = el.getAttribute(a); if (v) parts.push(v); } catch (_) {}
|
|
147
|
+
}
|
|
148
|
+
return parts.join(' ').toLowerCase();
|
|
149
|
+
}
|
|
354
150
|
|
|
355
|
-
function
|
|
151
|
+
function isSpecialLike(root) {
|
|
152
|
+
if (!(root instanceof Element)) return false;
|
|
153
|
+
const test = el => /adhesion|interstitial|anchor|sticky|outofpage/.test(textSig(el));
|
|
154
|
+
if (test(root)) return true;
|
|
356
155
|
try {
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
if (d?.cid) return `c:${d.cid}`;
|
|
156
|
+
const nodes = root.querySelectorAll('[id],[class],[name],[data-slot],[data-ad-slot]');
|
|
157
|
+
for (const n of nodes) if (test(n)) return true;
|
|
360
158
|
} catch (_) {}
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function getKind() {
|
|
365
|
-
const p = location.pathname;
|
|
366
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
367
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
368
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
369
|
-
if (document.querySelector(SEL.category)) return 'categories';
|
|
370
|
-
if (document.querySelector(SEL.post)) return 'topic';
|
|
371
|
-
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
372
|
-
return 'other';
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
376
|
-
|
|
377
|
-
function getPosts() {
|
|
378
|
-
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
379
|
-
if (!el.isConnected) return false;
|
|
380
|
-
if (!el.querySelector('[component="post/content"]')) return false;
|
|
381
|
-
const p = el.parentElement?.closest(SEL.post);
|
|
382
|
-
if (p && p !== el) return false;
|
|
383
|
-
return el.getAttribute('component') !== 'post/parent';
|
|
384
|
-
});
|
|
159
|
+
return false;
|
|
385
160
|
}
|
|
386
161
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (!key) return false;
|
|
400
|
-
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
401
|
-
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
402
|
-
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
403
|
-
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
404
|
-
const colonIdx = key.indexOf(':');
|
|
405
|
-
const klass = key.slice(0, colonIdx);
|
|
406
|
-
const anchorId = key.slice(colonIdx + 1);
|
|
407
|
-
const cfg = KIND[klass];
|
|
408
|
-
if (!cfg) return false;
|
|
409
|
-
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
410
|
-
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
411
|
-
const parent = wrap.parentElement;
|
|
412
|
-
if (parent) {
|
|
413
|
-
for (const sib of parent.children) {
|
|
414
|
-
if (sib === wrap) continue;
|
|
415
|
-
try {
|
|
416
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
417
|
-
return sib.isConnected;
|
|
418
|
-
}
|
|
419
|
-
} catch (_) {}
|
|
420
|
-
}
|
|
162
|
+
function hasFixedLike(root, max = 16) {
|
|
163
|
+
if (!(root instanceof Element)) return false;
|
|
164
|
+
const q = [root];
|
|
165
|
+
let seen = 0;
|
|
166
|
+
while (q.length && seen < max) {
|
|
167
|
+
const n = q.shift();
|
|
168
|
+
seen++;
|
|
169
|
+
try {
|
|
170
|
+
const cs = window.getComputedStyle(n);
|
|
171
|
+
if (cs.position === 'fixed' || cs.position === 'sticky') return true;
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
for (const c of n.children || []) q.push(c);
|
|
421
174
|
}
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
425
|
-
return !!(found?.isConnected);
|
|
426
|
-
} catch (_) { return false; }
|
|
175
|
+
return false;
|
|
427
176
|
}
|
|
428
177
|
|
|
429
|
-
function
|
|
430
|
-
return
|
|
178
|
+
function isFilled(wrap) {
|
|
179
|
+
try { return !!wrap?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'); } catch (_) { return false; }
|
|
431
180
|
}
|
|
432
181
|
|
|
433
|
-
|
|
182
|
+
function phEl(id) { return document.getElementById(`${PH_PREFIX}${id}`); }
|
|
434
183
|
|
|
435
|
-
|
|
436
|
-
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
437
|
-
* ou un fallback positionnel si l'attribut est absent.
|
|
438
|
-
*/
|
|
439
|
-
function stableId(klass, el) {
|
|
184
|
+
function anchorStableId(klass, el) {
|
|
440
185
|
const attr = KIND[klass]?.anchorAttr;
|
|
441
186
|
if (attr) {
|
|
442
187
|
const v = el.getAttribute(attr);
|
|
443
188
|
if (v !== null && v !== '') return v;
|
|
444
189
|
}
|
|
445
190
|
let i = 0;
|
|
446
|
-
for (const s of el.parentElement?.children
|
|
447
|
-
if (s === el) return `i${i}`;
|
|
448
|
-
i++;
|
|
449
|
-
}
|
|
191
|
+
for (const s of el.parentElement?.children || []) { if (s === el) return `i${i}`; i++; }
|
|
450
192
|
return 'i0';
|
|
451
193
|
}
|
|
194
|
+
const anchorKey = (klass, el) => `${klass}:${anchorStableId(klass, el)}`;
|
|
452
195
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
function findWrap(key) {
|
|
196
|
+
function findWrapByKey(key) {
|
|
456
197
|
const w = S.wrapByKey.get(key);
|
|
457
198
|
return (w?.isConnected) ? w : null;
|
|
458
199
|
}
|
|
459
200
|
|
|
460
|
-
|
|
201
|
+
function rememberWrapHeight(wrap) {
|
|
202
|
+
try {
|
|
203
|
+
if (!(wrap instanceof Element)) return;
|
|
204
|
+
const klass = [...(wrap.classList || [])].find(c => c.startsWith('ezoic-ad-'));
|
|
205
|
+
if (!klass) return;
|
|
206
|
+
const h = Math.round(wrap.getBoundingClientRect().height || 0);
|
|
207
|
+
if (h >= CFG.minHeightRememberMin) S.lastWrapHeightByClass.set(klass, h);
|
|
208
|
+
} catch (_) {}
|
|
209
|
+
}
|
|
461
210
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
for (let t = 0; t < pool.length; t++) {
|
|
470
|
-
const i = S.cursors[poolKey] % pool.length;
|
|
471
|
-
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
472
|
-
const id = pool[i];
|
|
473
|
-
if (!S.mountedIds.has(id)) return id;
|
|
474
|
-
}
|
|
475
|
-
return null;
|
|
211
|
+
function wrapNearViewport(wrap) {
|
|
212
|
+
try {
|
|
213
|
+
const r = wrap.getBoundingClientRect();
|
|
214
|
+
const b = viewportBuffer();
|
|
215
|
+
const vh = window.innerHeight || 800;
|
|
216
|
+
return r.bottom > -b && r.top < vh + b;
|
|
217
|
+
} catch (_) { return true; }
|
|
476
218
|
}
|
|
477
219
|
|
|
478
|
-
function
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
220
|
+
function ensureRegistry(id, wrap) {
|
|
221
|
+
let rec = S.regById.get(id);
|
|
222
|
+
if (!rec) {
|
|
223
|
+
rec = {
|
|
224
|
+
id,
|
|
225
|
+
wrap,
|
|
226
|
+
key: wrap?.getAttribute(A_ANCHOR) || '',
|
|
227
|
+
state: 'idle',
|
|
228
|
+
typeClass: [...(wrap?.classList || [])].find(c => c.startsWith('ezoic-ad-')) || '',
|
|
229
|
+
isSpecial: false,
|
|
230
|
+
isFixedLike: false,
|
|
231
|
+
createdAt: now(),
|
|
232
|
+
shownAt: 0,
|
|
233
|
+
lastSeenAt: 0,
|
|
234
|
+
lastMutationAt: now(),
|
|
235
|
+
cooldownUntil: 0,
|
|
236
|
+
};
|
|
237
|
+
S.regById.set(id, rec);
|
|
493
238
|
}
|
|
494
|
-
if (
|
|
495
|
-
|
|
239
|
+
if (wrap && rec.wrap !== wrap) rec.wrap = wrap;
|
|
240
|
+
if (wrap) {
|
|
241
|
+
rec.key = wrap.getAttribute(A_ANCHOR) || rec.key;
|
|
242
|
+
rec.typeClass = [...(wrap.classList || [])].find(c => c.startsWith('ezoic-ad-')) || rec.typeClass;
|
|
496
243
|
}
|
|
244
|
+
return rec;
|
|
497
245
|
}
|
|
498
246
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
503
|
-
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
504
|
-
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
505
|
-
*/
|
|
506
|
-
function recycleAndMove(klass, targetEl, newKey) {
|
|
507
|
-
const ez = window.ezstandalone;
|
|
508
|
-
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
509
|
-
typeof ez?.define !== 'function' ||
|
|
510
|
-
typeof ez?.displayMore !== 'function') return null;
|
|
511
|
-
|
|
512
|
-
const vh = window.innerHeight || 800;
|
|
513
|
-
const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
|
|
514
|
-
const farAbove = -vh;
|
|
515
|
-
const farBelow = vh * 2;
|
|
516
|
-
|
|
517
|
-
let bestPrefEmpty = null, bestPrefMetric = Infinity;
|
|
518
|
-
let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
|
|
519
|
-
let bestAnyEmpty = null, bestAnyMetric = Infinity;
|
|
520
|
-
let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
|
|
521
|
-
|
|
522
|
-
for (const wrap of S.wrapByKey.values()) {
|
|
523
|
-
if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
|
|
524
|
-
if (wrapRecentActivity(wrap)) continue;
|
|
525
|
-
if (hasSpecialSlotMarkers(wrap) || hasFixedLikeNode(wrap)) continue;
|
|
526
|
-
try {
|
|
527
|
-
const rect = wrap.getBoundingClientRect();
|
|
528
|
-
const isAbove = rect.bottom <= farAbove;
|
|
529
|
-
const isBelow = rect.top >= farBelow;
|
|
530
|
-
const anyFar = isAbove || isBelow;
|
|
531
|
-
if (!anyFar) continue;
|
|
532
|
-
|
|
533
|
-
const qualifies = preferAbove ? isAbove : isBelow;
|
|
534
|
-
const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
|
|
535
|
-
const filled = isFilled(wrap);
|
|
536
|
-
|
|
537
|
-
if (qualifies) {
|
|
538
|
-
if (!filled) {
|
|
539
|
-
if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
|
|
540
|
-
} else {
|
|
541
|
-
if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (!filled) {
|
|
545
|
-
if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
|
|
546
|
-
} else {
|
|
547
|
-
if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
|
|
548
|
-
}
|
|
549
|
-
} catch (_) {}
|
|
247
|
+
function updateWrapState(rec, state) {
|
|
248
|
+
rec.state = state;
|
|
249
|
+
try { rec.wrap?.setAttribute?.(A_STATE, state); } catch (_) {}
|
|
550
250
|
}
|
|
551
251
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
rememberWrapHeight(best);
|
|
561
|
-
best.setAttribute(A_ANCHOR, newKey);
|
|
562
|
-
best.setAttribute(A_CREATED, String(ts()));
|
|
563
|
-
best.setAttribute(A_SHOWN, '0');
|
|
564
|
-
best.classList.remove('is-empty');
|
|
565
|
-
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
566
|
-
if (ph) ph.innerHTML = '';
|
|
567
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
568
|
-
});
|
|
569
|
-
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
570
|
-
S.wrapByKey.set(newKey, best);
|
|
571
|
-
|
|
572
|
-
const doDestroy = () => {
|
|
573
|
-
if (S.ezShownSinceDestroy.has(id)) {
|
|
574
|
-
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
575
|
-
S.ezShownSinceDestroy.delete(id);
|
|
252
|
+
function pickId(poolKey) {
|
|
253
|
+
const pool = S.pools[poolKey] || [];
|
|
254
|
+
if (!pool.length) return null;
|
|
255
|
+
for (let t = 0; t < pool.length; t++) {
|
|
256
|
+
const idx = S.cursors[poolKey] % pool.length;
|
|
257
|
+
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
258
|
+
const id = pool[idx];
|
|
259
|
+
if (!S.mountedIds.has(id)) return id;
|
|
576
260
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
};
|
|
580
|
-
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
581
|
-
const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
|
|
582
|
-
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
583
|
-
|
|
584
|
-
return { id, wrap: best };
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
588
263
|
|
|
589
264
|
function makeWrap(id, klass, key) {
|
|
590
265
|
const w = document.createElement('div');
|
|
591
266
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
592
|
-
w.setAttribute(A_ANCHOR,
|
|
593
|
-
w.setAttribute(A_WRAPID,
|
|
594
|
-
w.setAttribute(A_CREATED, String(
|
|
595
|
-
w.setAttribute(A_SHOWN,
|
|
596
|
-
w.
|
|
597
|
-
|
|
598
|
-
|
|
267
|
+
w.setAttribute(A_ANCHOR, key);
|
|
268
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
269
|
+
w.setAttribute(A_CREATED, String(now()));
|
|
270
|
+
w.setAttribute(A_SHOWN, '0');
|
|
271
|
+
w.setAttribute(A_STATE, 'idle');
|
|
272
|
+
w.style.cssText = 'display:block;width:100%;';
|
|
273
|
+
const h = S.lastWrapHeightByClass.get(klass);
|
|
274
|
+
if (Number.isFinite(h) && h > 0) w.style.minHeight = `${h}px`;
|
|
599
275
|
const ph = document.createElement('div');
|
|
600
276
|
ph.id = `${PH_PREFIX}${id}`;
|
|
601
277
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -603,473 +279,396 @@ function recycleAndMove(klass, targetEl, newKey) {
|
|
|
603
279
|
return w;
|
|
604
280
|
}
|
|
605
281
|
|
|
606
|
-
function
|
|
607
|
-
if (!
|
|
608
|
-
if (
|
|
609
|
-
if (S.mountedIds.has(id))
|
|
610
|
-
if (
|
|
282
|
+
function insertWrapAfter(anchorEl, id, klass, key) {
|
|
283
|
+
if (!anchorEl?.insertAdjacentElement) return null;
|
|
284
|
+
if (findWrapByKey(key)) return null;
|
|
285
|
+
if (S.mountedIds.has(id)) return null;
|
|
286
|
+
if (phEl(id)?.isConnected) return null;
|
|
611
287
|
const w = makeWrap(id, klass, key);
|
|
612
|
-
mutate(() =>
|
|
288
|
+
mutate(() => anchorEl.insertAdjacentElement('afterend', w));
|
|
613
289
|
S.mountedIds.add(id);
|
|
614
290
|
S.wrapByKey.set(key, w);
|
|
291
|
+
const rec = ensureRegistry(id, w);
|
|
292
|
+
rec.key = key;
|
|
293
|
+
rec.createdAt = now();
|
|
294
|
+
rec.lastMutationAt = now();
|
|
295
|
+
rec.isSpecial = false;
|
|
296
|
+
rec.isFixedLike = false;
|
|
297
|
+
updateWrapState(rec, 'idle');
|
|
298
|
+
observePlaceholder(id);
|
|
615
299
|
return w;
|
|
616
300
|
}
|
|
617
301
|
|
|
618
|
-
function
|
|
302
|
+
function releaseWrap(wrap) {
|
|
619
303
|
try {
|
|
620
|
-
rememberWrapHeight(
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
304
|
+
rememberWrapHeight(wrap);
|
|
305
|
+
const id = parseInt(wrap.getAttribute(A_WRAPID), 10);
|
|
306
|
+
const key = wrap.getAttribute(A_ANCHOR) || '';
|
|
307
|
+
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
308
|
+
if (ph) try { S.io?.unobserve(ph); } catch (_) {}
|
|
309
|
+
if (key && S.wrapByKey.get(key) === wrap) S.wrapByKey.delete(key);
|
|
310
|
+
if (Number.isFinite(id)) {
|
|
311
|
+
S.mountedIds.delete(id);
|
|
312
|
+
S.pendingSet.delete(id);
|
|
313
|
+
S.lastShowById.delete(id);
|
|
314
|
+
const rec = S.regById.get(id);
|
|
315
|
+
if (rec) {
|
|
316
|
+
rec.wrap = null;
|
|
317
|
+
rec.state = 'retired';
|
|
318
|
+
rec.cooldownUntil = Math.max(rec.cooldownUntil || 0, now() + 3000);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
wrap.remove();
|
|
630
322
|
} catch (_) {}
|
|
631
323
|
}
|
|
632
324
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
647
|
-
|
|
648
|
-
function pruneOrphansBetween() {
|
|
649
|
-
const klass = 'ezoic-ad-between';
|
|
650
|
-
const cfg = KIND[klass];
|
|
325
|
+
function wrapHasAnchor(wrap) {
|
|
326
|
+
const key = wrap?.getAttribute?.(A_ANCHOR);
|
|
327
|
+
if (!key) return false;
|
|
328
|
+
const i = key.indexOf(':');
|
|
329
|
+
const klass = key.slice(0, i);
|
|
330
|
+
const sid = key.slice(i + 1);
|
|
331
|
+
const def = KIND[klass];
|
|
332
|
+
if (!def) return false;
|
|
333
|
+
try {
|
|
334
|
+
const sel = `${def.sel}[${def.anchorAttr}="${sid}"]`;
|
|
335
|
+
return !!document.querySelector(sel);
|
|
336
|
+
} catch (_) { return false; }
|
|
337
|
+
}
|
|
651
338
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (
|
|
655
|
-
|
|
339
|
+
function sweepDisconnected() {
|
|
340
|
+
for (const [key, wrap] of [...S.wrapByKey.entries()]) {
|
|
341
|
+
if (wrap?.isConnected) continue;
|
|
342
|
+
S.wrapByKey.delete(key);
|
|
343
|
+
const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
|
|
344
|
+
if (Number.isFinite(id)) {
|
|
345
|
+
S.mountedIds.delete(id);
|
|
346
|
+
S.pendingSet.delete(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
656
350
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
351
|
+
function classifyWrap(rec) {
|
|
352
|
+
const w = rec.wrap;
|
|
353
|
+
if (!w?.isConnected) return;
|
|
354
|
+
if (!rec.isSpecial) rec.isSpecial = isSpecialLike(w);
|
|
355
|
+
if (!rec.isFixedLike) rec.isFixedLike = hasFixedLike(w);
|
|
356
|
+
}
|
|
660
357
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
358
|
+
function maintenance() {
|
|
359
|
+
if (isBlocked()) return;
|
|
360
|
+
sweepDisconnected();
|
|
361
|
+
const t = now();
|
|
362
|
+
for (const [id, rec] of S.regById) {
|
|
363
|
+
const w = rec.wrap;
|
|
364
|
+
if (!w?.isConnected) continue;
|
|
365
|
+
classifyWrap(rec);
|
|
366
|
+
if (wrapNearViewport(w)) {
|
|
367
|
+
rec.lastSeenAt = t;
|
|
368
|
+
if (rec.state === 'retiring') updateWrapState(rec, 'live');
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10) || rec.createdAt || 0;
|
|
372
|
+
const shown = parseInt(w.getAttribute(A_SHOWN) || '0', 10) || rec.shownAt || 0;
|
|
373
|
+
const age = t - Math.max(created, shown || 0, rec.lastMutationAt || 0);
|
|
374
|
+
const activeRecently = (t - (rec.lastMutationAt || 0)) < CFG.recentActivityMs;
|
|
375
|
+
const filled = isFilled(w);
|
|
376
|
+
if (filled) { rec.lastMutationAt = t; rec.shownAt = Math.max(rec.shownAt || 0, shown || 0); }
|
|
377
|
+
if (rec.isSpecial || rec.isFixedLike) continue; // never retire plugin-side
|
|
378
|
+
if (activeRecently) continue;
|
|
379
|
+
if (filled) continue; // keep rendered slots stable; no aggressive destroy/recycle
|
|
380
|
+
if (age < CFG.retireGraceMs) continue;
|
|
381
|
+
if (rec.state !== 'retiring') {
|
|
382
|
+
updateWrapState(rec, 'retiring');
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
// second pass confirmation
|
|
386
|
+
if (wrapNearViewport(w) || isFilled(w)) { updateWrapState(rec, 'live'); continue; }
|
|
387
|
+
releaseWrap(w);
|
|
388
|
+
}
|
|
664
389
|
}
|
|
665
390
|
|
|
666
|
-
|
|
391
|
+
function scheduleMaintenance() {
|
|
392
|
+
if (S.maintenanceTimer) return;
|
|
393
|
+
S.maintenanceTimer = setTimeout(() => {
|
|
394
|
+
S.maintenanceTimer = 0;
|
|
395
|
+
maintenance();
|
|
396
|
+
}, CFG.maintenanceEveryMs);
|
|
397
|
+
}
|
|
667
398
|
|
|
668
|
-
/**
|
|
669
|
-
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
670
|
-
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
671
|
-
*/
|
|
672
399
|
function ordinal(klass, el) {
|
|
673
400
|
const attr = KIND[klass]?.ordinalAttr;
|
|
674
401
|
if (attr) {
|
|
675
402
|
const v = el.getAttribute(attr);
|
|
676
403
|
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
677
404
|
}
|
|
678
|
-
const fullSel = KIND[klass]?.sel ?? '';
|
|
679
405
|
let i = 0;
|
|
680
|
-
|
|
406
|
+
const fullSel = KIND[klass]?.sel || '';
|
|
407
|
+
for (const s of el.parentElement?.children || []) {
|
|
681
408
|
if (s === el) return i;
|
|
682
409
|
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
683
410
|
}
|
|
684
411
|
return 0;
|
|
685
412
|
}
|
|
686
413
|
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
414
|
+
function adjacentManagedWrap(el) {
|
|
415
|
+
const isManaged = n => !!(n?.classList?.contains(WRAP_CLASS) && n.isConnected);
|
|
416
|
+
return isManaged(el.nextElementSibling) || isManaged(el.previousElementSibling);
|
|
417
|
+
}
|
|
690
418
|
|
|
419
|
+
function injectFor(klass, items, interval, showFirst) {
|
|
420
|
+
let inserted = 0;
|
|
421
|
+
const poolKey = KIND[klass].pool;
|
|
691
422
|
for (const el of items) {
|
|
692
|
-
if (inserted >=
|
|
423
|
+
if (inserted >= CFG.maxInsertsPerRun) break;
|
|
693
424
|
if (!el?.isConnected) continue;
|
|
694
|
-
|
|
695
425
|
const ord = ordinal(klass, el);
|
|
696
426
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
697
|
-
if (
|
|
698
|
-
|
|
427
|
+
if (adjacentManagedWrap(el)) continue;
|
|
699
428
|
const key = anchorKey(klass, el);
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
if (id) {
|
|
705
|
-
const w = insertAfter(el, id, klass, key);
|
|
706
|
-
if (w) { observePh(id); inserted++; }
|
|
707
|
-
} else {
|
|
708
|
-
const recycled = recycleAndMove(klass, el, key);
|
|
709
|
-
if (!recycled) break;
|
|
710
|
-
inserted++;
|
|
711
|
-
}
|
|
429
|
+
if (findWrapByKey(key)) continue;
|
|
430
|
+
const id = pickId(poolKey);
|
|
431
|
+
if (!id) break; // no recycle = anti-churn by design
|
|
432
|
+
if (insertWrapAfter(el, id, klass, key)) inserted++;
|
|
712
433
|
}
|
|
713
434
|
return inserted;
|
|
714
435
|
}
|
|
715
436
|
|
|
716
|
-
|
|
437
|
+
function canShowId(id) {
|
|
438
|
+
const t = now();
|
|
439
|
+
const last = S.lastShowById.get(id) || 0;
|
|
440
|
+
if (t - last < CFG.showThrottleMs) return false;
|
|
441
|
+
const rec = S.regById.get(id);
|
|
442
|
+
if (rec && rec.cooldownUntil && t < rec.cooldownUntil) return false;
|
|
443
|
+
const ph = phEl(id);
|
|
444
|
+
if (!ph?.isConnected) return false;
|
|
445
|
+
const wrap = ph.closest?.(`.${WRAP_CLASS}`);
|
|
446
|
+
if (!wrap?.isConnected) return false;
|
|
447
|
+
if (!wrapNearViewport(wrap)) {
|
|
448
|
+
// allow preloading within IO margin only if reasonably close
|
|
449
|
+
try {
|
|
450
|
+
const r = ph.getBoundingClientRect();
|
|
451
|
+
const vh = window.innerHeight || 800;
|
|
452
|
+
const preload = isMobile() ? 1800 : 1400;
|
|
453
|
+
if (r.top > vh + preload || r.bottom < -preload) return false;
|
|
454
|
+
} catch (_) {}
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
717
458
|
|
|
718
|
-
function
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
} catch (_) { S.io = null; }
|
|
730
|
-
return S.io;
|
|
459
|
+
function enqueueShow(id) {
|
|
460
|
+
if (isBlocked()) return;
|
|
461
|
+
const n = parseInt(id, 10);
|
|
462
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
463
|
+
if (!S.pendingSet.has(n)) { S.pendingSet.add(n); S.pendingShow.push(n); }
|
|
464
|
+
if (!S.showTimer) {
|
|
465
|
+
S.showTimer = setTimeout(() => {
|
|
466
|
+
S.showTimer = 0;
|
|
467
|
+
drainShowQueue();
|
|
468
|
+
}, CFG.batchFlushMs);
|
|
469
|
+
}
|
|
731
470
|
}
|
|
732
471
|
|
|
733
|
-
function
|
|
472
|
+
function observePlaceholder(id) {
|
|
734
473
|
const ph = phEl(id);
|
|
735
|
-
if (ph?.isConnected)
|
|
736
|
-
|
|
474
|
+
if (!ph?.isConnected) return;
|
|
475
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
737
476
|
try {
|
|
738
|
-
|
|
739
|
-
const rect = ph.getBoundingClientRect();
|
|
477
|
+
const r = ph.getBoundingClientRect();
|
|
740
478
|
const vh = window.innerHeight || 800;
|
|
741
|
-
const preload = isMobile() ?
|
|
742
|
-
if (
|
|
479
|
+
const preload = isMobile() ? 1600 : 1200;
|
|
480
|
+
if (r.top <= vh + preload && r.bottom >= -preload) enqueueShow(id);
|
|
743
481
|
} catch (_) {}
|
|
744
482
|
}
|
|
745
483
|
|
|
746
|
-
function
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (S.showBatchTimer) return;
|
|
758
|
-
S.showBatchTimer = setTimeout(() => {
|
|
759
|
-
S.showBatchTimer = 0;
|
|
760
|
-
drainQueue();
|
|
761
|
-
}, BATCH_FLUSH_MS);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function drainQueue() {
|
|
765
|
-
if (isBlocked()) return;
|
|
766
|
-
const free = Math.max(0, MAX_INFLIGHT - S.inflight);
|
|
767
|
-
if (!free || !S.pending.length) return;
|
|
768
|
-
|
|
769
|
-
const picked = [];
|
|
770
|
-
const seen = new Set();
|
|
771
|
-
const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
|
|
772
|
-
while (S.pending.length && picked.length < batchCap) {
|
|
773
|
-
const id = S.pending.shift();
|
|
774
|
-
S.pendingSet.delete(id);
|
|
775
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
776
|
-
if (!phEl(id)?.isConnected) continue;
|
|
777
|
-
seen.add(id);
|
|
778
|
-
picked.push(id);
|
|
779
|
-
}
|
|
780
|
-
if (picked.length) startShowBatch(picked);
|
|
781
|
-
if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function startShowBatch(ids) {
|
|
785
|
-
if (!ids?.length || isBlocked()) return;
|
|
786
|
-
const reserve = ids.length;
|
|
787
|
-
S.inflight += reserve;
|
|
788
|
-
|
|
789
|
-
let done = false;
|
|
790
|
-
const release = () => {
|
|
791
|
-
if (done) return;
|
|
792
|
-
done = true;
|
|
793
|
-
S.inflight = Math.max(0, S.inflight - reserve);
|
|
794
|
-
drainQueue();
|
|
795
|
-
};
|
|
796
|
-
const timer = setTimeout(release, SHOW_FAILSAFE_MS);
|
|
797
|
-
|
|
798
|
-
requestAnimationFrame(() => {
|
|
799
|
-
try {
|
|
800
|
-
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
801
|
-
|
|
802
|
-
const valid = [];
|
|
803
|
-
const t = ts();
|
|
804
|
-
|
|
805
|
-
for (const raw of ids) {
|
|
806
|
-
const id = parseInt(raw, 10);
|
|
807
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
808
|
-
const ph = phEl(id);
|
|
809
|
-
if (!canShowPlaceholderId(id, t)) continue;
|
|
810
|
-
|
|
811
|
-
S.lastShow.set(id, t);
|
|
812
|
-
try { const wrap = ph.closest?.(WRAP_SEL); wrap?.setAttribute(A_SHOWN, String(t)); if (wrap) markWrapActivity(wrap); } catch (_) {}
|
|
813
|
-
valid.push(id);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (!valid.length) { clearTimeout(timer); return release(); }
|
|
817
|
-
|
|
818
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
819
|
-
const ez = window.ezstandalone;
|
|
820
|
-
const doShow = () => {
|
|
821
|
-
const prepared = destroyBeforeReuse(valid);
|
|
822
|
-
if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
|
|
823
|
-
try { ez.showAds(...prepared); } catch (_) {}
|
|
824
|
-
for (const id of prepared) {
|
|
825
|
-
S.ezActiveIds.add(id);
|
|
826
|
-
S.ezShownSinceDestroy.add(id);
|
|
827
|
-
}
|
|
828
|
-
setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
|
|
829
|
-
};
|
|
830
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
831
|
-
} catch (_) { clearTimeout(timer); release(); }
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
836
|
-
//
|
|
837
|
-
// Intercepte ez.showAds() pour :
|
|
838
|
-
// – ignorer les appels pendant blockedUntil
|
|
839
|
-
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
484
|
+
function startShow(ids) {
|
|
485
|
+
if (!ids.length || isBlocked()) return;
|
|
486
|
+
S.inflightShow += ids.length;
|
|
487
|
+
let released = false;
|
|
488
|
+
const release = () => {
|
|
489
|
+
if (released) return;
|
|
490
|
+
released = true;
|
|
491
|
+
S.inflightShow = Math.max(0, S.inflightShow - ids.length);
|
|
492
|
+
drainShowQueue();
|
|
493
|
+
};
|
|
494
|
+
const guard = setTimeout(release, CFG.showFailsafeMs);
|
|
840
495
|
|
|
841
|
-
|
|
842
|
-
const apply = () => {
|
|
496
|
+
requestAnimationFrame(() => {
|
|
843
497
|
try {
|
|
498
|
+
if (isBlocked()) { clearTimeout(guard); return release(); }
|
|
844
499
|
window.ezstandalone = window.ezstandalone || {};
|
|
845
500
|
const ez = window.ezstandalone;
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
501
|
+
const valid = [];
|
|
502
|
+
const t = now();
|
|
503
|
+
for (const id of ids) {
|
|
504
|
+
if (!canShowId(id)) continue;
|
|
505
|
+
const ph = phEl(id);
|
|
506
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
507
|
+
if (!wrap) continue;
|
|
508
|
+
wrap.setAttribute(A_SHOWN, String(t));
|
|
509
|
+
const rec = ensureRegistry(id, wrap);
|
|
510
|
+
rec.wrap = wrap;
|
|
511
|
+
rec.shownAt = t;
|
|
512
|
+
rec.lastMutationAt = t;
|
|
513
|
+
rec.cooldownUntil = t + CFG.showCooldownMs;
|
|
514
|
+
classifyWrap(rec);
|
|
515
|
+
updateWrapState(rec, 'showing');
|
|
516
|
+
S.lastShowById.set(id, t);
|
|
517
|
+
valid.push(id);
|
|
518
|
+
}
|
|
519
|
+
if (!valid.length) { clearTimeout(guard); return release(); }
|
|
520
|
+
const run = () => {
|
|
521
|
+
try { ez.showAds(...valid); } catch (_) {
|
|
522
|
+
try { ez.showAds(valid); } catch (_) {}
|
|
860
523
|
}
|
|
861
|
-
|
|
862
|
-
try { orig(...valid); } catch (_) {
|
|
524
|
+
setTimeout(() => {
|
|
863
525
|
for (const id of valid) {
|
|
864
|
-
|
|
526
|
+
const rec = S.regById.get(id);
|
|
527
|
+
if (!rec) continue;
|
|
528
|
+
const w = rec.wrap;
|
|
529
|
+
if (w?.isConnected) {
|
|
530
|
+
if (isFilled(w)) rec.lastMutationAt = now();
|
|
531
|
+
updateWrapState(rec, 'live');
|
|
532
|
+
}
|
|
865
533
|
}
|
|
866
|
-
|
|
534
|
+
clearTimeout(guard);
|
|
535
|
+
release();
|
|
536
|
+
}, CFG.showReleaseMs);
|
|
867
537
|
};
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// ── Core ───────────────────────────────────────────────────────────────────
|
|
878
|
-
|
|
879
|
-
async function runCore() {
|
|
880
|
-
if (isBlocked()) return 0;
|
|
881
|
-
patchShowAds();
|
|
882
|
-
sweepDeadWraps();
|
|
883
|
-
|
|
884
|
-
const cfg = await fetchConfig();
|
|
885
|
-
if (!cfg || cfg.excluded) return 0;
|
|
886
|
-
initPools(cfg);
|
|
887
|
-
|
|
888
|
-
const kind = getKind();
|
|
889
|
-
if (kind === 'other') return 0;
|
|
890
|
-
|
|
891
|
-
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
892
|
-
if (!normBool(cfgEnable)) return 0;
|
|
893
|
-
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
894
|
-
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
if (kind === 'topic') return exec(
|
|
898
|
-
'ezoic-ad-message', getPosts,
|
|
899
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
900
|
-
);
|
|
901
|
-
|
|
902
|
-
if (kind === 'categoryTopics') {
|
|
903
|
-
pruneOrphansBetween();
|
|
904
|
-
return exec(
|
|
905
|
-
'ezoic-ad-between', getTopics,
|
|
906
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
return exec(
|
|
911
|
-
'ezoic-ad-categories', getCategories,
|
|
912
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
917
|
-
|
|
918
|
-
function scheduleRun(cb) {
|
|
919
|
-
if (S.runQueued) return;
|
|
920
|
-
S.runQueued = true;
|
|
921
|
-
requestAnimationFrame(async () => {
|
|
922
|
-
S.runQueued = false;
|
|
923
|
-
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
924
|
-
let n = 0;
|
|
925
|
-
try { n = await runCore(); } catch (_) {}
|
|
926
|
-
try { cb?.(n); } catch (_) {}
|
|
538
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(run) : run();
|
|
539
|
+
} catch (_) {
|
|
540
|
+
clearTimeout(guard);
|
|
541
|
+
release();
|
|
542
|
+
}
|
|
927
543
|
});
|
|
928
544
|
}
|
|
929
545
|
|
|
930
|
-
function
|
|
546
|
+
function drainShowQueue() {
|
|
931
547
|
if (isBlocked()) return;
|
|
932
|
-
|
|
933
|
-
if (
|
|
934
|
-
S.
|
|
935
|
-
|
|
936
|
-
S.
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
548
|
+
if (S.inflightShow >= CFG.maxShowInflight) return;
|
|
549
|
+
if (!S.pendingShow.length) return;
|
|
550
|
+
const cap = Math.min(CFG.maxShowBatch, CFG.maxShowInflight - S.inflightShow);
|
|
551
|
+
const picked = [];
|
|
552
|
+
while (S.pendingShow.length && picked.length < cap) {
|
|
553
|
+
const id = S.pendingShow.shift();
|
|
554
|
+
S.pendingSet.delete(id);
|
|
555
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
556
|
+
if (!phEl(id)?.isConnected) continue;
|
|
557
|
+
picked.push(id);
|
|
558
|
+
}
|
|
559
|
+
if (picked.length) startShow(picked);
|
|
560
|
+
if (S.pendingShow.length) scheduleMaintenance();
|
|
561
|
+
}
|
|
941
562
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
563
|
+
function getIO() {
|
|
564
|
+
if (S.io) return S.io;
|
|
565
|
+
try {
|
|
566
|
+
S.io = new IntersectionObserver(entries => {
|
|
567
|
+
for (const e of entries) {
|
|
568
|
+
const t = e.target;
|
|
569
|
+
if (!(t instanceof Element)) continue;
|
|
570
|
+
const id = parseInt(t.getAttribute('data-ezoic-id'), 10);
|
|
571
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
572
|
+
const rec = S.regById.get(id);
|
|
573
|
+
if (rec) rec.lastSeenAt = now();
|
|
574
|
+
if (e.isIntersecting) enqueueShow(id);
|
|
575
|
+
}
|
|
576
|
+
}, {
|
|
577
|
+
root: null,
|
|
578
|
+
rootMargin: isMobile() ? CFG.ioMarginMobile : CFG.ioMarginDesktop,
|
|
579
|
+
threshold: 0,
|
|
950
580
|
});
|
|
951
|
-
};
|
|
952
|
-
|
|
581
|
+
} catch (_) { S.io = null; }
|
|
582
|
+
return S.io;
|
|
953
583
|
}
|
|
954
584
|
|
|
955
|
-
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
956
|
-
|
|
957
|
-
function cleanup() {
|
|
958
|
-
blockedUntil = ts() + 1500;
|
|
959
|
-
mutate(() => document.querySelectorAll(WRAP_SEL).forEach(dropWrap));
|
|
960
|
-
S.cfg = null;
|
|
961
|
-
S.poolsReady = false;
|
|
962
|
-
S.pools = { topics: [], posts: [], categories: [] };
|
|
963
|
-
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
964
|
-
S.mountedIds.clear();
|
|
965
|
-
S.lastShow.clear();
|
|
966
|
-
S.wrapByKey.clear();
|
|
967
|
-
S.ezActiveIds.clear();
|
|
968
|
-
S.ezShownSinceDestroy.clear();
|
|
969
|
-
S.wrapActivityAt.clear();
|
|
970
|
-
S.inflight = 0;
|
|
971
|
-
S.pending = [];
|
|
972
|
-
S.pendingSet.clear();
|
|
973
|
-
if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
|
|
974
|
-
if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
|
|
975
|
-
S.destroyPending = [];
|
|
976
|
-
S.destroyPendingSet.clear();
|
|
977
|
-
S.burstActive = false;
|
|
978
|
-
S.runQueued = false;
|
|
979
|
-
S.sweepQueued = false;
|
|
980
|
-
S.scrollSpeed = 0;
|
|
981
|
-
S.lastScrollY = 0;
|
|
982
|
-
S.lastScrollTs = 0;
|
|
983
|
-
}
|
|
984
585
|
|
|
985
|
-
function nodeMatchesAny(node, selectors) {
|
|
986
|
-
if (!(node instanceof Element)) return false;
|
|
987
|
-
for (const sel of selectors) {
|
|
988
|
-
try { if (node.matches(sel)) return true; } catch (_) {}
|
|
989
|
-
}
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
586
|
|
|
993
|
-
function
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
587
|
+
function healFalseEmpty(root) {
|
|
588
|
+
try {
|
|
589
|
+
const list = [];
|
|
590
|
+
if (root instanceof Element && root.classList.contains(WRAP_CLASS)) list.push(root);
|
|
591
|
+
if (root?.querySelectorAll) {
|
|
592
|
+
for (const w of root.querySelectorAll(`.${WRAP_CLASS}.is-empty`)) list.push(w);
|
|
593
|
+
}
|
|
594
|
+
for (const w of list) {
|
|
595
|
+
if (w.classList.contains('is-empty') && isFilled(w)) w.classList.remove('is-empty');
|
|
596
|
+
}
|
|
597
|
+
} catch (_) {}
|
|
999
598
|
}
|
|
1000
599
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
function ensureDomObserver() {
|
|
600
|
+
function ensureObserver() {
|
|
1004
601
|
if (S.domObs) return;
|
|
1005
602
|
S.domObs = new MutationObserver(muts => {
|
|
1006
|
-
if (S.
|
|
603
|
+
if (S.muting > 0 || isBlocked()) return;
|
|
604
|
+
let shouldRun = false;
|
|
1007
605
|
for (const m of muts) {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
606
|
+
for (const n of m.addedNodes) {
|
|
607
|
+
if (!(n instanceof Element)) continue;
|
|
608
|
+
healFalseEmpty(n);
|
|
609
|
+
const wrap = n.classList?.contains(WRAP_CLASS) ? n : n.querySelector?.(`.${WRAP_CLASS}`);
|
|
610
|
+
if (wrap) {
|
|
611
|
+
const id = parseInt(wrap.getAttribute(A_WRAPID), 10);
|
|
612
|
+
if (Number.isFinite(id)) {
|
|
613
|
+
const rec = ensureRegistry(id, wrap);
|
|
614
|
+
rec.lastMutationAt = now();
|
|
615
|
+
classifyWrap(rec);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (n.matches?.(SEL.post) || n.matches?.(SEL.topic) || n.matches?.(SEL.category) ||
|
|
619
|
+
n.querySelector?.(SEL.post) || n.querySelector?.(SEL.topic) || n.querySelector?.(SEL.category)) {
|
|
620
|
+
shouldRun = true;
|
|
1017
621
|
}
|
|
1018
622
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (n.
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
const w = (n instanceof Element && (n.classList?.contains(WRAP_CLASS) ? n : n.closest?.(WRAP_SEL))) || null;
|
|
1025
|
-
if (w) markWrapActivity(w);
|
|
1026
|
-
else if (n instanceof Element) { const inner = n.querySelector?.(WRAP_SEL); if (inner) markWrapActivity(inner); }
|
|
1027
|
-
} catch (_) {}
|
|
1028
|
-
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
1029
|
-
if (nodeMatchesAny(n, CONTENT_SEL_LIST) || nodeContainsAny(n, CONTENT_SEL_LIST)) {
|
|
1030
|
-
requestBurst(); return;
|
|
623
|
+
for (const n of m.removedNodes) {
|
|
624
|
+
if (!(n instanceof Element)) continue;
|
|
625
|
+
if (n.classList?.contains(WRAP_CLASS) || n.querySelector?.(`.${WRAP_CLASS}`)) {
|
|
626
|
+
shouldRun = true;
|
|
1031
627
|
}
|
|
1032
628
|
}
|
|
1033
629
|
}
|
|
630
|
+
if (shouldRun) {
|
|
631
|
+
scheduleRun();
|
|
632
|
+
scheduleMaintenance();
|
|
633
|
+
}
|
|
1034
634
|
});
|
|
1035
635
|
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
1036
636
|
}
|
|
1037
637
|
|
|
1038
|
-
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
1039
|
-
|
|
1040
638
|
function ensureTcfLocator() {
|
|
1041
639
|
try {
|
|
1042
640
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1043
641
|
const inject = () => {
|
|
1044
642
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
1045
643
|
const f = document.createElement('iframe');
|
|
1046
|
-
f.style.display = 'none';
|
|
644
|
+
f.style.display = 'none';
|
|
645
|
+
f.id = f.name = '__tcfapiLocator';
|
|
1047
646
|
(document.body || document.documentElement).appendChild(f);
|
|
1048
647
|
};
|
|
1049
648
|
inject();
|
|
1050
|
-
if (!window.
|
|
1051
|
-
window.
|
|
1052
|
-
window.
|
|
649
|
+
if (!window.__nbbTcfObsV22) {
|
|
650
|
+
window.__nbbTcfObsV22 = new MutationObserver(inject);
|
|
651
|
+
window.__nbbTcfObsV22.observe(document.documentElement, { childList: true, subtree: true });
|
|
1053
652
|
}
|
|
1054
653
|
} catch (_) {}
|
|
1055
654
|
}
|
|
1056
655
|
|
|
1057
|
-
const
|
|
656
|
+
const warmed = new Set();
|
|
1058
657
|
function warmNetwork() {
|
|
1059
658
|
const head = document.head;
|
|
1060
659
|
if (!head) return;
|
|
1061
660
|
const frag = document.createDocumentFragment();
|
|
1062
|
-
|
|
1063
|
-
['preconnect',
|
|
1064
|
-
['preconnect',
|
|
1065
|
-
['preconnect',
|
|
1066
|
-
['
|
|
1067
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
661
|
+
const defs = [
|
|
662
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
663
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
664
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
665
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
1068
666
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
1069
|
-
]
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
667
|
+
];
|
|
668
|
+
for (const [rel, href, cors] of defs) {
|
|
669
|
+
const key = `${rel}|${href}`;
|
|
670
|
+
if (warmed.has(key)) continue;
|
|
671
|
+
warmed.add(key);
|
|
1073
672
|
const l = document.createElement('link');
|
|
1074
673
|
l.rel = rel; l.href = href;
|
|
1075
674
|
if (cors) l.crossOrigin = 'anonymous';
|
|
@@ -1078,33 +677,91 @@ function startShowBatch(ids) {
|
|
|
1078
677
|
head.appendChild(frag);
|
|
1079
678
|
}
|
|
1080
679
|
|
|
1081
|
-
|
|
680
|
+
async function runCore() {
|
|
681
|
+
if (isBlocked()) return 0;
|
|
682
|
+
const cfg = await fetchConfig();
|
|
683
|
+
if (!cfg || cfg.excluded) return 0;
|
|
684
|
+
initPools(cfg);
|
|
685
|
+
|
|
686
|
+
const kind = getKind();
|
|
687
|
+
if (kind === 'other') return 0;
|
|
688
|
+
|
|
689
|
+
if (kind === 'topic') {
|
|
690
|
+
if (!normBool(cfg.enableMessageAds)) return 0;
|
|
691
|
+
const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
692
|
+
return injectFor('ezoic-ad-message', getPosts(), interval, normBool(cfg.showFirstMessageAd));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (kind === 'categoryTopics') {
|
|
696
|
+
if (!normBool(cfg.enableBetweenAds)) return 0;
|
|
697
|
+
const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 3);
|
|
698
|
+
return injectFor('ezoic-ad-between', getTopics(), interval, normBool(cfg.showFirstTopicAd));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!normBool(cfg.enableCategoryAds)) return 0;
|
|
702
|
+
const interval = Math.max(1, parseInt(cfg.intervalCategories, 10) || 3);
|
|
703
|
+
return injectFor('ezoic-ad-categories', getCategories(), interval, normBool(cfg.showFirstCategoryAd));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function scheduleRun() {
|
|
707
|
+
if (isBlocked()) return;
|
|
708
|
+
if (S.runTimer) return;
|
|
709
|
+
S.runTimer = setTimeout(async () => {
|
|
710
|
+
S.runTimer = 0;
|
|
711
|
+
const pk = pageKey();
|
|
712
|
+
if (S.pageKey && S.pageKey !== pk) return;
|
|
713
|
+
try { await runCore(); } catch (_) {}
|
|
714
|
+
scheduleMaintenance();
|
|
715
|
+
}, CFG.runDebounceMs);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function cleanup() {
|
|
719
|
+
S.blockedUntil = now() + 1500;
|
|
720
|
+
if (S.runTimer) { clearTimeout(S.runTimer); S.runTimer = 0; }
|
|
721
|
+
if (S.showTimer) { clearTimeout(S.showTimer); S.showTimer = 0; }
|
|
722
|
+
if (S.maintenanceTimer) { clearTimeout(S.maintenanceTimer); S.maintenanceTimer = 0; }
|
|
723
|
+
mutate(() => {
|
|
724
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) releaseWrap(w);
|
|
725
|
+
});
|
|
726
|
+
S.cfg = null;
|
|
727
|
+
S.poolsReady = false;
|
|
728
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
729
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
730
|
+
S.mountedIds.clear();
|
|
731
|
+
S.wrapByKey.clear();
|
|
732
|
+
S.regById.clear();
|
|
733
|
+
S.pendingShow = [];
|
|
734
|
+
S.pendingSet.clear();
|
|
735
|
+
S.lastShowById.clear();
|
|
736
|
+
S.inflightShow = 0;
|
|
737
|
+
}
|
|
1082
738
|
|
|
1083
739
|
function bindNodeBB() {
|
|
1084
740
|
const $ = window.jQuery;
|
|
1085
741
|
if (!$) return;
|
|
1086
|
-
|
|
1087
|
-
$(window).
|
|
1088
|
-
$(window).on('action:ajaxify.
|
|
1089
|
-
|
|
1090
|
-
S.
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
742
|
+
$(window).off('.nbbEzoicV22');
|
|
743
|
+
$(window).on('action:ajaxify.start.nbbEzoicV22', cleanup);
|
|
744
|
+
$(window).on('action:ajaxify.end.nbbEzoicV22', () => {
|
|
745
|
+
S.pageKey = pageKey();
|
|
746
|
+
S.blockedUntil = 0;
|
|
747
|
+
ensureTcfLocator();
|
|
748
|
+
warmNetwork();
|
|
749
|
+
getIO();
|
|
750
|
+
ensureObserver();
|
|
751
|
+
scheduleRun();
|
|
1094
752
|
});
|
|
1095
|
-
|
|
1096
|
-
const burstEvts = [
|
|
753
|
+
const events = [
|
|
1097
754
|
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
1098
755
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
1099
|
-
]
|
|
1100
|
-
|
|
1101
|
-
|
|
756
|
+
];
|
|
757
|
+
for (const ev of events) {
|
|
758
|
+
$(window).on(`${ev}.nbbEzoicV22`, () => { if (!isBlocked()) scheduleRun(); });
|
|
759
|
+
}
|
|
1102
760
|
try {
|
|
1103
761
|
require(['hooks'], hooks => {
|
|
1104
762
|
if (typeof hooks?.on !== 'function') return;
|
|
1105
|
-
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
1106
|
-
|
|
1107
|
-
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
763
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:topic.loaded']) {
|
|
764
|
+
try { hooks.on(ev, () => { if (!isBlocked()) scheduleRun(); }); } catch (_) {}
|
|
1108
765
|
}
|
|
1109
766
|
});
|
|
1110
767
|
} catch (_) {}
|
|
@@ -1112,39 +769,26 @@ function startShowBatch(ids) {
|
|
|
1112
769
|
|
|
1113
770
|
function bindScroll() {
|
|
1114
771
|
let ticking = false;
|
|
1115
|
-
try {
|
|
1116
|
-
S.lastScrollY = window.scrollY || window.pageYOffset || 0;
|
|
1117
|
-
S.lastScrollTs = ts();
|
|
1118
|
-
} catch (_) {}
|
|
1119
772
|
window.addEventListener('scroll', () => {
|
|
1120
|
-
try {
|
|
1121
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
1122
|
-
const t = ts();
|
|
1123
|
-
const dy = y - (S.lastScrollY || 0);
|
|
1124
|
-
const dt = Math.max(1, t - (S.lastScrollTs || t));
|
|
1125
|
-
if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
|
|
1126
|
-
const inst = Math.abs(dy) * 1000 / dt;
|
|
1127
|
-
S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
|
|
1128
|
-
S.lastScrollY = y;
|
|
1129
|
-
S.lastScrollTs = t;
|
|
1130
|
-
} catch (_) {}
|
|
1131
773
|
if (ticking) return;
|
|
1132
774
|
ticking = true;
|
|
1133
|
-
requestAnimationFrame(() => {
|
|
775
|
+
requestAnimationFrame(() => {
|
|
776
|
+
ticking = false;
|
|
777
|
+
if (!isBlocked()) {
|
|
778
|
+
scheduleMaintenance();
|
|
779
|
+
scheduleRun();
|
|
780
|
+
}
|
|
781
|
+
});
|
|
1134
782
|
}, { passive: true });
|
|
1135
783
|
}
|
|
1136
784
|
|
|
1137
|
-
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
1138
|
-
|
|
1139
785
|
S.pageKey = pageKey();
|
|
1140
786
|
ensureTcfLocator();
|
|
1141
787
|
warmNetwork();
|
|
1142
|
-
patchShowAds();
|
|
1143
788
|
getIO();
|
|
1144
|
-
|
|
789
|
+
ensureObserver();
|
|
1145
790
|
bindNodeBB();
|
|
1146
791
|
bindScroll();
|
|
1147
|
-
blockedUntil = 0;
|
|
1148
|
-
|
|
1149
|
-
|
|
792
|
+
S.blockedUntil = 0;
|
|
793
|
+
scheduleRun();
|
|
1150
794
|
})();
|