nodebb-plugin-ezoic-infinite 1.4.90 → 1.4.92
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/library.js +21 -5
- package/public/plugin.json +4 -0
- package/public/public/admin.js +2 -3
- package/public/public/client.js +697 -646
- package/public/public/style.css +3 -13
package/public/public/client.js
CHANGED
|
@@ -1,776 +1,827 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
3
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
|
-
|
|
6
4
|
const SELECTORS = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
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;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
retryById: new Map(),
|
|
39
|
-
retryTimer: null,
|
|
40
|
-
retryQueue: [],
|
|
41
|
-
retryQueueSet: new Set(),
|
|
42
|
-
retryQueueRunning: false,
|
|
43
|
-
badIds: new Set(),
|
|
44
|
-
definedIds: new Set(),
|
|
45
|
-
|
|
46
|
-
scheduled: false,
|
|
47
|
-
timer: null,
|
|
48
|
-
|
|
49
|
-
obs: null,
|
|
50
|
-
attempts: 0,
|
|
51
|
-
};
|
|
11
|
+
// Nécessaire pour savoir si on doit appeler destroyPlaceholders avant recyclage.
|
|
12
|
+
const sessionDefinedIds = new Set();
|
|
13
|
+
|
|
14
|
+
const insertingIds = new Set(), state = {
|
|
15
|
+
pageKey: null,
|
|
16
|
+
cfg: null,
|
|
17
|
+
cfgPromise: null,
|
|
18
|
+
|
|
19
|
+
poolTopics: [],
|
|
20
|
+
poolPosts: [],
|
|
21
|
+
poolCategories: [],
|
|
22
|
+
|
|
23
|
+
usedTopics: new Set(),
|
|
24
|
+
usedPosts: new Set(),
|
|
25
|
+
usedCategories: new Set(),
|
|
26
|
+
|
|
27
|
+
lastShowById: new Map(),
|
|
28
|
+
pendingById: new Set(),
|
|
29
|
+
definedIds: new Set(),
|
|
30
|
+
|
|
31
|
+
scheduled: false,
|
|
32
|
+
timer: null,
|
|
33
|
+
|
|
34
|
+
obs: null,
|
|
35
|
+
activeTimeouts: new Set(),
|
|
36
|
+
lastScrollRun: 0, };
|
|
52
37
|
|
|
53
38
|
function normalizeBool(v) {
|
|
54
|
-
|
|
39
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
55
40
|
}
|
|
56
41
|
|
|
57
42
|
function uniqInts(lines) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return out;
|
|
43
|
+
const out = [], seen = new Set();
|
|
44
|
+
for (const v of lines) {
|
|
45
|
+
const n = parseInt(v, 10);
|
|
46
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
47
|
+
seen.add(n);
|
|
48
|
+
out.push(n);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
68
52
|
}
|
|
69
53
|
|
|
70
54
|
function parsePool(raw) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
if (!raw) return [];
|
|
56
|
+
const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
57
|
+
return uniqInts(lines);
|
|
74
58
|
}
|
|
75
59
|
|
|
76
60
|
function getPageKey() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
try {
|
|
62
|
+
const ax = window.ajaxify;
|
|
63
|
+
if (ax && ax.data) {
|
|
64
|
+
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
65
|
+
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
return window.location.pathname;
|
|
85
69
|
}
|
|
86
70
|
|
|
87
71
|
function getKind() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
72
|
+
const p = window.location.pathname || '';
|
|
73
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
74
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
75
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
76
|
+
// fallback by DOM
|
|
77
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
78
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
79
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
80
|
+
return 'other';
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
function getTopicItems() {
|
|
100
|
-
|
|
84
|
+
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
101
85
|
}
|
|
102
86
|
|
|
103
87
|
function getCategoryItems() {
|
|
104
|
-
|
|
88
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
105
89
|
}
|
|
106
90
|
|
|
107
91
|
function getPostContainers() {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
92
|
+
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
93
|
+
return nodes.filter((el) => {
|
|
94
|
+
if (!el || !el.isConnected) return false;
|
|
95
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
96
|
+
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
97
|
+
if (parentPost && parentPost !== el) return false;
|
|
98
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
117
101
|
}
|
|
118
102
|
|
|
119
|
-
|
|
120
103
|
function safeRect(el) {
|
|
121
|
-
|
|
104
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
122
105
|
}
|
|
123
106
|
|
|
124
107
|
function destroyPlaceholderIds(ids) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
108
|
+
if (!ids || !ids.length) return;
|
|
109
|
+
// Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
|
|
110
|
+
const filtered = ids.filter((id) => {
|
|
111
|
+
// Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
|
|
112
|
+
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
113
|
+
});
|
|
114
|
+
if (!filtered.length) return;
|
|
115
|
+
|
|
116
|
+
const call = () => {
|
|
117
|
+
try {
|
|
118
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
119
|
+
window.ezstandalone.destroyPlaceholders(filtered);
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
125
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
126
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
127
|
+
else window.ezstandalone.cmd.push(call);
|
|
128
|
+
} catch (e) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// Nettoyer les wrappers vides (sans pub) pour éviter espaces verticaux
|
|
133
|
+
function cleanupEmptyWrappers() {
|
|
134
|
+
try {
|
|
135
|
+
document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
|
|
136
|
+
const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
|
|
137
|
+
if (ph && ph.children.length === 0) {
|
|
138
|
+
// Placeholder vide après 3s = pub non chargée
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
if (ph.children.length === 0) {
|
|
141
|
+
wrapper.remove();
|
|
142
|
+
}
|
|
143
|
+
}, 3000);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} catch (e) {}
|
|
153
147
|
}
|
|
154
148
|
|
|
155
149
|
function getRecyclable(liveArr) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function moveWrapAfter(wrap, target, kindClass, afterPos) {
|
|
170
|
-
try {
|
|
171
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
172
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
173
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
174
|
-
return true;
|
|
175
|
-
} catch (e) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
150
|
+
const margin = 600;
|
|
151
|
+
for (let i = 0; i < liveArr.length; i++) {
|
|
152
|
+
const entry = liveArr[i];
|
|
153
|
+
if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
|
|
154
|
+
const r = safeRect(entry.wrap);
|
|
155
|
+
if (r && r.bottom < -margin) {
|
|
156
|
+
liveArr.splice(i, 1);
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
178
161
|
}
|
|
179
162
|
|
|
180
163
|
function pickId(pool, liveArr) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
164
|
+
if (pool.length) return { id: pool.shift(), recycled: null };
|
|
165
|
+
const recycled = getRecyclable(liveArr);
|
|
166
|
+
if (recycled) return { id: recycled.id, recycled };
|
|
167
|
+
return { id: null, recycled: null };
|
|
185
168
|
}
|
|
186
169
|
|
|
187
|
-
|
|
188
170
|
function resetPlaceholderInWrap(wrap, id) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
171
|
+
try {
|
|
172
|
+
if (!wrap) return;
|
|
173
|
+
try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
|
|
174
|
+
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
175
|
+
const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
|
|
176
|
+
if (old) old.remove();
|
|
177
|
+
// Remove any leftover markup inside wrapper
|
|
178
|
+
wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
|
|
179
|
+
const ph = document.createElement('div');
|
|
180
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
181
|
+
wrap.appendChild(ph);
|
|
182
|
+
} catch (e) {}
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
function isAdjacentAd(target) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
186
|
+
if (!target || !target.nextElementSibling) return false;
|
|
187
|
+
const next = target.nextElementSibling;
|
|
188
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
189
|
+
return false;
|
|
208
190
|
}
|
|
209
191
|
|
|
210
192
|
function isPrevAd(target) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
193
|
+
if (!target || !target.previousElementSibling) return false;
|
|
194
|
+
const prev = target.previousElementSibling;
|
|
195
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
196
|
+
return false;
|
|
215
197
|
}
|
|
216
198
|
|
|
217
199
|
function buildWrap(id, kindClass, afterPos) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
200
|
+
const wrap = document.createElement('div');
|
|
201
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
202
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
203
|
+
wrap.style.width = '100%';
|
|
222
204
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
205
|
+
const ph = document.createElement('div');
|
|
206
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
207
|
+
wrap.appendChild(ph);
|
|
208
|
+
return wrap;
|
|
227
209
|
}
|
|
228
210
|
|
|
229
211
|
function findWrap(kindClass, afterPos) {
|
|
230
|
-
|
|
212
|
+
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
231
213
|
}
|
|
232
214
|
|
|
233
215
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const wrap = buildWrap(id, kindClass, afterPos);
|
|
237
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
238
|
-
attachFillObserver(wrap, id);
|
|
239
|
-
return wrap;
|
|
240
|
-
}
|
|
216
|
+
if (!target || !target.insertAdjacentElement) return null;
|
|
217
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
241
218
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
destroyPlaceholderIds(ids);
|
|
250
|
-
}
|
|
251
|
-
} catch (e) {}
|
|
252
|
-
};
|
|
219
|
+
// CRITICAL: Double-lock pour éviter race conditions sur les doublons
|
|
220
|
+
// 1. Vérifier qu'aucun autre thread n'est en train d'insérer cet ID
|
|
221
|
+
if (insertingIds.has(id)) return null;
|
|
222
|
+
|
|
223
|
+
// 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
|
|
224
|
+
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
225
|
+
if (existingPh && existingPh.isConnected) return null;
|
|
253
226
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
227
|
+
// Acquérir le lock
|
|
228
|
+
insertingIds.add(id);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const wrap = buildWrap(id, kindClass, afterPos);
|
|
232
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
233
|
+
attachFillObserver(wrap, id);
|
|
234
|
+
return wrap;
|
|
235
|
+
} finally {
|
|
236
|
+
// Libérer le lock après 100ms (le temps que le DOM soit stable)
|
|
237
|
+
setTimeout(() => insertingIds.delete(id), 50);
|
|
238
|
+
}
|
|
260
239
|
}
|
|
261
240
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
266
|
-
const ez = window.ezstandalone;
|
|
267
|
-
if (ez.__nodebbEzoicPatched) return;
|
|
268
|
-
if (typeof ez.showAds !== 'function') return;
|
|
269
|
-
|
|
270
|
-
ez.__nodebbEzoicPatched = true;
|
|
271
|
-
const orig = ez.showAds;
|
|
272
|
-
|
|
273
|
-
ez.showAds = function (arg) {
|
|
274
|
-
if (Array.isArray(arg)) {
|
|
275
|
-
const seen = new Set();
|
|
276
|
-
for (const v of arg) {
|
|
277
|
-
const id = parseInt(v, 10);
|
|
278
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
279
|
-
seen.add(id);
|
|
280
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
281
|
-
}
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
return orig.apply(ez, arguments);
|
|
285
|
-
};
|
|
286
|
-
} catch (e) {}
|
|
241
|
+
function destroyUsedPlaceholders() {
|
|
242
|
+
const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
|
|
243
|
+
if (ids.length) destroyPlaceholderIds(ids);
|
|
287
244
|
}
|
|
288
245
|
|
|
246
|
+
function patchShowAds() {
|
|
247
|
+
// Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
|
|
248
|
+
// Also ensures the patch is applied even if Ezoic loads after our script.
|
|
249
|
+
const applyPatch = () => {
|
|
250
|
+
try {
|
|
251
|
+
window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
|
|
252
|
+
if (window.__nodebbEzoicPatched) return;
|
|
253
|
+
if (typeof ez.showAds !== 'function') return;
|
|
254
|
+
|
|
255
|
+
window.__nodebbEzoicPatched = true;
|
|
256
|
+
const orig = ez.showAds;
|
|
257
|
+
|
|
258
|
+
ez.showAds = function (arg) {
|
|
259
|
+
if (Array.isArray(arg)) {
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const v of arg) {
|
|
262
|
+
const id = parseInt(v, 10);
|
|
263
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
264
|
+
seen.add(id);
|
|
265
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
return orig.apply(ez, arguments);
|
|
270
|
+
};
|
|
271
|
+
} catch (e) {}
|
|
272
|
+
};
|
|
289
273
|
|
|
274
|
+
applyPatch();
|
|
275
|
+
// Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
|
|
276
|
+
if (!window.__nodebbEzoicPatched) {
|
|
277
|
+
try {
|
|
278
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
279
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
280
|
+
window.ezstandalone.cmd.push(applyPatch);
|
|
281
|
+
} catch (e) {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
290
284
|
|
|
291
285
|
function markFilled(wrap) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
286
|
+
try {
|
|
287
|
+
if (!wrap) return;
|
|
288
|
+
// Disconnect the fill observer first (no need to remove+re-add the attribute)
|
|
289
|
+
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
290
|
+
wrap.setAttribute('data-ezoic-filled', '1');
|
|
291
|
+
} catch (e) {}
|
|
298
292
|
}
|
|
299
293
|
|
|
300
294
|
function isWrapMarkedFilled(wrap) {
|
|
301
|
-
|
|
295
|
+
try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
|
|
302
296
|
}
|
|
303
297
|
|
|
304
298
|
function attachFillObserver(wrap, id) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
299
|
+
try {
|
|
300
|
+
const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
|
|
301
|
+
if (!ph) return;
|
|
302
|
+
// Already filled?
|
|
303
|
+
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
304
|
+
markFilled(wrap);
|
|
305
|
+
state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const obs = new MutationObserver(() => {
|
|
309
|
+
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
310
|
+
markFilled(wrap);
|
|
311
|
+
try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
|
|
312
|
+
try { obs.disconnect(); } catch (e) {}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
obs.observe(ph, { childList: true, subtree: true });
|
|
316
|
+
// Keep a weak reference on the wrapper so we can disconnect on recycle/remove
|
|
317
|
+
wrap.__ezoicFillObs = obs;
|
|
318
|
+
} catch (e) {}
|
|
325
319
|
}
|
|
326
320
|
|
|
327
321
|
function isPlaceholderFilled(id) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
function refillUnfilled() {
|
|
394
|
-
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
|
|
395
|
-
let scheduledAny = false;
|
|
396
|
-
|
|
397
|
-
for (const wrap of wraps) {
|
|
398
|
-
const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
399
|
-
if (!ph) continue;
|
|
400
|
-
const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
401
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
402
|
-
|
|
403
|
-
if (isPlaceholderFilled(id)) {
|
|
404
|
-
state.retryById.delete(id);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
// If wrapper was marked filled, don't try to refill even if placeholder temporarily appears empty.
|
|
408
|
-
if (isWrapMarkedFilled(wrap)) {
|
|
409
|
-
state.retryById.delete(id);
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const tries = (state.retryById.get(id) || 0);
|
|
414
|
-
if (tries >= 8) { state.badIds && state.badIds.add(id); continue; }
|
|
415
|
-
|
|
416
|
-
const r = safeRect(wrap);
|
|
417
|
-
if (r && (r.top > window.innerHeight + 1200 || r.bottom < -1200)) continue;
|
|
418
|
-
|
|
419
|
-
state.retryById.set(id, tries + 1);
|
|
420
|
-
enqueueRetry(id);
|
|
421
|
-
scheduledAny = true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (scheduledAny) scheduleRefill(700);
|
|
322
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
323
|
+
if (!ph || !ph.isConnected) return false;
|
|
324
|
+
|
|
325
|
+
const wrap = ph.parentElement;
|
|
326
|
+
if (wrap && isWrapMarkedFilled(wrap)) return true;
|
|
327
|
+
|
|
328
|
+
const filled = !!(ph.childNodes && ph.childNodes.length > 0);
|
|
329
|
+
if (filled) {
|
|
330
|
+
try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
|
|
331
|
+
try { markFilled(wrap); } catch (e) {}
|
|
332
|
+
}
|
|
333
|
+
return filled;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Appeler showAds() en batch selon recommandations Ezoic
|
|
337
|
+
// Au lieu de showAds(id1), showAds(id2)... faire showAds(id1, id2, id3...)
|
|
338
|
+
let batchShowAdsTimer = null;
|
|
339
|
+
const pendingShowAdsIds = new Set();
|
|
340
|
+
|
|
341
|
+
function scheduleShowAdsBatch(id) {
|
|
342
|
+
if (!id) return;
|
|
343
|
+
|
|
344
|
+
// CRITIQUE: Si cet ID a déjà été défini (sessionDefinedIds), le détruire d'abord
|
|
345
|
+
if (sessionDefinedIds.has(id)) {
|
|
346
|
+
try {
|
|
347
|
+
destroyPlaceholderIds([id]);
|
|
348
|
+
sessionDefinedIds.delete(id);
|
|
349
|
+
} catch (e) {}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Throttle: ne pas rappeler le même ID trop vite
|
|
353
|
+
const now = Date.now(), last = state.lastShowById.get(id) || 0;
|
|
354
|
+
if (now - last < 3500) return;
|
|
355
|
+
|
|
356
|
+
// Ajouter à la batch
|
|
357
|
+
pendingShowAdsIds.add(id);
|
|
358
|
+
|
|
359
|
+
// Debounce: attendre 100ms pour collecter tous les IDs
|
|
360
|
+
clearTimeout(batchShowAdsTimer);
|
|
361
|
+
batchShowAdsTimer = setTimeout(() => {
|
|
362
|
+
if (pendingShowAdsIds.size === 0) return;
|
|
363
|
+
|
|
364
|
+
const idsArray = Array.from(pendingShowAdsIds);
|
|
365
|
+
pendingShowAdsIds.clear();
|
|
366
|
+
|
|
367
|
+
// Appeler showAds avec TOUS les IDs en une fois
|
|
368
|
+
try {
|
|
369
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
370
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
371
|
+
window.ezstandalone.cmd.push(function() {
|
|
372
|
+
if (typeof window.ezstandalone.showAds === 'function') {
|
|
373
|
+
// Appel batch: showAds(id1, id2, id3...)
|
|
374
|
+
window.ezstandalone.showAds(...idsArray);
|
|
375
|
+
// Tracker tous les IDs
|
|
376
|
+
idsArray.forEach(id => {
|
|
377
|
+
state.lastShowById.set(id, Date.now());
|
|
378
|
+
sessionDefinedIds.add(id);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
} catch (e) {}
|
|
383
|
+
}, 100);
|
|
425
384
|
}
|
|
426
385
|
|
|
427
386
|
function callShowAdsWhenReady(id) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
setTimeout(tick, 700);
|
|
478
|
-
})();
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (attempts < 50) setTimeout(waitForPh, 50);
|
|
483
|
-
})();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function nextId(pool) {
|
|
487
|
-
// backward compatible: the injector passes the pool array
|
|
488
|
-
if (Array.isArray(pool) && pool.length) return pool.shift();
|
|
489
|
-
return null;
|
|
387
|
+
if (!id) return;
|
|
388
|
+
|
|
389
|
+
const now = Date.now(), last = state.lastShowById.get(id) || 0;
|
|
390
|
+
if (now - last < 3500) return;
|
|
391
|
+
|
|
392
|
+
const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
|
|
393
|
+
try {
|
|
394
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
395
|
+
if (typeof window.ezstandalone.showAds === 'function') {
|
|
396
|
+
|
|
397
|
+
state.lastShowById.set(id, Date.now());
|
|
398
|
+
window.ezstandalone.showAds(id);
|
|
399
|
+
sessionDefinedIds.add(id);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
return false;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const startPageKey = state.pageKey;
|
|
407
|
+
let attempts = 0;
|
|
408
|
+
(function waitForPh() {
|
|
409
|
+
// Abort if the user navigated away since this showAds was scheduled
|
|
410
|
+
if (state.pageKey !== startPageKey) return;
|
|
411
|
+
// Abort if another concurrent call is already handling this id
|
|
412
|
+
if (state.pendingById.has(id)) return;
|
|
413
|
+
|
|
414
|
+
attempts += 1;
|
|
415
|
+
const el = document.getElementById(phId);
|
|
416
|
+
if (el && el.isConnected) {
|
|
417
|
+
// CRITIQUE: Vérifier que le placeholder est VISIBLE
|
|
418
|
+
|
|
419
|
+
// Si on arrive ici, soit visible, soit timeout
|
|
420
|
+
|
|
421
|
+
// Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
|
|
422
|
+
if (doCall()) {
|
|
423
|
+
state.pendingById.delete(id);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (attempts < 100) {
|
|
430
|
+
const timeoutId = setTimeout(waitForPh, 50);
|
|
431
|
+
state.activeTimeouts.add(timeoutId);
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
490
434
|
}
|
|
491
435
|
|
|
492
436
|
async function fetchConfig() {
|
|
493
|
-
|
|
494
|
-
|
|
437
|
+
if (state.cfg) return state.cfg;
|
|
438
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
495
439
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
440
|
+
state.cfgPromise = (async () => {
|
|
441
|
+
const MAX_TRIES = 3;
|
|
442
|
+
let delay = 800;
|
|
443
|
+
for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
446
|
+
if (res.ok) {
|
|
447
|
+
state.cfg = await res.json();
|
|
448
|
+
return state.cfg;
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {}
|
|
451
|
+
if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
|
|
452
|
+
delay *= 2;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
})();
|
|
508
456
|
|
|
509
|
-
|
|
457
|
+
try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
|
|
510
458
|
}
|
|
511
459
|
|
|
512
460
|
function initPools(cfg) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
461
|
+
if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
|
|
462
|
+
if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
|
|
463
|
+
if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
516
464
|
}
|
|
517
465
|
|
|
518
466
|
function computeTargets(count, interval, showFirst) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
467
|
+
const out = [];
|
|
468
|
+
if (count <= 0) return out;
|
|
469
|
+
if (showFirst) out.push(1);
|
|
470
|
+
for (let i = 1; i <= count; i++) {
|
|
471
|
+
if (i % interval === 0) out.push(i);
|
|
472
|
+
}
|
|
473
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
526
474
|
}
|
|
527
475
|
|
|
528
476
|
function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
477
|
+
if (!items.length) return 0;
|
|
478
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
479
|
+
|
|
480
|
+
let inserted = 0;
|
|
481
|
+
for (const afterPos of targets) {
|
|
482
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
483
|
+
|
|
484
|
+
const el = items[afterPos - 1];
|
|
485
|
+
if (!el || !el.isConnected) continue;
|
|
486
|
+
|
|
487
|
+
// Prevent adjacent ads (DOM-based, robust against virtualization)
|
|
488
|
+
if (isAdjacentAd(el) || isPrevAd(el)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Prevent back-to-back at load
|
|
493
|
+
const prevWrap = findWrap(kindClass, afterPos - 1);
|
|
494
|
+
if (prevWrap) continue;
|
|
495
|
+
|
|
496
|
+
if (findWrap(kindClass, afterPos)) continue;
|
|
497
|
+
|
|
498
|
+
const pick = pickId(kindPool, []);
|
|
499
|
+
const id = pick.id;
|
|
500
|
+
if (!id) break;
|
|
501
|
+
|
|
502
|
+
let wrap = null;
|
|
503
|
+
if (pick.recycled && pick.recycled.wrap) {
|
|
504
|
+
// Only destroy if Ezoic has actually defined this placeholder before
|
|
505
|
+
if (sessionDefinedIds.has(id)) {
|
|
506
|
+
destroyPlaceholderIds([id]);
|
|
507
|
+
}
|
|
508
|
+
// Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
|
|
509
|
+
const oldWrap = pick.recycled.wrap;
|
|
510
|
+
try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
|
|
511
|
+
try { oldWrap && oldWrap.remove(); } catch (e) {}
|
|
512
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
513
|
+
if (!wrap) continue;
|
|
514
|
+
// Attendre que le wrapper soit dans le DOM puis appeler showAds
|
|
515
|
+
setTimeout(() => {
|
|
516
|
+
callShowAdsWhenReady(id);
|
|
517
|
+
}, 50);
|
|
518
|
+
} else {
|
|
519
|
+
usedSet.add(id);
|
|
520
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
521
|
+
if (!wrap) continue;
|
|
522
|
+
// Micro-délai pour laisser le DOM se synchroniser
|
|
523
|
+
// Appel immédiat au lieu de 10ms delay
|
|
524
|
+
callShowAdsWhenReady(id);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
liveArr.push({ id, wrap });
|
|
528
|
+
// If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
|
|
529
|
+
if (wrap && (
|
|
530
|
+
(wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
|
|
531
|
+
)) {
|
|
532
|
+
try { wrap.remove(); } catch (e) {}
|
|
533
|
+
// Put id back if it was newly consumed (not recycled)
|
|
534
|
+
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
535
|
+
try { kindPool.unshift(id); } catch (e) {}
|
|
536
|
+
usedSet.delete(id);
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
inserted += 1;
|
|
541
|
+
}
|
|
542
|
+
return inserted;
|
|
595
543
|
}
|
|
596
544
|
|
|
597
545
|
function enforceNoAdjacentAds() {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
546
|
+
const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
|
|
547
|
+
for (let i = 0; i < ads.length; i++) {
|
|
548
|
+
const ad = ads[i], prev = ad.previousElementSibling;
|
|
549
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
550
|
+
// Supprimer le wrapper adjacent au lieu de le cacher
|
|
551
|
+
try {
|
|
552
|
+
const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
553
|
+
if (ph) {
|
|
554
|
+
const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
555
|
+
if (Number.isFinite(id) && id > 0) {
|
|
556
|
+
// Détruire le placeholder si Ezoic l'a déjà défini
|
|
557
|
+
if (sessionDefinedIds.has(id)) {
|
|
558
|
+
destroyPlaceholderIds([id]);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
ad.remove();
|
|
563
|
+
} catch (e) {}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
605
566
|
}
|
|
606
567
|
|
|
607
568
|
function cleanup() {
|
|
608
|
-
|
|
569
|
+
destroyUsedPlaceholders();
|
|
570
|
+
|
|
571
|
+
// CRITIQUE: Supprimer TOUS les wrappers .ezoic-ad du DOM
|
|
572
|
+
// Sinon ils restent et deviennent "unused" sur la nouvelle page
|
|
573
|
+
document.querySelectorAll('.ezoic-ad').forEach(el => {
|
|
574
|
+
try { el.remove(); } catch (e) {}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
state.pageKey = getPageKey();
|
|
578
|
+
state.cfg = null;
|
|
579
|
+
state.cfgPromise = null;
|
|
580
|
+
|
|
581
|
+
state.poolTopics = [];
|
|
582
|
+
state.poolPosts = [];
|
|
583
|
+
state.poolCategories = [];
|
|
584
|
+
state.usedTopics.clear();
|
|
585
|
+
state.usedPosts.clear();
|
|
586
|
+
state.usedCategories.clear();
|
|
587
|
+
state.lastShowById.clear();
|
|
588
|
+
// CRITIQUE: Vider pendingById pour annuler tous les showAds en cours
|
|
589
|
+
// Sinon Ezoic essaie d'accéder aux placeholders pendant que NodeBB vide le DOM
|
|
590
|
+
state.pendingById.clear();
|
|
591
|
+
state.definedIds.clear();
|
|
592
|
+
|
|
593
|
+
// NE PAS supprimer les wrappers Ezoic ici - ils seront supprimés naturellement
|
|
594
|
+
// quand NodeBB vide le DOM lors de la navigation ajaxify
|
|
595
|
+
// Les supprimer manuellement cause des problèmes avec l'état interne d'Ezoic
|
|
596
|
+
|
|
597
|
+
// CRITIQUE: Annuler TOUS les timeouts en cours pour éviter que les anciens
|
|
598
|
+
// showAds() continuent à s'exécuter après la navigation
|
|
599
|
+
state.activeTimeouts.forEach(id => {
|
|
600
|
+
try { clearTimeout(id); } catch (e) {}
|
|
601
|
+
});
|
|
602
|
+
state.activeTimeouts.clear();
|
|
603
|
+
|
|
604
|
+
// Vider aussi pendingById pour annuler les showAds en attente
|
|
605
|
+
state.pendingById.clear();
|
|
606
|
+
|
|
607
|
+
if (state.obs) { state.obs.disconnect(); state.obs = null; }
|
|
608
|
+
|
|
609
|
+
state.scheduled = false;
|
|
610
|
+
clearTimeout(state.timer);
|
|
611
|
+
state.timer = null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function ensureObserver() {
|
|
615
|
+
if (state.obs) return;
|
|
616
|
+
state.obs = new MutationObserver(() => scheduleRun('mutation'));
|
|
617
|
+
try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
618
|
+
}
|
|
609
619
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
620
|
+
async function runCore() {
|
|
621
|
+
// Attendre que canInsert soit true (protection race condition navigation)
|
|
622
|
+
if (!state.canShowAds) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
613
625
|
|
|
614
|
-
|
|
615
|
-
state.poolPosts = [];
|
|
616
|
-
state.poolCategories = [];
|
|
617
|
-
state.poolCategories = [];
|
|
618
|
-
state.usedTopics.clear();
|
|
619
|
-
state.usedPosts.clear();
|
|
620
|
-
state.usedCategories && state.usedCategories.clear();
|
|
621
|
-
state.liveBetween = [];
|
|
622
|
-
state.liveMessage = [];
|
|
623
|
-
state.liveCategory = [];
|
|
624
|
-
state.usedCategories.clear();
|
|
626
|
+
patchShowAds();
|
|
625
627
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
+
const cfg = await fetchConfig();
|
|
629
|
+
if (!cfg || cfg.excluded) return;
|
|
628
630
|
|
|
629
|
-
|
|
631
|
+
initPools(cfg);
|
|
630
632
|
|
|
631
|
-
|
|
633
|
+
const kind = getKind();
|
|
634
|
+
let inserted = 0;
|
|
632
635
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
636
|
+
if (kind === 'topic') {
|
|
637
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
638
|
+
inserted = injectBetween('ezoic-ad-message', getPostContainers(),
|
|
639
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
640
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
641
|
+
state.poolPosts,
|
|
642
|
+
state.usedPosts);
|
|
643
|
+
}
|
|
644
|
+
} else if (kind === 'categoryTopics') {
|
|
645
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
646
|
+
inserted = injectBetween('ezoic-ad-between', getTopicItems(),
|
|
647
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
648
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
649
|
+
state.poolTopics,
|
|
650
|
+
state.usedTopics);
|
|
651
|
+
}
|
|
652
|
+
} else if (kind === 'categories') {
|
|
653
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
654
|
+
inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
|
|
655
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
656
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
657
|
+
state.poolCategories,
|
|
658
|
+
state.usedCategories);
|
|
659
|
+
}
|
|
637
660
|
}
|
|
638
661
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
662
|
+
enforceNoAdjacentAds();
|
|
663
|
+
|
|
664
|
+
// If nothing inserted and list isn't in DOM yet (first click), retry a bit
|
|
665
|
+
let count = 0;
|
|
666
|
+
if (kind === 'topic') count = getPostContainers().length;
|
|
667
|
+
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
668
|
+
else if (kind === 'categories') count = getCategoryItems().length;
|
|
669
|
+
|
|
670
|
+
if (count === 0 && 0 < 25) {
|
|
671
|
+
setTimeout(arguments[0], 50);
|
|
672
|
+
return;
|
|
644
673
|
}
|
|
645
674
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
661
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
662
|
-
'message',
|
|
663
|
-
state.usedPosts);
|
|
664
|
-
}
|
|
665
|
-
} else if (kind === 'categoryTopics') {
|
|
666
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
667
|
-
inserted = injectBetween('ezoic-ad-between', getTopicItems(),
|
|
668
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
669
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
670
|
-
'between',
|
|
671
|
-
state.usedTopics);
|
|
672
|
-
}
|
|
673
|
-
} else if (kind === 'categories') {
|
|
674
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
675
|
-
inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
|
|
676
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
677
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
678
|
-
'categories',
|
|
679
|
-
state.usedCategories);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
enforceNoAdjacentAds();
|
|
684
|
-
scheduleRefill(250);
|
|
685
|
-
|
|
686
|
-
// If nothing inserted and list isn't in DOM yet (first click), retry a bit
|
|
687
|
-
let count = 0;
|
|
688
|
-
if (kind === 'topic') count = getPostContainers().length;
|
|
689
|
-
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
690
|
-
else if (kind === 'categories') count = getCategoryItems().length;
|
|
691
|
-
|
|
692
|
-
if (count === 0 && state.attempts < 25) {
|
|
693
|
-
state.attempts += 1;
|
|
694
|
-
setTimeout(() => scheduleRun('await-items'), 120);
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
|
|
675
|
+
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
676
|
+
// Plus d'insertions possibles ce cycle, continuer immédiatement
|
|
677
|
+
setTimeout(arguments[0], 50);
|
|
678
|
+
} else if (inserted === 0 && count > 0) {
|
|
679
|
+
// Pool épuisé ou recyclage pas encore disponible.
|
|
680
|
+
// Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
|
|
681
|
+
// le temps de défiler hors écran et devenir recyclables.
|
|
682
|
+
if (state.poolWaitAttempts < 8) {
|
|
683
|
+
state.poolWaitAttempts += 1;
|
|
684
|
+
setTimeout(arguments[0], 50);
|
|
685
|
+
} else {
|
|
686
|
+
}
|
|
687
|
+
} else if (inserted > 0) {
|
|
688
|
+
}
|
|
699
689
|
}
|
|
700
690
|
|
|
701
691
|
function scheduleRun() {
|
|
702
|
-
|
|
703
|
-
|
|
692
|
+
if (state.scheduled) return;
|
|
693
|
+
state.scheduled = true;
|
|
704
694
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
695
|
+
clearTimeout(state.timer);
|
|
696
|
+
state.timer = setTimeout(() => {
|
|
697
|
+
state.scheduled = false;
|
|
698
|
+
const pk = getPageKey();
|
|
699
|
+
if (state.pageKey && pk !== state.pageKey) return;
|
|
700
|
+
runCore().catch(() => {});
|
|
701
|
+
}, 50);
|
|
712
702
|
}
|
|
713
703
|
|
|
714
704
|
function bind() {
|
|
715
|
-
|
|
705
|
+
if (!$) return;
|
|
706
|
+
|
|
707
|
+
$(window).off('.ezoicInfinite');
|
|
708
|
+
|
|
709
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
|
|
716
710
|
|
|
717
|
-
|
|
711
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
712
|
+
state.pageKey = getPageKey();
|
|
713
|
+
ensureObserver();
|
|
714
|
+
|
|
715
|
+
// CRITIQUE: Attendre 300ms avant de permettre l'insertion de nouveaux placeholders
|
|
716
|
+
// pour laisser les anciens showAds() (en cours) se terminer ou échouer proprement
|
|
717
|
+
// Sinon race condition: NodeBB vide le DOM pendant que Ezoic essaie d'accéder aux placeholders
|
|
718
|
+
state.canShowAds = true;
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
$(window).on('action:category.loaded.ezoicInfinite', () => {
|
|
722
|
+
ensureObserver();
|
|
723
|
+
// category.loaded = infinite scroll, Ezoic déjà chargé normalement
|
|
724
|
+
waitForContentThenRun();
|
|
725
|
+
});
|
|
726
|
+
$(window).on('action:topics.loaded.ezoicInfinite', () => {
|
|
727
|
+
ensureObserver();
|
|
728
|
+
waitForContentThenRun();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
$(window).on('action:topic.loaded.ezoicInfinite', () => {
|
|
732
|
+
ensureObserver();
|
|
733
|
+
waitForContentThenRun();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
$(window).on('action:posts.loaded.ezoicInfinite', () => {
|
|
737
|
+
ensureObserver();
|
|
738
|
+
// posts.loaded = infinite scroll
|
|
739
|
+
waitForContentThenRun();
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function bindScroll() {
|
|
744
|
+
if (state.lastScrollRun > 0) return;
|
|
745
|
+
state.lastScrollRun = Date.now();
|
|
746
|
+
let ticking = false;
|
|
747
|
+
window.addEventListener('scroll', () => {
|
|
748
|
+
if (ticking) return;
|
|
749
|
+
ticking = true;
|
|
750
|
+
window.requestAnimationFrame(() => {
|
|
751
|
+
ticking = false;
|
|
752
|
+
enforceNoAdjacentAds();
|
|
753
|
+
// Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
|
|
754
|
+
const now = Date.now();
|
|
755
|
+
if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
|
|
756
|
+
state.lastScrollRun = now;
|
|
757
|
+
scheduleRun();
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}, { passive: true });
|
|
761
|
+
}
|
|
718
762
|
|
|
719
|
-
|
|
763
|
+
// Fonction qui attend que la page ait assez de contenu avant d'insérer les pubs
|
|
764
|
+
function waitForContentThenRun() {
|
|
765
|
+
const MIN_WORDS = 250;
|
|
766
|
+
let attempts = 0;
|
|
767
|
+
const maxAttempts = 20; // 20 × 200ms = 4s max
|
|
720
768
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
ensureObserver();
|
|
724
|
-
scheduleRun();
|
|
725
|
-
setTimeout(scheduleRun, 200);
|
|
726
|
-
setTimeout(scheduleRun, 700);
|
|
727
|
-
});
|
|
769
|
+
(function check() {
|
|
770
|
+
attempts++;
|
|
728
771
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
setTimeout(scheduleRun, 250);
|
|
733
|
-
});
|
|
772
|
+
// Compter les mots sur la page
|
|
773
|
+
const text = document.body.innerText || '';
|
|
774
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
734
775
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
776
|
+
if (wordCount >= MIN_WORDS) {
|
|
777
|
+
// Assez de contenu → lancer l'insertion
|
|
778
|
+
scheduleRun();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
740
781
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
782
|
+
// Pas assez de contenu
|
|
783
|
+
if (attempts >= maxAttempts) {
|
|
784
|
+
// Timeout après 4s → tenter quand même
|
|
785
|
+
scheduleRun();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
746
788
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
789
|
+
// Réessayer dans 200ms
|
|
790
|
+
setTimeout(check, 50);
|
|
791
|
+
})();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Fonction qui attend que Ezoic soit vraiment chargé
|
|
795
|
+
function waitForEzoicThenRun() {
|
|
796
|
+
let attempts = 0;
|
|
797
|
+
const maxAttempts = 50; // 50 × 200ms = 10s max
|
|
798
|
+
|
|
799
|
+
(function check() {
|
|
800
|
+
attempts++;
|
|
801
|
+
// Vérifier si Ezoic est chargé
|
|
802
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
803
|
+
// Ezoic est prêt → lancer l'insertion
|
|
804
|
+
scheduleRun();
|
|
805
|
+
waitForContentThenRun();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// Ezoic pas encore prêt
|
|
809
|
+
if (attempts >= maxAttempts) {
|
|
810
|
+
// Tenter quand même
|
|
811
|
+
scheduleRun();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// Réessayer dans 200ms
|
|
815
|
+
setTimeout(check, 50);
|
|
816
|
+
})();
|
|
752
817
|
}
|
|
753
818
|
|
|
754
819
|
cleanup();
|
|
755
820
|
bind();
|
|
821
|
+
bindScroll();
|
|
756
822
|
ensureObserver();
|
|
757
823
|
state.pageKey = getPageKey();
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (state.__scrollBound) return;
|
|
763
|
-
state.__scrollBound = true;
|
|
764
|
-
let ticking = false;
|
|
765
|
-
window.addEventListener('scroll', () => {
|
|
766
|
-
if (ticking) return;
|
|
767
|
-
ticking = true;
|
|
768
|
-
window.requestAnimationFrame(() => {
|
|
769
|
-
ticking = false;
|
|
770
|
-
enforceNoAdjacentAds();
|
|
771
|
-
scheduleRefill(200);
|
|
772
|
-
});
|
|
773
|
-
}, { passive: true });
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
;
|
|
824
|
+
|
|
825
|
+
// Attendre que Ezoic soit chargé avant d'insérer
|
|
826
|
+
waitForEzoicThenRun();
|
|
827
|
+
})();
|