nodebb-plugin-ezoic-infinite 1.5.9 → 1.5.10
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 +2 -3
- package/package.json +1 -1
- package/public/client.js +541 -304
- package/public/style.css +19 -7
package/library.js
CHANGED
|
@@ -68,7 +68,7 @@ async function getSettings() {
|
|
|
68
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
69
69
|
if (!uid || !excludedGroups.length) return false;
|
|
70
70
|
const userGroups = await groups.getUserGroups([uid]);
|
|
71
|
-
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name :
|
|
71
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : g));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
plugin.onSettingsSet = function (data) {
|
|
@@ -107,11 +107,10 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
107
107
|
|
|
108
108
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
109
|
const settings = await getSettings();
|
|
110
|
-
const excluded = await isUserExcluded(
|
|
110
|
+
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
111
111
|
|
|
112
112
|
res.json({
|
|
113
113
|
excluded,
|
|
114
|
-
excludedGroups: settings.excludedGroups,
|
|
115
114
|
enableBetweenAds: settings.enableBetweenAds,
|
|
116
115
|
showFirstTopicAd: settings.showFirstTopicAd,
|
|
117
116
|
placeholderIds: settings.placeholderIds,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,367 +1,604 @@
|
|
|
1
|
-
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* - Event-driven only (ajaxify hooks + MutationObserver + IntersectionObserver)
|
|
6
|
-
* - Hard gate on server config (respects excluded groups BEFORE any injection)
|
|
7
|
-
* - Placeholder Registry: keeps all placeholder elements alive in a hidden pool so ez-standalone never complains
|
|
8
|
-
* - Safe showAds wrapper (array, varargs, single) + tokenized execution across ajaxify navigations
|
|
9
|
-
*/
|
|
4
|
+
// NodeBB client context
|
|
5
|
+
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 (tune if you want even earlier)
|
|
14
|
+
const PRELOAD_ROOT_MARGIN = '1200px 0px';
|
|
15
|
+
|
|
16
|
+
const SELECTORS = {
|
|
17
|
+
topicItem: 'li[component="category/topic"]',
|
|
18
|
+
postItem: '[component="post"][data-pid]',
|
|
19
|
+
categoryItem: 'li[component="categories/category"]',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Hard block during navigation to avoid “placeholder does not exist” spam
|
|
23
|
+
let EZOIC_BLOCKED = false;
|
|
24
|
+
|
|
25
|
+
const state = {
|
|
26
|
+
pageKey: null,
|
|
27
|
+
cfg: null,
|
|
28
|
+
|
|
29
|
+
poolTopics: [],
|
|
30
|
+
poolPosts: [],
|
|
31
|
+
poolCategories: [],
|
|
32
|
+
|
|
33
|
+
usedTopics: new Set(),
|
|
34
|
+
usedPosts: new Set(),
|
|
35
|
+
usedCategories: new Set(),
|
|
36
|
+
|
|
37
|
+
// throttle per placeholder id
|
|
38
|
+
lastShowById: new Map(),
|
|
39
|
+
|
|
40
|
+
// observers / schedulers
|
|
41
|
+
domObs: null,
|
|
42
|
+
io: null,
|
|
43
|
+
runQueued: false,
|
|
44
|
+
|
|
45
|
+
// hero
|
|
46
|
+
heroDoneForPage: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sessionDefinedIds = new Set();
|
|
50
|
+
const insertingIds = new Set();
|
|
51
|
+
|
|
52
|
+
// ---------- small utils ----------
|
|
53
|
+
|
|
54
|
+
function normalizeBool(v) {
|
|
55
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
24
56
|
}
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let cfg = null;
|
|
35
|
-
|
|
36
|
-
// -------------------------
|
|
37
|
-
// Placeholder Registry (keeps IDs alive)
|
|
38
|
-
// -------------------------
|
|
39
|
-
const Pool = {
|
|
40
|
-
el: null,
|
|
41
|
-
ensurePool() {
|
|
42
|
-
if (this.el && document.body.contains(this.el)) return this.el;
|
|
43
|
-
const d = document.createElement('div');
|
|
44
|
-
d.id = 'ezoic-placeholder-pool';
|
|
45
|
-
d.style.display = 'none';
|
|
46
|
-
d.setAttribute('aria-hidden', 'true');
|
|
47
|
-
document.body.appendChild(d);
|
|
48
|
-
this.el = d;
|
|
49
|
-
return d;
|
|
50
|
-
},
|
|
51
|
-
ensurePlaceholder(id) {
|
|
52
|
-
if (!id) return null;
|
|
53
|
-
let ph = document.getElementById(id);
|
|
54
|
-
if (ph) return ph;
|
|
55
|
-
const pool = this.ensurePool();
|
|
56
|
-
ph = document.createElement('div');
|
|
57
|
-
ph.id = id;
|
|
58
|
-
pool.appendChild(ph);
|
|
59
|
-
return ph;
|
|
60
|
-
},
|
|
61
|
-
returnToPool(id) {
|
|
62
|
-
const ph = document.getElementById(id);
|
|
63
|
-
if (!ph) return;
|
|
64
|
-
const pool = this.ensurePool();
|
|
65
|
-
if (ph.parentElement !== pool) {
|
|
66
|
-
pool.appendChild(ph);
|
|
58
|
+
function uniqInts(lines) {
|
|
59
|
+
const out = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
for (const v of lines) {
|
|
62
|
+
const n = parseInt(v, 10);
|
|
63
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
64
|
+
seen.add(n);
|
|
65
|
+
out.push(n);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
|
-
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (args.length === 1 && Array.isArray(args[0])) return args[0].flat().map(String);
|
|
79
|
-
return args.flat().map(String);
|
|
71
|
+
function parsePool(raw) {
|
|
72
|
+
if (!raw) return [];
|
|
73
|
+
const lines = String(raw)
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.map(s => s.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
return uniqInts(lines);
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
function
|
|
83
|
-
|
|
80
|
+
function getPageKey() {
|
|
81
|
+
try {
|
|
82
|
+
const ax = window.ajaxify;
|
|
83
|
+
if (ax && ax.data) {
|
|
84
|
+
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
85
|
+
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {}
|
|
88
|
+
return window.location.pathname;
|
|
89
|
+
}
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
function getKind() {
|
|
92
|
+
const p = window.location.pathname || '';
|
|
93
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
94
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
95
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
96
|
+
|
|
97
|
+
// fallback by DOM
|
|
98
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
99
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
100
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
101
|
+
return 'other';
|
|
102
|
+
}
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
function getTopicItems() {
|
|
105
|
+
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
106
|
+
}
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
function getCategoryItems() {
|
|
109
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getPostContainers() {
|
|
113
|
+
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
114
|
+
return nodes.filter((el) => {
|
|
115
|
+
if (!el || !el.isConnected) return false;
|
|
116
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
117
|
+
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
118
|
+
if (parentPost && parentPost !== el) return false;
|
|
119
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------- warm-up & patching ----------
|
|
125
|
+
|
|
126
|
+
const _warmLinksDone = new Set();
|
|
127
|
+
function warmUpNetwork() {
|
|
128
|
+
try {
|
|
129
|
+
const head = document.head || document.getElementsByTagName('head')[0];
|
|
130
|
+
if (!head) return;
|
|
131
|
+
const links = [
|
|
132
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
133
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
134
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
135
|
+
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
136
|
+
];
|
|
137
|
+
for (const [rel, href, cors] of links) {
|
|
138
|
+
const key = `${rel}|${href}`;
|
|
139
|
+
if (_warmLinksDone.has(key)) continue;
|
|
140
|
+
_warmLinksDone.add(key);
|
|
141
|
+
const link = document.createElement('link');
|
|
142
|
+
link.rel = rel;
|
|
143
|
+
link.href = href;
|
|
144
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
145
|
+
head.appendChild(link);
|
|
102
146
|
}
|
|
147
|
+
} catch (e) {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
|
|
151
|
+
function patchShowAds() {
|
|
152
|
+
const applyPatch = () => {
|
|
153
|
+
try {
|
|
154
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
155
|
+
const ez = window.ezstandalone;
|
|
156
|
+
if (window.__nodebbEzoicPatched) return;
|
|
157
|
+
if (typeof ez.showAds !== 'function') return;
|
|
158
|
+
|
|
159
|
+
window.__nodebbEzoicPatched = true;
|
|
160
|
+
const orig = ez.showAds;
|
|
161
|
+
|
|
162
|
+
ez.showAds = function (...args) {
|
|
163
|
+
if (EZOIC_BLOCKED) return;
|
|
164
|
+
|
|
165
|
+
let ids = [];
|
|
166
|
+
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
167
|
+
else ids = args;
|
|
168
|
+
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
for (const v of ids) {
|
|
171
|
+
const id = parseInt(v, 10);
|
|
172
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
173
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
174
|
+
if (!ph || !ph.isConnected) continue;
|
|
175
|
+
seen.add(id);
|
|
176
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
} catch (e) {}
|
|
103
180
|
};
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
applyPatch();
|
|
183
|
+
if (!window.__nodebbEzoicPatched) {
|
|
184
|
+
try {
|
|
185
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
186
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
187
|
+
window.ezstandalone.cmd.push(applyPatch);
|
|
188
|
+
} catch (e) {}
|
|
189
|
+
}
|
|
107
190
|
}
|
|
108
191
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
192
|
+
// ---------- config & pools ----------
|
|
193
|
+
|
|
194
|
+
async function fetchConfigOnce() {
|
|
195
|
+
if (state.cfg) return state.cfg;
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
198
|
+
if (!res.ok) return null;
|
|
199
|
+
state.cfg = await res.json();
|
|
200
|
+
return state.cfg;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return null;
|
|
120
203
|
}
|
|
204
|
+
}
|
|
121
205
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
206
|
+
function initPools(cfg) {
|
|
207
|
+
if (!cfg) return;
|
|
208
|
+
if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
|
|
209
|
+
if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
|
|
210
|
+
if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
128
211
|
}
|
|
129
212
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
213
|
+
// ---------- insertion primitives ----------
|
|
214
|
+
|
|
215
|
+
function isAdjacentAd(target) {
|
|
216
|
+
if (!target) return false;
|
|
217
|
+
const next = target.nextElementSibling;
|
|
218
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
219
|
+
const prev = target.previousElementSibling;
|
|
220
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
221
|
+
return false;
|
|
139
222
|
}
|
|
140
223
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
224
|
+
function buildWrap(target, id, kindClass, afterPos) {
|
|
225
|
+
const tag = (target && target.tagName === 'LI') ? 'li' : 'div';
|
|
226
|
+
const wrap = document.createElement(tag);
|
|
227
|
+
if (tag === 'li') {
|
|
228
|
+
wrap.style.listStyle = 'none';
|
|
229
|
+
// preserve common NodeBB list styling
|
|
230
|
+
if (target && target.classList && target.classList.contains('list-group-item')) wrap.classList.add('list-group-item');
|
|
231
|
+
}
|
|
232
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
233
|
+
if (wrap.tagName === 'LI') {
|
|
234
|
+
wrap.setAttribute('role', 'presentation');
|
|
235
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
236
|
+
}
|
|
237
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
238
|
+
wrap.style.width = '100%';
|
|
239
|
+
|
|
240
|
+
const ph = document.createElement('div');
|
|
241
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
242
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
243
|
+
wrap.appendChild(ph);
|
|
160
244
|
|
|
161
|
-
return
|
|
245
|
+
return wrap;
|
|
162
246
|
}
|
|
163
247
|
|
|
164
|
-
function
|
|
165
|
-
return
|
|
166
|
-
.split(',')
|
|
167
|
-
.map(x => x.trim())
|
|
168
|
-
.filter(Boolean);
|
|
248
|
+
function findWrap(kindClass, afterPos) {
|
|
249
|
+
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
169
250
|
}
|
|
170
251
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
252
|
+
function insertAfter(target, id, kindClass, afterPos) {
|
|
253
|
+
if (!target || !target.insertAdjacentElement) return null;
|
|
254
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
255
|
+
if (insertingIds.has(id)) return null;
|
|
256
|
+
|
|
257
|
+
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
258
|
+
if (existingPh && existingPh.isConnected) return null;
|
|
259
|
+
|
|
260
|
+
insertingIds.add(id);
|
|
261
|
+
try {
|
|
262
|
+
const wrap = buildWrap(target, id, kindClass, afterPos);
|
|
263
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
264
|
+
return wrap;
|
|
265
|
+
} finally {
|
|
266
|
+
insertingIds.delete(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
178
269
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
270
|
+
function pickId(pool) {
|
|
271
|
+
return pool.length ? pool.shift() : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function showAd(id) {
|
|
275
|
+
if (!id || EZOIC_BLOCKED) return;
|
|
276
|
+
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const last = state.lastShowById.get(id) || 0;
|
|
279
|
+
if (now - last < 1500) return; // basic throttle
|
|
280
|
+
|
|
281
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
282
|
+
if (!ph || !ph.isConnected) return;
|
|
283
|
+
|
|
284
|
+
state.lastShowById.set(id, now);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
288
|
+
const ez = window.ezstandalone;
|
|
289
|
+
|
|
290
|
+
// Fast path
|
|
291
|
+
if (typeof ez.showAds === 'function') {
|
|
292
|
+
ez.showAds(id);
|
|
293
|
+
sessionDefinedIds.add(id);
|
|
294
|
+
return;
|
|
195
295
|
}
|
|
196
296
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
297
|
+
// Queue once for when Ezoic is ready
|
|
298
|
+
ez.cmd = ez.cmd || [];
|
|
299
|
+
if (!ph.__ezoicQueued) {
|
|
300
|
+
ph.__ezoicQueued = true;
|
|
301
|
+
ez.cmd.push(() => {
|
|
302
|
+
try {
|
|
303
|
+
if (EZOIC_BLOCKED) return;
|
|
304
|
+
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
305
|
+
if (!el || !el.isConnected) return;
|
|
306
|
+
window.ezstandalone.showAds(id);
|
|
307
|
+
sessionDefinedIds.add(id);
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
});
|
|
201
310
|
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
disconnectObservers() {
|
|
205
|
-
try { this.io && this.io.disconnect(); } catch (e) {}
|
|
206
|
-
try { this.mo && this.mo.disconnect(); } catch (e) {}
|
|
207
|
-
this.io = null;
|
|
208
|
-
this.mo = null;
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
scheduleScan() {
|
|
212
|
-
if (rafScheduled) return;
|
|
213
|
-
rafScheduled = true;
|
|
214
|
-
requestAnimationFrame(() => {
|
|
215
|
-
rafScheduled = false;
|
|
216
|
-
this.scanAndInject();
|
|
217
|
-
});
|
|
218
|
-
},
|
|
311
|
+
} catch (e) {}
|
|
312
|
+
}
|
|
219
313
|
|
|
220
|
-
|
|
221
|
-
const c = await prefetchConfig();
|
|
222
|
-
if (!c) return;
|
|
314
|
+
// ---------- preload / above-the-fold ----------
|
|
223
315
|
|
|
224
|
-
|
|
316
|
+
function ensurePreloadObserver() {
|
|
317
|
+
if (state.io) return state.io;
|
|
318
|
+
try {
|
|
319
|
+
state.io = new IntersectionObserver((entries) => {
|
|
320
|
+
for (const ent of entries) {
|
|
321
|
+
if (!ent.isIntersecting) continue;
|
|
322
|
+
const el = ent.target;
|
|
323
|
+
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
225
324
|
|
|
226
|
-
|
|
227
|
-
|
|
325
|
+
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
326
|
+
const id = parseInt(idAttr, 10);
|
|
327
|
+
if (Number.isFinite(id) && id > 0) showAd(id);
|
|
328
|
+
}
|
|
329
|
+
}, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
|
|
330
|
+
} catch (e) {
|
|
331
|
+
state.io = null;
|
|
332
|
+
}
|
|
333
|
+
return state.io;
|
|
334
|
+
}
|
|
228
335
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
336
|
+
function observePlaceholder(id) {
|
|
337
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
338
|
+
if (!ph || !ph.isConnected) return;
|
|
339
|
+
const io = ensurePreloadObserver();
|
|
340
|
+
try { io && io.observe(ph); } catch (e) {}
|
|
341
|
+
|
|
342
|
+
// If already above fold, fire immediately
|
|
343
|
+
try {
|
|
344
|
+
const r = ph.getBoundingClientRect();
|
|
345
|
+
if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
|
|
346
|
+
} catch (e) {}
|
|
347
|
+
}
|
|
232
348
|
|
|
233
|
-
|
|
234
|
-
injectBetweenTopics(c) {
|
|
235
|
-
const container =
|
|
236
|
-
document.querySelector('.category .topic-list') ||
|
|
237
|
-
document.querySelector('.topics .topic-list') ||
|
|
238
|
-
document.querySelector('.topic-list');
|
|
349
|
+
// ---------- insertion logic ----------
|
|
239
350
|
|
|
240
|
-
|
|
351
|
+
function computeTargets(count, interval, showFirst) {
|
|
352
|
+
const out = [];
|
|
353
|
+
if (count <= 0) return out;
|
|
354
|
+
if (showFirst) out.push(1);
|
|
355
|
+
for (let i = 1; i <= count; i++) {
|
|
356
|
+
if (i % interval === 0) out.push(i);
|
|
357
|
+
}
|
|
358
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
359
|
+
}
|
|
241
360
|
|
|
242
|
-
|
|
243
|
-
|
|
361
|
+
function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
|
|
362
|
+
if (!items.length) return 0;
|
|
244
363
|
|
|
245
|
-
|
|
246
|
-
|
|
364
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
365
|
+
let inserted = 0;
|
|
247
366
|
|
|
248
|
-
|
|
367
|
+
for (const afterPos of targets) {
|
|
368
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
249
369
|
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
370
|
+
const el = items[afterPos - 1];
|
|
371
|
+
if (!el || !el.isConnected) continue;
|
|
372
|
+
if (isAdjacentAd(el)) continue;
|
|
373
|
+
if (findWrap(kindClass, afterPos)) continue;
|
|
254
374
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
375
|
+
const id = pickId(pool);
|
|
376
|
+
if (!id) break;
|
|
377
|
+
|
|
378
|
+
usedSet.add(id);
|
|
379
|
+
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
380
|
+
if (!wrap) {
|
|
381
|
+
usedSet.delete(id);
|
|
382
|
+
pool.unshift(id);
|
|
383
|
+
continue;
|
|
260
384
|
}
|
|
261
|
-
},
|
|
262
385
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
386
|
+
observePlaceholder(id);
|
|
387
|
+
inserted += 1;
|
|
388
|
+
}
|
|
266
389
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.insertedKeys.add(key);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
390
|
+
return inserted;
|
|
391
|
+
}
|
|
272
392
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
393
|
+
async function insertHeroAdEarly() {
|
|
394
|
+
if (state.heroDoneForPage) return;
|
|
395
|
+
const cfg = await fetchConfigOnce();
|
|
396
|
+
if (!cfg || cfg.excluded) return;
|
|
397
|
+
|
|
398
|
+
initPools(cfg);
|
|
399
|
+
|
|
400
|
+
const kind = getKind();
|
|
401
|
+
let items = [];
|
|
402
|
+
let pool = null;
|
|
403
|
+
let usedSet = null;
|
|
404
|
+
let kindClass = '';
|
|
405
|
+
|
|
406
|
+
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
407
|
+
items = getPostContainers();
|
|
408
|
+
pool = state.poolPosts;
|
|
409
|
+
usedSet = state.usedPosts;
|
|
410
|
+
kindClass = 'ezoic-ad-message';
|
|
411
|
+
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
412
|
+
items = getTopicItems();
|
|
413
|
+
pool = state.poolTopics;
|
|
414
|
+
usedSet = state.usedTopics;
|
|
415
|
+
kindClass = 'ezoic-ad-between';
|
|
416
|
+
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
417
|
+
items = getCategoryItems();
|
|
418
|
+
pool = state.poolCategories;
|
|
419
|
+
usedSet = state.usedCategories;
|
|
420
|
+
kindClass = 'ezoic-ad-categories';
|
|
421
|
+
} else {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
278
424
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
425
|
+
if (!items.length) return;
|
|
426
|
+
|
|
427
|
+
// Insert after the very first item (above-the-fold)
|
|
428
|
+
const afterPos = 1;
|
|
429
|
+
const el = items[afterPos - 1];
|
|
430
|
+
if (!el || !el.isConnected) return;
|
|
431
|
+
if (isAdjacentAd(el)) return;
|
|
432
|
+
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
433
|
+
|
|
434
|
+
const id = pickId(pool);
|
|
435
|
+
if (!id) return;
|
|
436
|
+
|
|
437
|
+
usedSet.add(id);
|
|
438
|
+
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
439
|
+
if (!wrap) {
|
|
440
|
+
usedSet.delete(id);
|
|
441
|
+
pool.unshift(id);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
state.heroDoneForPage = true;
|
|
446
|
+
observePlaceholder(id);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function runCore() {
|
|
450
|
+
if (EZOIC_BLOCKED) return;
|
|
283
451
|
|
|
284
|
-
|
|
285
|
-
const ph = Pool.ensurePlaceholder(placeholderId);
|
|
286
|
-
// If placeholder is currently rendered elsewhere, move it (Ezoic uses id lookup)
|
|
287
|
-
try { wrapper.appendChild(ph); } catch (e) {}
|
|
452
|
+
patchShowAds();
|
|
288
453
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
454
|
+
const cfg = await fetchConfigOnce();
|
|
455
|
+
if (!cfg || cfg.excluded) return;
|
|
456
|
+
initPools(cfg);
|
|
292
457
|
|
|
293
|
-
|
|
294
|
-
if (this.io) this.io.observe(wrapper);
|
|
458
|
+
const kind = getKind();
|
|
295
459
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
460
|
+
if (kind === 'topic') {
|
|
461
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
462
|
+
injectBetween(
|
|
463
|
+
'ezoic-ad-message',
|
|
464
|
+
getPostContainers(),
|
|
465
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
466
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
467
|
+
state.poolPosts,
|
|
468
|
+
state.usedPosts
|
|
469
|
+
);
|
|
300
470
|
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
ezCmd(doShow);
|
|
323
|
-
} else {
|
|
324
|
-
doShow();
|
|
471
|
+
} else if (kind === 'categoryTopics') {
|
|
472
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
473
|
+
injectBetween(
|
|
474
|
+
'ezoic-ad-between',
|
|
475
|
+
getTopicItems(),
|
|
476
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
477
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
478
|
+
state.poolTopics,
|
|
479
|
+
state.usedTopics
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
} else if (kind === 'categories') {
|
|
483
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
484
|
+
injectBetween(
|
|
485
|
+
'ezoic-ad-categories',
|
|
486
|
+
getCategoryItems(),
|
|
487
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
488
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
489
|
+
state.poolCategories,
|
|
490
|
+
state.usedCategories
|
|
491
|
+
);
|
|
325
492
|
}
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
// On navigation, return all placeholders to pool so they still exist
|
|
329
|
-
reclaimPlaceholders() {
|
|
330
|
-
const wrappers = document.querySelectorAll('.ezoic-ad-wrapper[data-ezoic-id]');
|
|
331
|
-
wrappers.forEach(w => {
|
|
332
|
-
const id = w.getAttribute('data-ezoic-id');
|
|
333
|
-
if (id) Pool.returnToPool(id);
|
|
334
|
-
});
|
|
335
|
-
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
336
|
-
this.insertedKeys.clear();
|
|
337
|
-
this.lastShowAt.clear();
|
|
338
493
|
}
|
|
339
|
-
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function scheduleRun() {
|
|
497
|
+
if (state.runQueued) return;
|
|
498
|
+
state.runQueued = true;
|
|
499
|
+
window.requestAnimationFrame(() => {
|
|
500
|
+
state.runQueued = false;
|
|
501
|
+
const pk = getPageKey();
|
|
502
|
+
if (state.pageKey && pk !== state.pageKey) return;
|
|
503
|
+
runCore().catch(() => {});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
340
506
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
507
|
+
// ---------- observers / lifecycle ----------
|
|
508
|
+
|
|
509
|
+
function cleanup() {
|
|
510
|
+
EZOIC_BLOCKED = true;
|
|
511
|
+
|
|
512
|
+
// remove all wrappers
|
|
513
|
+
try {
|
|
514
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
515
|
+
try { el.remove(); } catch (e) {}
|
|
516
|
+
});
|
|
517
|
+
} catch (e) {}
|
|
518
|
+
|
|
519
|
+
// reset state
|
|
520
|
+
state.cfg = null;
|
|
521
|
+
state.poolTopics = [];
|
|
522
|
+
state.poolPosts = [];
|
|
523
|
+
state.poolCategories = [];
|
|
524
|
+
state.usedTopics.clear();
|
|
525
|
+
state.usedPosts.clear();
|
|
526
|
+
state.usedCategories.clear();
|
|
527
|
+
state.lastShowById.clear();
|
|
528
|
+
state.heroDoneForPage = false;
|
|
529
|
+
|
|
530
|
+
sessionDefinedIds.clear();
|
|
531
|
+
|
|
532
|
+
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
348
533
|
}
|
|
349
534
|
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
535
|
+
function ensureDomObserver() {
|
|
536
|
+
if (state.domObs) return;
|
|
537
|
+
state.domObs = new MutationObserver(() => {
|
|
538
|
+
if (!EZOIC_BLOCKED) scheduleRun();
|
|
539
|
+
});
|
|
540
|
+
try {
|
|
541
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
542
|
+
} catch (e) {}
|
|
354
543
|
}
|
|
355
544
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
545
|
+
function bindNodeBB() {
|
|
546
|
+
if (!$) return;
|
|
547
|
+
|
|
548
|
+
$(window).off('.ezoicInfinite');
|
|
549
|
+
|
|
550
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
551
|
+
cleanup();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
555
|
+
state.pageKey = getPageKey();
|
|
556
|
+
EZOIC_BLOCKED = false;
|
|
557
|
+
|
|
558
|
+
warmUpNetwork();
|
|
559
|
+
patchShowAds();
|
|
560
|
+
ensurePreloadObserver();
|
|
561
|
+
ensureDomObserver();
|
|
562
|
+
|
|
563
|
+
// Ultra-fast above-the-fold first
|
|
564
|
+
insertHeroAdEarly().catch(() => {});
|
|
565
|
+
|
|
566
|
+
// Then normal insertion
|
|
567
|
+
scheduleRun();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Infinite scroll / partial updates
|
|
571
|
+
$(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
|
|
572
|
+
if (EZOIC_BLOCKED) return;
|
|
573
|
+
scheduleRun();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function bindScroll() {
|
|
578
|
+
let ticking = false;
|
|
579
|
+
window.addEventListener('scroll', () => {
|
|
580
|
+
if (ticking) return;
|
|
581
|
+
ticking = true;
|
|
582
|
+
window.requestAnimationFrame(() => {
|
|
583
|
+
ticking = false;
|
|
584
|
+
if (!EZOIC_BLOCKED) scheduleRun();
|
|
585
|
+
});
|
|
586
|
+
}, { passive: true });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------- boot ----------
|
|
590
|
+
|
|
591
|
+
state.pageKey = getPageKey();
|
|
592
|
+
warmUpNetwork();
|
|
593
|
+
patchShowAds();
|
|
594
|
+
ensurePreloadObserver();
|
|
595
|
+
ensureDomObserver();
|
|
359
596
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
installShowAdsHook();
|
|
363
|
-
prefetchConfig();
|
|
364
|
-
Ad.scheduleScan();
|
|
365
|
-
});
|
|
597
|
+
bindNodeBB();
|
|
598
|
+
bindScroll();
|
|
366
599
|
|
|
600
|
+
// First paint: try hero + run
|
|
601
|
+
EZOIC_BLOCKED = false;
|
|
602
|
+
insertHeroAdEarly().catch(() => {});
|
|
603
|
+
scheduleRun();
|
|
367
604
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
.ezoic-ad
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
2
|
+
.ezoic-ad {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin: 0 !important;
|
|
6
|
+
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
.ezoic-ad-
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
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
|
+
}
|