nodebb-plugin-ezoic-infinite 1.6.59 → 1.6.61
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 +1153 -68
- package/public/style.css +77 -17
- package/public/test.txt +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.61",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"compatibility": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"private": false
|
|
21
|
-
}
|
|
21
|
+
}
|
package/public/client.js
CHANGED
|
@@ -1,87 +1,1172 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// ─── Scroll direction tracker ───────────────────────────────────────────────
|
|
5
|
+
// Prevents aggressive recycling when the user scrolls upward.
|
|
6
|
+
let lastScrollY = 0;
|
|
7
|
+
let scrollDir = 1; // 1 = down, -1 = up
|
|
8
|
+
try {
|
|
9
|
+
lastScrollY = window.scrollY || 0;
|
|
10
|
+
window.addEventListener('scroll', () => {
|
|
11
|
+
const y = window.scrollY || 0;
|
|
12
|
+
const d = y - lastScrollY;
|
|
13
|
+
if (Math.abs(d) > 4) { scrollDir = d > 0 ? 1 : -1; lastScrollY = y; }
|
|
14
|
+
}, { passive: true });
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
|
|
17
|
+
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
18
|
+
|
|
19
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
20
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
5
21
|
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
const HOST_CLASS = 'nodebb-ezoic-host';
|
|
23
|
+
const MAX_INSERTS_PER_RUN = 8;
|
|
24
|
+
|
|
25
|
+
function keepEmptyWrapMs() { return isMobile() ? 120_000 : 60_000; }
|
|
26
|
+
|
|
27
|
+
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
28
|
+
const PRELOAD_MARGIN_MOBILE = '3200px 0px 3200px 0px';
|
|
29
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
|
|
30
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '5200px 0px 5200px 0px';
|
|
31
|
+
const BOOST_DURATION_MS = 2500;
|
|
32
|
+
const BOOST_SPEED_PX_PER_MS = 2.2;
|
|
33
|
+
const MAX_INFLIGHT_DESKTOP = 4;
|
|
34
|
+
const MAX_INFLIGHT_MOBILE = 3;
|
|
35
|
+
|
|
36
|
+
const SELECTORS = {
|
|
37
|
+
topicItem: 'li[component="category/topic"]',
|
|
38
|
+
postItem: '[component="post"][data-pid]',
|
|
39
|
+
categoryItem: 'li[component="categories/category"]',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function now() { return Date.now(); }
|
|
45
|
+
function isMobile() { try { return window.innerWidth < 768; } catch (e) { return false; } }
|
|
46
|
+
function isBoosted(){ return now() < (state.scrollBoostUntil || 0); }
|
|
47
|
+
|
|
48
|
+
function normalizeBool(v) {
|
|
49
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns true if the node contains a rendered ad creative. */
|
|
53
|
+
function isFilled(node) {
|
|
54
|
+
return !!(node && node.querySelector &&
|
|
55
|
+
node.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Console noise filter ───────────────────────────────────────────────────
|
|
59
|
+
function muteNoisyConsole() {
|
|
60
|
+
try {
|
|
61
|
+
if (window.__nodebbEzoicConsoleMuted) return;
|
|
62
|
+
window.__nodebbEzoicConsoleMuted = true;
|
|
63
|
+
const shouldMute = (args) => {
|
|
64
|
+
try {
|
|
65
|
+
const s = typeof args[0] === 'string' ? args[0] : '';
|
|
66
|
+
return (
|
|
67
|
+
(s.includes('[EzoicAds JS]: Placeholder Id') && s.includes('has already been defined')) ||
|
|
68
|
+
s.includes('Debugger iframe already exists') ||
|
|
69
|
+
(s.includes('HTML element with id ezoic-pub-ad-placeholder-') && s.includes('does not exist'))
|
|
70
|
+
);
|
|
71
|
+
} catch (e) { return false; }
|
|
72
|
+
};
|
|
73
|
+
const wrap = (m) => {
|
|
74
|
+
const orig = console[m];
|
|
75
|
+
if (typeof orig !== 'function') return;
|
|
76
|
+
console[m] = function (...a) { if (shouldMute(a)) return; return orig.apply(console, a); };
|
|
77
|
+
};
|
|
78
|
+
['log','info','warn','error'].forEach(wrap);
|
|
79
|
+
} catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── CMP/TCF stability ──────────────────────────────────────────────────────
|
|
83
|
+
function ensureTcfApiLocator() {
|
|
84
|
+
try {
|
|
85
|
+
if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
|
|
86
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
87
|
+
const f = document.createElement('iframe');
|
|
88
|
+
f.style.display = 'none';
|
|
89
|
+
f.id = f.name = '__tcfapiLocator';
|
|
90
|
+
(document.body || document.documentElement).appendChild(f);
|
|
91
|
+
} catch (e) {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Ezoic min-height tightener ─────────────────────────────────────────────
|
|
95
|
+
// Ezoic injects `min-height:400px !important` on nested wrappers via inline
|
|
96
|
+
// style. CSS alone can't fix it — we must rewrite the inline style.
|
|
97
|
+
function tightenEzoicMinHeight(wrap) {
|
|
98
|
+
try {
|
|
99
|
+
if (!wrap || !wrap.querySelector) return;
|
|
100
|
+
const iframes = wrap.querySelectorAll('iframe');
|
|
101
|
+
if (!iframes.length) return;
|
|
102
|
+
|
|
103
|
+
let refNode = null;
|
|
104
|
+
let p = iframes[0].parentElement;
|
|
105
|
+
while (p && p !== wrap) {
|
|
106
|
+
if (p.classList && p.classList.contains('ezoic-ad')) {
|
|
107
|
+
const st = (p.getAttribute('style') || '').toLowerCase();
|
|
108
|
+
if (st.includes('min-height:400') || st.includes('min-height: 400')) { refNode = p; break; }
|
|
109
|
+
}
|
|
110
|
+
p = p.parentElement;
|
|
111
|
+
}
|
|
112
|
+
if (!refNode) refNode = wrap.querySelector('.ezoic-ad-adaptive') || wrap.querySelector('.ezoic-ad') || wrap;
|
|
113
|
+
|
|
114
|
+
let refTop = 0;
|
|
115
|
+
try { refTop = refNode.getBoundingClientRect().top; } catch (e) {}
|
|
116
|
+
|
|
117
|
+
let maxBottom = 0;
|
|
118
|
+
iframes.forEach((f) => {
|
|
119
|
+
const rect = f.getBoundingClientRect();
|
|
120
|
+
if (rect.width <= 1 || rect.height <= 1) return;
|
|
121
|
+
maxBottom = Math.max(maxBottom, rect.bottom - refTop);
|
|
122
|
+
});
|
|
123
|
+
if (!maxBottom) {
|
|
124
|
+
iframes.forEach((f) => {
|
|
125
|
+
maxBottom = Math.max(maxBottom, parseInt(f.getAttribute('height') || '0', 10), f.offsetHeight || 0);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (!maxBottom) return;
|
|
129
|
+
|
|
130
|
+
const h = Math.max(1, Math.ceil(maxBottom));
|
|
131
|
+
const tightenNode = (n) => {
|
|
132
|
+
if (!n || !n.style) return;
|
|
133
|
+
try { n.style.setProperty('min-height', h + 'px', 'important'); } catch (e) { n.style.minHeight = h + 'px'; }
|
|
134
|
+
try { n.style.setProperty('height', 'auto', 'important'); } catch (e) {}
|
|
135
|
+
try { n.style.setProperty('line-height', '0', 'important'); } catch (e) {}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let cur = refNode;
|
|
139
|
+
while (cur && cur !== wrap) {
|
|
140
|
+
if (cur.classList && cur.classList.contains('ezoic-ad')) {
|
|
141
|
+
const st = (cur.getAttribute('style') || '').toLowerCase();
|
|
142
|
+
if (st.includes('min-height:400') || st.includes('min-height: 400')) tightenNode(cur);
|
|
56
143
|
}
|
|
144
|
+
cur = cur.parentElement;
|
|
57
145
|
}
|
|
146
|
+
tightenNode(refNode);
|
|
147
|
+
refNode.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive').forEach((n) => {
|
|
148
|
+
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
149
|
+
if (st.includes('min-height:400') || st.includes('min-height: 400')) tightenNode(n);
|
|
150
|
+
});
|
|
151
|
+
if (isMobile()) {
|
|
152
|
+
try { refNode.style.setProperty('width', '100%', 'important'); } catch (e) {}
|
|
153
|
+
try { refNode.style.setProperty('max-width', '100%', 'important'); } catch (e) {}
|
|
154
|
+
try { refNode.style.setProperty('min-width', '0', 'important'); } catch (e) {}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function watchWrapForFill(wrap) {
|
|
160
|
+
try {
|
|
161
|
+
if (!wrap || wrap.__ezFillObs) return;
|
|
162
|
+
const start = now();
|
|
163
|
+
const tightenBurst = () => {
|
|
164
|
+
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
165
|
+
if (now() - start < 6000) setTimeout(tightenBurst, 350);
|
|
166
|
+
};
|
|
167
|
+
const obs = new MutationObserver((muts) => {
|
|
168
|
+
if (isFilled(wrap)) { wrap.classList.remove('is-empty'); tightenBurst(); }
|
|
169
|
+
for (const m of muts) {
|
|
170
|
+
if (m.type === 'attributes' && m.attributeName === 'style') {
|
|
171
|
+
try { tightenEzoicMinHeight(wrap); } catch (e) {}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (now() - start > 7000) { try { obs.disconnect(); } catch (e) {} wrap.__ezFillObs = null; }
|
|
176
|
+
});
|
|
177
|
+
obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
178
|
+
wrap.__ezFillObs = obs;
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function globalGapFixInit() {
|
|
183
|
+
try {
|
|
184
|
+
if (window.__nodebbEzoicGapFix) return;
|
|
185
|
+
window.__nodebbEzoicGapFix = true;
|
|
186
|
+
const root = document.getElementById('content') ||
|
|
187
|
+
document.querySelector('[component="content"], #panel') || document.body;
|
|
188
|
+
const inPostArea = (el) => {
|
|
189
|
+
try { return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]')); }
|
|
190
|
+
catch (e) { return false; }
|
|
191
|
+
};
|
|
192
|
+
const maybeFix = (r) => {
|
|
193
|
+
if (!r || !r.querySelectorAll) return;
|
|
194
|
+
r.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]')
|
|
195
|
+
.forEach((n) => {
|
|
196
|
+
const st = (n.getAttribute('style') || '').toLowerCase();
|
|
197
|
+
if (!st.includes('min-height:400')) return;
|
|
198
|
+
if (!inPostArea(n)) return;
|
|
199
|
+
try { tightenEzoicMinHeight(n.closest('.' + WRAP_CLASS) || n.parentElement || n); } catch (e) {}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
requestAnimationFrame(() => maybeFix(root));
|
|
203
|
+
const pending = new Set();
|
|
204
|
+
let scheduled = false;
|
|
205
|
+
const flush = () => {
|
|
206
|
+
scheduled = false;
|
|
207
|
+
pending.forEach((n) => { try { maybeFix(n); } catch (e) {} });
|
|
208
|
+
pending.clear();
|
|
209
|
+
};
|
|
210
|
+
new MutationObserver((muts) => {
|
|
211
|
+
for (const m of muts) {
|
|
212
|
+
if (m.type === 'attributes') {
|
|
213
|
+
const t = m.target && m.target.nodeType === 1 ? m.target : m.target && m.target.parentElement;
|
|
214
|
+
if (t) pending.add(t);
|
|
215
|
+
} else if (m.addedNodes) {
|
|
216
|
+
m.addedNodes.forEach((n) => { if (n && n.nodeType === 1) pending.add(n); });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (pending.size && !scheduled) { scheduled = true; requestAnimationFrame(flush); }
|
|
220
|
+
}).observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── State ──────────────────────────────────────────────────────────────────
|
|
225
|
+
const state = {
|
|
226
|
+
pageKey: null, cfg: null,
|
|
227
|
+
allTopics: [], allPosts: [], allCategories: [],
|
|
228
|
+
curTopics: 0, curPosts: 0, curCategories: 0,
|
|
229
|
+
lastShowById: new Map(),
|
|
230
|
+
domObs: null, io: null, ioMargin: null,
|
|
231
|
+
internalDomChange: 0,
|
|
232
|
+
inflight: 0, pending: [], pendingSet: new Set(),
|
|
233
|
+
scrollBoostUntil: 0, lastScrollY: 0, lastScrollTs: 0,
|
|
234
|
+
heroDoneForPage: false,
|
|
235
|
+
runQueued: false, burstActive: false, burstDeadline: 0, burstCount: 0, lastBurstReqTs: 0,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let blockedUntil = 0;
|
|
239
|
+
const insertingIds = new Set();
|
|
240
|
+
function isBlocked() { return now() < blockedUntil; }
|
|
241
|
+
|
|
242
|
+
// ─── Utils ──────────────────────────────────────────────────────────────────
|
|
243
|
+
function parsePool(raw) {
|
|
244
|
+
if (!raw) return [];
|
|
245
|
+
const seen = new Set(), out = [];
|
|
246
|
+
String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean).forEach((v) => {
|
|
247
|
+
const n = parseInt(v, 10);
|
|
248
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
249
|
+
});
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getPageKey() {
|
|
254
|
+
try {
|
|
255
|
+
const ax = window.ajaxify;
|
|
256
|
+
if (ax && ax.data) {
|
|
257
|
+
if (ax.data.tid) return 'topic:' + ax.data.tid;
|
|
258
|
+
if (ax.data.cid) return 'cid:' + ax.data.cid + ':' + window.location.pathname;
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {}
|
|
261
|
+
return window.location.pathname;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getPreloadRootMargin() {
|
|
265
|
+
return isMobile()
|
|
266
|
+
? (isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE)
|
|
267
|
+
: (isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getMaxInflight() {
|
|
271
|
+
return (isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP) + (isBoosted() ? 1 : 0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function withInternalDomChange(fn) {
|
|
275
|
+
state.internalDomChange++;
|
|
276
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── DOM helpers ────────────────────────────────────────────────────────────
|
|
280
|
+
function getTopicItems() { return Array.from(document.querySelectorAll(SELECTORS.topicItem)); }
|
|
281
|
+
function getCategoryItems() { return Array.from(document.querySelectorAll(SELECTORS.categoryItem)); }
|
|
282
|
+
|
|
283
|
+
function getPostContainers() {
|
|
284
|
+
return Array.from(document.querySelectorAll(SELECTORS.postItem)).filter((el) => {
|
|
285
|
+
if (!el.isConnected) return false;
|
|
286
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
287
|
+
const pp = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
288
|
+
if (pp && pp !== el) return false;
|
|
289
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
290
|
+
return true;
|
|
58
291
|
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getKind() {
|
|
295
|
+
const p = window.location.pathname || '';
|
|
296
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
297
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
298
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
299
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
300
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
301
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
302
|
+
return 'other';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isAdjacentAd(target) {
|
|
306
|
+
if (!target) return false;
|
|
307
|
+
const next = target.nextElementSibling;
|
|
308
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
309
|
+
const prev = target.previousElementSibling;
|
|
310
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findWrap(kindClass, afterPos) {
|
|
315
|
+
return document.querySelector('.' + WRAP_CLASS + '.' + kindClass + '[data-ezoic-after="' + afterPos + '"]');
|
|
316
|
+
}
|
|
59
317
|
|
|
60
|
-
|
|
61
|
-
|
|
318
|
+
// ─── Network warmup ─────────────────────────────────────────────────────────
|
|
319
|
+
const _warmLinksDone = new Set();
|
|
320
|
+
function warmUpNetwork() {
|
|
321
|
+
try {
|
|
322
|
+
const head = document.head || document.getElementsByTagName('head')[0];
|
|
323
|
+
if (!head) return;
|
|
324
|
+
[
|
|
325
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
326
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
327
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
328
|
+
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
329
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
330
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
331
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
332
|
+
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
333
|
+
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
334
|
+
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
335
|
+
].forEach(([rel, href, cors]) => {
|
|
336
|
+
const key = rel + '|' + href;
|
|
337
|
+
if (_warmLinksDone.has(key)) return;
|
|
338
|
+
_warmLinksDone.add(key);
|
|
339
|
+
const link = document.createElement('link');
|
|
340
|
+
link.rel = rel; link.href = href;
|
|
341
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
342
|
+
head.appendChild(link);
|
|
343
|
+
});
|
|
344
|
+
} catch (e) {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── Ezoic bridge ───────────────────────────────────────────────────────────
|
|
348
|
+
// Patch showAds so it silently skips IDs whose placeholder is not in the DOM.
|
|
349
|
+
function patchShowAds() {
|
|
350
|
+
const applyPatch = () => {
|
|
351
|
+
try {
|
|
352
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
353
|
+
const ez = window.ezstandalone;
|
|
354
|
+
if (window.__nodebbEzoicPatched) return;
|
|
355
|
+
if (typeof ez.showAds !== 'function') return;
|
|
356
|
+
window.__nodebbEzoicPatched = true;
|
|
357
|
+
const orig = ez.showAds;
|
|
358
|
+
ez.showAds = function (...args) {
|
|
359
|
+
if (isBlocked()) return;
|
|
360
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
361
|
+
const seen = new Set();
|
|
362
|
+
for (const v of ids) {
|
|
363
|
+
const id = parseInt(v, 10);
|
|
364
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
365
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
366
|
+
if (!ph || !ph.isConnected) continue;
|
|
367
|
+
seen.add(id);
|
|
368
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
} catch (e) {}
|
|
372
|
+
};
|
|
373
|
+
applyPatch();
|
|
374
|
+
if (!window.__nodebbEzoicPatched) {
|
|
375
|
+
try {
|
|
376
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
377
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
378
|
+
window.ezstandalone.cmd.push(applyPatch);
|
|
379
|
+
} catch (e) {}
|
|
62
380
|
}
|
|
63
381
|
}
|
|
64
382
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
383
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
384
|
+
async function fetchConfigOnce() {
|
|
385
|
+
if (state.cfg) return state.cfg;
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
388
|
+
if (!res.ok) return null;
|
|
389
|
+
state.cfg = await res.json();
|
|
390
|
+
return state.cfg;
|
|
391
|
+
} catch (e) { return null; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function initPools(cfg) {
|
|
395
|
+
if (!cfg) return;
|
|
396
|
+
// Pools are NOT pre-seeded into the DOM: connecting placeholders offscreen
|
|
397
|
+
// causes Ezoic to pre-define slots → "already defined" errors on SPA navigations.
|
|
398
|
+
if (!state.allTopics.length) state.allTopics = parsePool(cfg.placeholderIds);
|
|
399
|
+
if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
400
|
+
if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── Insertion primitives ───────────────────────────────────────────────────
|
|
404
|
+
function buildWrap(id, kindClass, afterPos, createPlaceholder) {
|
|
405
|
+
const wrap = document.createElement('div');
|
|
406
|
+
wrap.className = WRAP_CLASS + ' ' + kindClass;
|
|
407
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
408
|
+
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
409
|
+
wrap.setAttribute('data-created', String(now()));
|
|
410
|
+
if (afterPos === 1) wrap.setAttribute('data-ezoic-pin', '1');
|
|
411
|
+
wrap.style.width = '100%';
|
|
412
|
+
if (createPlaceholder) {
|
|
413
|
+
const ph = document.createElement('div');
|
|
414
|
+
ph.id = PLACEHOLDER_PREFIX + id;
|
|
415
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
416
|
+
wrap.appendChild(ph);
|
|
417
|
+
}
|
|
418
|
+
return wrap;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function insertAfter(target, id, kindClass, afterPos) {
|
|
422
|
+
if (!target || !target.insertAdjacentElement) return null;
|
|
423
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
424
|
+
if (insertingIds.has(id)) return null;
|
|
425
|
+
const existingPh = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
426
|
+
insertingIds.add(id);
|
|
427
|
+
try {
|
|
428
|
+
const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
|
|
429
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
430
|
+
if (existingPh) {
|
|
431
|
+
existingPh.setAttribute('data-ezoic-id', String(id));
|
|
432
|
+
if (!wrap.firstElementChild) wrap.appendChild(existingPh);
|
|
433
|
+
else wrap.replaceChild(existingPh, wrap.firstElementChild);
|
|
72
434
|
}
|
|
435
|
+
return wrap;
|
|
436
|
+
} finally {
|
|
437
|
+
insertingIds.delete(id);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function releaseWrapNode(wrap) {
|
|
442
|
+
try {
|
|
443
|
+
const ph = wrap && wrap.querySelector && wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
444
|
+
if (ph) {
|
|
445
|
+
try { (document.body || document.documentElement).appendChild(ph); } catch (e) {}
|
|
446
|
+
try { state.io && state.io.unobserve(ph); } catch (e) {}
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {}
|
|
449
|
+
try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function pickIdFromAll(allIds, cursorKey) {
|
|
453
|
+
const n = allIds.length;
|
|
454
|
+
if (!n) return null;
|
|
455
|
+
for (let tries = 0; tries < n; tries++) {
|
|
456
|
+
const idx = state[cursorKey] % n;
|
|
457
|
+
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
458
|
+
const id = allIds[idx];
|
|
459
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
460
|
+
// Skip if placeholder is already mounted in a visible wrap.
|
|
461
|
+
if (ph && ph.isConnected && ph.closest('.' + WRAP_CLASS)) continue;
|
|
462
|
+
return id;
|
|
73
463
|
}
|
|
74
464
|
return null;
|
|
75
465
|
}
|
|
76
466
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
467
|
+
// ─── Orphan / cluster management ────────────────────────────────────────────
|
|
468
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
469
|
+
if (!items || !items.length) return 0;
|
|
470
|
+
const itemSet = new Set(items);
|
|
471
|
+
const isMessage = kindClass === 'ezoic-ad-message';
|
|
472
|
+
let removed = 0;
|
|
473
|
+
|
|
474
|
+
const hasNearbyItem = (wrap) => {
|
|
475
|
+
let prev = wrap.previousElementSibling;
|
|
476
|
+
for (let i = 0; i < 14 && prev; i++) {
|
|
477
|
+
if (itemSet.has(prev)) return true;
|
|
478
|
+
prev = prev.previousElementSibling;
|
|
479
|
+
}
|
|
480
|
+
let next = wrap.nextElementSibling;
|
|
481
|
+
for (let i = 0; i < 14 && next; i++) {
|
|
482
|
+
if (itemSet.has(next)) return true;
|
|
483
|
+
next = next.nextElementSibling;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
|
|
489
|
+
// Never touch pinned placements.
|
|
490
|
+
if (wrap.getAttribute('data-ezoic-pin') === '1') return;
|
|
491
|
+
|
|
492
|
+
// Never prune very fresh wraps (slow auction / CMP fills).
|
|
493
|
+
const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
|
|
494
|
+
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
495
|
+
|
|
496
|
+
if (hasNearbyItem(wrap)) {
|
|
497
|
+
try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Anchor is gone (virtualized) → hide so ads don't stack visually.
|
|
502
|
+
try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
|
|
503
|
+
|
|
504
|
+
// Only fully release when far off-screen (avoids perceived "vanishing").
|
|
505
|
+
// FIX: between-ads are now also subject to this offscreen check, not
|
|
506
|
+
// just message-ads. Previously, filled between-ad wraps were NEVER pruned,
|
|
507
|
+
// causing orphaned filled wraps to drift near the top of the list.
|
|
508
|
+
try {
|
|
509
|
+
const r = wrap.getBoundingClientRect();
|
|
510
|
+
const vh = Math.max(1, window.innerHeight || 1);
|
|
511
|
+
const near = isMessage
|
|
512
|
+
? (r.bottom > -vh * 2 && r.top < vh * 4)
|
|
513
|
+
: (r.bottom > -vh * 3 && r.top < vh * 5);
|
|
514
|
+
if (near) return;
|
|
515
|
+
} catch (e) { return; }
|
|
516
|
+
|
|
517
|
+
withInternalDomChange(() => releaseWrapNode(wrap));
|
|
518
|
+
removed++;
|
|
519
|
+
});
|
|
520
|
+
return removed;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function decluster(kindClass) {
|
|
524
|
+
const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
|
|
525
|
+
if (wraps.length < 2) return 0;
|
|
526
|
+
const isWrap = (el) => !!(el && el.classList && el.classList.contains(WRAP_CLASS));
|
|
527
|
+
const isFresh = (w) => {
|
|
528
|
+
const c = parseInt(w.getAttribute('data-created') || '0', 10);
|
|
529
|
+
return c && (now() - c) < keepEmptyWrapMs();
|
|
530
|
+
};
|
|
531
|
+
let removed = 0;
|
|
532
|
+
for (const w of wraps) {
|
|
533
|
+
if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
534
|
+
let prev = w.previousElementSibling;
|
|
535
|
+
for (let i = 0; i < 3 && prev; i++) {
|
|
536
|
+
if (isWrap(prev)) {
|
|
537
|
+
if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
|
|
538
|
+
const prevFilled = isFilled(prev);
|
|
539
|
+
const curFilled = isFilled(w);
|
|
540
|
+
if (curFilled) {
|
|
541
|
+
if (!prevFilled && !isFresh(prev)) { withInternalDomChange(() => releaseWrapNode(prev)); removed++; }
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
if (prevFilled || !isFresh(w)) { withInternalDomChange(() => releaseWrapNode(w)); removed++; }
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
prev = prev.previousElementSibling;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return removed;
|
|
551
|
+
}
|
|
81
552
|
|
|
82
|
-
|
|
83
|
-
|
|
553
|
+
// ─── Pile-up repair for between-ads ─────────────────────────────────────────
|
|
554
|
+
// When NodeBB's DOM structure requires between-ad DIVs to live inside a <ul>,
|
|
555
|
+
// they must be wrapped in a <li> host. This module also corrects position
|
|
556
|
+
// drift that occurs after infinite-scroll mutations.
|
|
557
|
+
//
|
|
558
|
+
// FIX (v18): replaced the previous blind `previousTopicLi()` DOM walk with
|
|
559
|
+
// anchor lookup via `data-ezoic-after` + topic `data-index`. The old approach
|
|
560
|
+
// was the ROOT CAUSE of ads piling below the first topic: during pile-up
|
|
561
|
+
// detection it moved ALL piled wraps to after the SAME first topic it found.
|
|
562
|
+
//
|
|
563
|
+
// Now each wrap is moved to after the specific topic whose ordinal matches
|
|
564
|
+
// the wrap's own `data-ezoic-after` attribute.
|
|
565
|
+
|
|
566
|
+
function pileFixGetTopicList() {
|
|
567
|
+
try {
|
|
568
|
+
const li = document.querySelector(SELECTORS.topicItem);
|
|
569
|
+
if (!li) return null;
|
|
570
|
+
return li.closest ? li.closest('ul,ol') : null;
|
|
571
|
+
} catch (e) { return null; }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function ensureHostForWrap(wrap, ul) {
|
|
575
|
+
try {
|
|
576
|
+
if (!wrap || wrap.nodeType !== 1) return null;
|
|
577
|
+
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
578
|
+
if (!wrap.matches || !wrap.matches(wrapSel)) return null;
|
|
579
|
+
const existing = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
|
|
580
|
+
if (existing) return existing;
|
|
581
|
+
if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
|
|
582
|
+
if (!ul || !/^(UL|OL)$/i.test(ul.tagName)) return null;
|
|
583
|
+
if (wrap.parentElement !== ul) return null;
|
|
584
|
+
const host = document.createElement('li');
|
|
585
|
+
host.className = HOST_CLASS;
|
|
586
|
+
host.setAttribute('role', 'listitem');
|
|
587
|
+
host.style.cssText = 'list-style:none;width:100%;';
|
|
588
|
+
ul.insertBefore(host, wrap);
|
|
589
|
+
host.appendChild(wrap);
|
|
590
|
+
return host;
|
|
591
|
+
} catch (e) { return null; }
|
|
84
592
|
}
|
|
85
593
|
|
|
86
|
-
|
|
87
|
-
|
|
594
|
+
function detectPileUp(ul) {
|
|
595
|
+
try {
|
|
596
|
+
let run = 0, maxRun = 0;
|
|
597
|
+
const kids = ul.children;
|
|
598
|
+
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
599
|
+
for (let i = 0; i < kids.length; i++) {
|
|
600
|
+
const el = kids[i];
|
|
601
|
+
const isHost = el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS);
|
|
602
|
+
const isBetween = (isHost && el.querySelector && el.querySelector(wrapSel)) ||
|
|
603
|
+
(el.matches && el.matches(wrapSel));
|
|
604
|
+
if (isBetween) { maxRun = Math.max(maxRun, ++run); }
|
|
605
|
+
else if (el.matches && el.matches(SELECTORS.topicItem)) { run = 0; }
|
|
606
|
+
else { run = 0; }
|
|
607
|
+
}
|
|
608
|
+
return maxRun >= 2;
|
|
609
|
+
} catch (e) { return false; }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Move each pile-up host to after its correct anchor topic.
|
|
614
|
+
*
|
|
615
|
+
* Anchor is determined by matching the wrap's `data-ezoic-after` (1-based ordinal)
|
|
616
|
+
* against each topic LI's `data-index` (0-based) → ordinal = data-index + 1.
|
|
617
|
+
*/
|
|
618
|
+
function redistributePileUp(ul) {
|
|
619
|
+
try {
|
|
620
|
+
if (!ul) return;
|
|
621
|
+
// Step 1: wrap bare between-ad DIVs that are direct <ul> children.
|
|
622
|
+
const wrapSel = 'div.' + WRAP_CLASS + '.ezoic-ad-between';
|
|
623
|
+
const directs = Array.from(ul.querySelectorAll(':scope > ' + wrapSel));
|
|
624
|
+
directs.forEach((w) => ensureHostForWrap(w, ul));
|
|
625
|
+
|
|
626
|
+
// Step 2: bail if no pile-up.
|
|
627
|
+
if (!detectPileUp(ul)) return;
|
|
628
|
+
|
|
629
|
+
// Step 3: build ordinal → topic LI map.
|
|
630
|
+
const topicLis = Array.from(ul.querySelectorAll(':scope > ' + SELECTORS.topicItem));
|
|
631
|
+
const ordinalMap = new Map();
|
|
632
|
+
topicLis.forEach((li, i) => {
|
|
633
|
+
const di = li.dataset && (li.dataset.index !== undefined ? li.dataset.index : null);
|
|
634
|
+
const raw = di !== null && di !== undefined ? di : li.getAttribute('data-index');
|
|
635
|
+
const ord = (raw !== null && raw !== undefined && raw !== '' && !isNaN(raw))
|
|
636
|
+
? parseInt(raw, 10) + 1 // data-index is 0-based; ordinals are 1-based
|
|
637
|
+
: i + 1;
|
|
638
|
+
ordinalMap.set(ord, li);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Step 4: move each host to immediately after its correct anchor topic.
|
|
642
|
+
const hosts = Array.from(ul.querySelectorAll(':scope > li.' + HOST_CLASS));
|
|
643
|
+
hosts.forEach((host) => {
|
|
644
|
+
try {
|
|
645
|
+
const wrap = host.querySelector && host.querySelector(wrapSel);
|
|
646
|
+
if (!wrap) return;
|
|
647
|
+
|
|
648
|
+
const afterPos = parseInt(wrap.getAttribute('data-ezoic-after') || '0', 10);
|
|
649
|
+
if (!afterPos) return;
|
|
650
|
+
|
|
651
|
+
const anchor = ordinalMap.get(afterPos);
|
|
652
|
+
if (!anchor) return; // anchor topic not loaded yet — leave in place
|
|
653
|
+
|
|
654
|
+
if (host.previousElementSibling !== anchor) {
|
|
655
|
+
anchor.insertAdjacentElement('afterend', host);
|
|
656
|
+
}
|
|
657
|
+
} catch (e) {}
|
|
658
|
+
});
|
|
659
|
+
} catch (e) {}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let _pileFixScheduled = false;
|
|
663
|
+
let _pileFixLastRun = 0;
|
|
664
|
+
const PILE_FIX_COOLDOWN = 180;
|
|
665
|
+
|
|
666
|
+
function schedulePileFix() {
|
|
667
|
+
if (now() - _pileFixLastRun < PILE_FIX_COOLDOWN) return;
|
|
668
|
+
if (_pileFixScheduled) return;
|
|
669
|
+
_pileFixScheduled = true;
|
|
670
|
+
requestAnimationFrame(() => {
|
|
671
|
+
_pileFixScheduled = false;
|
|
672
|
+
_pileFixLastRun = now();
|
|
673
|
+
try { const ul = pileFixGetTopicList(); if (ul) redistributePileUp(ul); } catch (e) {}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function initPileFixObserver() {
|
|
678
|
+
try {
|
|
679
|
+
if (typeof MutationObserver === 'undefined') return;
|
|
680
|
+
const observe = (ul) => {
|
|
681
|
+
new MutationObserver(() => schedulePileFix())
|
|
682
|
+
.observe(ul, { childList: true, subtree: true });
|
|
683
|
+
};
|
|
684
|
+
const ul = pileFixGetTopicList();
|
|
685
|
+
if (ul) {
|
|
686
|
+
observe(ul);
|
|
687
|
+
} else {
|
|
688
|
+
const sentinel = new MutationObserver(() => {
|
|
689
|
+
const u = pileFixGetTopicList();
|
|
690
|
+
if (u) { try { sentinel.disconnect(); } catch (e) {} observe(u); }
|
|
691
|
+
});
|
|
692
|
+
sentinel.observe(document.documentElement || document.body, { childList: true, subtree: true });
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ─── IntersectionObserver (preload / fast fill) ─────────────────────────────
|
|
698
|
+
function ensurePreloadObserver() {
|
|
699
|
+
const desiredMargin = getPreloadRootMargin();
|
|
700
|
+
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
701
|
+
|
|
702
|
+
try { state.io && state.io.disconnect(); } catch (e) {}
|
|
703
|
+
state.io = null;
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
state.io = new IntersectionObserver((entries) => {
|
|
707
|
+
for (const ent of entries) {
|
|
708
|
+
if (!ent.isIntersecting) continue;
|
|
709
|
+
const el = ent.target;
|
|
710
|
+
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
711
|
+
const id = parseInt(el && el.getAttribute && el.getAttribute('data-ezoic-id'), 10);
|
|
712
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
713
|
+
}
|
|
714
|
+
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
715
|
+
state.ioMargin = desiredMargin;
|
|
716
|
+
document.querySelectorAll('[id^="' + PLACEHOLDER_PREFIX + '"]')
|
|
717
|
+
.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
718
|
+
} catch (e) { state.io = state.ioMargin = null; }
|
|
719
|
+
|
|
720
|
+
return state.io;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function observePlaceholder(id) {
|
|
724
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
725
|
+
if (!ph || !ph.isConnected) return;
|
|
726
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
727
|
+
try { const io = ensurePreloadObserver(); io && io.observe(ph); } catch (e) {}
|
|
728
|
+
try {
|
|
729
|
+
const r = ph.getBoundingClientRect();
|
|
730
|
+
const mobile = isMobile(), boosted = isBoosted();
|
|
731
|
+
const screens = boosted ? (mobile ? 9.0 : 5.0) : (mobile ? 6.0 : 3.0);
|
|
732
|
+
const minBot = boosted ? (mobile ? -2600 : -1500) : (mobile ? -1400 : -800);
|
|
733
|
+
if (r.top < window.innerHeight * screens && r.bottom > minBot) enqueueShow(id);
|
|
734
|
+
} catch (e) {}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ─── Show queue ─────────────────────────────────────────────────────────────
|
|
738
|
+
function enqueueShow(id) {
|
|
739
|
+
if (!id || isBlocked()) return;
|
|
740
|
+
const t = now();
|
|
741
|
+
if (t - (state.lastShowById.get(id) || 0) < 900) return;
|
|
742
|
+
const max = getMaxInflight();
|
|
743
|
+
if (state.inflight >= max) {
|
|
744
|
+
if (!state.pendingSet.has(id)) { state.pending.push(id); state.pendingSet.add(id); }
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
startShow(id);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function drainQueue() {
|
|
751
|
+
if (isBlocked()) return;
|
|
752
|
+
const max = getMaxInflight();
|
|
753
|
+
while (state.inflight < max && state.pending.length) {
|
|
754
|
+
const id = state.pending.shift();
|
|
755
|
+
state.pendingSet.delete(id);
|
|
756
|
+
startShow(id);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function markEmptyWrapper(id) {
|
|
761
|
+
try {
|
|
762
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
763
|
+
if (!ph || !ph.isConnected) return;
|
|
764
|
+
const wrap = ph.closest && ph.closest('.' + WRAP_CLASS);
|
|
765
|
+
if (!wrap) return;
|
|
766
|
+
setTimeout(() => {
|
|
767
|
+
try {
|
|
768
|
+
const ph2 = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
769
|
+
if (!ph2 || !ph2.isConnected) return;
|
|
770
|
+
const w2 = ph2.closest && ph2.closest('.' + WRAP_CLASS);
|
|
771
|
+
if (!w2) return;
|
|
772
|
+
const created = parseInt(w2.getAttribute('data-created') || '0', 10);
|
|
773
|
+
if (created && (now() - created) < keepEmptyWrapMs()) return;
|
|
774
|
+
if (!isFilled(ph2)) { w2.classList.add('is-empty'); watchWrapForFill(w2); }
|
|
775
|
+
else { w2.classList.remove('is-empty'); tightenEzoicMinHeight(w2); }
|
|
776
|
+
} catch (e) {}
|
|
777
|
+
}, 15_000);
|
|
778
|
+
} catch (e) {}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function startShow(id) {
|
|
782
|
+
if (!id || isBlocked()) return;
|
|
783
|
+
state.inflight++;
|
|
784
|
+
let released = false;
|
|
785
|
+
const release = () => {
|
|
786
|
+
if (released) return;
|
|
787
|
+
released = true;
|
|
788
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
789
|
+
drainQueue();
|
|
790
|
+
};
|
|
791
|
+
const hardTimer = setTimeout(release, 6500);
|
|
792
|
+
requestAnimationFrame(() => {
|
|
793
|
+
try {
|
|
794
|
+
if (isBlocked()) return;
|
|
795
|
+
const ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
796
|
+
if (!ph || !ph.isConnected) return;
|
|
797
|
+
if (isFilled(ph)) { clearTimeout(hardTimer); release(); return; }
|
|
798
|
+
const t = now();
|
|
799
|
+
if (t - (state.lastShowById.get(id) || 0) < 900) return;
|
|
800
|
+
state.lastShowById.set(id, t);
|
|
801
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
802
|
+
const ez = window.ezstandalone;
|
|
803
|
+
const doShow = () => {
|
|
804
|
+
try { ez.showAds(id); } catch (e) {}
|
|
805
|
+
try { markEmptyWrapper(id); } catch (e) {}
|
|
806
|
+
try {
|
|
807
|
+
const ww = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
808
|
+
const w2 = ww && ww.closest && ww.closest('.' + WRAP_CLASS);
|
|
809
|
+
if (w2) {
|
|
810
|
+
watchWrapForFill(w2);
|
|
811
|
+
setTimeout(() => { try { tightenEzoicMinHeight(w2); } catch (e) {} }, 900);
|
|
812
|
+
setTimeout(() => { try { tightenEzoicMinHeight(w2); } catch (e) {} }, 2200);
|
|
813
|
+
}
|
|
814
|
+
} catch (e) {}
|
|
815
|
+
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
816
|
+
};
|
|
817
|
+
if (Array.isArray(ez.cmd)) { try { ez.cmd.push(doShow); } catch (e) { doShow(); } }
|
|
818
|
+
else doShow();
|
|
819
|
+
} finally {
|
|
820
|
+
// hardTimer covers early returns.
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ─── Core injection ─────────────────────────────────────────────────────────
|
|
826
|
+
function getItemOrdinal(el, fallbackIndex) {
|
|
827
|
+
try {
|
|
828
|
+
if (!el) return fallbackIndex + 1;
|
|
829
|
+
const di = el.getAttribute('data-index') ||
|
|
830
|
+
(el.dataset && (el.dataset.index || el.dataset.postIndex));
|
|
831
|
+
if (di != null && di !== '' && !isNaN(di)) {
|
|
832
|
+
const n = parseInt(di, 10);
|
|
833
|
+
if (Number.isFinite(n) && n >= 0) return n + 1;
|
|
834
|
+
}
|
|
835
|
+
const d1 = el.getAttribute('data-idx') || el.getAttribute('data-position') ||
|
|
836
|
+
(el.dataset && (el.dataset.idx || el.dataset.position));
|
|
837
|
+
if (d1 != null && d1 !== '' && !isNaN(d1)) {
|
|
838
|
+
const n = parseInt(d1, 10);
|
|
839
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {}
|
|
842
|
+
return fallbackIndex + 1;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function buildOrdinalMap(items) {
|
|
846
|
+
const map = new Map();
|
|
847
|
+
let max = 0;
|
|
848
|
+
for (let i = 0; i < items.length; i++) {
|
|
849
|
+
const ord = getItemOrdinal(items[i], i);
|
|
850
|
+
map.set(ord, items[i]);
|
|
851
|
+
if (ord > max) max = ord;
|
|
852
|
+
}
|
|
853
|
+
return { map, max };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function computeTargets(count, interval, showFirst) {
|
|
857
|
+
const out = [];
|
|
858
|
+
if (count <= 0) return out;
|
|
859
|
+
if (showFirst) out.push(1);
|
|
860
|
+
for (let i = 1; i <= count; i++) { if (i % interval === 0) out.push(i); }
|
|
861
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function pickRecyclableWrap(kindClass) {
|
|
865
|
+
const wraps = document.querySelectorAll('.' + kindClass + '[data-ezoic-wrapid]');
|
|
866
|
+
if (!wraps || !wraps.length) return null;
|
|
867
|
+
const vh = Math.max(300, window.innerHeight || 800);
|
|
868
|
+
const threshold = -Math.min(9000, Math.round(vh * 6));
|
|
869
|
+
let best = null, bestBottom = Infinity;
|
|
870
|
+
for (const w of wraps) {
|
|
871
|
+
if (!w.isConnected) continue;
|
|
872
|
+
if (w.getAttribute('data-ezoic-pin') === '1') continue;
|
|
873
|
+
// FIX: do not recycle near-top wraps (afterPos ≤ 6).
|
|
874
|
+
// These are likely to be visible again when the user scrolls back up.
|
|
875
|
+
const afterPos = parseInt(w.getAttribute('data-ezoic-after') || '0', 10);
|
|
876
|
+
if (afterPos > 0 && afterPos <= 6) continue;
|
|
877
|
+
const rect = w.getBoundingClientRect();
|
|
878
|
+
if (rect.bottom < threshold && rect.bottom < bestBottom) {
|
|
879
|
+
bestBottom = rect.bottom;
|
|
880
|
+
best = w;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return best;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function moveWrapAfter(anchorEl, wrap, kindClass, afterPos) {
|
|
887
|
+
try {
|
|
888
|
+
if (!anchorEl || !wrap || !wrap.isConnected) return null;
|
|
889
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
890
|
+
anchorEl.insertAdjacentElement('afterend', wrap);
|
|
891
|
+
try { wrap.style.contain = 'layout style paint'; } catch (e) {}
|
|
892
|
+
return wrap;
|
|
893
|
+
} catch (e) { return null; }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
897
|
+
if (!items.length) return 0;
|
|
898
|
+
const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
|
|
899
|
+
const targets = computeTargets(maxOrdinal, interval, showFirst);
|
|
900
|
+
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
901
|
+
let inserted = 0;
|
|
902
|
+
|
|
903
|
+
for (const afterPos of targets) {
|
|
904
|
+
if (inserted >= maxInserts) break;
|
|
905
|
+
const el = ordinalMap.get(afterPos);
|
|
906
|
+
if (!el || !el.isConnected) continue;
|
|
907
|
+
if (isAdjacentAd(el)) continue;
|
|
908
|
+
if (findWrap(kindClass, afterPos)) continue;
|
|
909
|
+
|
|
910
|
+
let id = pickIdFromAll(allIds, cursorKey);
|
|
911
|
+
let recycledWrap = null;
|
|
912
|
+
|
|
913
|
+
if (!id) {
|
|
914
|
+
// Pool exhausted: try to recycle a wrap that is far above the viewport.
|
|
915
|
+
// Recycling is disabled when scrolling up and for message ads.
|
|
916
|
+
const allowRecycle = kindClass !== 'ezoic-ad-message' && scrollDir > 0;
|
|
917
|
+
recycledWrap = allowRecycle ? pickRecyclableWrap(kindClass) : null;
|
|
918
|
+
if (recycledWrap) id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!id) break;
|
|
922
|
+
|
|
923
|
+
const wrap = recycledWrap
|
|
924
|
+
? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
|
|
925
|
+
: insertAfter(el, id, kindClass, afterPos);
|
|
926
|
+
if (!wrap) continue;
|
|
927
|
+
|
|
928
|
+
observePlaceholder(id);
|
|
929
|
+
inserted++;
|
|
930
|
+
}
|
|
931
|
+
return inserted;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ─── runCore ────────────────────────────────────────────────────────────────
|
|
935
|
+
async function runCore() {
|
|
936
|
+
if (isBlocked()) return 0;
|
|
937
|
+
patchShowAds();
|
|
938
|
+
const cfg = await fetchConfigOnce();
|
|
939
|
+
if (!cfg || cfg.excluded) return 0;
|
|
940
|
+
initPools(cfg);
|
|
941
|
+
|
|
942
|
+
const kind = getKind();
|
|
943
|
+
let inserted = 0;
|
|
944
|
+
|
|
945
|
+
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
946
|
+
const items = getPostContainers();
|
|
947
|
+
pruneOrphanWraps('ezoic-ad-message', items);
|
|
948
|
+
inserted += injectBetween('ezoic-ad-message', items,
|
|
949
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
950
|
+
normalizeBool(cfg.showFirstMessageAd), state.allPosts, 'curPosts');
|
|
951
|
+
decluster('ezoic-ad-message');
|
|
952
|
+
|
|
953
|
+
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
954
|
+
const items = getTopicItems();
|
|
955
|
+
pruneOrphanWraps('ezoic-ad-between', items);
|
|
956
|
+
inserted += injectBetween('ezoic-ad-between', items,
|
|
957
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
958
|
+
normalizeBool(cfg.showFirstTopicAd), state.allTopics, 'curTopics');
|
|
959
|
+
decluster('ezoic-ad-between');
|
|
960
|
+
schedulePileFix();
|
|
961
|
+
|
|
962
|
+
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
963
|
+
const items = getCategoryItems();
|
|
964
|
+
pruneOrphanWraps('ezoic-ad-categories', items);
|
|
965
|
+
inserted += injectBetween('ezoic-ad-categories', items,
|
|
966
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
967
|
+
normalizeBool(cfg.showFirstCategoryAd), state.allCategories, 'curCategories');
|
|
968
|
+
decluster('ezoic-ad-categories');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return inserted;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ─── Hero ad (first item, early insert) ─────────────────────────────────────
|
|
975
|
+
async function insertHeroAdEarly() {
|
|
976
|
+
if (state.heroDoneForPage || isBlocked()) return;
|
|
977
|
+
const cfg = await fetchConfigOnce();
|
|
978
|
+
if (!cfg || cfg.excluded) return;
|
|
979
|
+
initPools(cfg);
|
|
980
|
+
|
|
981
|
+
const kind = getKind();
|
|
982
|
+
let items = [], allIds = [], cursorKey = '', kindClass = '', showFirst = false;
|
|
983
|
+
|
|
984
|
+
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
985
|
+
items = getPostContainers(); allIds = state.allPosts; cursorKey = 'curPosts';
|
|
986
|
+
kindClass = 'ezoic-ad-message'; showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
987
|
+
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
988
|
+
items = getTopicItems(); allIds = state.allTopics; cursorKey = 'curTopics';
|
|
989
|
+
kindClass = 'ezoic-ad-between'; showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
990
|
+
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
991
|
+
items = getCategoryItems(); allIds = state.allCategories; cursorKey = 'curCategories';
|
|
992
|
+
kindClass = 'ezoic-ad-categories'; showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
993
|
+
} else { return; }
|
|
994
|
+
|
|
995
|
+
if (!items.length || !showFirst) { state.heroDoneForPage = true; return; }
|
|
996
|
+
|
|
997
|
+
const el = items[0];
|
|
998
|
+
if (!el || !el.isConnected || isAdjacentAd(el)) return;
|
|
999
|
+
if (findWrap(kindClass, 1)) { state.heroDoneForPage = true; return; }
|
|
1000
|
+
|
|
1001
|
+
const id = pickIdFromAll(allIds, cursorKey);
|
|
1002
|
+
if (!id) return;
|
|
1003
|
+
|
|
1004
|
+
const wrap = insertAfter(el, id, kindClass, 1);
|
|
1005
|
+
if (!wrap) return;
|
|
1006
|
+
|
|
1007
|
+
state.heroDoneForPage = true;
|
|
1008
|
+
observePlaceholder(id);
|
|
1009
|
+
enqueueShow(id);
|
|
1010
|
+
drainQueue(); // FIX: was calling undefined startShowQueue()
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// ─── Scheduler ──────────────────────────────────────────────────────────────
|
|
1014
|
+
function scheduleRun(delayMs, cb) {
|
|
1015
|
+
if (state.runQueued) return;
|
|
1016
|
+
state.runQueued = true;
|
|
1017
|
+
const run = async () => {
|
|
1018
|
+
state.runQueued = false;
|
|
1019
|
+
const pk = getPageKey();
|
|
1020
|
+
if (state.pageKey && pk !== state.pageKey) return;
|
|
1021
|
+
let inserted = 0;
|
|
1022
|
+
try { inserted = await runCore(); } catch (e) {}
|
|
1023
|
+
try { cb && cb(inserted); } catch (e) {}
|
|
1024
|
+
};
|
|
1025
|
+
const doRun = () => requestAnimationFrame(run);
|
|
1026
|
+
if (delayMs > 0) setTimeout(doRun, delayMs);
|
|
1027
|
+
else doRun();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function requestBurst() {
|
|
1031
|
+
if (isBlocked()) return;
|
|
1032
|
+
const t = now();
|
|
1033
|
+
if (t - state.lastBurstReqTs < 120) return;
|
|
1034
|
+
state.lastBurstReqTs = t;
|
|
1035
|
+
const pk = getPageKey();
|
|
1036
|
+
state.pageKey = pk;
|
|
1037
|
+
state.burstDeadline = t + 1800;
|
|
1038
|
+
if (state.burstActive) return;
|
|
1039
|
+
state.burstActive = true;
|
|
1040
|
+
state.burstCount = 0;
|
|
1041
|
+
const step = () => {
|
|
1042
|
+
if (getPageKey() !== pk || isBlocked() || now() > state.burstDeadline || state.burstCount >= 8) {
|
|
1043
|
+
state.burstActive = false; return;
|
|
1044
|
+
}
|
|
1045
|
+
state.burstCount++;
|
|
1046
|
+
scheduleRun(0, (inserted) => {
|
|
1047
|
+
if (!inserted && !state.pending.length) { state.burstActive = false; return; }
|
|
1048
|
+
setTimeout(step, inserted > 0 ? 120 : 220);
|
|
1049
|
+
});
|
|
1050
|
+
};
|
|
1051
|
+
step();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
1055
|
+
function cleanup() {
|
|
1056
|
+
blockedUntil = now() + 1200;
|
|
1057
|
+
try { document.querySelectorAll('.' + WRAP_CLASS).forEach((el) => releaseWrapNode(el)); } catch (e) {}
|
|
1058
|
+
state.cfg = null;
|
|
1059
|
+
state.allTopics = []; state.allPosts = []; state.allCategories = [];
|
|
1060
|
+
state.curTopics = 0; state.curPosts = 0; state.curCategories = 0;
|
|
1061
|
+
state.lastShowById.clear();
|
|
1062
|
+
state.inflight = 0; state.pending = []; state.pendingSet.clear();
|
|
1063
|
+
state.heroDoneForPage = false;
|
|
1064
|
+
// Observers are intentionally kept alive across ajaxify cycles.
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function shouldReactToMutations(mutations) {
|
|
1068
|
+
for (const m of mutations) {
|
|
1069
|
+
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
1070
|
+
for (const n of m.addedNodes) {
|
|
1071
|
+
if (!n || n.nodeType !== 1) continue;
|
|
1072
|
+
const el = n;
|
|
1073
|
+
if (
|
|
1074
|
+
(el.matches && (
|
|
1075
|
+
el.matches(SELECTORS.postItem) ||
|
|
1076
|
+
el.matches(SELECTORS.topicItem) ||
|
|
1077
|
+
el.matches(SELECTORS.categoryItem)
|
|
1078
|
+
)) ||
|
|
1079
|
+
(el.querySelector && (
|
|
1080
|
+
el.querySelector(SELECTORS.postItem) ||
|
|
1081
|
+
el.querySelector(SELECTORS.topicItem) ||
|
|
1082
|
+
el.querySelector(SELECTORS.categoryItem)
|
|
1083
|
+
))
|
|
1084
|
+
) return true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function ensureDomObserver() {
|
|
1091
|
+
if (state.domObs) return;
|
|
1092
|
+
state.domObs = new MutationObserver((mutations) => {
|
|
1093
|
+
if (state.internalDomChange > 0 || isBlocked()) return;
|
|
1094
|
+
if (!shouldReactToMutations(mutations)) return;
|
|
1095
|
+
requestBurst();
|
|
1096
|
+
});
|
|
1097
|
+
try { state.domObs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ─── NodeBB / scroll bindings ────────────────────────────────────────────────
|
|
1101
|
+
function bindNodeBB() {
|
|
1102
|
+
if (!$) return;
|
|
1103
|
+
$(window).off('.ezoicInfinite');
|
|
1104
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => { cleanup(); });
|
|
1105
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
1106
|
+
state.pageKey = getPageKey();
|
|
1107
|
+
blockedUntil = 0;
|
|
1108
|
+
muteNoisyConsole(); ensureTcfApiLocator(); warmUpNetwork(); patchShowAds();
|
|
1109
|
+
globalGapFixInit(); ensurePreloadObserver(); ensureDomObserver(); initPileFixObserver();
|
|
1110
|
+
insertHeroAdEarly().catch(() => {});
|
|
1111
|
+
requestBurst();
|
|
1112
|
+
setTimeout(schedulePileFix, 80);
|
|
1113
|
+
setTimeout(schedulePileFix, 500);
|
|
1114
|
+
});
|
|
1115
|
+
$(window).on('action:ajaxify.contentLoaded.ezoicInfinite', () => {
|
|
1116
|
+
if (!isBlocked()) requestBurst();
|
|
1117
|
+
});
|
|
1118
|
+
$(window).on([
|
|
1119
|
+
'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded',
|
|
1120
|
+
'action:category.loaded', 'action:topic.loaded', 'action:infiniteScroll.loaded',
|
|
1121
|
+
].map((e) => e + '.ezoicInfinite').join(' '), () => {
|
|
1122
|
+
if (!isBlocked()) {
|
|
1123
|
+
requestBurst();
|
|
1124
|
+
setTimeout(schedulePileFix, 80);
|
|
1125
|
+
setTimeout(schedulePileFix, 500);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
try {
|
|
1129
|
+
require(['hooks'], (hooks) => {
|
|
1130
|
+
if (!hooks || typeof hooks.on !== 'function') return;
|
|
1131
|
+
['action:ajaxify.end','action:ajaxify.contentLoaded','action:posts.loaded',
|
|
1132
|
+
'action:topics.loaded','action:categories.loaded','action:category.loaded',
|
|
1133
|
+
'action:topic.loaded','action:infiniteScroll.loaded'].forEach((ev) => {
|
|
1134
|
+
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (e) {}
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
} catch (e) {}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function bindScroll() {
|
|
1141
|
+
let ticking = false;
|
|
1142
|
+
window.addEventListener('scroll', () => {
|
|
1143
|
+
try {
|
|
1144
|
+
const t = now(), y = window.scrollY || window.pageYOffset || 0;
|
|
1145
|
+
if (state.lastScrollTs) {
|
|
1146
|
+
const dt = t - state.lastScrollTs;
|
|
1147
|
+
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
1148
|
+
if (dt > 0 && dy / dt >= BOOST_SPEED_PX_PER_MS) {
|
|
1149
|
+
const wasBoosted = isBoosted();
|
|
1150
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, t + BOOST_DURATION_MS);
|
|
1151
|
+
if (!wasBoosted) ensurePreloadObserver();
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
state.lastScrollY = y; state.lastScrollTs = t;
|
|
1155
|
+
} catch (e) {}
|
|
1156
|
+
if (ticking) return;
|
|
1157
|
+
ticking = true;
|
|
1158
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
1159
|
+
}, { passive: true });
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ─── Boot ────────────────────────────────────────────────────────────────────
|
|
1163
|
+
state.pageKey = getPageKey();
|
|
1164
|
+
muteNoisyConsole(); ensureTcfApiLocator(); warmUpNetwork(); patchShowAds();
|
|
1165
|
+
ensurePreloadObserver(); ensureDomObserver(); initPileFixObserver();
|
|
1166
|
+
bindNodeBB(); bindScroll();
|
|
1167
|
+
blockedUntil = 0;
|
|
1168
|
+
insertHeroAdEarly().catch(() => {});
|
|
1169
|
+
requestBurst();
|
|
1170
|
+
schedulePileFix();
|
|
1171
|
+
|
|
1172
|
+
})();
|
package/public/style.css
CHANGED
|
@@ -1,28 +1,88 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/*
|
|
2
|
+
Keep our NodeBB-inserted wrappers CLS-safe.
|
|
3
|
+
NOTE: must not rely on `.ezoic-ad` because Ezoic uses that class internally.
|
|
4
|
+
*/
|
|
2
5
|
.nodebb-ezoic-wrap {
|
|
6
|
+
display: block;
|
|
7
|
+
width: 100%;
|
|
8
|
+
margin: 0 !important;
|
|
9
|
+
padding: 0 !important;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
|
|
14
|
+
margin: 0 !important;
|
|
15
|
+
padding: 0 !important;
|
|
16
|
+
/* Keep the placeholder measurable (IO) but visually negligible */
|
|
17
|
+
min-height: 1px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* If Ezoic wraps inside our wrapper, keep it tight */
|
|
21
|
+
.nodebb-ezoic-wrap span.ezoic-ad,
|
|
22
|
+
.nodebb-ezoic-wrap .ezoic-ad {
|
|
23
|
+
margin: 0 !important;
|
|
24
|
+
padding: 0 !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Remove the classic "gap under iframe" (baseline/inline-block) */
|
|
28
|
+
.nodebb-ezoic-wrap,
|
|
29
|
+
.nodebb-ezoic-wrap * {
|
|
30
|
+
line-height: 0 !important;
|
|
31
|
+
font-size: 0 !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.nodebb-ezoic-wrap iframe,
|
|
35
|
+
.nodebb-ezoic-wrap div[id$="__container__"] iframe {
|
|
3
36
|
display: block !important;
|
|
4
|
-
|
|
5
|
-
clear: both !important;
|
|
6
|
-
margin: 25px 0 !important;
|
|
7
|
-
min-height: 100px; /* Crucial pour que Ezoic détecte l'emplacement */
|
|
8
|
-
text-align: center;
|
|
37
|
+
vertical-align: top !important;
|
|
9
38
|
}
|
|
10
39
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
15
|
-
margin-top: 0 !important;
|
|
40
|
+
.nodebb-ezoic-wrap div[id$="__container__"] {
|
|
41
|
+
display: block !important;
|
|
42
|
+
line-height: 0 !important;
|
|
16
43
|
}
|
|
17
44
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
|
|
46
|
+
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
47
|
+
.nodebb-ezoic-wrap.is-empty {
|
|
48
|
+
display: block !important;
|
|
49
|
+
margin: 0 !important;
|
|
50
|
+
padding: 0 !important;
|
|
51
|
+
/* Don't fully collapse (can prevent fill / triggers "unused"), keep it at 1px */
|
|
52
|
+
height: 1px !important;
|
|
53
|
+
min-height: 1px !important;
|
|
54
|
+
overflow: hidden !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
Optional: also neutralize spacing on native Ezoic `.ezoic-ad` blocks.
|
|
59
|
+
(Keeps your previous "CSS very good" behavior.)
|
|
60
|
+
*/
|
|
61
|
+
.ezoic-ad {
|
|
22
62
|
margin: 0 !important;
|
|
63
|
+
padding: 0 !important;
|
|
64
|
+
}
|
|
65
|
+
/* Remove Ezoic's large reserved min-height inside our wrappers (topics/messages) */
|
|
66
|
+
.nodebb-ezoic-wrap .ezoic-ad,
|
|
67
|
+
.nodebb-ezoic-wrap span.ezoic-ad {
|
|
68
|
+
min-height: 1px !important; /* kill 400px gaps */
|
|
69
|
+
height: auto !important;
|
|
23
70
|
}
|
|
24
71
|
|
|
25
|
-
/*
|
|
72
|
+
/* Ensure Ezoic reportline doesn't affect layout */
|
|
73
|
+
.nodebb-ezoic-wrap .reportline{position:absolute!important;}
|
|
74
|
+
|
|
75
|
+
/* Ezoic sometimes injects `position: sticky` inside placements. In long NodeBB topics,
|
|
76
|
+
this can create "gliding" and sudden disappear/reappear effects while scrolling.
|
|
77
|
+
We neutralize sticky positioning *inside our injected wrappers* only. */
|
|
26
78
|
.nodebb-ezoic-wrap .ezads-sticky-intradiv {
|
|
27
79
|
position: static !important;
|
|
28
|
-
|
|
80
|
+
top: auto !important;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/* ===== V17 host styling ===== */
|
|
85
|
+
li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
|
|
86
|
+
li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
|
|
87
|
+
/* ===== /V17 ===== */
|
|
88
|
+
|
package/public/test.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hi
|