nodebb-plugin-ezoic-infinite 1.5.5 → 1.5.6
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 +3 -2
- package/package.json +1 -1
- package/public/client.js +327 -550
- package/public/style.css +7 -19
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 : g));
|
|
71
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : String(g)));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
plugin.onSettingsSet = function (data) {
|
|
@@ -107,10 +107,11 @@ 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(req.uid, settings.excludedGroups);
|
|
110
|
+
const excluded = await isUserExcluded((req.uid || (req.user && req.user.uid)), settings.excludedGroups);
|
|
111
111
|
|
|
112
112
|
res.json({
|
|
113
113
|
excluded,
|
|
114
|
+
excludedGroups: settings.excludedGroups,
|
|
114
115
|
enableBetweenAds: settings.enableBetweenAds,
|
|
115
116
|
showFirstTopicAd: settings.showFirstTopicAd,
|
|
116
117
|
placeholderIds: settings.placeholderIds,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,609 +1,386 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// NodeBB client context
|
|
10
|
-
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
11
|
-
|
|
12
|
-
const WRAP_CLASS = 'ezoic-ad';
|
|
13
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
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
|
-
|
|
21
|
-
const SELECTORS = {
|
|
22
|
-
topicItem: 'li[component="category/topic"]',
|
|
23
|
-
postItem: '[component="post"][data-pid]',
|
|
24
|
-
categoryItem: 'li[component="categories/category"]',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Hard block during navigation to avoid “placeholder does not exist” spam
|
|
28
|
-
let EZOIC_BLOCKED = false;
|
|
29
|
-
|
|
30
|
-
const state = {
|
|
31
|
-
pageKey: null,
|
|
32
|
-
cfg: null,
|
|
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,
|
|
47
|
-
io: null,
|
|
48
|
-
runQueued: false,
|
|
49
|
-
|
|
50
|
-
// hero
|
|
51
|
-
heroDoneForPage: false,
|
|
52
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Ezoic Infinite Ads for NodeBB 4.x
|
|
4
|
+
* - Event-driven only (ajaxify hooks + MutationObserver + IntersectionObserver)
|
|
5
|
+
* - Hard gate on server config (respects excluded groups BEFORE any injection)
|
|
6
|
+
* - Placeholder Registry: keeps all placeholder elements alive in a hidden pool so ez-standalone never complains
|
|
7
|
+
* - Safe showAds wrapper (array, varargs, single) + tokenized execution across ajaxify navigations
|
|
8
|
+
*/
|
|
53
9
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
10
|
+
(function () {
|
|
11
|
+
const CFG_URL = '/api/plugins/ezoic-infinite/config';
|
|
12
|
+
|
|
13
|
+
// -------------------------
|
|
14
|
+
// Utilities
|
|
15
|
+
// -------------------------
|
|
16
|
+
const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
17
|
+
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
|
|
18
|
+
|
|
19
|
+
function on(ev, fn) { document.addEventListener(ev, fn); }
|
|
20
|
+
function once(ev, fn) {
|
|
21
|
+
const h = (e) => { document.removeEventListener(ev, h); fn(e); };
|
|
22
|
+
document.addEventListener(ev, h);
|
|
61
23
|
}
|
|
62
24
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
25
|
+
// -------------------------
|
|
26
|
+
// Global state / navigation token
|
|
27
|
+
// -------------------------
|
|
28
|
+
let navToken = 0; // increment on ajaxify.start (page is about to be replaced)
|
|
29
|
+
let rafScheduled = false;
|
|
30
|
+
|
|
31
|
+
// resolved config (or null if failed)
|
|
32
|
+
let cfgPromise = null;
|
|
33
|
+
let cfg = null;
|
|
34
|
+
|
|
35
|
+
// -------------------------
|
|
36
|
+
// Placeholder Registry (keeps IDs alive)
|
|
37
|
+
// -------------------------
|
|
38
|
+
const Pool = {
|
|
39
|
+
el: null,
|
|
40
|
+
ensurePool() {
|
|
41
|
+
if (this.el && document.body.contains(this.el)) return this.el;
|
|
42
|
+
const d = document.createElement('div');
|
|
43
|
+
d.id = 'ezoic-placeholder-pool';
|
|
44
|
+
d.style.display = 'none';
|
|
45
|
+
d.setAttribute('aria-hidden', 'true');
|
|
46
|
+
document.body.appendChild(d);
|
|
47
|
+
this.el = d;
|
|
48
|
+
return d;
|
|
49
|
+
},
|
|
50
|
+
ensurePlaceholder(id) {
|
|
51
|
+
if (!id) return null;
|
|
52
|
+
let ph = document.getElementById(id);
|
|
53
|
+
if (ph) return ph;
|
|
54
|
+
const pool = this.ensurePool();
|
|
55
|
+
ph = document.createElement('div');
|
|
56
|
+
ph.id = id;
|
|
57
|
+
pool.appendChild(ph);
|
|
58
|
+
return ph;
|
|
59
|
+
},
|
|
60
|
+
returnToPool(id) {
|
|
61
|
+
const ph = document.getElementById(id);
|
|
62
|
+
if (!ph) return;
|
|
63
|
+
const pool = this.ensurePool();
|
|
64
|
+
if (ph.parentElement !== pool) {
|
|
65
|
+
pool.appendChild(ph);
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parsePool(raw) {
|
|
77
|
-
if (!raw) return [];
|
|
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;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getKind() {
|
|
97
|
-
const p = window.location.pathname || '';
|
|
98
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
99
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
100
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
101
|
-
|
|
102
|
-
// fallback by DOM
|
|
103
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
104
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
105
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
106
|
-
return 'other';
|
|
107
|
-
}
|
|
68
|
+
};
|
|
108
69
|
|
|
109
|
-
|
|
110
|
-
|
|
70
|
+
// -------------------------
|
|
71
|
+
// Ezoic showAds safe wrapper (installed even if defined later)
|
|
72
|
+
// -------------------------
|
|
73
|
+
function normalizeIds(argsLike) {
|
|
74
|
+
const args = Array.from(argsLike);
|
|
75
|
+
if (!args.length) return [];
|
|
76
|
+
// showAds([id1,id2]) or showAds([[...]]) edge
|
|
77
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0].flat().map(String);
|
|
78
|
+
return args.flat().map(String);
|
|
111
79
|
}
|
|
112
80
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
}
|
|
81
|
+
function wrapShowAds(original) {
|
|
82
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
116
83
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!el || !el.isConnected) return false;
|
|
121
|
-
if (!el.querySelector('[component="post/content"]')) return false;
|
|
122
|
-
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
123
|
-
if (parentPost && parentPost !== el) return false;
|
|
124
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
125
|
-
return true;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
84
|
+
const wrapped = function (...args) {
|
|
85
|
+
const ids = normalizeIds(args);
|
|
86
|
+
if (!ids.length) return;
|
|
128
87
|
|
|
129
|
-
|
|
88
|
+
for (const id of ids) {
|
|
89
|
+
// Ensure placeholder exists somewhere (pool), so ez-standalone won't log "does not exist"
|
|
90
|
+
Pool.ensurePlaceholder(id);
|
|
130
91
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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);
|
|
151
|
-
}
|
|
152
|
-
} catch (e) {}
|
|
153
|
-
}
|
|
154
|
-
|
|
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) {}
|
|
92
|
+
const el = document.getElementById(id);
|
|
93
|
+
if (el && document.body.contains(el)) {
|
|
94
|
+
try {
|
|
95
|
+
// Call one-by-one to avoid batch logging on missing nodes
|
|
96
|
+
original.call(window.ezstandalone, id);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// swallow: Ezoic can throw if called during transitions
|
|
182
99
|
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
185
102
|
};
|
|
186
103
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
191
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
192
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
193
|
-
} catch (e) {}
|
|
194
|
-
}
|
|
104
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
105
|
+
return wrapped;
|
|
195
106
|
}
|
|
196
107
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
try {
|
|
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;
|
|
108
|
+
function installShowAdsHook() {
|
|
109
|
+
// If ezstandalone already exists, wrap now.
|
|
110
|
+
if (window.ezstandalone && window.ezstandalone.showAds) {
|
|
111
|
+
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
208
112
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
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
|
-
}
|
|
217
|
-
|
|
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;
|
|
227
|
-
}
|
|
228
|
-
|
|
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
|
-
}
|
|
237
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
238
|
-
if (wrap.tagName === 'LI') {
|
|
239
|
-
wrap.setAttribute('role', 'presentation');
|
|
240
|
-
wrap.setAttribute('aria-hidden', 'true');
|
|
241
|
-
}
|
|
242
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
243
|
-
wrap.style.width = '100%';
|
|
244
|
-
|
|
245
|
-
const ph = document.createElement('div');
|
|
246
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
247
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
248
|
-
wrap.appendChild(ph);
|
|
249
|
-
|
|
250
|
-
return wrap;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function findWrap(kindClass, afterPos) {
|
|
254
|
-
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function insertAfter(target, id, kindClass, afterPos) {
|
|
258
|
-
if (!target || !target.insertAdjacentElement) return null;
|
|
259
|
-
if (findWrap(kindClass, afterPos)) return null;
|
|
260
|
-
if (insertingIds.has(id)) return null;
|
|
261
|
-
|
|
262
|
-
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
263
|
-
if (existingPh && existingPh.isConnected) return null;
|
|
264
|
-
|
|
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);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function pickId(pool) {
|
|
276
|
-
return pool.length ? pool.shift() : null;
|
|
277
|
-
}
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
287
|
-
if (!ph || !ph.isConnected) return;
|
|
288
|
-
|
|
289
|
-
state.lastShowById.set(id, now);
|
|
290
113
|
|
|
114
|
+
// Hook future assignment (no polling)
|
|
291
115
|
try {
|
|
292
|
-
window.ezstandalone
|
|
116
|
+
if (!window.ezstandalone) window.ezstandalone = {};
|
|
293
117
|
const ez = window.ezstandalone;
|
|
294
118
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
sessionDefinedIds.add(id);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
119
|
+
// If already has a setter installed, do nothing
|
|
120
|
+
const desc = Object.getOwnPropertyDescriptor(ez, 'showAds');
|
|
121
|
+
if (desc && (desc.set || desc.get)) return;
|
|
301
122
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
}
|
|
316
|
-
} catch (e) {}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ---------- preload / above-the-fold ----------
|
|
123
|
+
let _showAds = ez.showAds;
|
|
124
|
+
Object.defineProperty(ez, 'showAds', {
|
|
125
|
+
configurable: true,
|
|
126
|
+
enumerable: true,
|
|
127
|
+
get() { return _showAds; },
|
|
128
|
+
set(v) { _showAds = wrapShowAds(v); }
|
|
129
|
+
});
|
|
320
130
|
|
|
321
|
-
|
|
322
|
-
|
|
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 });
|
|
131
|
+
// Re-assign current value through setter
|
|
132
|
+
ez.showAds = _showAds;
|
|
335
133
|
} catch (e) {
|
|
336
|
-
|
|
134
|
+
// If defineProperty fails, best-effort wrap is still in place above.
|
|
337
135
|
}
|
|
338
|
-
return state.io;
|
|
339
136
|
}
|
|
340
137
|
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
|
|
351
|
-
} catch (e) {}
|
|
138
|
+
function ezCmd(fn) {
|
|
139
|
+
// Tokenize so queued callbacks don't run after navigation
|
|
140
|
+
const tokenAtSchedule = navToken;
|
|
141
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
142
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
143
|
+
window.ezstandalone.cmd.push(function () {
|
|
144
|
+
if (tokenAtSchedule !== navToken) return;
|
|
145
|
+
try { fn(); } catch (e) {}
|
|
146
|
+
});
|
|
352
147
|
}
|
|
353
148
|
|
|
354
|
-
//
|
|
149
|
+
// -------------------------
|
|
150
|
+
// Config (hard gate)
|
|
151
|
+
// -------------------------
|
|
152
|
+
function prefetchConfig() {
|
|
153
|
+
if (cfgPromise) return cfgPromise;
|
|
154
|
+
|
|
155
|
+
cfgPromise = fetch(CFG_URL, { credentials: 'same-origin' })
|
|
156
|
+
.then(r => r.ok ? r.json() : Promise.reject(new Error('config http ' + r.status)))
|
|
157
|
+
.then(data => {
|
|
158
|
+
cfg = data;
|
|
159
|
+
// Pre-create placeholders in pool so Ezoic never complains even before first injection
|
|
160
|
+
const ids = parsePlaceholderIds((cfg && cfg.placeholderIds) || '');
|
|
161
|
+
ids.forEach(id => Pool.ensurePlaceholder(id));
|
|
162
|
+
return cfg;
|
|
163
|
+
})
|
|
164
|
+
.catch(() => {
|
|
165
|
+
cfg = null;
|
|
166
|
+
return null;
|
|
167
|
+
});
|
|
355
168
|
|
|
356
|
-
|
|
357
|
-
const out = [];
|
|
358
|
-
if (count <= 0) return out;
|
|
359
|
-
if (showFirst) out.push(1);
|
|
360
|
-
for (let i = 1; i <= count; i++) {
|
|
361
|
-
if (i % interval === 0) out.push(i);
|
|
362
|
-
}
|
|
363
|
-
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
169
|
+
return cfgPromise;
|
|
364
170
|
}
|
|
365
171
|
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
for (const afterPos of targets) {
|
|
373
|
-
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
374
|
-
|
|
375
|
-
const el = items[afterPos - 1];
|
|
376
|
-
if (!el || !el.isConnected) continue;
|
|
377
|
-
if (isAdjacentAd(el)) continue;
|
|
378
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
172
|
+
function parsePlaceholderIds(s) {
|
|
173
|
+
return String(s || '')
|
|
174
|
+
.split(',')
|
|
175
|
+
.map(x => x.trim())
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
}
|
|
379
178
|
|
|
380
|
-
|
|
381
|
-
|
|
179
|
+
// -------------------------
|
|
180
|
+
// Ad insertion logic
|
|
181
|
+
// -------------------------
|
|
182
|
+
const Ad = {
|
|
183
|
+
// throttle showAds per placeholder id
|
|
184
|
+
lastShowAt: new Map(),
|
|
185
|
+
minShowIntervalMs: 1500,
|
|
382
186
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
187
|
+
// Observers
|
|
188
|
+
io: null,
|
|
189
|
+
mo: null,
|
|
190
|
+
|
|
191
|
+
// Bookkeeping
|
|
192
|
+
insertedKeys: new Set(), // avoid duplicate injection on re-renders
|
|
193
|
+
|
|
194
|
+
initObservers() {
|
|
195
|
+
if (!this.io) {
|
|
196
|
+
this.io = new IntersectionObserver((entries) => {
|
|
197
|
+
for (const e of entries) {
|
|
198
|
+
if (!e.isIntersecting) continue;
|
|
199
|
+
const id = e.target && e.target.getAttribute('data-ezoic-id');
|
|
200
|
+
if (id) this.requestShow(id);
|
|
201
|
+
}
|
|
202
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0.01 });
|
|
389
203
|
}
|
|
390
204
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
205
|
+
if (!this.mo) {
|
|
206
|
+
this.mo = new MutationObserver(() => this.scheduleScan());
|
|
207
|
+
const root = document.querySelector('#content, #panel, main, body');
|
|
208
|
+
if (root) this.mo.observe(root, { childList: true, subtree: true });
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
disconnectObservers() {
|
|
213
|
+
try { this.io && this.io.disconnect(); } catch (e) {}
|
|
214
|
+
try { this.mo && this.mo.disconnect(); } catch (e) {}
|
|
215
|
+
this.io = null;
|
|
216
|
+
this.mo = null;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
scheduleScan() {
|
|
220
|
+
if (rafScheduled) return;
|
|
221
|
+
rafScheduled = true;
|
|
222
|
+
requestAnimationFrame(() => {
|
|
223
|
+
rafScheduled = false;
|
|
224
|
+
this.scanAndInject();
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async scanAndInject() {
|
|
229
|
+
const c = await prefetchConfig();
|
|
230
|
+
if (!c) return;
|
|
231
|
+
|
|
232
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
233
|
+
|
|
234
|
+
installShowAdsHook();
|
|
235
|
+
this.initObservers();
|
|
236
|
+
|
|
237
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
238
|
+
// Extend: categories ads if you use them
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// NodeBB topic list injection (between li rows)
|
|
242
|
+
injectBetweenTopics(c) {
|
|
243
|
+
// NodeBB 4.x (Harmony) uses: <ul component="category" class="topics-list ...">
|
|
244
|
+
// Older themes may use .topic-list
|
|
245
|
+
const container =
|
|
246
|
+
document.querySelector('ul[component="category"].topics-list') ||
|
|
247
|
+
document.querySelector('ul[component="category"].topic-list') ||
|
|
248
|
+
document.querySelector('ul.topics-list[component="category"]') ||
|
|
249
|
+
document.querySelector('ul.topics-list') ||
|
|
250
|
+
document.querySelector('.category ul.topics-list') ||
|
|
251
|
+
document.querySelector('.category .topic-list') ||
|
|
252
|
+
document.querySelector('.topics .topic-list') ||
|
|
253
|
+
document.querySelector('.topic-list');
|
|
254
|
+
|
|
255
|
+
if (!container) return;
|
|
256
|
+
|
|
257
|
+
let rows = Array.from(container.querySelectorAll(':scope > li')).filter(el => el && el.nodeType === 1);
|
|
258
|
+
if (!rows.length) {
|
|
259
|
+
// Fallback for older markups
|
|
260
|
+
rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item')).filter(el => el && el.nodeType === 1);
|
|
261
|
+
}
|
|
262
|
+
if (!rows.length) return;
|
|
394
263
|
|
|
395
|
-
|
|
396
|
-
|
|
264
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
265
|
+
if (!ids.length) return;
|
|
397
266
|
|
|
398
|
-
|
|
399
|
-
if (state.heroDoneForPage) return;
|
|
400
|
-
const cfg = await fetchConfigOnce();
|
|
401
|
-
if (!cfg || cfg.excluded) return;
|
|
402
|
-
|
|
403
|
-
initPools(cfg);
|
|
404
|
-
|
|
405
|
-
const kind = getKind();
|
|
406
|
-
let items = [];
|
|
407
|
-
let pool = null;
|
|
408
|
-
let usedSet = null;
|
|
409
|
-
let kindClass = '';
|
|
410
|
-
|
|
411
|
-
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
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 {
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
267
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
429
268
|
|
|
430
|
-
|
|
269
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
270
|
+
if (c.showFirstTopicAd) {
|
|
271
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
272
|
+
}
|
|
431
273
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
274
|
+
// Between rows
|
|
275
|
+
let idIndex = 0;
|
|
276
|
+
for (let i = interval; i < rows.length; i += interval) {
|
|
277
|
+
idIndex = (idIndex + 1) % ids.length;
|
|
278
|
+
this.insertAdAfterRow(rows[i - 1], ids[idIndex], `between-topic-${i}`);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
438
281
|
|
|
439
|
-
|
|
440
|
-
|
|
282
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
283
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
284
|
+
if (this.insertedKeys.has(key)) return;
|
|
441
285
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
286
|
+
// If already present nearby, skip
|
|
287
|
+
if (rowEl.nextElementSibling && rowEl.nextElementSibling.classList && rowEl.nextElementSibling.classList.contains('ezoic-ad-wrapper')) {
|
|
288
|
+
this.insertedKeys.add(key);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
449
291
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
292
|
+
const isLi = rowEl.tagName && rowEl.tagName.toLowerCase() === 'li';
|
|
293
|
+
const wrapper = document.createElement(isLi ? 'li' : 'div');
|
|
294
|
+
wrapper.className = 'ezoic-ad-wrapper';
|
|
295
|
+
wrapper.setAttribute('role', 'presentation');
|
|
296
|
+
wrapper.setAttribute('data-ezoic-id', placeholderId);
|
|
453
297
|
|
|
454
|
-
|
|
455
|
-
|
|
298
|
+
// Theme compatibility: if list uses list-group-item, keep it to avoid layout/JS breaks
|
|
299
|
+
if (isLi && rowEl.classList.contains('list-group-item')) {
|
|
300
|
+
wrapper.classList.add('list-group-item');
|
|
301
|
+
}
|
|
456
302
|
|
|
457
|
-
|
|
303
|
+
// Ensure placeholder exists (in pool) then move it into wrapper
|
|
304
|
+
const ph = Pool.ensurePlaceholder(placeholderId);
|
|
305
|
+
// If placeholder is currently rendered elsewhere, move it (Ezoic uses id lookup)
|
|
306
|
+
try { wrapper.appendChild(ph); } catch (e) {}
|
|
458
307
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
308
|
+
// Insert into DOM
|
|
309
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
310
|
+
this.insertedKeys.add(key);
|
|
462
311
|
|
|
463
|
-
|
|
312
|
+
// Observe for preloading and request a show
|
|
313
|
+
if (this.io) this.io.observe(wrapper);
|
|
464
314
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
);
|
|
315
|
+
// If above fold, request immediately (no timeout)
|
|
316
|
+
const rect = wrapper.getBoundingClientRect();
|
|
317
|
+
if (rect.top < (window.innerHeight * 1.5)) {
|
|
318
|
+
this.requestShow(placeholderId);
|
|
486
319
|
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
requestShow(placeholderId) {
|
|
323
|
+
if (!placeholderId) return;
|
|
324
|
+
|
|
325
|
+
const last = this.lastShowAt.get(placeholderId) || 0;
|
|
326
|
+
const t = now();
|
|
327
|
+
if ((t - last) < this.minShowIntervalMs) return;
|
|
328
|
+
this.lastShowAt.set(placeholderId, t);
|
|
329
|
+
|
|
330
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
331
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
332
|
+
|
|
333
|
+
// Use ez cmd queue if available
|
|
334
|
+
const doShow = () => {
|
|
335
|
+
if (!window.ezstandalone || !window.ezstandalone.showAds) return;
|
|
336
|
+
// showAds is wrapped; calling with one id is safest
|
|
337
|
+
window.ezstandalone.showAds(placeholderId);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
341
|
+
ezCmd(doShow);
|
|
342
|
+
} else {
|
|
343
|
+
doShow();
|
|
497
344
|
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const pk = getPageKey();
|
|
507
|
-
if (state.pageKey && pk !== state.pageKey) return;
|
|
508
|
-
runCore().catch(() => {});
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// ---------- observers / lifecycle ----------
|
|
513
|
-
|
|
514
|
-
function cleanup() {
|
|
515
|
-
EZOIC_BLOCKED = true;
|
|
516
|
-
|
|
517
|
-
// remove all wrappers
|
|
518
|
-
try {
|
|
519
|
-
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
520
|
-
try { el.remove(); } catch (e) {}
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// On navigation, return all placeholders to pool so they still exist
|
|
348
|
+
reclaimPlaceholders() {
|
|
349
|
+
const wrappers = document.querySelectorAll('.ezoic-ad-wrapper[data-ezoic-id]');
|
|
350
|
+
wrappers.forEach(w => {
|
|
351
|
+
const id = w.getAttribute('data-ezoic-id');
|
|
352
|
+
if (id) Pool.returnToPool(id);
|
|
521
353
|
});
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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)
|
|
538
|
-
}
|
|
539
|
-
|
|
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() {
|
|
551
|
-
if (!$) return;
|
|
552
|
-
|
|
553
|
-
$(window).off('.ezoicInfinite');
|
|
554
|
-
|
|
555
|
-
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
556
|
-
cleanup();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
560
|
-
state.pageKey = getPageKey();
|
|
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();
|
|
573
|
-
});
|
|
354
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
355
|
+
this.insertedKeys.clear();
|
|
356
|
+
this.lastShowAt.clear();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
574
359
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
360
|
+
// -------------------------
|
|
361
|
+
// NodeBB hooks
|
|
362
|
+
// -------------------------
|
|
363
|
+
function onAjaxifyStart() {
|
|
364
|
+
navToken++;
|
|
365
|
+
Ad.disconnectObservers();
|
|
366
|
+
Ad.reclaimPlaceholders();
|
|
580
367
|
}
|
|
581
368
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
ticking = true;
|
|
587
|
-
window.requestAnimationFrame(() => {
|
|
588
|
-
ticking = false;
|
|
589
|
-
if (!EZOIC_BLOCKED) scheduleRun();
|
|
590
|
-
});
|
|
591
|
-
}, { passive: true });
|
|
369
|
+
function onAjaxifyEnd() {
|
|
370
|
+
// Kick scan for new page
|
|
371
|
+
prefetchConfig();
|
|
372
|
+
Ad.scheduleScan();
|
|
592
373
|
}
|
|
593
374
|
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
warmUpNetwork();
|
|
598
|
-
patchShowAds();
|
|
599
|
-
ensurePreloadObserver();
|
|
600
|
-
ensureDomObserver();
|
|
375
|
+
// NodeBB exposes ajaxify events on document
|
|
376
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
377
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
601
378
|
|
|
602
|
-
|
|
603
|
-
|
|
379
|
+
// First load
|
|
380
|
+
once('DOMContentLoaded', () => {
|
|
381
|
+
installShowAdsHook();
|
|
382
|
+
prefetchConfig();
|
|
383
|
+
Ad.scheduleScan();
|
|
384
|
+
});
|
|
604
385
|
|
|
605
|
-
// First paint: try hero + run
|
|
606
|
-
EZOIC_BLOCKED = false;
|
|
607
|
-
insertHeroAdEarly().catch(() => {});
|
|
608
|
-
scheduleRun();
|
|
609
386
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
.ezoic-ad
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
overflow: hidden;
|
|
1
|
+
.ezoic-ad,
|
|
2
|
+
.ezoic-ad *,
|
|
3
|
+
span.ezoic-ad,
|
|
4
|
+
span[class*="ezoic"] {
|
|
5
|
+
min-height: 0 !important;
|
|
6
|
+
min-width: 0 !important;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
.ezoic-ad
|
|
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
|
-
}
|
|
9
|
+
.ezoic-ad-wrapper{margin:0;padding:0;list-style:none;}
|