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