nodebb-plugin-ezoic-infinite 1.5.8 → 1.5.9
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 +304 -546
- 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) {
|
|
@@ -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',
|
|
108
|
+
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
109
|
const settings = await getSettings();
|
|
110
|
-
const
|
|
111
|
-
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
110
|
+
const excluded = await isUserExcluded((req.uid || (req.user && req.user.uid)), settings.excludedGroups);
|
|
112
111
|
|
|
113
112
|
res.json({
|
|
114
113
|
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,609 +1,367 @@
|
|
|
1
|
-
|
|
2
|
-
'use strict';
|
|
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
|
|
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
|
-
};
|
|
53
|
-
|
|
54
|
-
const sessionDefinedIds = new Set();
|
|
55
|
-
const insertingIds = new Set();
|
|
1
|
+
'use strict';
|
|
56
2
|
|
|
57
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Ezoic Infinite Ads for NodeBB 4.x
|
|
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
|
+
*/
|
|
58
10
|
|
|
59
|
-
|
|
60
|
-
|
|
11
|
+
(function () {
|
|
12
|
+
const CFG_URL = '/api/plugins/ezoic-infinite/config';
|
|
13
|
+
|
|
14
|
+
// -------------------------
|
|
15
|
+
// Utilities
|
|
16
|
+
// -------------------------
|
|
17
|
+
const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
18
|
+
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
|
|
19
|
+
|
|
20
|
+
function on(ev, fn) { document.addEventListener(ev, fn); }
|
|
21
|
+
function once(ev, fn) {
|
|
22
|
+
const h = (e) => { document.removeEventListener(ev, h); fn(e); };
|
|
23
|
+
document.addEventListener(ev, h);
|
|
61
24
|
}
|
|
62
25
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
26
|
+
// -------------------------
|
|
27
|
+
// Global state / navigation token
|
|
28
|
+
// -------------------------
|
|
29
|
+
let navToken = 0; // increment on ajaxify.start (page is about to be replaced)
|
|
30
|
+
let rafScheduled = false;
|
|
31
|
+
|
|
32
|
+
// resolved config (or null if failed)
|
|
33
|
+
let cfgPromise = null;
|
|
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);
|
|
71
67
|
}
|
|
72
68
|
}
|
|
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
|
-
}
|
|
69
|
+
};
|
|
108
70
|
|
|
109
|
-
|
|
110
|
-
|
|
71
|
+
// -------------------------
|
|
72
|
+
// Ezoic showAds safe wrapper (installed even if defined later)
|
|
73
|
+
// -------------------------
|
|
74
|
+
function normalizeIds(argsLike) {
|
|
75
|
+
const args = Array.from(argsLike);
|
|
76
|
+
if (!args.length) return [];
|
|
77
|
+
// showAds([id1,id2]) or showAds([[...]]) edge
|
|
78
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0].flat().map(String);
|
|
79
|
+
return args.flat().map(String);
|
|
111
80
|
}
|
|
112
81
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
}
|
|
82
|
+
function wrapShowAds(original) {
|
|
83
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
116
84
|
|
|
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
|
-
}
|
|
85
|
+
const wrapped = function (...args) {
|
|
86
|
+
const ids = normalizeIds(args);
|
|
87
|
+
if (!ids.length) return;
|
|
128
88
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
function warmUpNetwork() {
|
|
133
|
-
try {
|
|
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);
|
|
151
|
-
}
|
|
152
|
-
} catch (e) {}
|
|
153
|
-
}
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
// Ensure placeholder exists somewhere (pool), so ez-standalone won't log "does not exist"
|
|
91
|
+
Pool.ensurePlaceholder(id);
|
|
154
92
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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) {}
|
|
93
|
+
const el = document.getElementById(id);
|
|
94
|
+
if (el && document.body.contains(el)) {
|
|
95
|
+
try {
|
|
96
|
+
// Call one-by-one to avoid batch logging on missing nodes
|
|
97
|
+
original.call(window.ezstandalone, id);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// swallow: Ezoic can throw if called during transitions
|
|
182
100
|
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
185
103
|
};
|
|
186
104
|
|
|
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
|
-
}
|
|
105
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
106
|
+
return wrapped;
|
|
195
107
|
}
|
|
196
108
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
109
|
+
function installShowAdsHook() {
|
|
110
|
+
// Minimal + Ezoic-safe:
|
|
111
|
+
// - ensure queues exist (prevents `_ezaq` / `cmd` undefined)
|
|
112
|
+
// - wrap showAds when it exists
|
|
113
|
+
// - ALSO queue a wrap inside ezstandalone.cmd for when Ezoic initializes later
|
|
114
|
+
window._ezaq = window._ezaq || [];
|
|
115
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
116
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
117
|
+
|
|
118
|
+
if (window.ezstandalone.showAds) {
|
|
119
|
+
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
208
120
|
}
|
|
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
121
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return false;
|
|
122
|
+
// When standalone finishes loading, it will run cmd queue — wrap then too.
|
|
123
|
+
window.ezstandalone.cmd.push(function () {
|
|
124
|
+
if (window.ezstandalone && window.ezstandalone.showAds) {
|
|
125
|
+
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
227
128
|
}
|
|
228
129
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
130
|
+
function ezCmd(fn) {
|
|
131
|
+
// Tokenize so queued callbacks don't run after navigation
|
|
132
|
+
const tokenAtSchedule = navToken;
|
|
133
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
134
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
135
|
+
window.ezstandalone.cmd.push(function () {
|
|
136
|
+
if (tokenAtSchedule !== navToken) return;
|
|
137
|
+
try { fn(); } catch (e) {}
|
|
138
|
+
});
|
|
251
139
|
}
|
|
252
140
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
141
|
+
// -------------------------
|
|
142
|
+
// Config (hard gate)
|
|
143
|
+
// -------------------------
|
|
144
|
+
function prefetchConfig() {
|
|
145
|
+
if (cfgPromise) return cfgPromise;
|
|
146
|
+
|
|
147
|
+
cfgPromise = fetch(CFG_URL, { credentials: 'same-origin' })
|
|
148
|
+
.then(r => r.ok ? r.json() : Promise.reject(new Error('config http ' + r.status)))
|
|
149
|
+
.then(data => {
|
|
150
|
+
cfg = data;
|
|
151
|
+
// Pre-create placeholders in pool so Ezoic never complains even before first injection
|
|
152
|
+
const ids = parsePlaceholderIds((cfg && cfg.placeholderIds) || '');
|
|
153
|
+
ids.forEach(id => Pool.ensurePlaceholder(id));
|
|
154
|
+
return cfg;
|
|
155
|
+
})
|
|
156
|
+
.catch(() => {
|
|
157
|
+
cfg = null;
|
|
158
|
+
return null;
|
|
159
|
+
});
|
|
256
160
|
|
|
257
|
-
|
|
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
|
-
}
|
|
161
|
+
return cfgPromise;
|
|
273
162
|
}
|
|
274
163
|
|
|
275
|
-
function
|
|
276
|
-
return
|
|
164
|
+
function parsePlaceholderIds(s) {
|
|
165
|
+
return String(s || '')
|
|
166
|
+
.split(',')
|
|
167
|
+
.map(x => x.trim())
|
|
168
|
+
.filter(Boolean);
|
|
277
169
|
}
|
|
278
170
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
287
|
-
if (!ph || !ph.isConnected) return;
|
|
288
|
-
|
|
289
|
-
state.lastShowById.set(id, now);
|
|
171
|
+
// -------------------------
|
|
172
|
+
// Ad insertion logic
|
|
173
|
+
// -------------------------
|
|
174
|
+
const Ad = {
|
|
175
|
+
// throttle showAds per placeholder id
|
|
176
|
+
lastShowAt: new Map(),
|
|
177
|
+
minShowIntervalMs: 1500,
|
|
290
178
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
179
|
+
// Observers
|
|
180
|
+
io: null,
|
|
181
|
+
mo: null,
|
|
182
|
+
|
|
183
|
+
// Bookkeeping
|
|
184
|
+
insertedKeys: new Set(), // avoid duplicate injection on re-renders
|
|
185
|
+
|
|
186
|
+
initObservers() {
|
|
187
|
+
if (!this.io) {
|
|
188
|
+
this.io = new IntersectionObserver((entries) => {
|
|
189
|
+
for (const e of entries) {
|
|
190
|
+
if (!e.isIntersecting) continue;
|
|
191
|
+
const id = e.target && e.target.getAttribute('data-ezoic-id');
|
|
192
|
+
if (id) this.requestShow(id);
|
|
193
|
+
}
|
|
194
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0.01 });
|
|
300
195
|
}
|
|
301
196
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
});
|
|
197
|
+
if (!this.mo) {
|
|
198
|
+
this.mo = new MutationObserver(() => this.scheduleScan());
|
|
199
|
+
const root = document.querySelector('#content, #panel, main, body');
|
|
200
|
+
if (root) this.mo.observe(root, { childList: true, subtree: true });
|
|
315
201
|
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
},
|
|
320
219
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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) {}
|
|
220
|
+
async scanAndInject() {
|
|
221
|
+
const c = await prefetchConfig();
|
|
222
|
+
if (!c) return;
|
|
329
223
|
|
|
330
|
-
|
|
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
|
-
}
|
|
224
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
340
225
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (!ph || !ph.isConnected) return;
|
|
344
|
-
const io = ensurePreloadObserver();
|
|
345
|
-
try { io && io.observe(ph); } catch (e) {}
|
|
346
|
-
|
|
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) {}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ---------- insertion logic ----------
|
|
226
|
+
installShowAdsHook();
|
|
227
|
+
this.initObservers();
|
|
355
228
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
364
|
-
}
|
|
229
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
230
|
+
// Extend: categories ads if you use them
|
|
231
|
+
},
|
|
365
232
|
|
|
366
|
-
|
|
367
|
-
|
|
233
|
+
// NodeBB topic list injection (between li rows)
|
|
234
|
+
injectBetweenTopics(c) {
|
|
235
|
+
const container =
|
|
236
|
+
document.querySelector('.category .topic-list') ||
|
|
237
|
+
document.querySelector('.topics .topic-list') ||
|
|
238
|
+
document.querySelector('.topic-list');
|
|
368
239
|
|
|
369
|
-
|
|
370
|
-
let inserted = 0;
|
|
240
|
+
if (!container) return;
|
|
371
241
|
|
|
372
|
-
|
|
373
|
-
if (
|
|
242
|
+
const rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item'));
|
|
243
|
+
if (!rows.length) return;
|
|
374
244
|
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
377
|
-
if (isAdjacentAd(el)) continue;
|
|
378
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
245
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
246
|
+
if (!ids.length) return;
|
|
379
247
|
|
|
380
|
-
const
|
|
381
|
-
if (!id) break;
|
|
248
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
382
249
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
usedSet.delete(id);
|
|
387
|
-
pool.unshift(id);
|
|
388
|
-
continue;
|
|
250
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
251
|
+
if (c.showFirstTopicAd) {
|
|
252
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
389
253
|
}
|
|
390
254
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
async function insertHeroAdEarly() {
|
|
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
|
-
}
|
|
429
|
-
|
|
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; }
|
|
255
|
+
// Between rows
|
|
256
|
+
let idIndex = 0;
|
|
257
|
+
for (let i = interval; i < rows.length; i += interval) {
|
|
258
|
+
idIndex = (idIndex + 1) % ids.length;
|
|
259
|
+
this.insertAdAfterRow(rows[i - 1], ids[idIndex], `between-topic-${i}`);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
438
262
|
|
|
439
|
-
|
|
440
|
-
|
|
263
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
264
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
265
|
+
if (this.insertedKeys.has(key)) return;
|
|
441
266
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
267
|
+
// If already present nearby, skip
|
|
268
|
+
if (rowEl.nextElementSibling && rowEl.nextElementSibling.classList && rowEl.nextElementSibling.classList.contains('ezoic-ad-wrapper')) {
|
|
269
|
+
this.insertedKeys.add(key);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
449
272
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
273
|
+
const isLi = rowEl.tagName && rowEl.tagName.toLowerCase() === 'li';
|
|
274
|
+
const wrapper = document.createElement(isLi ? 'li' : 'div');
|
|
275
|
+
wrapper.className = 'ezoic-ad-wrapper';
|
|
276
|
+
wrapper.setAttribute('role', 'presentation');
|
|
277
|
+
wrapper.setAttribute('data-ezoic-id', placeholderId);
|
|
453
278
|
|
|
454
|
-
|
|
455
|
-
|
|
279
|
+
// Theme compatibility: if list uses list-group-item, keep it to avoid layout/JS breaks
|
|
280
|
+
if (isLi && rowEl.classList.contains('list-group-item')) {
|
|
281
|
+
wrapper.classList.add('list-group-item');
|
|
282
|
+
}
|
|
456
283
|
|
|
457
|
-
|
|
284
|
+
// Ensure placeholder exists (in pool) then move it into wrapper
|
|
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) {}
|
|
458
288
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
289
|
+
// Insert into DOM
|
|
290
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
291
|
+
this.insertedKeys.add(key);
|
|
462
292
|
|
|
463
|
-
|
|
293
|
+
// Observe for preloading and request a show
|
|
294
|
+
if (this.io) this.io.observe(wrapper);
|
|
464
295
|
|
|
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
|
-
);
|
|
296
|
+
// If above fold, request immediately (no timeout)
|
|
297
|
+
const rect = wrapper.getBoundingClientRect();
|
|
298
|
+
if (rect.top < (window.innerHeight * 1.5)) {
|
|
299
|
+
this.requestShow(placeholderId);
|
|
475
300
|
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
requestShow(placeholderId) {
|
|
304
|
+
if (!placeholderId) return;
|
|
305
|
+
|
|
306
|
+
const last = this.lastShowAt.get(placeholderId) || 0;
|
|
307
|
+
const t = now();
|
|
308
|
+
if ((t - last) < this.minShowIntervalMs) return;
|
|
309
|
+
this.lastShowAt.set(placeholderId, t);
|
|
310
|
+
|
|
311
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
312
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
313
|
+
|
|
314
|
+
// Use ez cmd queue if available
|
|
315
|
+
const doShow = () => {
|
|
316
|
+
if (!window.ezstandalone || !window.ezstandalone.showAds) return;
|
|
317
|
+
// showAds is wrapped; calling with one id is safest
|
|
318
|
+
window.ezstandalone.showAds(placeholderId);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
322
|
+
ezCmd(doShow);
|
|
323
|
+
} else {
|
|
324
|
+
doShow();
|
|
486
325
|
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
state.usedCategories
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
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 ----------
|
|
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) {}
|
|
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);
|
|
521
334
|
});
|
|
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
|
-
});
|
|
335
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
336
|
+
this.insertedKeys.clear();
|
|
337
|
+
this.lastShowAt.clear();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
574
340
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
341
|
+
// -------------------------
|
|
342
|
+
// NodeBB hooks
|
|
343
|
+
// -------------------------
|
|
344
|
+
function onAjaxifyStart() {
|
|
345
|
+
navToken++;
|
|
346
|
+
Ad.disconnectObservers();
|
|
347
|
+
Ad.reclaimPlaceholders();
|
|
580
348
|
}
|
|
581
349
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
ticking = true;
|
|
587
|
-
window.requestAnimationFrame(() => {
|
|
588
|
-
ticking = false;
|
|
589
|
-
if (!EZOIC_BLOCKED) scheduleRun();
|
|
590
|
-
});
|
|
591
|
-
}, { passive: true });
|
|
350
|
+
function onAjaxifyEnd() {
|
|
351
|
+
// Kick scan for new page
|
|
352
|
+
prefetchConfig();
|
|
353
|
+
Ad.scheduleScan();
|
|
592
354
|
}
|
|
593
355
|
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
warmUpNetwork();
|
|
598
|
-
patchShowAds();
|
|
599
|
-
ensurePreloadObserver();
|
|
600
|
-
ensureDomObserver();
|
|
356
|
+
// NodeBB exposes ajaxify events on document
|
|
357
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
358
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
601
359
|
|
|
602
|
-
|
|
603
|
-
|
|
360
|
+
// First load
|
|
361
|
+
once('DOMContentLoaded', () => {
|
|
362
|
+
installShowAdsHook();
|
|
363
|
+
prefetchConfig();
|
|
364
|
+
Ad.scheduleScan();
|
|
365
|
+
});
|
|
604
366
|
|
|
605
|
-
// First paint: try hero + run
|
|
606
|
-
EZOIC_BLOCKED = false;
|
|
607
|
-
insertHeroAdEarly().catch(() => {});
|
|
608
|
-
scheduleRun();
|
|
609
367
|
})();
|
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;}
|