nodebb-plugin-ezoic-infinite 1.6.78 → 1.6.80
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/library.js +33 -97
- package/package.json +1 -1
- package/public/client.js +167 -1448
- package/public/style.css +20 -64
package/public/client.js
CHANGED
|
@@ -1,1510 +1,229 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// Track scroll direction to avoid aggressive recycling when the user scrolls upward.
|
|
5
|
-
// Recycling while scrolling up is a common cause of ads "bunching" and a "disappearing too fast" feeling.
|
|
6
4
|
let lastScrollY = 0;
|
|
7
|
-
let scrollDir = 1;
|
|
5
|
+
let scrollDir = 1;
|
|
8
6
|
try {
|
|
9
7
|
lastScrollY = window.scrollY || 0;
|
|
10
|
-
window.addEventListener(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
{ passive: true }
|
|
21
|
-
);
|
|
8
|
+
window.addEventListener('scroll', () => {
|
|
9
|
+
const y = window.scrollY || 0;
|
|
10
|
+
const d = y - lastScrollY;
|
|
11
|
+
if (Math.abs(d) > 4) {
|
|
12
|
+
scrollDir = d > 0 ? 1 : -1;
|
|
13
|
+
lastScrollY = y;
|
|
14
|
+
}
|
|
15
|
+
}, { passive: true });
|
|
22
16
|
} catch (e) {}
|
|
23
17
|
|
|
24
|
-
// NodeBB client context
|
|
25
|
-
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
26
|
-
|
|
27
|
-
// IMPORTANT: must NOT collide with Ezoic's own markup (they use `.ezoic-ad`).
|
|
28
18
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
29
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
30
19
|
const POOL_ID = 'nodebb-ezoic-placeholder-pool';
|
|
31
|
-
|
|
32
|
-
// Smoothness caps
|
|
33
|
-
// Limit how many placements we inject per scan pass.
|
|
34
|
-
// Too low = you end up with only a handful of placeholders after ajaxify.
|
|
35
|
-
// Too high = jank on very long pages.
|
|
36
20
|
const MAX_INSERTS_PER_RUN = 8;
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function keepEmptyWrapMs() { return isMobile() ? 120000 : 60000; }
|
|
42
|
-
|
|
43
|
-
// Preload margins
|
|
44
|
-
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
45
|
-
// Mobile: larger preload window so ad fill requests start earlier and
|
|
46
|
-
// users don't scroll past empty placeholders.
|
|
47
|
-
const PRELOAD_MARGIN_MOBILE = '3200px 0px 3200px 0px';
|
|
48
|
-
const PRELOAD_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
|
|
49
|
-
const PRELOAD_MARGIN_MOBILE_BOOST = '5200px 0px 5200px 0px';
|
|
50
|
-
|
|
51
|
-
const BOOST_DURATION_MS = 2500;
|
|
52
|
-
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
53
|
-
|
|
54
|
-
const MAX_INFLIGHT_DESKTOP = 4;
|
|
55
|
-
const MAX_INFLIGHT_MOBILE = 3;
|
|
56
|
-
|
|
57
|
-
const SELECTORS = {
|
|
58
|
-
topicItem: 'li[component="category/topic"]',
|
|
59
|
-
postItem: '[component="post"][data-pid]',
|
|
60
|
-
categoryItem: 'li[component="categories/category"]',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Production build: debug disabled
|
|
64
|
-
function dbg() {}
|
|
65
|
-
|
|
66
|
-
// Ezoic (and some partner scripts) can be very noisy in console on SPA/Ajaxify setups.
|
|
67
|
-
// These warnings are not actionable for end-users and can flood the console.
|
|
68
|
-
// We selectively silence the known spam patterns while keeping other warnings intact.
|
|
69
|
-
function muteNoisyConsole() {
|
|
70
|
-
try {
|
|
71
|
-
if (window.__nodebbEzoicConsoleMuted) return;
|
|
72
|
-
window.__nodebbEzoicConsoleMuted = true;
|
|
73
|
-
|
|
74
|
-
const shouldMute = (args) => {
|
|
75
|
-
try {
|
|
76
|
-
if (!args || !args.length) return false;
|
|
77
|
-
const s0 = typeof args[0] === 'string' ? args[0] : '';
|
|
78
|
-
// Duplicate placeholder definition spam (common when reusing ids in SPA/Ajaxify).
|
|
79
|
-
if (s0.includes('[EzoicAds JS]: Placeholder Id') && s0.includes('has already been defined')) return true;
|
|
80
|
-
// Ezoic debugger iframe spam.
|
|
81
|
-
if (s0.includes('Debugger iframe already exists')) return true;
|
|
82
|
-
// Missing placeholder spam (we already guard showAds; Ezoic still logs sometimes).
|
|
83
|
-
if (s0.includes('HTML element with id ezoic-pub-ad-placeholder-') && s0.includes('does not exist')) return true;
|
|
84
|
-
return false;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const wrap = (method) => {
|
|
91
|
-
const orig = console[method];
|
|
92
|
-
if (typeof orig !== 'function') return;
|
|
93
|
-
console[method] = function (...args) {
|
|
94
|
-
if (shouldMute(args)) return;
|
|
95
|
-
return orig.apply(console, args);
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
wrap('log');
|
|
100
|
-
wrap('info');
|
|
101
|
-
wrap('warn');
|
|
102
|
-
wrap('error');
|
|
103
|
-
} catch (e) {}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Some CMP/TCF stubs rely on a hidden iframe named `__tcfapiLocator` to route postMessage calls.
|
|
107
|
-
// In SPA/Ajaxify navigations, that iframe can be removed/replaced unexpectedly, causing noisy
|
|
108
|
-
// "postMessage" / "addtlConsent" null errors. Ensuring it's present makes the environment stable.
|
|
109
|
-
function ensureTcfApiLocator() {
|
|
110
|
-
try {
|
|
111
|
-
// If a CMP is not present, do nothing.
|
|
112
|
-
if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
|
|
113
|
-
if (document.getElementById('__tcfapiLocator')) return;
|
|
114
|
-
const f = document.createElement('iframe');
|
|
115
|
-
f.style.display = 'none';
|
|
116
|
-
f.id = '__tcfapiLocator';
|
|
117
|
-
f.name = '__tcfapiLocator';
|
|
118
|
-
(document.body || document.documentElement).appendChild(f);
|
|
119
|
-
} catch (e) {}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
function isFilledNode(node) {
|
|
125
|
-
return !!(node && node.querySelector && node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Ezoic injects inline `min-height:400px !important` on one or more nested wrappers.
|
|
129
|
-
// If the creative is 250px, this leaves ~150px empty space. Because it's inline+important,
|
|
130
|
-
// CSS alone cannot fix it reliably — we must rewrite the inline style.
|
|
131
|
-
function tightenEzoicMinHeight(wrap) {
|
|
132
|
-
try {
|
|
133
|
-
if (!wrap || !wrap.querySelector) return;
|
|
134
|
-
|
|
135
|
-
const iframes = wrap.querySelectorAll('iframe');
|
|
136
|
-
if (!iframes || !iframes.length) return;
|
|
137
|
-
|
|
138
|
-
// Find the closest "big" ezoic container that carries the 400px min-height.
|
|
139
|
-
const firstIframe = iframes[0];
|
|
140
|
-
let refNode = null;
|
|
141
|
-
let p = firstIframe && firstIframe.parentElement;
|
|
142
|
-
while (p && p !== wrap) {
|
|
143
|
-
if (p.classList && p.classList.contains('ezoic-ad')) {
|
|
144
|
-
const st = (p.getAttribute('style') || '').toLowerCase();
|
|
145
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
146
|
-
refNode = p;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
p = p.parentElement;
|
|
151
|
-
}
|
|
152
|
-
if (!refNode) {
|
|
153
|
-
refNode = wrap.querySelector('.ezoic-ad-adaptive') || wrap.querySelector('.ezoic-ad') || wrap;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let refTop = 0;
|
|
157
|
-
try { refTop = refNode.getBoundingClientRect().top; } catch (e) { refTop = 0; }
|
|
158
|
-
|
|
159
|
-
// Compute the rendered height needed inside refNode (visible iframes only).
|
|
160
|
-
let maxBottom = 0;
|
|
161
|
-
iframes.forEach((f) => {
|
|
162
|
-
if (!f || !f.getBoundingClientRect) return;
|
|
163
|
-
const rect = f.getBoundingClientRect();
|
|
164
|
-
if (rect.width <= 1 || rect.height <= 1) return;
|
|
165
|
-
const bottom = rect.bottom - refTop;
|
|
166
|
-
maxBottom = Math.max(maxBottom, bottom);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Fallback to attr/offset if layout metrics are not available.
|
|
170
|
-
if (!maxBottom) {
|
|
171
|
-
iframes.forEach((f) => {
|
|
172
|
-
const ah = parseInt(f.getAttribute('height') || '0', 10);
|
|
173
|
-
const oh = f.offsetHeight || 0;
|
|
174
|
-
maxBottom = Math.max(maxBottom, ah, oh);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (!maxBottom) return;
|
|
178
|
-
|
|
179
|
-
// NOTE: Do NOT add the Ezoic badge (reportline) height here.
|
|
180
|
-
// It is absolutely positioned and should not reserve layout space.
|
|
181
|
-
|
|
182
|
-
const h = Math.max(1, Math.ceil(maxBottom));
|
|
183
|
-
|
|
184
|
-
const tightenNode = (node) => {
|
|
185
|
-
if (!node || !node.style) return;
|
|
186
|
-
try { node.style.setProperty('min-height', h + 'px', 'important'); } catch (e) { node.style.minHeight = h + 'px'; }
|
|
187
|
-
try { node.style.setProperty('height', 'auto', 'important'); } catch (e) {}
|
|
188
|
-
try { node.style.setProperty('line-height', '0', 'important'); } catch (e) {}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Tighten refNode and any ancestor ezoic-ad nodes with the problematic min-height.
|
|
192
|
-
let cur = refNode;
|
|
193
|
-
while (cur && cur !== wrap) {
|
|
194
|
-
if (cur.classList && cur.classList.contains('ezoic-ad')) {
|
|
195
|
-
const st = (cur.getAttribute('style') || '').toLowerCase();
|
|
196
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
197
|
-
tightenNode(cur);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
cur = cur.parentElement;
|
|
201
|
-
}
|
|
202
|
-
tightenNode(refNode);
|
|
203
|
-
|
|
204
|
-
// Tighten any nested wrappers that also have the 400px min-height inline.
|
|
205
|
-
refNode.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive').forEach((n) => {
|
|
206
|
-
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
207
|
-
if (st.includes('min-height:400') || st.includes('min-height: 400') || st.includes('min-height:400px')) {
|
|
208
|
-
tightenNode(n);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Mobile friendliness: avoid giant fixed widths causing overflow/reflow.
|
|
213
|
-
if (isMobile()) {
|
|
214
|
-
[refNode].forEach((n) => {
|
|
215
|
-
try { n.style.setProperty('width', '100%', 'important'); } catch (e) {}
|
|
216
|
-
try { n.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
|
|
217
|
-
try { n.style.setProperty('min-width', '0', 'important'); } catch (e) {}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function watchWrapForFill(wrap) {
|
|
224
|
-
try {
|
|
225
|
-
if (!wrap || wrap.__ezFillObs) return;
|
|
226
|
-
|
|
227
|
-
// Ezoic can (re)apply inline styles after fill; keep tightening for a short window.
|
|
228
|
-
const start = now();
|
|
229
|
-
const tightenBurst = () => {
|
|
230
|
-
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
231
|
-
if (now() - start < 6000) {
|
|
232
|
-
setTimeout(tightenBurst, 350);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const obs = new MutationObserver((muts) => {
|
|
237
|
-
// If anything that looks like ad content appears, treat as filled.
|
|
238
|
-
if (isFilledNode(wrap)) {
|
|
239
|
-
wrap.classList.remove('is-empty');
|
|
240
|
-
tightenBurst();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// If Ezoic changes inline style on descendants (min-height:400!important), tighten again.
|
|
244
|
-
for (const m of muts) {
|
|
245
|
-
if (m.type === 'attributes' && m.attributeName === 'style') {
|
|
246
|
-
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Disconnect only after the burst window to avoid missing late style rewrites.
|
|
252
|
-
if (now() - start > 7000) {
|
|
253
|
-
try { obs.disconnect(); } catch (e) {}
|
|
254
|
-
wrap.__ezFillObs = null;
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
259
|
-
wrap.__ezFillObs = obs;
|
|
260
|
-
} catch (e) {}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Global safety net: sometimes Ezoic swaps nodes in ways that bypass our per-wrap observers.
|
|
264
|
-
// When we see an Ezoic container with min-height:400!important inside posts/topics, shrink it.
|
|
265
|
-
function globalGapFixInit() {
|
|
266
|
-
try {
|
|
267
|
-
if (window.__nodebbEzoicGapFix) return;
|
|
268
|
-
window.__nodebbEzoicGapFix = true;
|
|
269
|
-
|
|
270
|
-
// Observe only the main content area to minimize overhead.
|
|
271
|
-
const root = document.getElementById('content') || document.querySelector('[component="content"], #panel') || document.body;
|
|
272
|
-
|
|
273
|
-
const inPostArea = (el) => {
|
|
274
|
-
try {
|
|
275
|
-
return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
|
|
276
|
-
} catch (e) { return false; }
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const maybeFix = (root) => {
|
|
280
|
-
if (!root || !root.querySelectorAll) return;
|
|
281
|
-
const nodes = root.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]');
|
|
282
|
-
nodes.forEach((n) => {
|
|
283
|
-
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
284
|
-
if (!st.includes('min-height:400')) return;
|
|
285
|
-
if (!inPostArea(n)) return;
|
|
286
|
-
try {
|
|
287
|
-
const tmpWrap = n.closest('.' + WRAP_CLASS) || n.parentElement;
|
|
288
|
-
tightenEzoicMinHeight(tmpWrap || n);
|
|
289
|
-
} catch (e) {}
|
|
290
|
-
});
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
requestAnimationFrame(() => maybeFix(root));
|
|
294
|
-
|
|
295
|
-
// Batch DOM mutation processing into a single rAF to avoid doing work per mutation.
|
|
296
|
-
const pending = new Set();
|
|
297
|
-
let scheduled = false;
|
|
298
|
-
const scheduleFlush = () => {
|
|
299
|
-
if (scheduled) return;
|
|
300
|
-
scheduled = true;
|
|
301
|
-
requestAnimationFrame(() => {
|
|
302
|
-
scheduled = false;
|
|
303
|
-
pending.forEach((n) => {
|
|
304
|
-
try { maybeFix(n); } catch (e) {}
|
|
305
|
-
});
|
|
306
|
-
pending.clear();
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const obs = new MutationObserver((muts) => {
|
|
311
|
-
for (const m of muts) {
|
|
312
|
-
if (m.type === 'attributes') {
|
|
313
|
-
const t = m.target && m.target.nodeType === 1 ? m.target : m.target && m.target.parentElement;
|
|
314
|
-
if (t) pending.add(t);
|
|
315
|
-
} else if (m.addedNodes && m.addedNodes.length) {
|
|
316
|
-
m.addedNodes.forEach((n) => {
|
|
317
|
-
if (n && n.nodeType === 1) pending.add(n);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (pending.size) scheduleFlush();
|
|
322
|
-
});
|
|
323
|
-
obs.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
|
|
324
|
-
} catch (e) {}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ---------------- state ----------------
|
|
328
|
-
|
|
329
|
-
const state = {
|
|
330
|
-
pageKey: null,
|
|
331
|
-
cfg: null,
|
|
332
|
-
|
|
333
|
-
// pools (full lists) + cursors
|
|
334
|
-
allTopics: [],
|
|
335
|
-
allPosts: [],
|
|
336
|
-
allCategories: [],
|
|
337
|
-
curTopics: 0,
|
|
338
|
-
curPosts: 0,
|
|
339
|
-
curCategories: 0,
|
|
340
|
-
|
|
341
|
-
// per-id throttle
|
|
342
|
-
lastShowById: new Map(),
|
|
343
|
-
|
|
344
|
-
// observers / schedulers
|
|
345
|
-
domObs: null,
|
|
346
|
-
io: null,
|
|
347
|
-
ioMargin: null,
|
|
348
|
-
|
|
349
|
-
// internal mutations guard
|
|
350
|
-
internalDomChange: 0,
|
|
351
|
-
|
|
352
|
-
// preloading budget
|
|
353
|
-
inflight: 0,
|
|
354
|
-
pending: [],
|
|
355
|
-
pendingSet: new Set(),
|
|
356
|
-
|
|
357
|
-
// scroll boost
|
|
358
|
-
scrollBoostUntil: 0,
|
|
359
|
-
lastScrollY: 0,
|
|
360
|
-
lastScrollTs: 0,
|
|
361
|
-
|
|
362
|
-
// hero
|
|
363
|
-
heroDoneForPage: false,
|
|
364
|
-
|
|
365
|
-
// run scheduler
|
|
366
|
-
runQueued: false,
|
|
367
|
-
burstActive: false,
|
|
368
|
-
burstDeadline: 0,
|
|
369
|
-
burstCount: 0,
|
|
370
|
-
lastBurstReqTs: 0,
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
// Soft block during navigation / heavy DOM churn
|
|
374
|
-
let blockedUntil = 0;
|
|
375
|
-
const insertingIds = new Set();
|
|
376
|
-
|
|
377
|
-
function now() { return Date.now(); }
|
|
378
|
-
function isBlocked() { return now() < blockedUntil; }
|
|
379
|
-
|
|
380
|
-
// ---------------- utils ----------------
|
|
381
|
-
|
|
382
|
-
function normalizeBool(v) {
|
|
383
|
-
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function uniqInts(lines) {
|
|
387
|
-
const out = [];
|
|
388
|
-
const seen = new Set();
|
|
389
|
-
for (const v of lines) {
|
|
390
|
-
const n = parseInt(v, 10);
|
|
391
|
-
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
392
|
-
seen.add(n);
|
|
393
|
-
out.push(n);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return out;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function parsePool(raw) {
|
|
400
|
-
if (!raw) return [];
|
|
401
|
-
const lines = String(raw)
|
|
402
|
-
.split(/\r?\n/)
|
|
403
|
-
.map(s => s.trim())
|
|
404
|
-
.filter(Boolean);
|
|
405
|
-
return uniqInts(lines);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function getPageKey() {
|
|
409
|
-
try {
|
|
410
|
-
const ax = window.ajaxify;
|
|
411
|
-
if (ax && ax.data) {
|
|
412
|
-
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
413
|
-
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
414
|
-
}
|
|
415
|
-
} catch (e) {}
|
|
416
|
-
return window.location.pathname;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function isMobile() {
|
|
420
|
-
try { return window.innerWidth < 768; } catch (e) { return false; }
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function isBoosted() {
|
|
424
|
-
return now() < (state.scrollBoostUntil || 0);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function getPreloadRootMargin() {
|
|
428
|
-
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
429
|
-
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function getMaxInflight() {
|
|
433
|
-
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
434
|
-
return base + (isBoosted() ? 1 : 0);
|
|
435
|
-
}
|
|
22
|
+
let config = null;
|
|
23
|
+
let isInternalChange = false;
|
|
24
|
+
let ezEnabled = false; // Flag pour savoir si ezstandalone.enable() a été appelé
|
|
436
25
|
|
|
437
26
|
function withInternalDomChange(fn) {
|
|
438
|
-
|
|
439
|
-
try { fn(); } finally {
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ---------------- DOM helpers ----------------
|
|
443
|
-
|
|
444
|
-
function getTopicItems() {
|
|
445
|
-
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function getCategoryItems() {
|
|
449
|
-
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function getPostContainers() {
|
|
453
|
-
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
454
|
-
return nodes.filter((el) => {
|
|
455
|
-
if (!el || !el.isConnected) return false;
|
|
456
|
-
if (!el.querySelector('[component="post/content"]')) return false;
|
|
457
|
-
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
458
|
-
if (parentPost && parentPost !== el) return false;
|
|
459
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
460
|
-
return true;
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function getKind() {
|
|
465
|
-
const p = window.location.pathname || '';
|
|
466
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
467
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
468
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
469
|
-
|
|
470
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
471
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
472
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
473
|
-
return 'other';
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function isAdjacentAd(target) {
|
|
477
|
-
if (!target) return false;
|
|
478
|
-
const next = target.nextElementSibling;
|
|
479
|
-
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
480
|
-
const prev = target.previousElementSibling;
|
|
481
|
-
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function findWrap(kindClass, afterPos) {
|
|
486
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ---------------- placeholder pool ----------------
|
|
490
|
-
|
|
491
|
-
function getPoolEl() {
|
|
492
|
-
let el = document.getElementById(POOL_ID);
|
|
493
|
-
if (el) return el;
|
|
494
|
-
el = document.createElement('div');
|
|
495
|
-
el.id = POOL_ID;
|
|
496
|
-
el.style.cssText = 'position:fixed;left:-99999px;top:-99999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;';
|
|
497
|
-
(document.body || document.documentElement).appendChild(el);
|
|
498
|
-
return el;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function primePlaceholderPool(allIds) {
|
|
502
|
-
try {
|
|
503
|
-
if (!Array.isArray(allIds) || !allIds.length) return;
|
|
504
|
-
const pool = getPoolEl();
|
|
505
|
-
for (const id of allIds) {
|
|
506
|
-
if (!id) continue;
|
|
507
|
-
const domId = `${PLACEHOLDER_PREFIX}${id}`;
|
|
508
|
-
if (document.getElementById(domId)) continue;
|
|
509
|
-
const ph = document.createElement('div');
|
|
510
|
-
ph.id = domId;
|
|
511
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
512
|
-
pool.appendChild(ph);
|
|
513
|
-
}
|
|
514
|
-
} catch (e) {}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function isInPool(ph) {
|
|
518
|
-
try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function releaseWrapNode(wrap) {
|
|
522
|
-
try {
|
|
523
|
-
const ph = wrap && wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
524
|
-
if (ph) {
|
|
525
|
-
try { getPoolEl().appendChild(ph); } catch (e) {}
|
|
526
|
-
try { state.io && state.io.unobserve(ph); } catch (e) {}
|
|
527
|
-
}
|
|
528
|
-
} catch (e) {}
|
|
529
|
-
try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
|
|
27
|
+
isInternalChange = true;
|
|
28
|
+
try { fn(); } finally { isInternalChange = false; }
|
|
530
29
|
}
|
|
531
30
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const links = [
|
|
540
|
-
// Ezoic
|
|
541
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
542
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
543
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
544
|
-
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
545
|
-
// Google ads
|
|
546
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
547
|
-
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
548
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
549
|
-
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
550
|
-
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
551
|
-
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
552
|
-
];
|
|
553
|
-
for (const [rel, href, cors] of links) {
|
|
554
|
-
const key = `${rel}|${href}`;
|
|
555
|
-
if (_warmLinksDone.has(key)) continue;
|
|
556
|
-
_warmLinksDone.add(key);
|
|
557
|
-
const link = document.createElement('link');
|
|
558
|
-
link.rel = rel;
|
|
559
|
-
link.href = href;
|
|
560
|
-
if (cors) link.crossOrigin = 'anonymous';
|
|
561
|
-
head.appendChild(link);
|
|
562
|
-
}
|
|
563
|
-
} catch (e) {}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// ---------------- Ezoic bridge ----------------
|
|
567
|
-
|
|
568
|
-
// Patch showAds to silently skip ids not in DOM. This prevents console spam.
|
|
569
|
-
function patchShowAds() {
|
|
570
|
-
const applyPatch = () => {
|
|
571
|
-
try {
|
|
572
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
573
|
-
const ez = window.ezstandalone;
|
|
574
|
-
if (window.__nodebbEzoicPatched) return;
|
|
575
|
-
if (typeof ez.showAds !== 'function') return;
|
|
576
|
-
|
|
577
|
-
window.__nodebbEzoicPatched = true;
|
|
578
|
-
const orig = ez.showAds;
|
|
579
|
-
|
|
580
|
-
ez.showAds = function (...args) {
|
|
581
|
-
if (isBlocked()) return;
|
|
582
|
-
|
|
583
|
-
let ids = [];
|
|
584
|
-
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
585
|
-
else ids = args;
|
|
586
|
-
|
|
587
|
-
const seen = new Set();
|
|
588
|
-
for (const v of ids) {
|
|
589
|
-
const id = parseInt(v, 10);
|
|
590
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
591
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
592
|
-
if (!ph || !ph.isConnected) continue;
|
|
593
|
-
seen.add(id);
|
|
594
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
} catch (e) {}
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
applyPatch();
|
|
601
|
-
if (!window.__nodebbEzoicPatched) {
|
|
602
|
-
try {
|
|
603
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
604
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
605
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
606
|
-
} catch (e) {}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// ---------------- config ----------------
|
|
611
|
-
|
|
612
|
-
async function fetchConfigOnce() {
|
|
613
|
-
if (state.cfg) return state.cfg;
|
|
614
|
-
try {
|
|
615
|
-
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
616
|
-
if (!res.ok) return null;
|
|
617
|
-
state.cfg = await res.json();
|
|
618
|
-
return state.cfg;
|
|
619
|
-
} catch (e) {
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function initPools(cfg) {
|
|
625
|
-
if (!cfg) return;
|
|
626
|
-
if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
|
|
627
|
-
if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
628
|
-
if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
629
|
-
|
|
630
|
-
// IMPORTANT:
|
|
631
|
-
// We do NOT prime a DOM pool anymore.
|
|
632
|
-
// Keeping placeholders connected (even offscreen) can lead Ezoic/GPT to
|
|
633
|
-
// pre-define slots, which then causes "Placeholder Id X has already been defined".
|
|
634
|
-
// Instead, we create the placeholder element only when we actually inject its wrapper.
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// ---------------- insertion primitives ----------------
|
|
638
|
-
|
|
639
|
-
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
640
|
-
const wrap = document.createElement('div');
|
|
641
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
642
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
643
|
-
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
644
|
-
wrap.setAttribute('data-created', String(now()));
|
|
645
|
-
// "Pinned" placements (after the first item) should remain stable.
|
|
646
|
-
if (afterPos === 1) {
|
|
647
|
-
wrap.setAttribute('data-ezoic-pin', '1');
|
|
648
|
-
}
|
|
649
|
-
wrap.style.width = '100%';
|
|
650
|
-
|
|
651
|
-
if (createPlaceholder) {
|
|
652
|
-
const ph = document.createElement('div');
|
|
653
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
654
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
655
|
-
wrap.appendChild(ph);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return wrap;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function insertAfter(target, id, kindClass, afterPos) {
|
|
662
|
-
if (!target || !target.insertAdjacentElement) return null;
|
|
663
|
-
if (findWrap(kindClass, afterPos)) return null;
|
|
664
|
-
if (insertingIds.has(id)) return null;
|
|
665
|
-
|
|
666
|
-
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
667
|
-
|
|
668
|
-
insertingIds.add(id);
|
|
669
|
-
try {
|
|
670
|
-
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
671
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
672
|
-
|
|
673
|
-
// If placeholder exists elsewhere (including pool), move it into the wrapper.
|
|
674
|
-
if (existingPh) {
|
|
675
|
-
try {
|
|
676
|
-
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
677
|
-
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
678
|
-
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
679
|
-
} catch (e) {}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return wrap;
|
|
683
|
-
} finally {
|
|
684
|
-
insertingIds.delete(id);
|
|
31
|
+
function getPool() {
|
|
32
|
+
let p = document.getElementById(POOL_ID);
|
|
33
|
+
if (!p) {
|
|
34
|
+
p = document.createElement('div');
|
|
35
|
+
p.id = POOL_ID;
|
|
36
|
+
p.style.display = 'none';
|
|
37
|
+
document.body.appendChild(p);
|
|
685
38
|
}
|
|
39
|
+
return p;
|
|
686
40
|
}
|
|
687
41
|
|
|
688
|
-
function
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
695
|
-
const id = allIds[idx];
|
|
696
|
-
|
|
697
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
698
|
-
if (ph && ph.isConnected && !isInPool(ph)) continue;
|
|
699
|
-
|
|
700
|
-
return id;
|
|
42
|
+
function releaseWrapNode(wrap) {
|
|
43
|
+
if (!wrap) return;
|
|
44
|
+
const pool = getPool();
|
|
45
|
+
wrap.classList.remove('ez-orphan-hidden');
|
|
46
|
+
if (wrap.parentNode !== pool) {
|
|
47
|
+
pool.appendChild(wrap);
|
|
701
48
|
}
|
|
702
|
-
|
|
703
|
-
return null;
|
|
704
49
|
}
|
|
705
50
|
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
// nearby post containers, which leads to ads clustering together when scrolling back up.
|
|
710
|
-
// We prune only *true* orphans that are far offscreen to keep the UI stable.
|
|
711
|
-
if (!items || !items.length) return 0;
|
|
712
|
-
const itemSet = new Set(items);
|
|
713
|
-
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
714
|
-
let removed = 0;
|
|
715
|
-
|
|
716
|
-
const isFilled = (wrap) => {
|
|
717
|
-
return !!(wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
const hasNearbyItem = (wrap) => {
|
|
721
|
-
// NodeBB/skins can inject separators/spacers; be tolerant.
|
|
722
|
-
let prev = wrap.previousElementSibling;
|
|
723
|
-
for (let i = 0; i < 14 && prev; i++) {
|
|
724
|
-
if (itemSet.has(prev)) return true;
|
|
725
|
-
prev = prev.previousElementSibling;
|
|
726
|
-
}
|
|
51
|
+
function decluster(container) {
|
|
52
|
+
const wraps = Array.from(container.querySelectorAll(`.${WRAP_CLASS}`));
|
|
53
|
+
wraps.forEach(wrap => {
|
|
727
54
|
let next = wrap.nextElementSibling;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
next = next.nextElementSibling;
|
|
55
|
+
if (next && next.classList.contains(WRAP_CLASS)) {
|
|
56
|
+
withInternalDomChange(() => releaseWrapNode(next));
|
|
731
57
|
}
|
|
732
|
-
return false;
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
wraps.forEach((wrap) => {
|
|
736
|
-
// Never prune pinned placements.
|
|
737
|
-
try {
|
|
738
|
-
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
739
|
-
} catch (e) {}
|
|
740
|
-
|
|
741
|
-
// For message/topic pages we may prune filled or empty orphans if they are far away,
|
|
742
|
-
// otherwise consecutive "stacks" can appear when posts are virtualized.
|
|
743
|
-
const isMessage = (kindClass === 'ezoic-ad-message');
|
|
744
|
-
if (!isMessage && isFilled(wrap)) return; // never prune filled ads for non-message lists
|
|
745
|
-
|
|
746
|
-
// Never prune a fresh wrap: it may fill late.
|
|
747
|
-
try {
|
|
748
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
749
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
750
|
-
} catch (e) {}
|
|
751
|
-
|
|
752
|
-
if (hasNearbyItem(wrap)) {
|
|
753
|
-
try { wrap.classList && wrap.classList.remove('ez-orphan-hidden'); wrap.style && (wrap.style.display = ''); } catch (e) {}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// If the anchor item is no longer in the DOM (virtualized), hide the wrap so ads never "stack"
|
|
758
|
-
// back-to-back while scrolling. We'll recycle it when its anchor comes back.
|
|
759
|
-
try { wrap.classList && wrap.classList.add('ez-orphan-hidden'); wrap.style && (wrap.style.display = 'none'); } catch (e) {}
|
|
760
|
-
|
|
761
|
-
// For message ads: only release if far offscreen to avoid perceived "vanishing" during fast scroll.
|
|
762
|
-
if (isMessage) {
|
|
763
|
-
try {
|
|
764
|
-
const r = wrap.getBoundingClientRect();
|
|
765
|
-
const vh = Math.max(1, window.innerHeight || 1);
|
|
766
|
-
const farAbove = r.bottom < -vh * 2;
|
|
767
|
-
const farBelow = r.top > vh * 4;
|
|
768
|
-
if (!farAbove && !farBelow) return;
|
|
769
|
-
} catch (e) {
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
775
|
-
removed++;
|
|
776
58
|
});
|
|
777
|
-
|
|
778
|
-
return removed;
|
|
779
59
|
}
|
|
780
60
|
|
|
781
|
-
function
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
787
|
-
|
|
788
|
-
const isFilled = (wrap) => {
|
|
789
|
-
return !!(wrap && wrap.querySelector && wrap.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
const isFresh = (wrap) => {
|
|
793
|
-
try {
|
|
794
|
-
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
795
|
-
return created && (now() - created) < keepEmptyWrapMs();
|
|
796
|
-
} catch (e) {
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
let removed = 0;
|
|
802
|
-
for (const w of wraps) {
|
|
803
|
-
// Never decluster pinned placements.
|
|
804
|
-
try {
|
|
805
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
806
|
-
} catch (e) {}
|
|
61
|
+
function pruneOrphanWraps() {
|
|
62
|
+
const wraps = document.querySelectorAll(`.${WRAP_CLASS}`);
|
|
63
|
+
const items = document.querySelectorAll('[component="category/topic"], [component="post"]');
|
|
64
|
+
const itemSet = new Set(items);
|
|
807
65
|
|
|
808
|
-
|
|
66
|
+
wraps.forEach(wrap => {
|
|
67
|
+
if (wrap.parentElement && wrap.parentElement.id === POOL_ID) return;
|
|
68
|
+
let hasNeighbor = false;
|
|
69
|
+
let prev = wrap.previousElementSibling;
|
|
809
70
|
for (let i = 0; i < 3 && prev; i++) {
|
|
810
|
-
if (
|
|
811
|
-
// If the previous wrap is pinned, keep this one (spacing is intentional).
|
|
812
|
-
try {
|
|
813
|
-
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
814
|
-
} catch (e) {}
|
|
815
|
-
|
|
816
|
-
// Never remove a wrap that is already filled; otherwise it looks like
|
|
817
|
-
// ads "disappear" while scrolling. Only remove the empty neighbour.
|
|
818
|
-
const prevFilled = isFilled(prev);
|
|
819
|
-
const curFilled = isFilled(w);
|
|
820
|
-
|
|
821
|
-
if (curFilled) {
|
|
822
|
-
// If the previous one is empty (and not fresh), drop the previous instead.
|
|
823
|
-
if (!prevFilled && !isFresh(prev)) {
|
|
824
|
-
withInternalDomChange(() => releaseWrapNode(prev));
|
|
825
|
-
removed++;
|
|
826
|
-
}
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Current is empty.
|
|
831
|
-
// Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
|
|
832
|
-
// Only decluster when previous is filled, or when current is stale.
|
|
833
|
-
if (prevFilled || !isFresh(w)) {
|
|
834
|
-
withInternalDomChange(() => releaseWrapNode(w));
|
|
835
|
-
removed++;
|
|
836
|
-
}
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
71
|
+
if (itemSet.has(prev)) { hasNeighbor = true; break; }
|
|
839
72
|
prev = prev.previousElementSibling;
|
|
840
73
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
function ensurePreloadObserver() {
|
|
848
|
-
const desiredMargin = getPreloadRootMargin();
|
|
849
|
-
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
850
|
-
|
|
851
|
-
if (state.io) {
|
|
852
|
-
try { state.io.disconnect(); } catch (e) {}
|
|
853
|
-
state.io = null;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
state.io = new IntersectionObserver((entries) => {
|
|
858
|
-
for (const ent of entries) {
|
|
859
|
-
if (!ent.isIntersecting) continue;
|
|
860
|
-
const el = ent.target;
|
|
861
|
-
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
862
|
-
|
|
863
|
-
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
864
|
-
const id = parseInt(idAttr, 10);
|
|
865
|
-
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
74
|
+
if (!hasNeighbor) {
|
|
75
|
+
let next = wrap.nextElementSibling;
|
|
76
|
+
for (let i = 0; i < 3 && next; i++) {
|
|
77
|
+
if (itemSet.has(next)) { hasNeighbor = true; break; }
|
|
78
|
+
next = next.nextElementSibling;
|
|
866
79
|
}
|
|
867
|
-
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
868
|
-
|
|
869
|
-
state.ioMargin = desiredMargin;
|
|
870
|
-
} catch (e) {
|
|
871
|
-
state.io = null;
|
|
872
|
-
state.ioMargin = null;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Re-observe current placeholders
|
|
876
|
-
try {
|
|
877
|
-
if (state.io) {
|
|
878
|
-
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
879
|
-
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
880
|
-
}
|
|
881
|
-
} catch (e) {}
|
|
882
|
-
|
|
883
|
-
return state.io;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function observePlaceholder(id) {
|
|
887
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
888
|
-
if (!ph || !ph.isConnected) return;
|
|
889
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
890
|
-
|
|
891
|
-
const io = ensurePreloadObserver();
|
|
892
|
-
try { io && io.observe(ph); } catch (e) {}
|
|
893
|
-
|
|
894
|
-
// If already near viewport, fire immediately.
|
|
895
|
-
// Mobile tends to scroll faster + has slower auctions, so we fire earlier.
|
|
896
|
-
try {
|
|
897
|
-
const r = ph.getBoundingClientRect();
|
|
898
|
-
const mobile = isMobile();
|
|
899
|
-
const screens = isBoosted()
|
|
900
|
-
? (mobile ? 9.0 : 5.0)
|
|
901
|
-
: (mobile ? 6.0 : 3.0);
|
|
902
|
-
const minBottom = isBoosted()
|
|
903
|
-
? (mobile ? -2600 : -1500)
|
|
904
|
-
: (mobile ? -1400 : -800);
|
|
905
|
-
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
906
|
-
} catch (e) {}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function enqueueShow(id) {
|
|
910
|
-
if (!id || isBlocked()) return;
|
|
911
|
-
|
|
912
|
-
// per-id throttle
|
|
913
|
-
const t = now();
|
|
914
|
-
const last = state.lastShowById.get(id) || 0;
|
|
915
|
-
if (t - last < 900) return;
|
|
916
|
-
|
|
917
|
-
const max = getMaxInflight();
|
|
918
|
-
if (state.inflight >= max) {
|
|
919
|
-
if (!state.pendingSet.has(id)) {
|
|
920
|
-
state.pending.push(id);
|
|
921
|
-
state.pendingSet.add(id);
|
|
922
80
|
}
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
startShow(id);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function drainQueue() {
|
|
930
|
-
if (isBlocked()) return;
|
|
931
|
-
const max = getMaxInflight();
|
|
932
|
-
while (state.inflight < max && state.pending.length) {
|
|
933
|
-
const id = state.pending.shift();
|
|
934
|
-
state.pendingSet.delete(id);
|
|
935
|
-
startShow(id);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function markEmptyWrapper(id) {
|
|
940
|
-
// If still empty after delay, mark empty for CSS (1px)
|
|
941
|
-
try {
|
|
942
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
943
|
-
if (!ph || !ph.isConnected) return;
|
|
944
|
-
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
945
|
-
if (!wrap) return;
|
|
946
|
-
|
|
947
|
-
setTimeout(() => {
|
|
948
|
-
try {
|
|
949
|
-
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
950
|
-
if (!ph2 || !ph2.isConnected) return;
|
|
951
|
-
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
952
|
-
if (!w2) return;
|
|
953
|
-
|
|
954
|
-
// Don't collapse "fresh" placements; slow auctions/CMP can fill late.
|
|
955
|
-
try {
|
|
956
|
-
const created = parseInt(w2.getAttribute('data-created') || '0', 10);
|
|
957
|
-
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
958
|
-
} catch (e) {}
|
|
959
|
-
|
|
960
|
-
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
961
|
-
if (!hasAd) {
|
|
962
|
-
w2.classList.add('is-empty');
|
|
963
|
-
watchWrapForFill(w2);
|
|
964
|
-
} else {
|
|
965
|
-
w2.classList.remove('is-empty');
|
|
966
|
-
tightenEzoicMinHeight(w2);
|
|
967
|
-
}
|
|
968
|
-
} catch (e) {}
|
|
969
|
-
}, 15000);
|
|
970
|
-
} catch (e) {}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function startShow(id) {
|
|
974
|
-
if (!id || isBlocked()) return;
|
|
975
|
-
|
|
976
|
-
state.inflight++;
|
|
977
|
-
let released = false;
|
|
978
|
-
const release = () => {
|
|
979
|
-
if (released) return;
|
|
980
|
-
released = true;
|
|
981
|
-
state.inflight = Math.max(0, state.inflight - 1);
|
|
982
|
-
drainQueue();
|
|
983
|
-
};
|
|
984
|
-
|
|
985
|
-
const hardTimer = setTimeout(release, 6500);
|
|
986
|
-
|
|
987
|
-
requestAnimationFrame(() => {
|
|
988
|
-
try {
|
|
989
|
-
if (isBlocked()) return;
|
|
990
|
-
|
|
991
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
992
|
-
if (!ph || !ph.isConnected) return;
|
|
993
|
-
|
|
994
|
-
// If the placeholder already has creative, avoid re-showing.
|
|
995
|
-
// Re-showing is a common source of "Placeholder Id X has already been defined".
|
|
996
|
-
try {
|
|
997
|
-
if (ph.querySelector && ph.querySelector('iframe, ins, img, video, [data-google-container-id]')) {
|
|
998
|
-
clearTimeout(hardTimer);
|
|
999
|
-
release();
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
} catch (e) {}
|
|
1003
|
-
|
|
1004
|
-
const t = now();
|
|
1005
|
-
const last = state.lastShowById.get(id) || 0;
|
|
1006
|
-
if (t - last < 900) return;
|
|
1007
|
-
state.lastShowById.set(id, t);
|
|
1008
81
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const doShow = () => {
|
|
1013
|
-
try { ez.showAds(id); } catch (e) {}
|
|
1014
|
-
try { markEmptyWrapper(id); } catch (e) {}
|
|
1015
|
-
try {
|
|
1016
|
-
const phw = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
1017
|
-
const ww = phw && phw.closest ? phw.closest(`.${WRAP_CLASS}`) : null;
|
|
1018
|
-
if (ww) {
|
|
1019
|
-
watchWrapForFill(ww);
|
|
1020
|
-
setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 900);
|
|
1021
|
-
setTimeout(() => { try { tightenEzoicMinHeight(ww); } catch (e) {} }, 2200);
|
|
1022
|
-
}
|
|
1023
|
-
} catch (e) {}
|
|
1024
|
-
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
if (Array.isArray(ez.cmd)) {
|
|
1028
|
-
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
82
|
+
if (!hasNeighbor) {
|
|
83
|
+
if (scrollDir === -1) {
|
|
84
|
+
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
1029
85
|
} else {
|
|
1030
|
-
|
|
86
|
+
wrap.classList.add('ez-orphan-hidden');
|
|
1031
87
|
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
88
|
+
} else {
|
|
89
|
+
wrap.classList.remove('ez-orphan-hidden');
|
|
1034
90
|
}
|
|
1035
91
|
});
|
|
1036
92
|
}
|
|
1037
93
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function buildOrdinalMap(items) {
|
|
1058
|
-
const map = new Map();
|
|
1059
|
-
let max = 0;
|
|
1060
|
-
for (let i = 0; i < items.length; i++) {
|
|
1061
|
-
const el = items[i];
|
|
1062
|
-
const ord = getItemOrdinal(el, i);
|
|
1063
|
-
map.set(ord, el);
|
|
1064
|
-
if (ord > max) max = ord;
|
|
1065
|
-
}
|
|
1066
|
-
return { map, max };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
function computeTargets(count, interval, showFirst) {
|
|
1071
|
-
const out = [];
|
|
1072
|
-
if (count <= 0) return out;
|
|
1073
|
-
if (showFirst) out.push(1);
|
|
1074
|
-
for (let i = 1; i <= count; i++) {
|
|
1075
|
-
if (i % interval === 0) out.push(i);
|
|
1076
|
-
}
|
|
1077
|
-
// unique + sorted
|
|
1078
|
-
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
1082
|
-
if (!items.length) return 0;
|
|
1083
|
-
|
|
1084
|
-
const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
|
|
1085
|
-
const targets = computeTargets(maxOrdinal, interval, showFirst);
|
|
1086
|
-
let inserted = 0;
|
|
1087
|
-
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
1088
|
-
|
|
1089
|
-
for (const afterPos of targets) {
|
|
1090
|
-
if (inserted >= maxInserts) break;
|
|
1091
|
-
const el = ordinalMap.get(afterPos);
|
|
1092
|
-
if (!el) continue;
|
|
1093
|
-
if (!el || !el.isConnected) continue;
|
|
1094
|
-
if (isAdjacentAd(el)) continue;
|
|
1095
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
1096
|
-
|
|
1097
|
-
let id = pickIdFromAll(allIds, cursorKey);
|
|
1098
|
-
let recycledWrap = null;
|
|
1099
|
-
|
|
1100
|
-
// If the pool is exhausted (all placeholder ids already mounted), recycle a wrap that is far
|
|
1101
|
-
// above the viewport by moving it to the new target instead of creating a new placeholder.
|
|
1102
|
-
// This avoids "Placeholder Id X has already been defined" and also ensures ads keep
|
|
1103
|
-
// appearing on very long infinite scroll sessions.
|
|
1104
|
-
if (!id) {
|
|
1105
|
-
// Safe mode: disable recycling for topic message ads to prevent visual "jumping"
|
|
1106
|
-
// (ads seemingly moving back under the first post during virtualized scroll).
|
|
1107
|
-
const allowRecycle = kindClass !== 'ezoic-ad-message';
|
|
1108
|
-
recycledWrap = (allowRecycle && scrollDir > 0) ? pickRecyclableWrap(kindClass) : null;
|
|
1109
|
-
if (recycledWrap) {
|
|
1110
|
-
id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (!id) break;
|
|
1115
|
-
|
|
1116
|
-
const wrap = recycledWrap
|
|
1117
|
-
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
1118
|
-
: insertAfter(el, id, kindClass, afterPos);
|
|
1119
|
-
if (!wrap) continue;
|
|
1120
|
-
|
|
1121
|
-
// observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
|
|
1122
|
-
// after ajaxify/infinite scroll mutations.
|
|
1123
|
-
observePlaceholder(id);
|
|
1124
|
-
inserted++;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
return inserted;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
function pickRecyclableWrap(kindClass) {
|
|
1131
|
-
// Only recycle wrappers that are well above the viewport to avoid visible "disappearing".
|
|
1132
|
-
// With very small id pools (e.g. 6 ids), recycling is the only way to keep ads appearing
|
|
1133
|
-
// on long topics without redefining placeholders.
|
|
1134
|
-
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
1135
|
-
if (!wraps || !wraps.length) return null;
|
|
1136
|
-
|
|
1137
|
-
const vh = Math.max(300, window.innerHeight || 800);
|
|
1138
|
-
// Recycle only when the wrapper is far above the viewport.
|
|
1139
|
-
// This keeps ads on-screen longer (especially on mobile) and reduces "blink".
|
|
1140
|
-
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
1141
|
-
|
|
1142
|
-
let best = null;
|
|
1143
|
-
let bestBottom = Infinity;
|
|
1144
|
-
for (const w of wraps) {
|
|
1145
|
-
if (!w || !w.isConnected) continue;
|
|
1146
|
-
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
1147
|
-
const rect = w.getBoundingClientRect();
|
|
1148
|
-
if (rect.bottom < threshold) {
|
|
1149
|
-
if (rect.bottom < bestBottom) {
|
|
1150
|
-
bestBottom = rect.bottom;
|
|
1151
|
-
best = w;
|
|
1152
|
-
}
|
|
94
|
+
function callEzoic(placeholderId) {
|
|
95
|
+
if (typeof window.ezstandalone === 'undefined') return;
|
|
96
|
+
|
|
97
|
+
const pid = parseInt(placeholderId, 10);
|
|
98
|
+
try {
|
|
99
|
+
if (!ezEnabled) {
|
|
100
|
+
window.ezstandalone.define(pid);
|
|
101
|
+
window.ezstandalone.enable();
|
|
102
|
+
window.ezstandalone.display();
|
|
103
|
+
ezEnabled = true;
|
|
104
|
+
} else {
|
|
105
|
+
window.ezstandalone.define(pid);
|
|
106
|
+
// Utiliser une petite pause pour être sûr que le DOM est prêt
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
if (document.getElementById('ezoic-pub-ad-placeholder-' + pid)) {
|
|
109
|
+
window.ezstandalone.refresh();
|
|
110
|
+
}
|
|
111
|
+
}, 100);
|
|
1153
112
|
}
|
|
1154
|
-
}
|
|
1155
|
-
return best;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
1159
|
-
try {
|
|
1160
|
-
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
1161
|
-
|
|
1162
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
1163
|
-
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
1164
|
-
|
|
1165
|
-
// Ensure minimal layout impact.
|
|
1166
|
-
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
1167
|
-
try { tightenStickyIn(wrap); } catch (e) {}
|
|
1168
|
-
return wrap;
|
|
1169
113
|
} catch (e) {
|
|
1170
|
-
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
async function runCore() {
|
|
1175
|
-
if (isBlocked()) return 0;
|
|
1176
|
-
|
|
1177
|
-
patchShowAds();
|
|
1178
|
-
|
|
1179
|
-
const cfg = await fetchConfigOnce();
|
|
1180
|
-
if (!cfg || cfg.excluded) return 0;
|
|
1181
|
-
initPools(cfg);
|
|
1182
|
-
|
|
1183
|
-
const kind = getKind();
|
|
1184
|
-
let inserted = 0;
|
|
1185
|
-
|
|
1186
|
-
if (kind === 'topic') {
|
|
1187
|
-
if (normalizeBool(cfg.enableMessageAds)) {
|
|
1188
|
-
const items = getPostContainers();
|
|
1189
|
-
pruneOrphanWraps('ezoic-ad-message', items);
|
|
1190
|
-
inserted += injectBetween(
|
|
1191
|
-
'ezoic-ad-message',
|
|
1192
|
-
items,
|
|
1193
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
1194
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
1195
|
-
state.allPosts,
|
|
1196
|
-
'curPosts'
|
|
1197
|
-
);
|
|
1198
|
-
decluster('ezoic-ad-message');
|
|
1199
|
-
}
|
|
1200
|
-
} else if (kind === 'categoryTopics') {
|
|
1201
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
1202
|
-
const items = getTopicItems();
|
|
1203
|
-
pruneOrphanWraps('ezoic-ad-between', items);
|
|
1204
|
-
inserted += injectBetween(
|
|
1205
|
-
'ezoic-ad-between',
|
|
1206
|
-
items,
|
|
1207
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
1208
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
1209
|
-
state.allTopics,
|
|
1210
|
-
'curTopics'
|
|
1211
|
-
);
|
|
1212
|
-
decluster('ezoic-ad-between');
|
|
1213
|
-
}
|
|
1214
|
-
} else if (kind === 'categories') {
|
|
1215
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
1216
|
-
const items = getCategoryItems();
|
|
1217
|
-
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
1218
|
-
inserted += injectBetween(
|
|
1219
|
-
'ezoic-ad-categories',
|
|
1220
|
-
items,
|
|
1221
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
1222
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
1223
|
-
state.allCategories,
|
|
1224
|
-
'curCategories'
|
|
1225
|
-
);
|
|
1226
|
-
decluster('ezoic-ad-categories');
|
|
1227
|
-
}
|
|
114
|
+
console.warn('[Ezoic-Infinite] Error refreshing ad:', e);
|
|
1228
115
|
}
|
|
1229
|
-
|
|
1230
|
-
return inserted;
|
|
1231
116
|
}
|
|
1232
117
|
|
|
1233
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
initPools(cfg);
|
|
118
|
+
function redistribute(container) {
|
|
119
|
+
if (!container || !config || config.excluded) return;
|
|
120
|
+
|
|
121
|
+
pruneOrphanWraps();
|
|
122
|
+
decluster(container);
|
|
1239
123
|
|
|
1240
|
-
const
|
|
124
|
+
const isTopicList = container.getAttribute('component') === 'category' || container.classList.contains('topic-list');
|
|
125
|
+
const isPostList = container.getAttribute('component') === 'topic' || container.querySelectorAll('[component="post"]').length > 0;
|
|
1241
126
|
|
|
127
|
+
let enabled = false;
|
|
128
|
+
let interval = 10;
|
|
129
|
+
let kind = '';
|
|
1242
130
|
let items = [];
|
|
1243
|
-
let allIds = [];
|
|
1244
|
-
let cursorKey = '';
|
|
1245
|
-
let kindClass = '';
|
|
1246
131
|
let showFirst = false;
|
|
1247
132
|
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
showFirst =
|
|
1254
|
-
} else if (
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
showFirst =
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
observePlaceholder(id);
|
|
1287
|
-
// Hero placement is expected to be visible right away (after first item),
|
|
1288
|
-
// so kick a fill request immediately instead of waiting only for IO callbacks.
|
|
1289
|
-
enqueueShow(id);
|
|
1290
|
-
startShowQueue();
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// ---------------- scheduler ----------------
|
|
1294
|
-
|
|
1295
|
-
function scheduleRun(delayMs = 0, cb) {
|
|
1296
|
-
if (state.runQueued) return;
|
|
1297
|
-
state.runQueued = true;
|
|
1298
|
-
|
|
1299
|
-
const run = async () => {
|
|
1300
|
-
state.runQueued = false;
|
|
1301
|
-
const pk = getPageKey();
|
|
1302
|
-
if (state.pageKey && pk !== state.pageKey) return;
|
|
1303
|
-
let inserted = 0;
|
|
1304
|
-
try { inserted = await runCore(); } catch (e) { inserted = 0; }
|
|
1305
|
-
try { cb && cb(inserted); } catch (e) {}
|
|
1306
|
-
};
|
|
1307
|
-
|
|
1308
|
-
const doRun = () => requestAnimationFrame(run);
|
|
1309
|
-
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
1310
|
-
else doRun();
|
|
133
|
+
if (isTopicList && config.enableBetweenAds) {
|
|
134
|
+
enabled = true;
|
|
135
|
+
interval = parseInt(config.intervalPosts, 10) || 10;
|
|
136
|
+
kind = 'between';
|
|
137
|
+
items = Array.from(container.querySelectorAll('[component="category/topic"]'));
|
|
138
|
+
showFirst = config.showFirstTopicAd;
|
|
139
|
+
} else if (isPostList && config.enableMessageAds) {
|
|
140
|
+
enabled = true;
|
|
141
|
+
interval = parseInt(config.messageIntervalPosts, 10) || 10;
|
|
142
|
+
kind = 'message';
|
|
143
|
+
items = Array.from(container.querySelectorAll('[component="post"]'));
|
|
144
|
+
showFirst = config.showFirstMessageAd;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!enabled || items.length === 0) return;
|
|
148
|
+
|
|
149
|
+
let inserts = 0;
|
|
150
|
+
items.forEach((item, index) => {
|
|
151
|
+
if (inserts >= MAX_INSERTS_PER_RUN) return;
|
|
152
|
+
const pos = index + 1;
|
|
153
|
+
let shouldHaveAd = (pos % interval === 0);
|
|
154
|
+
if (pos === 1 && showFirst) shouldHaveAd = true;
|
|
155
|
+
|
|
156
|
+
const next = item.nextElementSibling;
|
|
157
|
+
const hasAdAfter = next && next.classList.contains(WRAP_CLASS);
|
|
158
|
+
|
|
159
|
+
if (shouldHaveAd && !hasAdAfter) {
|
|
160
|
+
const pool = getPool();
|
|
161
|
+
const available = pool.querySelector(`.${WRAP_CLASS}[data-kind="${kind}"]`);
|
|
162
|
+
if (available) {
|
|
163
|
+
withInternalDomChange(() => {
|
|
164
|
+
item.parentNode.insertBefore(available, item.nextSibling);
|
|
165
|
+
callEzoic(available.getAttribute('data-placeholder-id'));
|
|
166
|
+
});
|
|
167
|
+
inserts++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
1311
171
|
}
|
|
1312
172
|
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (!hasWork) { state.burstActive = false; return; }
|
|
1340
|
-
// Short delay keeps UI smooth while catching late DOM waves.
|
|
1341
|
-
setTimeout(step, inserted > 0 ? 120 : 220);
|
|
173
|
+
function fetchConfig(cb) {
|
|
174
|
+
fetch('/api/plugins/ezoic-infinite/config')
|
|
175
|
+
.then(r => r.json())
|
|
176
|
+
.then(data => {
|
|
177
|
+
config = data;
|
|
178
|
+
const pool = getPool();
|
|
179
|
+
const setupPool = (idsRaw, kind) => {
|
|
180
|
+
if (!idsRaw) return;
|
|
181
|
+
const ids = idsRaw.split(/[\s,]+/).filter(Boolean);
|
|
182
|
+
ids.forEach(id => {
|
|
183
|
+
if (!document.querySelector(`[data-placeholder-id="${id}"]`)) {
|
|
184
|
+
const d = document.createElement('div');
|
|
185
|
+
d.className = WRAP_CLASS + ' nodebb-ezoic-ad-' + kind;
|
|
186
|
+
d.setAttribute('data-kind', kind);
|
|
187
|
+
d.setAttribute('data-placeholder-id', id);
|
|
188
|
+
const inner = document.createElement('div');
|
|
189
|
+
inner.id = 'ezoic-pub-ad-placeholder-' + id;
|
|
190
|
+
inner.className = 'ezoic-ad';
|
|
191
|
+
d.appendChild(inner);
|
|
192
|
+
pool.appendChild(d);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
setupPool(config.placeholderIds, 'between');
|
|
197
|
+
setupPool(config.messagePlaceholderIds, 'message');
|
|
198
|
+
cb();
|
|
1342
199
|
});
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
step();
|
|
1346
200
|
}
|
|
1347
201
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
} catch (e) {}
|
|
1356
|
-
|
|
1357
|
-
state.cfg = null;
|
|
1358
|
-
state.allTopics = [];
|
|
1359
|
-
state.allPosts = [];
|
|
1360
|
-
state.allCategories = [];
|
|
1361
|
-
state.curTopics = 0;
|
|
1362
|
-
state.curPosts = 0;
|
|
1363
|
-
state.curCategories = 0;
|
|
1364
|
-
state.lastShowById.clear();
|
|
1365
|
-
|
|
1366
|
-
state.inflight = 0;
|
|
1367
|
-
state.pending = [];
|
|
1368
|
-
state.pendingSet.clear();
|
|
1369
|
-
|
|
1370
|
-
state.heroDoneForPage = false;
|
|
1371
|
-
|
|
1372
|
-
// keep observers alive
|
|
202
|
+
let timer = null;
|
|
203
|
+
function schedule() {
|
|
204
|
+
if (timer) clearTimeout(timer);
|
|
205
|
+
timer = setTimeout(() => {
|
|
206
|
+
const lists = document.querySelectorAll('[component="category"], .topic-list, [component="topic"], [component="category/topic/list"]');
|
|
207
|
+
lists.forEach(redistribute);
|
|
208
|
+
}, 150); // Un peu plus de délai pour NodeBB 4.x
|
|
1373
209
|
}
|
|
1374
210
|
|
|
1375
|
-
function
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
el.matches?.(SELECTORS.topicItem) ||
|
|
1385
|
-
el.matches?.(SELECTORS.categoryItem) ||
|
|
1386
|
-
el.querySelector?.(SELECTORS.postItem) ||
|
|
1387
|
-
el.querySelector?.(SELECTORS.topicItem) ||
|
|
1388
|
-
el.querySelector?.(SELECTORS.categoryItem)
|
|
1389
|
-
) {
|
|
1390
|
-
return true;
|
|
1391
|
-
}
|
|
211
|
+
function init() {
|
|
212
|
+
fetchConfig(() => {
|
|
213
|
+
schedule();
|
|
214
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
215
|
+
const mo = new MutationObserver((muts) => {
|
|
216
|
+
if (isInternalChange) return;
|
|
217
|
+
schedule();
|
|
218
|
+
});
|
|
219
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
1392
220
|
}
|
|
1393
|
-
}
|
|
1394
|
-
return false;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function ensureDomObserver() {
|
|
1398
|
-
if (state.domObs) return;
|
|
1399
|
-
|
|
1400
|
-
state.domObs = new MutationObserver((mutations) => {
|
|
1401
|
-
if (state.internalDomChange > 0) return;
|
|
1402
|
-
if (isBlocked()) return;
|
|
1403
|
-
if (!shouldReactToMutations(mutations)) return;
|
|
1404
|
-
requestBurst();
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
try {
|
|
1408
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
1409
|
-
} catch (e) {}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
function bindNodeBB() {
|
|
1413
|
-
if (!$) return;
|
|
1414
|
-
|
|
1415
|
-
$(window).off('.ezoicInfinite');
|
|
1416
|
-
|
|
1417
|
-
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
1418
|
-
cleanup();
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
1422
|
-
state.pageKey = getPageKey();
|
|
1423
|
-
blockedUntil = 0;
|
|
1424
|
-
|
|
1425
|
-
muteNoisyConsole();
|
|
1426
|
-
ensureTcfApiLocator();
|
|
1427
|
-
warmUpNetwork();
|
|
1428
|
-
patchShowAds();
|
|
1429
|
-
globalGapFixInit();
|
|
1430
|
-
ensurePreloadObserver();
|
|
1431
|
-
ensureDomObserver();
|
|
1432
|
-
|
|
1433
|
-
insertHeroAdEarly().catch(() => {});
|
|
1434
|
-
requestBurst();
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
// Some setups populate content in multiple phases; ensure we re-scan.
|
|
1438
|
-
$(window).on('action:ajaxify.contentLoaded.ezoicInfinite', () => {
|
|
1439
|
-
if (isBlocked()) return;
|
|
1440
|
-
requestBurst();
|
|
1441
221
|
});
|
|
1442
|
-
|
|
1443
|
-
$(window).on(
|
|
1444
|
-
'action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:categories.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite',
|
|
1445
|
-
() => {
|
|
1446
|
-
if (isBlocked()) return;
|
|
1447
|
-
requestBurst();
|
|
1448
|
-
}
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
// Also listen through NodeBB's AMD hooks module when available.
|
|
1452
|
-
try {
|
|
1453
|
-
require(['hooks'], (hooks) => {
|
|
1454
|
-
if (!hooks || typeof hooks.on !== 'function') return;
|
|
1455
|
-
['action:ajaxify.end', 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded'].forEach((ev) => {
|
|
1456
|
-
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (e) {}
|
|
1457
|
-
});
|
|
1458
|
-
});
|
|
1459
|
-
} catch (e) {}
|
|
1460
222
|
}
|
|
1461
223
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
try {
|
|
1467
|
-
const t = now();
|
|
1468
|
-
const y = window.scrollY || window.pageYOffset || 0;
|
|
1469
|
-
if (state.lastScrollTs) {
|
|
1470
|
-
const dt = t - state.lastScrollTs;
|
|
1471
|
-
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
1472
|
-
if (dt > 0) {
|
|
1473
|
-
const speed = dy / dt;
|
|
1474
|
-
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
1475
|
-
const wasBoosted = isBoosted();
|
|
1476
|
-
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, t + BOOST_DURATION_MS);
|
|
1477
|
-
if (!wasBoosted) ensurePreloadObserver();
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
state.lastScrollY = y;
|
|
1482
|
-
state.lastScrollTs = t;
|
|
1483
|
-
} catch (e) {}
|
|
1484
|
-
|
|
1485
|
-
if (ticking) return;
|
|
1486
|
-
ticking = true;
|
|
1487
|
-
requestAnimationFrame(() => {
|
|
1488
|
-
ticking = false;
|
|
1489
|
-
requestBurst();
|
|
1490
|
-
});
|
|
1491
|
-
}, { passive: true });
|
|
224
|
+
if (document.readyState === 'loading') {
|
|
225
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
226
|
+
} else {
|
|
227
|
+
init();
|
|
1492
228
|
}
|
|
1493
|
-
|
|
1494
|
-
// ---------------- boot ----------------
|
|
1495
|
-
|
|
1496
|
-
state.pageKey = getPageKey();
|
|
1497
|
-
muteNoisyConsole();
|
|
1498
|
-
ensureTcfApiLocator();
|
|
1499
|
-
warmUpNetwork();
|
|
1500
|
-
patchShowAds();
|
|
1501
|
-
ensurePreloadObserver();
|
|
1502
|
-
ensureDomObserver();
|
|
1503
|
-
|
|
1504
|
-
bindNodeBB();
|
|
1505
|
-
bindScroll();
|
|
1506
|
-
|
|
1507
|
-
blockedUntil = 0;
|
|
1508
|
-
insertHeroAdEarly().catch(() => {});
|
|
1509
|
-
requestBurst();
|
|
1510
|
-
})();
|
|
229
|
+
})();
|