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