nodebb-plugin-ezoic-infinite 1.4.68 → 1.4.70
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 +422 -786
- package/public/style.css +36 -5
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,828 +1,464 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
|
-
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
4
|
-
const SELECTORS = {
|
|
5
|
-
topicItem: 'li[component="category/topic"]',
|
|
6
|
-
postItem: '[component="post"][data-pid]',
|
|
7
|
-
categoryItem: 'li[component="categories/category"]',
|
|
8
|
-
}, WRAP_CLASS = 'ezoic-ad';
|
|
9
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
|
|
10
|
-
|
|
11
|
-
const sessionDefinedIds = new Set();
|
|
12
|
-
|
|
13
|
-
const insertingIds = new Set(), state = {
|
|
14
|
-
pageKey: null,
|
|
15
|
-
cfg: null,
|
|
16
|
-
cfgPromise: null,
|
|
17
|
-
|
|
18
|
-
poolTopics: [],
|
|
19
|
-
poolPosts: [],
|
|
20
|
-
poolCategories: [],
|
|
21
|
-
|
|
22
|
-
usedTopics: new Set(),
|
|
23
|
-
usedPosts: new Set(),
|
|
24
|
-
usedCategories: new Set(),
|
|
25
|
-
|
|
26
|
-
lastShowById: new Map(),
|
|
27
|
-
pendingById: new Set(),
|
|
28
|
-
definedIds: new Set(),
|
|
29
3
|
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
/**
|
|
5
|
+
* NodeBB + Ezoic infinite ad injector
|
|
6
|
+
* Production-ready refactor:
|
|
7
|
+
* - No blank space: wrappers hidden until filled, removed on timeout
|
|
8
|
+
* - Bounded work per tick + throttled MutationObserver
|
|
9
|
+
* - Clean ajaxify navigation handling
|
|
10
|
+
*/
|
|
32
11
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function uniqInts(lines) {
|
|
42
|
-
const out = [], seen = new Set();
|
|
43
|
-
for (const v of lines) {
|
|
44
|
-
const n = parseInt(v, 10);
|
|
45
|
-
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
46
|
-
seen.add(n);
|
|
47
|
-
out.push(n);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parsePool(raw) {
|
|
54
|
-
if (!raw) return [];
|
|
55
|
-
const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
56
|
-
return uniqInts(lines);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getPageKey() {
|
|
60
|
-
try {
|
|
61
|
-
const ax = window.ajaxify;
|
|
62
|
-
if (ax && ax.data) {
|
|
63
|
-
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
64
|
-
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {}
|
|
67
|
-
return window.location.pathname;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function getKind() {
|
|
71
|
-
const p = window.location.pathname || '';
|
|
72
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
73
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
74
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
75
|
-
// fallback by DOM
|
|
76
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
77
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
78
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
79
|
-
return 'other';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getTopicItems() {
|
|
83
|
-
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getCategoryItems() {
|
|
87
|
-
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPostContainers() {
|
|
91
|
-
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
92
|
-
return nodes.filter((el) => {
|
|
93
|
-
if (!el || !el.isConnected) return false;
|
|
94
|
-
if (!el.querySelector('[component="post/content"]')) return false;
|
|
95
|
-
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
96
|
-
if (parentPost && parentPost !== el) return false;
|
|
97
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
98
|
-
return true;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function safeRect(el) {
|
|
103
|
-
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function destroyPlaceholderIds(ids) {
|
|
107
|
-
if (!ids || !ids.length) return;
|
|
108
|
-
const filtered = ids.filter((id) => {
|
|
109
|
-
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
110
|
-
});
|
|
111
|
-
if (!filtered.length) return;
|
|
112
|
-
|
|
113
|
-
const call = () => {
|
|
114
|
-
try {
|
|
115
|
-
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
116
|
-
window.ezstandalone.destroyPlaceholders(filtered);
|
|
117
|
-
}
|
|
118
|
-
} catch (e) {}
|
|
119
|
-
|
|
120
|
-
// Recyclage: libérer IDs après 100ms
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
123
|
-
}, 100);
|
|
12
|
+
const SELECTORS = {
|
|
13
|
+
topicItem: 'li[component="category/topic"]',
|
|
14
|
+
categoryItem: 'li[component="categories/category"]',
|
|
15
|
+
postItem: '[component="post"][data-pid]',
|
|
16
|
+
postContent: '[component="post/content"]',
|
|
124
17
|
};
|
|
125
|
-
try {
|
|
126
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
127
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
128
|
-
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
129
|
-
else window.ezstandalone.cmd.push(call);
|
|
130
|
-
} catch (e) {}
|
|
131
|
-
|
|
132
|
-
// Recyclage: libérer IDs après 100ms
|
|
133
|
-
setTimeout(() => {
|
|
134
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
135
|
-
}, 100);
|
|
136
|
-
}
|
|
137
18
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// 2. Ou opacity: 0
|
|
162
|
-
// 3. Ou visibility: hidden
|
|
163
|
-
const hasContent = child.textContent.trim().length > 0 ||
|
|
164
|
-
child.querySelector('img, iframe, video');
|
|
165
|
-
|
|
166
|
-
if (!hasContent || computed.opacity === '0' || computed.visibility === 'hidden') {
|
|
167
|
-
child.remove();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
} catch (e) {}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function cleanupEmptyWrappers() {
|
|
176
|
-
try {
|
|
177
|
-
document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
|
|
178
|
-
const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
|
|
179
|
-
if (ph && ph.children.length === 0) {
|
|
180
|
-
// Placeholder vide après 3s = pub non chargée
|
|
181
|
-
setTimeout(() => {
|
|
182
|
-
if (ph.children.length === 0) {
|
|
183
|
-
wrapper.remove();
|
|
184
|
-
}
|
|
185
|
-
}, 1500);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
} catch (e) {}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function getRecyclable(liveArr) {
|
|
192
|
-
const margin = 600;
|
|
193
|
-
for (let i = 0; i < liveArr.length; i++) {
|
|
194
|
-
const entry = liveArr[i];
|
|
195
|
-
if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
|
|
196
|
-
const r = safeRect(entry.wrap);
|
|
197
|
-
if (r && r.bottom < -margin) {
|
|
198
|
-
liveArr.splice(i, 1);
|
|
199
|
-
return entry;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function pickId(pool, liveArr) {
|
|
206
|
-
if (pool.length) return { id: pool.shift(), recycled: null };
|
|
207
|
-
const recycled = getRecyclable(liveArr);
|
|
208
|
-
if (recycled) return { id: recycled.id, recycled };
|
|
209
|
-
return { id: null, recycled: null };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function resetPlaceholderInWrap(wrap, id) {
|
|
213
|
-
try {
|
|
214
|
-
if (!wrap) return;
|
|
215
|
-
try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
|
|
216
|
-
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
217
|
-
const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
|
|
218
|
-
if (old) old.remove();
|
|
219
|
-
// Remove any leftover markup inside wrapper
|
|
220
|
-
wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
|
|
221
|
-
const ph = document.createElement('div');
|
|
222
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
223
|
-
wrap.appendChild(ph);
|
|
224
|
-
} catch (e) {}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function isAdjacentAd(target) {
|
|
228
|
-
if (!target || !target.nextElementSibling) return false;
|
|
229
|
-
const next = target.nextElementSibling;
|
|
230
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function isPrevAd(target) {
|
|
235
|
-
if (!target || !target.previousElementSibling) return false;
|
|
236
|
-
const prev = target.previousElementSibling;
|
|
237
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function buildWrap(id, kindClass, afterPos) {
|
|
242
|
-
const wrap = document.createElement('div');
|
|
243
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
244
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
245
|
-
wrap.style.width = '100%';
|
|
246
|
-
|
|
247
|
-
const ph = document.createElement('div');
|
|
248
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
249
|
-
wrap.appendChild(ph);
|
|
250
|
-
return wrap;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function findWrap(kindClass, afterPos) {
|
|
254
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function insertAfter(target, id, kindClass, afterPos) {
|
|
258
|
-
if (!target || !target.insertAdjacentElement) return null;
|
|
259
|
-
if (findWrap(kindClass, afterPos)) return null;
|
|
260
|
-
|
|
261
|
-
// CRITICAL: Double-lock pour éviter race conditions sur les doublons
|
|
262
|
-
if (insertingIds.has(id)) return null;
|
|
263
|
-
|
|
264
|
-
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
265
|
-
if (existingPh && existingPh.isConnected) return null;
|
|
266
|
-
|
|
267
|
-
// Acquérir le lock
|
|
268
|
-
insertingIds.add(id);
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const wrap = buildWrap(id, kindClass, afterPos);
|
|
272
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
273
|
-
attachFillObserver(wrap, id);
|
|
274
|
-
return wrap;
|
|
275
|
-
} finally {
|
|
276
|
-
setTimeout(() => insertingIds.delete(id), 50);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function destroyUsedPlaceholders() {
|
|
281
|
-
const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
|
|
282
|
-
if (ids.length) destroyPlaceholderIds(ids);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function patchShowAds() {
|
|
286
|
-
const applyPatch = () => {
|
|
287
|
-
try {
|
|
288
|
-
window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
|
|
289
|
-
if (window.__nodebbEzoicPatched) return;
|
|
290
|
-
if (typeof ez.showAds !== 'function') return;
|
|
291
|
-
|
|
292
|
-
window.__nodebbEzoicPatched = true;
|
|
293
|
-
const orig = ez.showAds;
|
|
294
|
-
|
|
295
|
-
ez.showAds = function (arg) {
|
|
296
|
-
if (Array.isArray(arg)) {
|
|
297
|
-
const seen = new Set();
|
|
298
|
-
for (const v of arg) {
|
|
299
|
-
const id = parseInt(v, 10);
|
|
300
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
301
|
-
seen.add(id);
|
|
302
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
return orig.apply(ez, arguments);
|
|
307
|
-
};
|
|
308
|
-
} catch (e) {}
|
|
19
|
+
const CLASS_WRAP = 'ezoic-ad';
|
|
20
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
|
+
|
|
22
|
+
const MAX_INSERTS_PER_TICK = 3;
|
|
23
|
+
// Ezoic can be late to render on ajaxified pages; keep wrappers around a bit longer
|
|
24
|
+
// (they are hidden until filled anyway, so no visual blank space).
|
|
25
|
+
const FILL_TIMEOUT_MS = 8000;
|
|
26
|
+
const RECYCLE_MARGIN_PX = 600;
|
|
27
|
+
const SHOW_DEBOUNCE_MS = 500;
|
|
28
|
+
|
|
29
|
+
const state = {
|
|
30
|
+
pageKey: null,
|
|
31
|
+
cfg: null,
|
|
32
|
+
cfgPromise: null,
|
|
33
|
+
|
|
34
|
+
pools: { topics: [], categories: [], messages: [] },
|
|
35
|
+
live: { topics: [], categories: [], messages: [] },
|
|
36
|
+
|
|
37
|
+
scheduled: false,
|
|
38
|
+
lastRunAt: 0,
|
|
39
|
+
timeouts: new Set(),
|
|
40
|
+
observer: null,
|
|
41
|
+
lastShowById: new Map(),
|
|
309
42
|
};
|
|
310
43
|
|
|
311
|
-
|
|
312
|
-
if (!window.__nodebbEzoicPatched) {
|
|
313
|
-
try {
|
|
314
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
315
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
316
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
317
|
-
} catch (e) {}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function markFilled(wrap) {
|
|
322
|
-
try {
|
|
323
|
-
if (!wrap) return;
|
|
324
|
-
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
325
|
-
wrap.setAttribute('data-ezoic-filled', '1');
|
|
326
|
-
} catch (e) {}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function isWrapMarkedFilled(wrap) {
|
|
330
|
-
try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function attachFillObserver(wrap, id) {
|
|
334
|
-
try {
|
|
335
|
-
const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
|
|
336
|
-
if (!ph) return;
|
|
337
|
-
// Already filled?
|
|
338
|
-
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
339
|
-
markFilled(wrap); // Afficher wrapper
|
|
340
|
-
sessionDefinedIds.add(id);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const obs = new MutationObserver(() => {
|
|
344
|
-
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
345
|
-
markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
|
|
346
|
-
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
347
|
-
try { obs.disconnect(); } catch (e) {}
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
obs.observe(ph, { childList: true, subtree: true });
|
|
351
|
-
wrap.__ezoicFillObs = obs;
|
|
352
|
-
} catch (e) {}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function isPlaceholderFilled(id) {
|
|
356
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
357
|
-
if (!ph || !ph.isConnected) return false;
|
|
358
|
-
|
|
359
|
-
const wrap = ph.parentElement;
|
|
360
|
-
if (wrap && isWrapMarkedFilled(wrap)) return true;
|
|
361
|
-
|
|
362
|
-
const filled = !!(ph.childNodes && ph.childNodes.length > 0);
|
|
363
|
-
if (filled) {
|
|
364
|
-
try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
|
|
365
|
-
try { markFilled(wrap); } catch (e) {}
|
|
366
|
-
}
|
|
367
|
-
return filled;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
let batchShowAdsTimer = null;
|
|
371
|
-
const pendingShowAdsIds = new Set();
|
|
372
|
-
|
|
373
|
-
function scheduleShowAdsBatch(id) {
|
|
374
|
-
if (!id) return;
|
|
375
|
-
|
|
376
|
-
if (sessionDefinedIds.has(id)) {
|
|
377
|
-
try {
|
|
378
|
-
destroyPlaceholderIds([id]);
|
|
379
|
-
sessionDefinedIds.delete(id);
|
|
380
|
-
} catch (e) {}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Throttle: ne pas rappeler le même ID trop vite
|
|
384
|
-
const now = Date.now(), last = state.lastShowById.get(id) || 0;
|
|
385
|
-
if (now - last < 3500) return;
|
|
386
|
-
|
|
387
|
-
// Ajouter à la batch
|
|
388
|
-
pendingShowAdsIds.add(id);
|
|
389
|
-
|
|
390
|
-
clearTimeout(batchShowAdsTimer);
|
|
391
|
-
batchShowAdsTimer = setTimeout(() => {
|
|
392
|
-
if (pendingShowAdsIds.size === 0) return;
|
|
393
|
-
|
|
394
|
-
const idsArray = Array.from(pendingShowAdsIds);
|
|
395
|
-
pendingShowAdsIds.clear();
|
|
396
|
-
|
|
397
|
-
// Appeler showAds avec TOUS les IDs en une fois
|
|
398
|
-
try {
|
|
399
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
400
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
401
|
-
window.ezstandalone.cmd.push(function() {
|
|
402
|
-
if (typeof window.ezstandalone.showAds === 'function') {
|
|
403
|
-
// Appel batch: showAds(id1, id2, id3...)
|
|
404
|
-
window.ezstandalone.showAds(...idsArray);
|
|
405
|
-
// Tracker tous les IDs
|
|
406
|
-
idsArray.forEach(id => {
|
|
407
|
-
state.lastShowById.set(id, Date.now());
|
|
408
|
-
sessionDefinedIds.add(id);
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
} catch (e) {}
|
|
413
|
-
|
|
414
|
-
// CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
|
|
415
|
-
setTimeout(() => {
|
|
416
|
-
cleanupInvisibleEzoicElements();
|
|
417
|
-
}, 800); // 1.5s pour laisser Ezoic charger
|
|
418
|
-
}, 100);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function callShowAdsWhenReady(id) {
|
|
422
|
-
if (!id) return;
|
|
423
|
-
|
|
424
|
-
const now = Date.now(), last = state.lastShowById.get(id) || 0;
|
|
425
|
-
if (now - last < 3500) return;
|
|
426
|
-
|
|
427
|
-
const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
|
|
428
|
-
try {
|
|
429
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
430
|
-
if (typeof window.ezstandalone.showAds === 'function') {
|
|
431
|
-
|
|
432
|
-
state.lastShowById.set(id, Date.now());
|
|
433
|
-
window.ezstandalone.showAds(id);
|
|
434
|
-
sessionDefinedIds.add(id);
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
} catch (e) {}
|
|
438
|
-
return false;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const startPageKey = state.pageKey;
|
|
442
|
-
let attempts = 0;
|
|
443
|
-
(function waitForPh() {
|
|
444
|
-
if (state.pageKey !== startPageKey) return;
|
|
445
|
-
if (state.pendingById.has(id)) return;
|
|
446
|
-
|
|
447
|
-
attempts += 1;
|
|
448
|
-
const el = document.getElementById(phId);
|
|
449
|
-
if (el && el.isConnected) {
|
|
450
|
-
|
|
451
|
-
// Si on arrive ici, soit visible, soit timeout
|
|
452
|
-
|
|
453
|
-
if (doCall()) {
|
|
454
|
-
state.pendingById.delete(id);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
44
|
+
function now() { return Date.now(); }
|
|
457
45
|
|
|
46
|
+
function getPageKey() {
|
|
47
|
+
try {
|
|
48
|
+
const ax = window.ajaxify;
|
|
49
|
+
if (ax && ax.data) {
|
|
50
|
+
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
51
|
+
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
return window.location.pathname || '';
|
|
458
55
|
}
|
|
459
56
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
57
|
+
function getKind() {
|
|
58
|
+
const p = window.location.pathname || '';
|
|
59
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
60
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
61
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
62
|
+
|
|
63
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
64
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
65
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
66
|
+
return 'other';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseIdPool(raw) {
|
|
70
|
+
if (!raw) return [];
|
|
71
|
+
const tokens = String(raw)
|
|
72
|
+
.split(/[\s,;]+/)
|
|
73
|
+
.map(s => s.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const t of tokens) {
|
|
79
|
+
const n = parseInt(t, 10);
|
|
80
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
81
|
+
seen.add(n);
|
|
82
|
+
out.push(n);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
465
86
|
}
|
|
466
87
|
|
|
467
88
|
async function fetchConfig() {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
89
|
+
if (state.cfg) return state.cfg;
|
|
90
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
91
|
+
|
|
92
|
+
state.cfgPromise = fetch('/api/plugins/ezoic-infinite/config', {
|
|
93
|
+
credentials: 'same-origin',
|
|
94
|
+
headers: { 'Accept': 'application/json' },
|
|
95
|
+
})
|
|
96
|
+
.then(r => (r && r.ok ? r.json() : null))
|
|
97
|
+
.catch(() => null)
|
|
98
|
+
.then((cfg) => {
|
|
99
|
+
state.cfg = cfg || { excluded: true };
|
|
100
|
+
state.pools.topics = parseIdPool(state.cfg.placeholderIds);
|
|
101
|
+
state.pools.categories = parseIdPool(state.cfg.categoryPlaceholderIds);
|
|
102
|
+
state.pools.messages = parseIdPool(state.cfg.messagePlaceholderIds);
|
|
103
|
+
return state.cfg;
|
|
104
|
+
})
|
|
105
|
+
.finally(() => { state.cfgPromise = null; });
|
|
106
|
+
|
|
107
|
+
return state.cfgPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rect(el) {
|
|
111
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function takeRecyclable(kind) {
|
|
115
|
+
const arr = state.live[kind];
|
|
116
|
+
for (let i = 0; i < arr.length; i++) {
|
|
117
|
+
const entry = arr[i];
|
|
118
|
+
if (!entry || !entry.wrap || !entry.wrap.isConnected) {
|
|
119
|
+
arr.splice(i, 1); i--; continue;
|
|
120
|
+
}
|
|
121
|
+
const r = rect(entry.wrap);
|
|
122
|
+
if (r && r.bottom < -RECYCLE_MARGIN_PX) {
|
|
123
|
+
arr.splice(i, 1);
|
|
124
|
+
return entry;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pickId(kind) {
|
|
131
|
+
const pool = state.pools[kind];
|
|
132
|
+
if (pool && pool.length) return { id: pool.shift(), recycled: null };
|
|
133
|
+
const recycled = takeRecyclable(kind);
|
|
134
|
+
if (recycled) return { id: recycled.id, recycled };
|
|
135
|
+
return { id: null, recycled: null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildWrap(kind, afterIndex, id) {
|
|
139
|
+
const wrap = document.createElement('div');
|
|
140
|
+
wrap.className = `${CLASS_WRAP} ${CLASS_WRAP}--${kind}`;
|
|
141
|
+
wrap.dataset.ezoicKind = kind;
|
|
142
|
+
wrap.dataset.ezoicAfter = String(afterIndex);
|
|
143
|
+
wrap.dataset.ezoicId = String(id);
|
|
144
|
+
|
|
145
|
+
const ph = document.createElement('div');
|
|
146
|
+
ph.id = `${PH_PREFIX}${id}`;
|
|
147
|
+
wrap.appendChild(ph);
|
|
148
|
+
|
|
149
|
+
return wrap;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findWrap(kind, afterIndex) {
|
|
153
|
+
return document.querySelector(`.${CLASS_WRAP}[data-ezoic-kind="${kind}"][data-ezoic-after="${afterIndex}"]`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resetWrapPlaceholder(wrap, id) {
|
|
157
|
+
if (!wrap) return;
|
|
158
|
+
if (wrap.__ezoicObs) {
|
|
159
|
+
try { wrap.__ezoicObs.disconnect(); } catch (e) {}
|
|
160
|
+
wrap.__ezoicObs = null;
|
|
161
|
+
}
|
|
162
|
+
wrap.removeAttribute('data-ezoic-filled');
|
|
163
|
+
wrap.dataset.ezoicId = String(id);
|
|
164
|
+
|
|
165
|
+
while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
|
|
166
|
+
const ph = document.createElement('div');
|
|
167
|
+
ph.id = `${PH_PREFIX}${id}`;
|
|
168
|
+
wrap.appendChild(ph);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function forcePlaceholderAutoHeight(wrap, id) {
|
|
172
|
+
const ph = wrap && wrap.querySelector ? wrap.querySelector(`#${PH_PREFIX}${id}`) : null;
|
|
173
|
+
if (!ph) return;
|
|
174
|
+
ph.style.setProperty('height', 'auto', 'important');
|
|
175
|
+
ph.style.setProperty('min-height', '0px', 'important');
|
|
176
|
+
requestAnimationFrame(() => {
|
|
177
|
+
ph.style.setProperty('height', 'auto', 'important');
|
|
178
|
+
ph.style.setProperty('min-height', '0px', 'important');
|
|
179
|
+
});
|
|
495
180
|
}
|
|
496
181
|
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
182
|
+
function markFilled(wrap) {
|
|
183
|
+
if (!wrap || !wrap.isConnected) return;
|
|
184
|
+
if (wrap.getAttribute('data-ezoic-filled') === '1') return;
|
|
185
|
+
wrap.setAttribute('data-ezoic-filled', '1');
|
|
186
|
+
|
|
187
|
+
const id = parseInt(wrap.dataset.ezoicId, 10);
|
|
188
|
+
if (Number.isFinite(id) && id > 0) forcePlaceholderAutoHeight(wrap, id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeWrap(wrap) {
|
|
192
|
+
if (!wrap) return;
|
|
193
|
+
if (wrap.__ezoicObs) {
|
|
194
|
+
try { wrap.__ezoicObs.disconnect(); } catch (e) {}
|
|
195
|
+
wrap.__ezoicObs = null;
|
|
196
|
+
}
|
|
197
|
+
try { wrap.remove(); } catch (e) {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function showEzoicAd(id) {
|
|
201
|
+
if (!id || id <= 0) return;
|
|
202
|
+
|
|
203
|
+
const last = state.lastShowById.get(id) || 0;
|
|
204
|
+
if (now() - last < SHOW_DEBOUNCE_MS) return;
|
|
205
|
+
state.lastShowById.set(id, now());
|
|
206
|
+
|
|
207
|
+
// Capture current page key so queued calls don't fire on the next ajaxify page.
|
|
208
|
+
const expectedPageKey = state.pageKey;
|
|
209
|
+
|
|
210
|
+
const safeCall = () => {
|
|
211
|
+
try {
|
|
212
|
+
// Only attempt if we're still on the same page and the placeholder exists *now*.
|
|
213
|
+
if (expectedPageKey !== state.pageKey) return;
|
|
214
|
+
const el = document.getElementById(`${PH_PREFIX}${id}`);
|
|
215
|
+
if (!el) return;
|
|
216
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
217
|
+
// Let the DOM settle one frame (helps with ajaxify/morphdom timing)
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
if (expectedPageKey !== state.pageKey) return;
|
|
220
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)) return;
|
|
221
|
+
window.ezstandalone.showAds(id);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
229
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
230
|
+
if (typeof window.ezstandalone.showAds === 'function') safeCall();
|
|
231
|
+
else window.ezstandalone.cmd.push(safeCall);
|
|
232
|
+
} catch (e) {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function observeFill(wrap, id) {
|
|
236
|
+
const ph = wrap.querySelector(`#${PH_PREFIX}${id}`);
|
|
237
|
+
if (!ph) return;
|
|
238
|
+
|
|
239
|
+
// immediate
|
|
240
|
+
if (ph.querySelector('iframe, ins, img') || ph.childElementCount > 0) {
|
|
241
|
+
markFilled(wrap);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const obs = new MutationObserver(() => {
|
|
246
|
+
if (!wrap.isConnected) return;
|
|
247
|
+
const hasCreative = !!ph.querySelector('iframe, ins, img');
|
|
248
|
+
const r = rect(ph);
|
|
249
|
+
if (hasCreative || (r && r.height > 20)) {
|
|
250
|
+
markFilled(wrap);
|
|
251
|
+
try { obs.disconnect(); } catch (e) {}
|
|
252
|
+
wrap.__ezoicObs = null;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
wrap.__ezoicObs = obs;
|
|
257
|
+
try { obs.observe(ph, { childList: true, subtree: true, attributes: true }); } catch (e) {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function scheduleRemovalIfUnfilled(wrap, kind, id) {
|
|
261
|
+
const t = setTimeout(() => {
|
|
262
|
+
state.timeouts.delete(t);
|
|
263
|
+
if (!wrap || !wrap.isConnected) return;
|
|
264
|
+
if (wrap.getAttribute('data-ezoic-filled') === '1') return;
|
|
265
|
+
removeWrap(wrap);
|
|
266
|
+
if (state.pools[kind] && typeof id === 'number' && id > 0) state.pools[kind].push(id);
|
|
267
|
+
}, FILL_TIMEOUT_MS);
|
|
268
|
+
state.timeouts.add(t);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function insertAfter(target, wrap) {
|
|
272
|
+
if (!target || !wrap) return false;
|
|
273
|
+
const next = target.nextElementSibling;
|
|
274
|
+
if (next && next.classList && next.classList.contains(CLASS_WRAP)) return false;
|
|
275
|
+
try {
|
|
276
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
277
|
+
return true;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function computeAfterIndexes(count, interval, showFirst) {
|
|
284
|
+
const out = [];
|
|
285
|
+
const step = Math.max(1, parseInt(interval, 10) || 1);
|
|
286
|
+
const start = showFirst ? 1 : step;
|
|
287
|
+
for (let i = start; i < count; i += step) out.push(i);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function runCategoryTopics(cfg) {
|
|
292
|
+
if (!cfg.enableBetweenAds) return;
|
|
293
|
+
|
|
294
|
+
const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
295
|
+
if (!items.length) return;
|
|
296
|
+
|
|
297
|
+
const after = computeAfterIndexes(items.length, cfg.intervalPosts || 6, !!cfg.showFirstTopicAd);
|
|
298
|
+
let inserts = 0;
|
|
299
|
+
|
|
300
|
+
for (const afterIndex of after) {
|
|
301
|
+
if (inserts >= MAX_INSERTS_PER_TICK) break;
|
|
302
|
+
const anchor = items[afterIndex - 1];
|
|
303
|
+
if (!anchor) continue;
|
|
304
|
+
if (findWrap('topics', afterIndex)) continue;
|
|
305
|
+
|
|
306
|
+
const { id, recycled } = pickId('topics');
|
|
307
|
+
if (!id) break;
|
|
308
|
+
|
|
309
|
+
const wrap = recycled ? recycled.wrap : buildWrap('topics', afterIndex, id);
|
|
310
|
+
if (recycled) resetWrapPlaceholder(wrap, id);
|
|
311
|
+
|
|
312
|
+
if (!insertAfter(anchor, wrap)) { state.pools.topics.push(id); continue; }
|
|
313
|
+
|
|
314
|
+
state.live.topics.push({ id, wrap });
|
|
315
|
+
observeFill(wrap, id);
|
|
316
|
+
scheduleRemovalIfUnfilled(wrap, 'topics', id);
|
|
317
|
+
if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
|
|
318
|
+
inserts++;
|
|
319
|
+
}
|
|
505
320
|
}
|
|
506
321
|
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
const targets = computeTargets(items.length, interval, showFirst);
|
|
322
|
+
function runCategories(cfg) {
|
|
323
|
+
if (!cfg.enableCategoryAds) return;
|
|
510
324
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
325
|
+
const items = Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
326
|
+
if (!items.length) return;
|
|
514
327
|
|
|
515
|
-
|
|
516
|
-
|
|
328
|
+
const after = computeAfterIndexes(items.length, cfg.intervalCategories || 4, !!cfg.showFirstCategoryAd);
|
|
329
|
+
let inserts = 0;
|
|
517
330
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
331
|
+
for (const afterIndex of after) {
|
|
332
|
+
if (inserts >= MAX_INSERTS_PER_TICK) break;
|
|
333
|
+
const anchor = items[afterIndex - 1];
|
|
334
|
+
if (!anchor) continue;
|
|
335
|
+
if (findWrap('categories', afterIndex)) continue;
|
|
521
336
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (prevWrap) continue;
|
|
337
|
+
const { id, recycled } = pickId('categories');
|
|
338
|
+
if (!id) break;
|
|
525
339
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const pick = pickId(kindPool, []);
|
|
529
|
-
const id = pick.id;
|
|
530
|
-
if (!id) break;
|
|
531
|
-
|
|
532
|
-
let wrap = null;
|
|
533
|
-
if (pick.recycled && pick.recycled.wrap) {
|
|
534
|
-
if (sessionDefinedIds.has(id)) {
|
|
535
|
-
destroyPlaceholderIds([id]);
|
|
536
|
-
}
|
|
537
|
-
const oldWrap = pick.recycled.wrap;
|
|
538
|
-
try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
|
|
539
|
-
try { oldWrap && oldWrap.remove(); } catch (e) {}
|
|
540
|
-
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
541
|
-
if (!wrap) continue;
|
|
542
|
-
setTimeout(() => {
|
|
543
|
-
callShowAdsWhenReady(id);
|
|
544
|
-
}, 50);
|
|
545
|
-
} else {
|
|
546
|
-
usedSet.add(id);
|
|
547
|
-
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
548
|
-
if (!wrap) continue;
|
|
549
|
-
// Micro-délai pour laisser le DOM se synchroniser
|
|
550
|
-
// Appel immédiat au lieu de 10ms delay
|
|
551
|
-
callShowAdsWhenReady(id);
|
|
552
|
-
}
|
|
340
|
+
const wrap = recycled ? recycled.wrap : buildWrap('categories', afterIndex, id);
|
|
341
|
+
if (recycled) resetWrapPlaceholder(wrap, id);
|
|
553
342
|
|
|
554
|
-
|
|
555
|
-
if (wrap && (
|
|
556
|
-
(wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
|
|
557
|
-
)) {
|
|
558
|
-
try { wrap.remove(); } catch (e) {}
|
|
559
|
-
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
560
|
-
try { kindPool.unshift(id); } catch (e) {}
|
|
561
|
-
usedSet.delete(id);
|
|
562
|
-
}
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
inserted += 1;
|
|
566
|
-
}
|
|
567
|
-
return inserted;
|
|
568
|
-
}
|
|
343
|
+
if (!insertAfter(anchor, wrap)) { state.pools.categories.push(id); continue; }
|
|
569
344
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
577
|
-
if (ph) {
|
|
578
|
-
const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
579
|
-
if (Number.isFinite(id) && id > 0) {
|
|
580
|
-
// Détruire le placeholder si Ezoic l'a déjà défini
|
|
581
|
-
if (sessionDefinedIds.has(id)) {
|
|
582
|
-
destroyPlaceholderIds([id]);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
345
|
+
state.live.categories.push({ id, wrap });
|
|
346
|
+
observeFill(wrap, id);
|
|
347
|
+
scheduleRemovalIfUnfilled(wrap, 'categories', id);
|
|
348
|
+
if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
|
|
349
|
+
inserts++;
|
|
350
|
+
}
|
|
585
351
|
}
|
|
586
|
-
ad.remove();
|
|
587
|
-
} catch (e) {}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function cleanup() {
|
|
593
|
-
destroyUsedPlaceholders();
|
|
594
|
-
|
|
595
|
-
document.querySelectorAll('.ezoic-ad').forEach(el => {
|
|
596
|
-
try { el.remove(); } catch (e) {}
|
|
597
|
-
});
|
|
598
352
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
state.cfgPromise = null;
|
|
353
|
+
function runMessageAds(cfg) {
|
|
354
|
+
if (!cfg.enableMessageAds) return;
|
|
602
355
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
state.usedTopics.clear();
|
|
607
|
-
state.usedPosts.clear();
|
|
608
|
-
state.usedCategories.clear();
|
|
609
|
-
state.lastShowById.clear();
|
|
610
|
-
state.pendingById.clear();
|
|
611
|
-
state.definedIds.clear();
|
|
356
|
+
const posts = Array.from(document.querySelectorAll(SELECTORS.postItem))
|
|
357
|
+
.filter(p => p && p.isConnected && p.querySelector(SELECTORS.postContent));
|
|
358
|
+
if (!posts.length) return;
|
|
612
359
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
});
|
|
616
|
-
state.activeTimeouts.clear();
|
|
360
|
+
const after = computeAfterIndexes(posts.length, cfg.messageIntervalPosts || 3, !!cfg.showFirstMessageAd);
|
|
361
|
+
let inserts = 0;
|
|
617
362
|
|
|
618
|
-
|
|
363
|
+
for (const afterIndex of after) {
|
|
364
|
+
if (inserts >= MAX_INSERTS_PER_TICK) break;
|
|
365
|
+
const anchor = posts[afterIndex - 1];
|
|
366
|
+
if (!anchor) continue;
|
|
367
|
+
if (findWrap('messages', afterIndex)) continue;
|
|
619
368
|
|
|
620
|
-
|
|
369
|
+
const { id, recycled } = pickId('messages');
|
|
370
|
+
if (!id) break;
|
|
621
371
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function ensureObserver() {
|
|
628
|
-
if (state.obs) return;
|
|
629
|
-
state.obs = new MutationObserver(() => scheduleRun('mutation'));
|
|
630
|
-
try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
async function runCore() {
|
|
634
|
-
if (!state.canShowAds) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
patchShowAds();
|
|
372
|
+
const wrap = recycled ? recycled.wrap : buildWrap('messages', afterIndex, id);
|
|
373
|
+
wrap.classList.add('ezoic-ad--message');
|
|
374
|
+
if (recycled) resetWrapPlaceholder(wrap, id);
|
|
639
375
|
|
|
640
|
-
|
|
641
|
-
if (!cfg || cfg.excluded) return;
|
|
376
|
+
if (!insertAfter(anchor, wrap)) { state.pools.messages.push(id); continue; }
|
|
642
377
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (normalizeBool(cfg.enableMessageAds)) {
|
|
650
|
-
inserted = injectBetween('ezoic-ad-message', getPostContainers(),
|
|
651
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
652
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
653
|
-
state.poolPosts,
|
|
654
|
-
state.usedPosts);
|
|
655
|
-
}
|
|
656
|
-
} else if (kind === 'categoryTopics') {
|
|
657
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
658
|
-
inserted = injectBetween('ezoic-ad-between', getTopicItems(),
|
|
659
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
660
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
661
|
-
state.poolTopics,
|
|
662
|
-
state.usedTopics);
|
|
663
|
-
}
|
|
664
|
-
} else if (kind === 'categories') {
|
|
665
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
666
|
-
inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
|
|
667
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
668
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
669
|
-
state.poolCategories,
|
|
670
|
-
state.usedCategories);
|
|
671
|
-
}
|
|
378
|
+
state.live.messages.push({ id, wrap });
|
|
379
|
+
observeFill(wrap, id);
|
|
380
|
+
scheduleRemovalIfUnfilled(wrap, 'messages', id);
|
|
381
|
+
if (wrap.querySelector(`#${PH_PREFIX}${id}`)) showEzoicAd(id);
|
|
382
|
+
inserts++;
|
|
383
|
+
}
|
|
672
384
|
}
|
|
673
385
|
|
|
674
|
-
|
|
386
|
+
function cleanupForNavigation() {
|
|
387
|
+
for (const t of state.timeouts) clearTimeout(t);
|
|
388
|
+
state.timeouts.clear();
|
|
675
389
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
390
|
+
if (state.observer) {
|
|
391
|
+
try { state.observer.disconnect(); } catch (e) {}
|
|
392
|
+
state.observer = null;
|
|
393
|
+
}
|
|
680
394
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
687
|
-
setTimeout(arguments[0], 50);
|
|
688
|
-
} else if (inserted === 0 && count > 0) {
|
|
689
|
-
// Pool épuisé ou recyclage pas encore disponible.
|
|
690
|
-
if (state.poolWaitAttempts < 8) {
|
|
691
|
-
state.poolWaitAttempts += 1;
|
|
692
|
-
setTimeout(arguments[0], 50);
|
|
693
|
-
} else {
|
|
694
|
-
}
|
|
695
|
-
} else if (inserted > 0) {
|
|
696
|
-
}
|
|
395
|
+
state.live.topics = [];
|
|
396
|
+
state.live.categories = [];
|
|
397
|
+
state.live.messages = [];
|
|
398
|
+
state.lastShowById.clear();
|
|
697
399
|
}
|
|
698
400
|
|
|
699
401
|
function scheduleRun() {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
}, { passive: true });
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function waitForContentThenRun() {
|
|
767
|
-
const MIN_WORDS = 250;
|
|
768
|
-
let attempts = 0;
|
|
769
|
-
const maxAttempts = 20; // 20 × 200ms = 4s max
|
|
770
|
-
|
|
771
|
-
(function check() {
|
|
772
|
-
attempts++;
|
|
773
|
-
|
|
774
|
-
// Compter les mots sur la page
|
|
775
|
-
const text = document.body.innerText || '';
|
|
776
|
-
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
777
|
-
|
|
778
|
-
if (wordCount >= MIN_WORDS) {
|
|
779
|
-
// Assez de contenu → lancer l'insertion
|
|
780
|
-
scheduleRun();
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Pas assez de contenu
|
|
785
|
-
if (attempts >= maxAttempts) {
|
|
786
|
-
// Timeout après 4s → tenter quand même
|
|
787
|
-
scheduleRun();
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Réessayer dans 200ms
|
|
792
|
-
setTimeout(check, 50);
|
|
793
|
-
})();
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function waitForEzoicThenRun() {
|
|
797
|
-
let attempts = 0;
|
|
798
|
-
const maxAttempts = 50; // 50 × 200ms = 10s max
|
|
799
|
-
|
|
800
|
-
(function check() {
|
|
801
|
-
attempts++;
|
|
802
|
-
// Vérifier si Ezoic est chargé
|
|
803
|
-
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
804
|
-
// Ezoic est prêt → lancer l'insertion
|
|
805
|
-
scheduleRun();
|
|
806
|
-
waitForContentThenRun();
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
// Ezoic pas encore prêt
|
|
810
|
-
if (attempts >= maxAttempts) {
|
|
811
|
-
// Tenter quand même
|
|
812
|
-
scheduleRun();
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
// Réessayer dans 200ms
|
|
816
|
-
setTimeout(check, 50);
|
|
817
|
-
})();
|
|
402
|
+
if (state.scheduled) return;
|
|
403
|
+
state.scheduled = true;
|
|
404
|
+
requestAnimationFrame(() => {
|
|
405
|
+
state.scheduled = false;
|
|
406
|
+
run().catch(() => {});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function run() {
|
|
411
|
+
const pk = getPageKey();
|
|
412
|
+
if (pk !== state.pageKey) {
|
|
413
|
+
state.pageKey = pk;
|
|
414
|
+
cleanupForNavigation();
|
|
415
|
+
state.cfg = null;
|
|
416
|
+
state.cfgPromise = null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const cfg = await fetchConfig();
|
|
420
|
+
if (!cfg || cfg.excluded) return;
|
|
421
|
+
|
|
422
|
+
const kind = getKind();
|
|
423
|
+
if (kind === 'categoryTopics') runCategoryTopics(cfg);
|
|
424
|
+
if (kind === 'categories') runCategories(cfg);
|
|
425
|
+
if (kind === 'topic') runMessageAds(cfg);
|
|
426
|
+
|
|
427
|
+
// remove obvious empty spacer nodes injected by networks
|
|
428
|
+
try {
|
|
429
|
+
document.querySelectorAll(`.${CLASS_WRAP} > div:empty`).forEach(n => n.remove());
|
|
430
|
+
} catch (e) {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function start() {
|
|
434
|
+
scheduleRun();
|
|
435
|
+
|
|
436
|
+
state.observer = new MutationObserver(() => {
|
|
437
|
+
const t = now();
|
|
438
|
+
if (t - state.lastRunAt < 200) return;
|
|
439
|
+
state.lastRunAt = t;
|
|
440
|
+
scheduleRun();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
try { state.observer.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
444
|
+
|
|
445
|
+
// NodeBB ajaxify hook
|
|
446
|
+
if (window.jQuery && window.jQuery(window).on) {
|
|
447
|
+
window.jQuery(window).on('action:ajaxify.end', () => {
|
|
448
|
+
state.pageKey = null;
|
|
449
|
+
scheduleRun();
|
|
450
|
+
});
|
|
451
|
+
} else {
|
|
452
|
+
window.addEventListener('popstate', () => {
|
|
453
|
+
state.pageKey = null;
|
|
454
|
+
scheduleRun();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (document.readyState === 'loading') {
|
|
460
|
+
document.addEventListener('DOMContentLoaded', start);
|
|
461
|
+
} else {
|
|
462
|
+
start();
|
|
818
463
|
}
|
|
819
|
-
|
|
820
|
-
cleanup();
|
|
821
|
-
bind();
|
|
822
|
-
bindScroll();
|
|
823
|
-
ensureObserver();
|
|
824
|
-
state.pageKey = getPageKey();
|
|
825
|
-
|
|
826
|
-
// Attendre que Ezoic soit chargé avant d'insérer
|
|
827
|
-
waitForEzoicThenRun();
|
|
828
|
-
})();
|
|
464
|
+
})();
|
package/public/style.css
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Ezoic Infinite Ads
|
|
3
|
+
- Wrappers are hidden until ad is detected as filled (data-ezoic-filled="1").
|
|
4
|
+
- Prevent reserved heights/margins creating blank space under creatives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
.ezoic-ad {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
display: none;
|
|
9
|
+
width: 100%;
|
|
10
|
+
clear: both;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
5
13
|
}
|
|
6
14
|
|
|
7
|
-
.ezoic-ad
|
|
15
|
+
.ezoic-ad[data-ezoic-filled="1"] {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Do not let the placeholder reserve fixed height */
|
|
20
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
21
|
+
height: auto !important;
|
|
22
|
+
min-height: 0 !important;
|
|
8
23
|
margin: 0 !important;
|
|
9
24
|
padding: 0 !important;
|
|
10
|
-
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Avoid baseline gaps under iframes/ins */
|
|
28
|
+
.ezoic-ad iframe,
|
|
29
|
+
.ezoic-ad ins {
|
|
30
|
+
display: block !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Remove empty spacer divs that can appear after injection */
|
|
34
|
+
.ezoic-ad > div:empty {
|
|
35
|
+
display: none !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Optional: message-style ad spacing (looks nicer between posts) */
|
|
39
|
+
.ezoic-ad--message[data-ezoic-filled="1"] {
|
|
40
|
+
margin: 0.75rem 0;
|
|
41
|
+
}
|