nodebb-plugin-ezoic-infinite 1.5.48 → 1.5.49
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 +21 -5
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/public/admin.js +2 -3
- package/public/client.js +728 -193
- package/public/style.css +32 -3
package/library.js
CHANGED
|
@@ -27,13 +27,21 @@ async function getAllGroups() {
|
|
|
27
27
|
}
|
|
28
28
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
29
|
const data = await groups.getGroupsData(filtered);
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
31
|
+
const valid = data.filter(g => g && g.name);
|
|
32
|
+
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
33
|
+
return valid;
|
|
33
34
|
}
|
|
35
|
+
let _settingsCache = null;
|
|
36
|
+
let _settingsCacheAt = 0;
|
|
37
|
+
const SETTINGS_TTL = 30000; // 30s
|
|
38
|
+
|
|
34
39
|
async function getSettings() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
35
42
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
36
|
-
|
|
43
|
+
_settingsCacheAt = Date.now();
|
|
44
|
+
_settingsCache = {
|
|
37
45
|
// Between-post ads (simple blocks) in category topic list
|
|
38
46
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
39
47
|
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
@@ -54,6 +62,7 @@ async function getSettings() {
|
|
|
54
62
|
|
|
55
63
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
56
64
|
};
|
|
65
|
+
return _settingsCache;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
@@ -62,6 +71,13 @@ async function isUserExcluded(uid, excludedGroups) {
|
|
|
62
71
|
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
plugin.onSettingsSet = function (data) {
|
|
75
|
+
// Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
|
|
76
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
77
|
+
_settingsCache = null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
plugin.addAdminNavigation = async (header) => {
|
|
66
82
|
header.plugins = header.plugins || [];
|
|
67
83
|
header.plugins.push({
|
|
@@ -89,7 +105,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
89
105
|
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
90
106
|
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
91
107
|
|
|
92
|
-
router.get('/api/plugins/ezoic-infinite/config',
|
|
108
|
+
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
93
109
|
const settings = await getSettings();
|
|
94
110
|
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
95
111
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
e.preventDefault();
|
|
14
14
|
|
|
15
15
|
Settings.save('ezoic-infinite', $form, function () {
|
|
16
|
-
// Toast vert (NodeBB core)
|
|
17
16
|
if (alerts && typeof alerts.success === 'function') {
|
|
18
|
-
alerts.success('
|
|
17
|
+
alerts.success('[[admin/settings:saved]]');
|
|
19
18
|
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
20
|
-
window.app.alertSuccess('
|
|
19
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
21
20
|
}
|
|
22
21
|
});
|
|
23
22
|
});
|
package/public/client.js
CHANGED
|
@@ -1,43 +1,135 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// NodeBB client context
|
|
4
5
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
6
|
|
|
7
|
+
const WRAP_CLASS = 'ezoic-ad';
|
|
8
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
9
|
+
|
|
10
|
+
// Insert at most N ads per run to keep the UI smooth on infinite scroll
|
|
11
|
+
const MAX_INSERTS_PER_RUN = 3;
|
|
12
|
+
|
|
13
|
+
// Preload before viewport (earlier load for smoother scroll)
|
|
14
|
+
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
15
|
+
const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
|
|
16
|
+
|
|
17
|
+
// When the user scrolls very fast, temporarily preload more aggressively.
|
|
18
|
+
// This helps ensure ads are already in-flight before the user reaches them.
|
|
19
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '4200px 0px 4200px 0px';
|
|
20
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '2500px 0px 2500px 0px';
|
|
21
|
+
const BOOST_DURATION_MS = 2500;
|
|
22
|
+
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
23
|
+
|
|
24
|
+
const MAX_INFLIGHT_DESKTOP = 4;
|
|
25
|
+
const MAX_INFLIGHT_MOBILE = 3;
|
|
26
|
+
|
|
27
|
+
function isBoosted() {
|
|
28
|
+
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isMobile() {
|
|
32
|
+
try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPreloadRootMargin() {
|
|
36
|
+
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
37
|
+
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getMaxInflight() {
|
|
41
|
+
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
42
|
+
return base + (isBoosted() ? 1 : 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
const SELECTORS = {
|
|
7
46
|
topicItem: 'li[component="category/topic"]',
|
|
8
47
|
postItem: '[component="post"][data-pid]',
|
|
9
48
|
categoryItem: 'li[component="categories/category"]',
|
|
10
49
|
};
|
|
11
50
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
51
|
+
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
52
|
+
let blockedUntil = 0;
|
|
53
|
+
function isBlocked() {
|
|
54
|
+
return Date.now() < blockedUntil;
|
|
55
|
+
}
|
|
15
56
|
|
|
16
57
|
const state = {
|
|
17
58
|
pageKey: null,
|
|
18
59
|
cfg: null,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
|
|
61
|
+
// Full lists (never consumed) + cursors for round-robin reuse
|
|
62
|
+
allTopics: [],
|
|
63
|
+
allPosts: [],
|
|
64
|
+
allCategories: [],
|
|
65
|
+
curTopics: 0,
|
|
66
|
+
curPosts: 0,
|
|
67
|
+
curCategories: 0,
|
|
68
|
+
|
|
69
|
+
// throttle per placeholder id
|
|
26
70
|
lastShowById: new Map(),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
internalDomChange: 0,
|
|
72
|
+
lastRecycleAt: { topic: 0, categoryTopics: 0, categories: 0 },
|
|
73
|
+
|
|
74
|
+
// track placeholders that have been shown at least once in this pageview
|
|
75
|
+
usedOnce: new Set(),
|
|
76
|
+
|
|
77
|
+
// observers / schedulers
|
|
78
|
+
domObs: null,
|
|
79
|
+
io: null,
|
|
80
|
+
runQueued: false,
|
|
81
|
+
|
|
82
|
+
// preloading budget
|
|
83
|
+
inflight: 0,
|
|
84
|
+
pending: [],
|
|
85
|
+
pendingSet: new Set(),
|
|
86
|
+
|
|
87
|
+
// fast scroll boosting
|
|
88
|
+
scrollBoostUntil: 0,
|
|
89
|
+
lastScrollY: 0,
|
|
90
|
+
lastScrollTs: 0,
|
|
91
|
+
ioMargin: null,
|
|
92
|
+
|
|
93
|
+
// hero)
|
|
94
|
+
heroDoneForPage: false,
|
|
31
95
|
};
|
|
32
96
|
|
|
33
|
-
const
|
|
97
|
+
const insertingIds = new Set();
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function markEmptyWrapper(id) {
|
|
101
|
+
try {
|
|
102
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
103
|
+
if (!ph || !ph.isConnected) return;
|
|
104
|
+
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
105
|
+
if (!wrap) return;
|
|
106
|
+
// If still empty after a delay, collapse it.
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
try {
|
|
109
|
+
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
110
|
+
if (!ph2 || !ph2.isConnected) return;
|
|
111
|
+
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
112
|
+
if (!w2) return;
|
|
113
|
+
// consider empty if only whitespace and no iframes/ins/img
|
|
114
|
+
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
115
|
+
if (!hasAd) w2.classList.add('is-empty');
|
|
116
|
+
} catch (e) {}
|
|
117
|
+
}, 3500);
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Production build: debug disabled
|
|
122
|
+
function dbg() {}
|
|
123
|
+
|
|
124
|
+
// ---------- small utils ----------
|
|
34
125
|
|
|
35
126
|
function normalizeBool(v) {
|
|
36
127
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
37
128
|
}
|
|
38
129
|
|
|
39
130
|
function uniqInts(lines) {
|
|
40
|
-
const out = []
|
|
131
|
+
const out = [];
|
|
132
|
+
const seen = new Set();
|
|
41
133
|
for (const v of lines) {
|
|
42
134
|
const n = parseInt(v, 10);
|
|
43
135
|
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
@@ -50,7 +142,10 @@
|
|
|
50
142
|
|
|
51
143
|
function parsePool(raw) {
|
|
52
144
|
if (!raw) return [];
|
|
53
|
-
const lines = String(raw)
|
|
145
|
+
const lines = String(raw)
|
|
146
|
+
.split(/\r?\n/)
|
|
147
|
+
.map(s => s.trim())
|
|
148
|
+
.filter(Boolean);
|
|
54
149
|
return uniqInts(lines);
|
|
55
150
|
}
|
|
56
151
|
|
|
@@ -70,6 +165,11 @@
|
|
|
70
165
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
166
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
167
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
168
|
+
|
|
169
|
+
// fallback by DOM
|
|
170
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
171
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
172
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
73
173
|
return 'other';
|
|
74
174
|
}
|
|
75
175
|
|
|
@@ -77,76 +177,221 @@
|
|
|
77
177
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
78
178
|
}
|
|
79
179
|
|
|
180
|
+
function getCategoryItems() {
|
|
181
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
182
|
+
}
|
|
183
|
+
|
|
80
184
|
function getPostContainers() {
|
|
81
|
-
|
|
185
|
+
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
186
|
+
return nodes.filter((el) => {
|
|
187
|
+
if (!el || !el.isConnected) return false;
|
|
188
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
189
|
+
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
190
|
+
if (parentPost && parentPost !== el) return false;
|
|
191
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
82
194
|
}
|
|
83
195
|
|
|
84
|
-
|
|
85
|
-
|
|
196
|
+
// ---------- warm-up & patching ----------
|
|
197
|
+
|
|
198
|
+
const _warmLinksDone = new Set();
|
|
199
|
+
function warmUpNetwork() {
|
|
200
|
+
try {
|
|
201
|
+
const head = document.head || document.getElementsByTagName('head')[0];
|
|
202
|
+
if (!head) return;
|
|
203
|
+
const links = [
|
|
204
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
205
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
206
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
207
|
+
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
208
|
+
];
|
|
209
|
+
for (const [rel, href, cors] of links) {
|
|
210
|
+
const key = `${rel}|${href}`;
|
|
211
|
+
if (_warmLinksDone.has(key)) continue;
|
|
212
|
+
_warmLinksDone.add(key);
|
|
213
|
+
const link = document.createElement('link');
|
|
214
|
+
link.rel = rel;
|
|
215
|
+
link.href = href;
|
|
216
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
217
|
+
head.appendChild(link);
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {}
|
|
86
220
|
}
|
|
87
221
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
222
|
+
// Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
|
|
223
|
+
function patchShowAds() {
|
|
224
|
+
const applyPatch = () => {
|
|
225
|
+
try {
|
|
226
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
227
|
+
const ez = window.ezstandalone;
|
|
228
|
+
if (window.__nodebbEzoicPatched) return;
|
|
229
|
+
if (typeof ez.showAds !== 'function') return;
|
|
230
|
+
|
|
231
|
+
window.__nodebbEzoicPatched = true;
|
|
232
|
+
const orig = ez.showAds;
|
|
233
|
+
|
|
234
|
+
ez.showAds = function (...args) {
|
|
235
|
+
if (isBlocked()) return;
|
|
236
|
+
|
|
237
|
+
let ids = [];
|
|
238
|
+
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
239
|
+
else ids = args;
|
|
240
|
+
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const v of ids) {
|
|
243
|
+
const id = parseInt(v, 10);
|
|
244
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
245
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
246
|
+
if (!ph || !ph.isConnected) continue;
|
|
247
|
+
seen.add(id);
|
|
248
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
};
|
|
91
253
|
|
|
92
|
-
|
|
254
|
+
applyPatch();
|
|
255
|
+
if (!window.__nodebbEzoicPatched) {
|
|
93
256
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})();
|
|
257
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
258
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
259
|
+
window.ezstandalone.cmd.push(applyPatch);
|
|
260
|
+
} catch (e) {}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const RECYCLE_COOLDOWN_MS = 1500;
|
|
103
265
|
|
|
104
|
-
|
|
266
|
+
function kindKeyFromClass(kindClass) {
|
|
267
|
+
if (kindClass === 'ezoic-ad-message') return 'topic';
|
|
268
|
+
if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
|
|
269
|
+
if (kindClass === 'ezoic-ad-categories') return 'categories';
|
|
270
|
+
return 'topic';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function withInternalDomChange(fn) {
|
|
274
|
+
state.internalDomChange++;
|
|
275
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function canRecycle(kind) {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const last = state.lastRecycleAt[kind] || 0;
|
|
281
|
+
if (now - last < RECYCLE_COOLDOWN_MS) return false;
|
|
282
|
+
state.lastRecycleAt[kind] = now;
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
// ---------- config & pools ----------
|
|
286
|
+
|
|
287
|
+
async function fetchConfigOnce() {
|
|
288
|
+
if (state.cfg) return state.cfg;
|
|
289
|
+
try {
|
|
290
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
291
|
+
if (!res.ok) return null;
|
|
292
|
+
state.cfg = await res.json();
|
|
293
|
+
return state.cfg;
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
105
297
|
}
|
|
106
298
|
|
|
107
299
|
function initPools(cfg) {
|
|
108
|
-
if (!
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
300
|
+
if (!cfg) return;
|
|
301
|
+
if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
|
|
302
|
+
if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
303
|
+
if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
111
304
|
}
|
|
112
305
|
|
|
113
|
-
|
|
114
|
-
if (!ids || !ids.length) return;
|
|
115
|
-
const filtered = ids.filter(id => Number.isFinite(id) && id > 0);
|
|
116
|
-
if (!filtered.length) return;
|
|
306
|
+
// ---------- insertion primitives ----------
|
|
117
307
|
|
|
308
|
+
function isAdjacentAd(target) {
|
|
309
|
+
if (!target) return false;
|
|
310
|
+
const next = target.nextElementSibling;
|
|
311
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
312
|
+
const prev = target.previousElementSibling;
|
|
313
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
function getWrapIdFromWrap(wrap) {
|
|
118
319
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
320
|
+
const v = wrap.getAttribute('data-ezoic-wrapid');
|
|
321
|
+
if (v) return String(v);
|
|
322
|
+
const ph = wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
323
|
+
if (ph && ph.id) return ph.id.replace(PLACEHOLDER_PREFIX, '');
|
|
324
|
+
} catch (e) {}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
127
327
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
328
|
+
function safeDestroyById(id) {
|
|
329
|
+
try {
|
|
330
|
+
const ez = window.ezstandalone;
|
|
331
|
+
if (ez && typeof ez.destroyPlaceholders === 'function') {
|
|
332
|
+
ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
|
|
132
333
|
}
|
|
133
|
-
|
|
134
|
-
// Recyclage: libérer après 100ms
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
137
|
-
}, 100);
|
|
138
334
|
} catch (e) {}
|
|
139
335
|
}
|
|
140
336
|
|
|
141
|
-
function
|
|
337
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
338
|
+
if (!items || !items.length) return 0;
|
|
339
|
+
const itemSet = new Set(items);
|
|
340
|
+
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
341
|
+
let removed = 0;
|
|
342
|
+
|
|
343
|
+
wraps.forEach((wrap) => {
|
|
344
|
+
// NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
|
|
345
|
+
let ok = false;
|
|
346
|
+
let prev = wrap.previousElementSibling;
|
|
347
|
+
for (let i = 0; i < 3 && prev; i++) {
|
|
348
|
+
if (itemSet.has(prev)) { ok = true; break; }
|
|
349
|
+
prev = prev.previousElementSibling;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!ok) {
|
|
353
|
+
const id = getWrapIdFromWrap(wrap);
|
|
354
|
+
withInternalDomChange(() => {
|
|
355
|
+
try {
|
|
356
|
+
if (id) safeDestroyById(id);
|
|
357
|
+
wrap.remove();
|
|
358
|
+
} catch (e) {}
|
|
359
|
+
});
|
|
360
|
+
removed++;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
365
|
+
return removed;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function refreshEmptyState(id) {
|
|
369
|
+
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
370
|
+
window.setTimeout(() => {
|
|
371
|
+
try {
|
|
372
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
373
|
+
if (!ph || !ph.isConnected) return;
|
|
374
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
375
|
+
if (!wrap) return;
|
|
376
|
+
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
377
|
+
if (hasContent) wrap.classList.remove('is-empty');
|
|
378
|
+
else wrap.classList.add('is-empty');
|
|
379
|
+
} catch (e) {}
|
|
380
|
+
}, 3500);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
142
384
|
const wrap = document.createElement('div');
|
|
143
385
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
144
386
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
387
|
+
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
145
388
|
wrap.style.width = '100%';
|
|
146
389
|
|
|
147
390
|
const ph = document.createElement('div');
|
|
148
391
|
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
392
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
149
393
|
wrap.appendChild(ph);
|
|
394
|
+
|
|
150
395
|
return wrap;
|
|
151
396
|
}
|
|
152
397
|
|
|
@@ -157,215 +402,505 @@
|
|
|
157
402
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
158
403
|
if (!target || !target.insertAdjacentElement) return null;
|
|
159
404
|
if (findWrap(kindClass, afterPos)) return null;
|
|
405
|
+
if (insertingIds.has(id)) return null;
|
|
406
|
+
|
|
407
|
+
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
408
|
+
if (existingPh && existingPh.isConnected) return null;
|
|
160
409
|
|
|
410
|
+
insertingIds.add(id);
|
|
161
411
|
try {
|
|
162
412
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
163
413
|
target.insertAdjacentElement('afterend', wrap);
|
|
164
414
|
return wrap;
|
|
415
|
+
} finally {
|
|
416
|
+
insertingIds.delete(id);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function pickIdFromAll(allIds, cursorKey) {
|
|
421
|
+
const n = allIds.length;
|
|
422
|
+
if (!n) return null;
|
|
423
|
+
|
|
424
|
+
// Try at most n ids to find one that's not already in the DOM
|
|
425
|
+
for (let tries = 0; tries < n; tries++) {
|
|
426
|
+
const idx = state[cursorKey] % n;
|
|
427
|
+
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
428
|
+
|
|
429
|
+
const id = allIds[idx];
|
|
430
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
431
|
+
if (ph && ph.isConnected) continue;
|
|
432
|
+
|
|
433
|
+
return id;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
function removeOneOldWrap(kindClass) {
|
|
440
|
+
try {
|
|
441
|
+
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
442
|
+
if (!wraps.length) return false;
|
|
443
|
+
|
|
444
|
+
// Prefer a wrap far above the viewport
|
|
445
|
+
let victim = null;
|
|
446
|
+
for (const w of wraps) {
|
|
447
|
+
const r = w.getBoundingClientRect();
|
|
448
|
+
if (r.bottom < -2000) { victim = w; break; }
|
|
449
|
+
}
|
|
450
|
+
// Otherwise remove the earliest one in the document
|
|
451
|
+
if (!victim) victim = wraps[0];
|
|
452
|
+
|
|
453
|
+
// Unobserve placeholder if still observed
|
|
454
|
+
try {
|
|
455
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
456
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
457
|
+
} catch (e) {}
|
|
458
|
+
|
|
459
|
+
victim.remove();
|
|
460
|
+
return true;
|
|
165
461
|
} catch (e) {
|
|
166
|
-
return
|
|
462
|
+
return false;
|
|
167
463
|
}
|
|
168
464
|
}
|
|
169
465
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
466
|
+
function enqueueShow(id) {
|
|
467
|
+
if (!id || isBlocked()) return;
|
|
468
|
+
|
|
469
|
+
// Basic per-id throttle (prevents rapid re-requests when DOM churns)
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const last = state.lastShowById.get(id) || 0;
|
|
472
|
+
if (now - last < 900) return;
|
|
473
|
+
|
|
474
|
+
const max = getMaxInflight();
|
|
475
|
+
if (state.inflight >= max) {
|
|
476
|
+
if (!state.pendingSet.has(id)) {
|
|
477
|
+
state.pending.push(id);
|
|
478
|
+
state.pendingSet.add(id);
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
startShow(id);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function drainQueue() {
|
|
486
|
+
if (isBlocked()) return;
|
|
487
|
+
const max = getMaxInflight();
|
|
488
|
+
while (state.inflight < max && state.pending.length) {
|
|
489
|
+
const id = state.pending.shift();
|
|
490
|
+
state.pendingSet.delete(id);
|
|
491
|
+
startShow(id);
|
|
173
492
|
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function startShow(id) {
|
|
496
|
+
if (!id || isBlocked()) return;
|
|
497
|
+
|
|
498
|
+
state.inflight++;
|
|
499
|
+
let released = false;
|
|
500
|
+
const release = () => {
|
|
501
|
+
if (released) return;
|
|
502
|
+
released = true;
|
|
503
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
504
|
+
drainQueue();
|
|
505
|
+
};
|
|
174
506
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
507
|
+
const hardTimer = setTimeout(release, 6500);
|
|
508
|
+
|
|
509
|
+
requestAnimationFrame(() => {
|
|
178
510
|
try {
|
|
511
|
+
if (isBlocked()) return;
|
|
512
|
+
|
|
513
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
514
|
+
if (!ph || !ph.isConnected) return;
|
|
515
|
+
|
|
516
|
+
const now2 = Date.now();
|
|
517
|
+
const last2 = state.lastShowById.get(id) || 0;
|
|
518
|
+
if (now2 - last2 < 900) return;
|
|
519
|
+
state.lastShowById.set(id, now2);
|
|
520
|
+
|
|
179
521
|
window.ezstandalone = window.ezstandalone || {};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
522
|
+
const ez = window.ezstandalone;
|
|
523
|
+
|
|
524
|
+
const doShow = () => {
|
|
525
|
+
try {
|
|
526
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
527
|
+
try { ez.destroyPlaceholders(id); } catch (e) {}
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {}
|
|
530
|
+
|
|
531
|
+
try { ez.showAds(id); } catch (e) {}
|
|
532
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
533
|
+
try { markEmptyWrapper(id); } catch (e) {}
|
|
534
|
+
|
|
535
|
+
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (Array.isArray(ez.cmd)) {
|
|
539
|
+
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
540
|
+
} else {
|
|
541
|
+
doShow();
|
|
542
|
+
}
|
|
543
|
+
} finally {
|
|
544
|
+
// If we returned early, hardTimer will release.
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
// ---------- preload / above-the-fold ----------
|
|
551
|
+
|
|
552
|
+
function ensurePreloadObserver() {
|
|
553
|
+
const desiredMargin = getPreloadRootMargin();
|
|
554
|
+
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
555
|
+
|
|
556
|
+
// Rebuild IO if margin changed (e.g., scroll boost toggled)
|
|
557
|
+
if (state.io) {
|
|
558
|
+
try { state.io.disconnect(); } catch (e) {}
|
|
559
|
+
state.io = null;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
state.io = new IntersectionObserver((entries) => {
|
|
563
|
+
for (const ent of entries) {
|
|
564
|
+
if (!ent.isIntersecting) continue;
|
|
565
|
+
const el = ent.target;
|
|
566
|
+
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
567
|
+
|
|
568
|
+
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
569
|
+
const id = parseInt(idAttr, 10);
|
|
570
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
186
571
|
}
|
|
187
|
-
});
|
|
572
|
+
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
573
|
+
state.ioMargin = desiredMargin;
|
|
574
|
+
} catch (e) {
|
|
575
|
+
state.io = null;
|
|
576
|
+
state.ioMargin = null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// If we rebuilt the observer, re-observe existing placeholders so we don't miss them.
|
|
580
|
+
try {
|
|
581
|
+
if (state.io) {
|
|
582
|
+
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
583
|
+
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
584
|
+
}
|
|
188
585
|
} catch (e) {}
|
|
586
|
+
return state.io;
|
|
189
587
|
}
|
|
190
588
|
|
|
191
|
-
function
|
|
192
|
-
|
|
589
|
+
function observePlaceholder(id) {
|
|
590
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
591
|
+
if (!ph || !ph.isConnected) return;
|
|
592
|
+
const io = ensurePreloadObserver();
|
|
593
|
+
try { io && io.observe(ph); } catch (e) {}
|
|
193
594
|
|
|
194
|
-
|
|
195
|
-
|
|
595
|
+
// If already above fold, fire immediately
|
|
596
|
+
try {
|
|
597
|
+
const r = ph.getBoundingClientRect();
|
|
598
|
+
const screens = isBoosted() ? 5.0 : 3.0;
|
|
599
|
+
const minBottom = isBoosted() ? -1500 : -800;
|
|
600
|
+
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
601
|
+
} catch (e) {}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------- insertion logic ----------
|
|
196
605
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
606
|
+
function computeTargets(count, interval, showFirst) {
|
|
607
|
+
const out = [];
|
|
608
|
+
if (count <= 0) return out;
|
|
609
|
+
if (showFirst) out.push(1);
|
|
610
|
+
for (let i = 1; i <= count; i++) {
|
|
611
|
+
if (i % interval === 0) out.push(i);
|
|
202
612
|
}
|
|
613
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
617
|
+
if (!items.length) return 0;
|
|
618
|
+
|
|
619
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
620
|
+
let inserted = 0;
|
|
621
|
+
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
203
622
|
|
|
204
623
|
for (const afterPos of targets) {
|
|
205
|
-
if (inserted >=
|
|
624
|
+
if (inserted >= maxInserts) break;
|
|
206
625
|
|
|
207
626
|
const el = items[afterPos - 1];
|
|
208
627
|
if (!el || !el.isConnected) continue;
|
|
628
|
+
if (isAdjacentAd(el)) continue;
|
|
209
629
|
if (findWrap(kindClass, afterPos)) continue;
|
|
210
630
|
|
|
211
|
-
|
|
212
|
-
if (!id)
|
|
213
|
-
|
|
631
|
+
let id = pickIdFromAll(allIds, cursorKey);
|
|
632
|
+
if (!id) {
|
|
633
|
+
// No free ids: recycle an old ad wrapper so we can reuse its placeholder id.
|
|
634
|
+
// Guard against tight observer loops.
|
|
635
|
+
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
636
|
+
dbg('recycle-skip-cooldown', kindClass);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
let recycled = false;
|
|
640
|
+
withInternalDomChange(() => {
|
|
641
|
+
recycled = removeOneOldWrap(kindClass);
|
|
642
|
+
});
|
|
643
|
+
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
644
|
+
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
214
647
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
215
|
-
if (!wrap)
|
|
648
|
+
if (!wrap) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
216
651
|
|
|
217
|
-
|
|
218
|
-
callShowAds(id);
|
|
652
|
+
observePlaceholder(id);
|
|
219
653
|
inserted += 1;
|
|
220
654
|
}
|
|
221
655
|
|
|
222
656
|
return inserted;
|
|
223
657
|
}
|
|
224
658
|
|
|
225
|
-
async function
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
659
|
+
async function insertHeroAdEarly() {
|
|
660
|
+
if (state.heroDoneForPage) return;
|
|
661
|
+
const cfg = await fetchConfigOnce();
|
|
662
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
663
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
230
664
|
|
|
231
665
|
initPools(cfg);
|
|
232
666
|
|
|
233
667
|
const kind = getKind();
|
|
234
|
-
let
|
|
668
|
+
let items = [];
|
|
669
|
+
let allIds = [];
|
|
670
|
+
let cursorKey = '';
|
|
671
|
+
let kindClass = '';
|
|
672
|
+
let showFirst = false;
|
|
235
673
|
|
|
236
674
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
state.poolPosts,
|
|
243
|
-
state.usedPosts
|
|
244
|
-
);
|
|
675
|
+
items = getPostContainers();
|
|
676
|
+
allIds = state.allPosts;
|
|
677
|
+
cursorKey = 'curPosts';
|
|
678
|
+
kindClass = 'ezoic-ad-message';
|
|
679
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
245
680
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
state.poolTopics,
|
|
252
|
-
state.usedTopics
|
|
253
|
-
);
|
|
681
|
+
items = getTopicItems();
|
|
682
|
+
allIds = state.allTopics;
|
|
683
|
+
cursorKey = 'curTopics';
|
|
684
|
+
kindClass = 'ezoic-ad-between';
|
|
685
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
254
686
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
);
|
|
687
|
+
items = getCategoryItems();
|
|
688
|
+
allIds = state.allCategories;
|
|
689
|
+
cursorKey = 'curCategories';
|
|
690
|
+
kindClass = 'ezoic-ad-categories';
|
|
691
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
692
|
+
} else {
|
|
693
|
+
return;
|
|
263
694
|
}
|
|
695
|
+
|
|
696
|
+
if (!items.length) return;
|
|
697
|
+
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
698
|
+
|
|
699
|
+
// Insert after the very first item (above-the-fold)
|
|
700
|
+
const afterPos = 1;
|
|
701
|
+
const el = items[afterPos - 1];
|
|
702
|
+
if (!el || !el.isConnected) return;
|
|
703
|
+
if (isAdjacentAd(el)) return;
|
|
704
|
+
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
705
|
+
|
|
706
|
+
const id = pickIdFromAll(allIds, cursorKey);
|
|
707
|
+
if (!id) return;
|
|
708
|
+
|
|
709
|
+
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
710
|
+
if (!wrap) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
state.heroDoneForPage = true;
|
|
715
|
+
observePlaceholder(id);
|
|
264
716
|
}
|
|
265
717
|
|
|
266
|
-
function
|
|
267
|
-
if (
|
|
268
|
-
|
|
718
|
+
async function runCore() {
|
|
719
|
+
if (isBlocked()) { dbg('blocked'); return; }
|
|
720
|
+
|
|
721
|
+
patchShowAds();
|
|
269
722
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
723
|
+
const cfg = await fetchConfigOnce();
|
|
724
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
725
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
726
|
+
initPools(cfg);
|
|
727
|
+
|
|
728
|
+
const kind = getKind();
|
|
729
|
+
|
|
730
|
+
if (kind === 'topic') {
|
|
731
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
732
|
+
const __items = getPostContainers();
|
|
733
|
+
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
734
|
+
injectBetween(
|
|
735
|
+
'ezoic-ad-message',
|
|
736
|
+
__items,
|
|
737
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
738
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
739
|
+
state.allPosts,
|
|
740
|
+
'curPosts'
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
} else if (kind === 'categoryTopics') {
|
|
744
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
745
|
+
const __items = getTopicItems();
|
|
746
|
+
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
747
|
+
injectBetween(
|
|
748
|
+
'ezoic-ad-between',
|
|
749
|
+
__items,
|
|
750
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
751
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
752
|
+
state.allTopics,
|
|
753
|
+
'curTopics'
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
} else if (kind === 'categories') {
|
|
757
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
758
|
+
const __items = getCategoryItems();
|
|
759
|
+
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
760
|
+
injectBetween(
|
|
761
|
+
'ezoic-ad-categories',
|
|
762
|
+
__items,
|
|
763
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
764
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
765
|
+
state.allCategories,
|
|
766
|
+
'curCategories'
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function scheduleRun() {
|
|
773
|
+
if (state.runQueued) return;
|
|
774
|
+
state.runQueued = true;
|
|
775
|
+
window.requestAnimationFrame(() => {
|
|
776
|
+
state.runQueued = false;
|
|
273
777
|
const pk = getPageKey();
|
|
274
778
|
if (state.pageKey && pk !== state.pageKey) return;
|
|
275
779
|
runCore().catch(() => {});
|
|
276
|
-
}
|
|
780
|
+
});
|
|
277
781
|
}
|
|
278
782
|
|
|
783
|
+
// ---------- observers / lifecycle ----------
|
|
784
|
+
|
|
279
785
|
function cleanup() {
|
|
280
|
-
|
|
281
|
-
if (allIds.length) destroyPlaceholderIds(allIds);
|
|
786
|
+
blockedUntil = Date.now() + 1200;
|
|
282
787
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
788
|
+
// remove all wrappers
|
|
789
|
+
try {
|
|
790
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
791
|
+
try { el.remove(); } catch (e) {}
|
|
792
|
+
});
|
|
793
|
+
} catch (e) {}
|
|
286
794
|
|
|
287
|
-
|
|
795
|
+
// reset state
|
|
288
796
|
state.cfg = null;
|
|
289
|
-
state.
|
|
290
|
-
state.
|
|
291
|
-
state.
|
|
292
|
-
state.
|
|
293
|
-
state.
|
|
294
|
-
state.
|
|
295
|
-
state.usedCategories.clear();
|
|
797
|
+
state.allTopics = [];
|
|
798
|
+
state.allPosts = [];
|
|
799
|
+
state.allCategories = [];
|
|
800
|
+
state.curTopics = 0;
|
|
801
|
+
state.curPosts = 0;
|
|
802
|
+
state.curCategories = 0;
|
|
296
803
|
state.lastShowById.clear();
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
804
|
+
state.inflight = 0;
|
|
805
|
+
state.pending = [];
|
|
806
|
+
try { state.pendingSet && state.pendingSet.clear(); } catch (e) {}
|
|
807
|
+
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
808
|
+
state.heroDoneForPage = false;
|
|
303
809
|
|
|
304
|
-
|
|
305
|
-
clearTimeout(state.timer);
|
|
306
|
-
state.timer = null;
|
|
810
|
+
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
307
811
|
}
|
|
308
812
|
|
|
309
|
-
function
|
|
310
|
-
if (state.
|
|
311
|
-
state.
|
|
813
|
+
function ensureDomObserver() {
|
|
814
|
+
if (state.domObs) return;
|
|
815
|
+
state.domObs = new MutationObserver(() => {
|
|
816
|
+
if (state.internalDomChange > 0) return;
|
|
817
|
+
if (!isBlocked()) scheduleRun();
|
|
818
|
+
});
|
|
312
819
|
try {
|
|
313
|
-
state.
|
|
820
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
314
821
|
} catch (e) {}
|
|
315
822
|
}
|
|
316
823
|
|
|
317
|
-
function
|
|
318
|
-
const kind = getKind();
|
|
319
|
-
let selector = SELECTORS.postItem;
|
|
320
|
-
if (kind === 'categoryTopics') selector = SELECTORS.topicItem;
|
|
321
|
-
else if (kind === 'categories') selector = SELECTORS.categoryItem;
|
|
322
|
-
|
|
323
|
-
const check = () => {
|
|
324
|
-
if (document.querySelector(selector)) {
|
|
325
|
-
scheduleRun();
|
|
326
|
-
} else {
|
|
327
|
-
setTimeout(check, 200);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
check();
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function bind() {
|
|
824
|
+
function bindNodeBB() {
|
|
334
825
|
if (!$) return;
|
|
335
826
|
|
|
336
827
|
$(window).off('.ezoicInfinite');
|
|
337
|
-
|
|
828
|
+
|
|
829
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
830
|
+
cleanup();
|
|
831
|
+
});
|
|
832
|
+
|
|
338
833
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
339
834
|
state.pageKey = getPageKey();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
835
|
+
blockedUntil = 0;
|
|
836
|
+
|
|
837
|
+
warmUpNetwork();
|
|
838
|
+
patchShowAds();
|
|
839
|
+
ensurePreloadObserver();
|
|
840
|
+
ensureDomObserver();
|
|
841
|
+
|
|
842
|
+
// Ultra-fast above-the-fold first
|
|
843
|
+
insertHeroAdEarly().catch(() => {});
|
|
344
844
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
waitForContentThenRun();
|
|
845
|
+
// Then normal insertion
|
|
846
|
+
scheduleRun();
|
|
348
847
|
});
|
|
349
848
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
849
|
+
// Infinite scroll / partial updates
|
|
850
|
+
$(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
|
|
851
|
+
if (isBlocked()) return;
|
|
852
|
+
scheduleRun();
|
|
353
853
|
});
|
|
354
854
|
}
|
|
355
855
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
856
|
+
function bindScroll() {
|
|
857
|
+
let ticking = false;
|
|
858
|
+
window.addEventListener('scroll', () => {
|
|
859
|
+
// Detect very fast scrolling and temporarily boost preload/parallelism.
|
|
860
|
+
try {
|
|
861
|
+
const now = Date.now();
|
|
862
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
863
|
+
if (state.lastScrollTs) {
|
|
864
|
+
const dt = now - state.lastScrollTs;
|
|
865
|
+
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
866
|
+
if (dt > 0) {
|
|
867
|
+
const speed = dy / dt; // px/ms
|
|
868
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
869
|
+
const wasBoosted = isBoosted();
|
|
870
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, now + BOOST_DURATION_MS);
|
|
871
|
+
if (!wasBoosted) {
|
|
872
|
+
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
873
|
+
ensurePreloadObserver();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
state.lastScrollY = y;
|
|
879
|
+
state.lastScrollTs = now;
|
|
880
|
+
} catch (e) {}
|
|
881
|
+
|
|
882
|
+
if (ticking) return;
|
|
883
|
+
ticking = true;
|
|
884
|
+
window.requestAnimationFrame(() => {
|
|
885
|
+
ticking = false;
|
|
886
|
+
if (!isBlocked()) scheduleRun();
|
|
887
|
+
});
|
|
888
|
+
}, { passive: true });
|
|
362
889
|
}
|
|
363
890
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
891
|
+
// ---------- boot ----------
|
|
892
|
+
|
|
893
|
+
state.pageKey = getPageKey();
|
|
894
|
+
warmUpNetwork();
|
|
895
|
+
patchShowAds();
|
|
896
|
+
ensurePreloadObserver();
|
|
897
|
+
ensureDomObserver();
|
|
898
|
+
|
|
899
|
+
bindNodeBB();
|
|
900
|
+
bindScroll();
|
|
901
|
+
|
|
902
|
+
// First paint: try hero + run
|
|
903
|
+
blockedUntil = 0;
|
|
904
|
+
insertHeroAdEarly().catch(() => {});
|
|
905
|
+
scheduleRun();
|
|
371
906
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
1
2
|
.ezoic-ad {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin: 0 !important;
|
|
2
6
|
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
3
11
|
margin: 0 !important;
|
|
12
|
+
padding: 0 !important;
|
|
13
|
+
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
+
.ezoic-ad span.ezoic-ad,
|
|
18
|
+
.ezoic-ad .ezoic-ad {
|
|
19
|
+
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
+
.ezoic-ad.is-empty {
|
|
26
|
+
display: block !important;
|
|
27
|
+
margin: 0 !important;
|
|
28
|
+
padding: 0 !important;
|
|
29
|
+
height: 0 !important;
|
|
30
|
+
min-height: 0 !important;
|
|
31
|
+
overflow: hidden !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ezoic-ad {
|
|
4
35
|
min-height: 0 !important;
|
|
5
|
-
min-width: 0 !important;
|
|
6
36
|
}
|
|
7
37
|
|
|
8
|
-
.ezoic-ad
|
|
38
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
9
39
|
min-height: 0 !important;
|
|
10
|
-
min-width: 0 !important;
|
|
11
40
|
}
|