nodebb-plugin-ezoic-infinite 1.5.7 → 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 +1 -1
- package/package.json +1 -1
- package/public/client.js +306 -416
- package/public/style.css +3 -1
package/library.js
CHANGED
|
@@ -107,7 +107,7 @@ 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,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,477 +1,367 @@
|
|
|
1
|
-
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
|
-
|
|
6
|
-
const WRAP_CLASS = 'ezoic-ad';
|
|
7
|
-
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
8
|
-
|
|
9
|
-
const SELECTORS = {
|
|
10
|
-
topicItem: 'li[component="category/topic"]',
|
|
11
|
-
categoryItem: 'li[component="categories/category"]',
|
|
12
|
-
postItem: '[component="post"][data-pid]',
|
|
13
|
-
postContent: '[component="post/content"]',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// ----------------------------
|
|
17
|
-
// State
|
|
18
|
-
// ----------------------------
|
|
19
|
-
const state = {
|
|
20
|
-
pageKey: null,
|
|
21
|
-
pageToken: 0,
|
|
22
|
-
cfgPromise: null,
|
|
23
|
-
cfg: null,
|
|
24
|
-
// throttle per id
|
|
25
|
-
lastShowAt: new Map(),
|
|
26
|
-
// observed placeholders -> ids
|
|
27
|
-
io: null,
|
|
28
|
-
mo: null,
|
|
29
|
-
scheduled: false,
|
|
30
|
-
};
|
|
1
|
+
'use strict';
|
|
31
2
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
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
|
+
*/
|
|
45
10
|
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
48
24
|
}
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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);
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
|
-
|
|
61
|
-
}
|
|
69
|
+
};
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
function wrapShowAds(original) {
|
|
83
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
84
|
+
|
|
85
|
+
const wrapped = function (...args) {
|
|
86
|
+
const ids = normalizeIds(args);
|
|
87
|
+
if (!ids.length) return;
|
|
88
|
+
|
|
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);
|
|
92
|
+
|
|
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
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
return
|
|
105
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
106
|
+
return wrapped;
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
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);
|
|
120
|
+
}
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const parentPost = el.parentElement && el.parentElement.closest(SELECTORS.postItem);
|
|
94
|
-
if (parentPost && parentPost !== el) return false;
|
|
95
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
96
|
-
return true;
|
|
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
|
+
}
|
|
97
127
|
});
|
|
98
128
|
}
|
|
99
129
|
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
state.scheduled = true;
|
|
108
|
-
requestAnimationFrame(() => {
|
|
109
|
-
state.scheduled = false;
|
|
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;
|
|
110
137
|
try { fn(); } catch (e) {}
|
|
111
138
|
});
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Normalize ids from:
|
|
133
|
-
// - showAds([1,2])
|
|
134
|
-
// - showAds(1,2,3)
|
|
135
|
-
// - showAds(1)
|
|
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);
|
|
162
|
-
}
|
|
163
|
-
} catch (e) {}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function safeCmd(token, fn) {
|
|
167
|
-
try {
|
|
168
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
169
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
170
|
-
window.ezstandalone.cmd.push(function () {
|
|
171
|
-
// Drop stale work after ajaxify navigation
|
|
172
|
-
if (token !== state.pageToken) return;
|
|
173
|
-
if (getPageKey() !== state.pageKey) return;
|
|
174
|
-
try { fn(); } catch (e) {}
|
|
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;
|
|
175
159
|
});
|
|
176
|
-
} catch (e) {}
|
|
177
|
-
}
|
|
178
160
|
|
|
179
|
-
|
|
180
|
-
if (!id) return;
|
|
181
|
-
// throttle to avoid repeated calls during rerenders
|
|
182
|
-
const now = Date.now();
|
|
183
|
-
const last = state.lastShowAt.get(id) || 0;
|
|
184
|
-
if (now - last < 1500) return;
|
|
185
|
-
state.lastShowAt.set(id, now);
|
|
186
|
-
|
|
187
|
-
const token = state.pageToken;
|
|
188
|
-
safeCmd(token, () => {
|
|
189
|
-
if (!isPlaceholderPresent(id)) return;
|
|
190
|
-
patchShowAds();
|
|
191
|
-
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
192
|
-
window.ezstandalone.showAds(id);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ----------------------------
|
|
198
|
-
// DOM insertion (HTML-valid: if target is <li>, wrapper is <li>)
|
|
199
|
-
// ----------------------------
|
|
200
|
-
function buildWrap(id, kindClass, afterPos, liLike) {
|
|
201
|
-
const wrap = document.createElement(liLike ? 'li' : 'div');
|
|
202
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
203
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
204
|
-
wrap.setAttribute('role', 'presentation');
|
|
205
|
-
wrap.style.width = '100%';
|
|
206
|
-
|
|
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
|
-
const ph = document.createElement('div');
|
|
214
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
215
|
-
wrap.appendChild(ph);
|
|
216
|
-
return wrap;
|
|
161
|
+
return cfgPromise;
|
|
217
162
|
}
|
|
218
163
|
|
|
219
|
-
function
|
|
220
|
-
return
|
|
164
|
+
function parsePlaceholderIds(s) {
|
|
165
|
+
return String(s || '')
|
|
166
|
+
.split(',')
|
|
167
|
+
.map(x => x.trim())
|
|
168
|
+
.filter(Boolean);
|
|
221
169
|
}
|
|
222
170
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const liLike = String(target.tagName).toUpperCase() === 'LI';
|
|
231
|
-
const wrap = buildWrap(id, kindClass, afterPos, liLike);
|
|
171
|
+
// -------------------------
|
|
172
|
+
// Ad insertion logic
|
|
173
|
+
// -------------------------
|
|
174
|
+
const Ad = {
|
|
175
|
+
// throttle showAds per placeholder id
|
|
176
|
+
lastShowAt: new Map(),
|
|
177
|
+
minShowIntervalMs: 1500,
|
|
232
178
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
179
|
+
// Observers
|
|
180
|
+
io: null,
|
|
181
|
+
mo: null,
|
|
237
182
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 });
|
|
195
|
+
}
|
|
241
196
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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) {}
|
|
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 });
|
|
256
201
|
}
|
|
257
|
-
},
|
|
258
|
-
|
|
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
|
+
},
|
|
259
219
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
264
|
-
if (!ph) return;
|
|
265
|
-
try { state.io.observe(ph); } catch (e) {}
|
|
266
|
-
}
|
|
220
|
+
async scanAndInject() {
|
|
221
|
+
const c = await prefetchConfig();
|
|
222
|
+
if (!c) return;
|
|
267
223
|
|
|
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
|
-
}
|
|
224
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
273
225
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// ----------------------------
|
|
277
|
-
async function fetchConfig() {
|
|
278
|
-
if (state.cfg) return state.cfg;
|
|
279
|
-
if (state.cfgPromise) return state.cfgPromise;
|
|
280
|
-
|
|
281
|
-
state.cfgPromise = (async () => {
|
|
282
|
-
try {
|
|
283
|
-
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
284
|
-
if (!res.ok) return null;
|
|
285
|
-
const cfg = await res.json();
|
|
286
|
-
state.cfg = cfg;
|
|
287
|
-
return cfg;
|
|
288
|
-
} catch (e) {
|
|
289
|
-
return null;
|
|
290
|
-
} finally {
|
|
291
|
-
state.cfgPromise = null;
|
|
292
|
-
}
|
|
293
|
-
})();
|
|
226
|
+
installShowAdsHook();
|
|
227
|
+
this.initObservers();
|
|
294
228
|
|
|
295
|
-
|
|
296
|
-
|
|
229
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
230
|
+
// Extend: categories ads if you use them
|
|
231
|
+
},
|
|
297
232
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const groupsA = u.groups || u.group_names || u.groupNames;
|
|
305
|
-
if (Array.isArray(groupsA)) return groupsA.map(g => (g && g.name) ? g.name : String(g)).filter(Boolean);
|
|
306
|
-
if (typeof groupsA === 'string') return groupsA.split(',').map(s => s.trim()).filter(Boolean);
|
|
307
|
-
} catch (e) {}
|
|
308
|
-
return [];
|
|
309
|
-
}
|
|
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');
|
|
310
239
|
|
|
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
|
-
}
|
|
240
|
+
if (!container) return;
|
|
317
241
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (cfg && cfg.excluded === true) return true;
|
|
242
|
+
const rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item'));
|
|
243
|
+
if (!rows.length) return;
|
|
321
244
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!excludedGroups.length) return false;
|
|
325
|
-
const userGroups = getUserGroupNamesFromAjaxify();
|
|
326
|
-
if (!userGroups.length) return false;
|
|
245
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
246
|
+
if (!ids.length) return;
|
|
327
247
|
|
|
328
|
-
|
|
329
|
-
return excludedGroups.some(g => set.has(g));
|
|
330
|
-
}
|
|
248
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
331
249
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const out = [];
|
|
337
|
-
if (count <= 0) return out;
|
|
338
|
-
if (showFirst) out.push(1);
|
|
339
|
-
for (let i = interval; i <= count; i += interval) out.push(i);
|
|
340
|
-
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
341
|
-
}
|
|
250
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
251
|
+
if (c.showFirstTopicAd) {
|
|
252
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
253
|
+
}
|
|
342
254
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
for (const afterPos of targets) {
|
|
349
|
-
const el = items[afterPos - 1];
|
|
350
|
-
if (!el || !el.isConnected) continue;
|
|
351
|
-
if (!pool.length) break;
|
|
352
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
353
|
-
|
|
354
|
-
const id = pool.shift();
|
|
355
|
-
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
356
|
-
if (!wrap) {
|
|
357
|
-
// push back if couldn't insert
|
|
358
|
-
pool.unshift(id);
|
|
359
|
-
continue;
|
|
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}`);
|
|
360
260
|
}
|
|
261
|
+
},
|
|
361
262
|
|
|
362
|
-
|
|
263
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
264
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
265
|
+
if (this.insertedKeys.has(key)) return;
|
|
363
266
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
showAd(id);
|
|
369
|
-
} else {
|
|
370
|
-
observePlaceholder(id);
|
|
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;
|
|
371
271
|
}
|
|
372
|
-
}
|
|
373
|
-
return insertedIds;
|
|
374
|
-
}
|
|
375
272
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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);
|
|
379
278
|
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
283
|
|
|
383
|
-
|
|
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) {}
|
|
384
288
|
|
|
385
|
-
|
|
386
|
-
|
|
289
|
+
// Insert into DOM
|
|
290
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
291
|
+
this.insertedKeys.add(key);
|
|
387
292
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
removeAllAds();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const kind = getKind();
|
|
395
|
-
|
|
396
|
-
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
397
|
-
const pool = parsePool(cfg.messagePlaceholderIds);
|
|
398
|
-
injectBetween(
|
|
399
|
-
'ezoic-ad-message',
|
|
400
|
-
getPostContainers(),
|
|
401
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
402
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
403
|
-
pool
|
|
404
|
-
);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
293
|
+
// Observe for preloading and request a show
|
|
294
|
+
if (this.io) this.io.observe(wrapper);
|
|
407
295
|
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
415
|
-
pool
|
|
416
|
-
);
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
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);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
419
302
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
injectBetween(
|
|
423
|
-
'ezoic-ad-categories',
|
|
424
|
-
getCategoryItems(),
|
|
425
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
426
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
427
|
-
pool
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
303
|
+
requestShow(placeholderId) {
|
|
304
|
+
if (!placeholderId) return;
|
|
431
305
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
state.cfgPromise = null;
|
|
437
|
-
state.lastShowAt.clear();
|
|
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);
|
|
438
310
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
state.io = null;
|
|
442
|
-
try { if (state.mo) state.mo.disconnect(); } catch (e) {}
|
|
443
|
-
state.mo = null;
|
|
311
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
312
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
444
313
|
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
};
|
|
447
320
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
321
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
322
|
+
ezCmd(doShow);
|
|
323
|
+
} else {
|
|
324
|
+
doShow();
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// On navigation, return all placeholders to pool so they still exist
|
|
329
|
+
reclaimPlaceholders() {
|
|
330
|
+
const wrappers = document.querySelectorAll('.ezoic-ad-wrapper[data-ezoic-id]');
|
|
331
|
+
wrappers.forEach(w => {
|
|
332
|
+
const id = w.getAttribute('data-ezoic-id');
|
|
333
|
+
if (id) Pool.returnToPool(id);
|
|
334
|
+
});
|
|
335
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
336
|
+
this.insertedKeys.clear();
|
|
337
|
+
this.lastShowAt.clear();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
453
340
|
|
|
454
|
-
|
|
341
|
+
// -------------------------
|
|
342
|
+
// NodeBB hooks
|
|
343
|
+
// -------------------------
|
|
344
|
+
function onAjaxifyStart() {
|
|
345
|
+
navToken++;
|
|
346
|
+
Ad.disconnectObservers();
|
|
347
|
+
Ad.reclaimPlaceholders();
|
|
348
|
+
}
|
|
455
349
|
|
|
456
|
-
|
|
350
|
+
function onAjaxifyEnd() {
|
|
351
|
+
// Kick scan for new page
|
|
352
|
+
prefetchConfig();
|
|
353
|
+
Ad.scheduleScan();
|
|
354
|
+
}
|
|
457
355
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
schedule(runCore);
|
|
462
|
-
});
|
|
356
|
+
// NodeBB exposes ajaxify events on document
|
|
357
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
358
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
463
359
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
360
|
+
// First load
|
|
361
|
+
once('DOMContentLoaded', () => {
|
|
362
|
+
installShowAdsHook();
|
|
363
|
+
prefetchConfig();
|
|
364
|
+
Ad.scheduleScan();
|
|
365
|
+
});
|
|
470
366
|
|
|
471
|
-
|
|
472
|
-
cleanupForNav();
|
|
473
|
-
bind();
|
|
474
|
-
ensureMO();
|
|
475
|
-
state.pageKey = getPageKey();
|
|
476
|
-
schedule(runCore);
|
|
477
|
-
})();
|
|
367
|
+
})();
|