nodebb-plugin-ezoic-infinite 1.4.99 → 1.5.0
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 +313 -415
- 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,375 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
};
|
|
31
|
-
|
|
32
|
-
// ----------------------------
|
|
33
|
-
// Small utils
|
|
34
|
-
// ----------------------------
|
|
35
|
-
function getPageKey() {
|
|
36
|
-
try {
|
|
37
|
-
const ax = window.ajaxify;
|
|
38
|
-
if (ax && ax.data) {
|
|
39
|
-
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
40
|
-
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {}
|
|
43
|
-
return window.location.pathname || '';
|
|
44
|
-
}
|
|
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
|
+
*/
|
|
45
9
|
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
48
23
|
}
|
|
49
24
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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);
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parsePool(raw) {
|
|
64
|
-
if (!raw) return [];
|
|
65
|
-
return uniqInts(String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getKind() {
|
|
69
|
-
const p = window.location.pathname || '';
|
|
70
|
-
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
|
-
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
|
-
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
73
|
-
// fallback by DOM
|
|
74
|
-
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
75
|
-
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
76
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
77
|
-
return 'other';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getTopicItems() {
|
|
81
|
-
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
82
|
-
}
|
|
68
|
+
};
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
function wrapShowAds(original) {
|
|
82
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
83
|
+
|
|
84
|
+
const wrapped = function (...args) {
|
|
85
|
+
const ids = normalizeIds(args);
|
|
86
|
+
if (!ids.length) return;
|
|
87
|
+
|
|
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);
|
|
91
|
+
|
|
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
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return !!(el && el.isConnected);
|
|
104
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
105
|
+
return wrapped;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
try { fn(); } catch (e) {}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
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);
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
// Ezoic showAds patch (handles arrays, varargs, and filters absent placeholders)
|
|
116
|
-
// ----------------------------
|
|
117
|
-
function patchShowAds() {
|
|
114
|
+
// Hook future assignment (no polling)
|
|
118
115
|
try {
|
|
119
|
-
window.ezstandalone
|
|
116
|
+
if (!window.ezstandalone) window.ezstandalone = {};
|
|
120
117
|
const ez = window.ezstandalone;
|
|
121
|
-
if (ez.__nodebbEzoicPatched) return;
|
|
122
|
-
|
|
123
|
-
// If showAds isn't ready yet, patch when it appears via cmd (no polling)
|
|
124
|
-
const apply = () => {
|
|
125
|
-
try {
|
|
126
|
-
if (!window.ezstandalone || typeof window.ezstandalone.showAds !== 'function') return;
|
|
127
|
-
const ez2 = window.ezstandalone;
|
|
128
|
-
if (ez2.__nodebbEzoicPatched) return;
|
|
129
|
-
|
|
130
|
-
const orig = ez2.showAds;
|
|
131
|
-
ez2.showAds = function () {
|
|
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
118
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
ez.cmd = ez.cmd || [];
|
|
161
|
-
ez.cmd.push(apply);
|
|
162
|
-
}
|
|
163
|
-
} catch (e) {}
|
|
164
|
-
}
|
|
119
|
+
// If already has a setter installed, do nothing
|
|
120
|
+
const desc = Object.getOwnPropertyDescriptor(ez, 'showAds');
|
|
121
|
+
if (desc && (desc.set || desc.get)) return;
|
|
165
122
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (token !== state.pageToken) return;
|
|
173
|
-
if (getPageKey() !== state.pageKey) return;
|
|
174
|
-
try { fn(); } catch (e) {}
|
|
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); }
|
|
175
129
|
});
|
|
176
|
-
|
|
130
|
+
|
|
131
|
+
// Re-assign current value through setter
|
|
132
|
+
ez.showAds = _showAds;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// If defineProperty fails, best-effort wrap is still in place above.
|
|
135
|
+
}
|
|
177
136
|
}
|
|
178
137
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
}
|
|
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) {}
|
|
194
146
|
});
|
|
195
147
|
}
|
|
196
148
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
});
|
|
212
168
|
|
|
213
|
-
|
|
214
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
215
|
-
wrap.appendChild(ph);
|
|
216
|
-
return wrap;
|
|
169
|
+
return cfgPromise;
|
|
217
170
|
}
|
|
218
171
|
|
|
219
|
-
function
|
|
220
|
-
return
|
|
172
|
+
function parsePlaceholderIds(s) {
|
|
173
|
+
return String(s || '')
|
|
174
|
+
.split(',')
|
|
175
|
+
.map(x => x.trim())
|
|
176
|
+
.filter(Boolean);
|
|
221
177
|
}
|
|
222
178
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const liLike = String(target.tagName).toUpperCase() === 'LI';
|
|
231
|
-
const wrap = buildWrap(id, kindClass, afterPos, liLike);
|
|
179
|
+
// -------------------------
|
|
180
|
+
// Ad insertion logic
|
|
181
|
+
// -------------------------
|
|
182
|
+
const Ad = {
|
|
183
|
+
// throttle showAds per placeholder id
|
|
184
|
+
lastShowAt: new Map(),
|
|
185
|
+
minShowIntervalMs: 1500,
|
|
232
186
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
187
|
+
// Observers
|
|
188
|
+
io: null,
|
|
189
|
+
mo: null,
|
|
237
190
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 });
|
|
203
|
+
}
|
|
241
204
|
|
|
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) {}
|
|
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 });
|
|
256
209
|
}
|
|
257
|
-
},
|
|
258
|
-
|
|
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
|
+
},
|
|
259
227
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
264
|
-
if (!ph) return;
|
|
265
|
-
try { state.io.observe(ph); } catch (e) {}
|
|
266
|
-
}
|
|
228
|
+
async scanAndInject() {
|
|
229
|
+
const c = await prefetchConfig();
|
|
230
|
+
if (!c) return;
|
|
267
231
|
|
|
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
|
-
}
|
|
232
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
273
233
|
|
|
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
|
-
})();
|
|
234
|
+
installShowAdsHook();
|
|
235
|
+
this.initObservers();
|
|
294
236
|
|
|
295
|
-
|
|
296
|
-
|
|
237
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
238
|
+
// Extend: categories ads if you use them
|
|
239
|
+
},
|
|
297
240
|
|
|
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
|
-
}
|
|
241
|
+
// NodeBB topic list injection (between li rows)
|
|
242
|
+
injectBetweenTopics(c) {
|
|
243
|
+
const container =
|
|
244
|
+
document.querySelector('.category .topic-list') ||
|
|
245
|
+
document.querySelector('.topics .topic-list') ||
|
|
246
|
+
document.querySelector('.topic-list');
|
|
310
247
|
|
|
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
|
-
}
|
|
248
|
+
if (!container) return;
|
|
317
249
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (cfg && cfg.excluded === true) return true;
|
|
250
|
+
const rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item'));
|
|
251
|
+
if (!rows.length) return;
|
|
321
252
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!excludedGroups.length) return false;
|
|
325
|
-
const userGroups = getUserGroupNamesFromAjaxify();
|
|
326
|
-
if (!userGroups.length) return false;
|
|
253
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
254
|
+
if (!ids.length) return;
|
|
327
255
|
|
|
328
|
-
|
|
329
|
-
return excludedGroups.some(g => set.has(g));
|
|
330
|
-
}
|
|
256
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
331
257
|
|
|
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
|
-
}
|
|
258
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
259
|
+
if (c.showFirstTopicAd) {
|
|
260
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
261
|
+
}
|
|
342
262
|
|
|
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;
|
|
263
|
+
// Between rows
|
|
264
|
+
let idIndex = 0;
|
|
265
|
+
for (let i = interval; i < rows.length; i += interval) {
|
|
266
|
+
idIndex = (idIndex + 1) % ids.length;
|
|
267
|
+
this.insertAdAfterRow(rows[i - 1], ids[idIndex], `between-topic-${i}`);
|
|
360
268
|
}
|
|
269
|
+
},
|
|
361
270
|
|
|
362
|
-
|
|
271
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
272
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
273
|
+
if (this.insertedKeys.has(key)) return;
|
|
363
274
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
showAd(id);
|
|
369
|
-
} else {
|
|
370
|
-
observePlaceholder(id);
|
|
275
|
+
// If already present nearby, skip
|
|
276
|
+
if (rowEl.nextElementSibling && rowEl.nextElementSibling.classList && rowEl.nextElementSibling.classList.contains('ezoic-ad-wrapper')) {
|
|
277
|
+
this.insertedKeys.add(key);
|
|
278
|
+
return;
|
|
371
279
|
}
|
|
372
|
-
}
|
|
373
|
-
return insertedIds;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function removeAllAds() {
|
|
377
|
-
try { document.querySelectorAll(`.${WRAP_CLASS}`).forEach(n => n.remove()); } catch (e) {}
|
|
378
|
-
}
|
|
379
280
|
|
|
380
|
-
|
|
381
|
-
|
|
281
|
+
const isLi = rowEl.tagName && rowEl.tagName.toLowerCase() === 'li';
|
|
282
|
+
const wrapper = document.createElement(isLi ? 'li' : 'div');
|
|
283
|
+
wrapper.className = 'ezoic-ad-wrapper';
|
|
284
|
+
wrapper.setAttribute('role', 'presentation');
|
|
285
|
+
wrapper.setAttribute('data-ezoic-id', placeholderId);
|
|
382
286
|
|
|
383
|
-
|
|
287
|
+
// Theme compatibility: if list uses list-group-item, keep it to avoid layout/JS breaks
|
|
288
|
+
if (isLi && rowEl.classList.contains('list-group-item')) {
|
|
289
|
+
wrapper.classList.add('list-group-item');
|
|
290
|
+
}
|
|
384
291
|
|
|
385
|
-
|
|
386
|
-
|
|
292
|
+
// Ensure placeholder exists (in pool) then move it into wrapper
|
|
293
|
+
const ph = Pool.ensurePlaceholder(placeholderId);
|
|
294
|
+
// If placeholder is currently rendered elsewhere, move it (Ezoic uses id lookup)
|
|
295
|
+
try { wrapper.appendChild(ph); } catch (e) {}
|
|
387
296
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
297
|
+
// Insert into DOM
|
|
298
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
299
|
+
this.insertedKeys.add(key);
|
|
393
300
|
|
|
394
|
-
|
|
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
|
-
}
|
|
301
|
+
// Observe for preloading and request a show
|
|
302
|
+
if (this.io) this.io.observe(wrapper);
|
|
407
303
|
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
415
|
-
pool
|
|
416
|
-
);
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
304
|
+
// If above fold, request immediately (no timeout)
|
|
305
|
+
const rect = wrapper.getBoundingClientRect();
|
|
306
|
+
if (rect.top < (window.innerHeight * 1.5)) {
|
|
307
|
+
this.requestShow(placeholderId);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
419
310
|
|
|
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
|
-
}
|
|
311
|
+
requestShow(placeholderId) {
|
|
312
|
+
if (!placeholderId) return;
|
|
431
313
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
state.cfgPromise = null;
|
|
437
|
-
state.lastShowAt.clear();
|
|
314
|
+
const last = this.lastShowAt.get(placeholderId) || 0;
|
|
315
|
+
const t = now();
|
|
316
|
+
if ((t - last) < this.minShowIntervalMs) return;
|
|
317
|
+
this.lastShowAt.set(placeholderId, t);
|
|
438
318
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
state.io = null;
|
|
442
|
-
try { if (state.mo) state.mo.disconnect(); } catch (e) {}
|
|
443
|
-
state.mo = null;
|
|
319
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
320
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
444
321
|
|
|
445
|
-
|
|
446
|
-
|
|
322
|
+
// Use ez cmd queue if available
|
|
323
|
+
const doShow = () => {
|
|
324
|
+
if (!window.ezstandalone || !window.ezstandalone.showAds) return;
|
|
325
|
+
// showAds is wrapped; calling with one id is safest
|
|
326
|
+
window.ezstandalone.showAds(placeholderId);
|
|
327
|
+
};
|
|
447
328
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
329
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
330
|
+
ezCmd(doShow);
|
|
331
|
+
} else {
|
|
332
|
+
doShow();
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// On navigation, return all placeholders to pool so they still exist
|
|
337
|
+
reclaimPlaceholders() {
|
|
338
|
+
const wrappers = document.querySelectorAll('.ezoic-ad-wrapper[data-ezoic-id]');
|
|
339
|
+
wrappers.forEach(w => {
|
|
340
|
+
const id = w.getAttribute('data-ezoic-id');
|
|
341
|
+
if (id) Pool.returnToPool(id);
|
|
342
|
+
});
|
|
343
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
344
|
+
this.insertedKeys.clear();
|
|
345
|
+
this.lastShowAt.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
453
348
|
|
|
454
|
-
|
|
349
|
+
// -------------------------
|
|
350
|
+
// NodeBB hooks
|
|
351
|
+
// -------------------------
|
|
352
|
+
function onAjaxifyStart() {
|
|
353
|
+
navToken++;
|
|
354
|
+
Ad.disconnectObservers();
|
|
355
|
+
Ad.reclaimPlaceholders();
|
|
356
|
+
}
|
|
455
357
|
|
|
456
|
-
|
|
358
|
+
function onAjaxifyEnd() {
|
|
359
|
+
// Kick scan for new page
|
|
360
|
+
prefetchConfig();
|
|
361
|
+
Ad.scheduleScan();
|
|
362
|
+
}
|
|
457
363
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
schedule(runCore);
|
|
462
|
-
});
|
|
364
|
+
// NodeBB exposes ajaxify events on document
|
|
365
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
366
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
463
367
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
368
|
+
// First load
|
|
369
|
+
once('DOMContentLoaded', () => {
|
|
370
|
+
installShowAdsHook();
|
|
371
|
+
prefetchConfig();
|
|
372
|
+
Ad.scheduleScan();
|
|
373
|
+
});
|
|
470
374
|
|
|
471
|
-
|
|
472
|
-
cleanupForNav();
|
|
473
|
-
bind();
|
|
474
|
-
ensureMO();
|
|
475
|
-
state.pageKey = getPageKey();
|
|
476
|
-
schedule(runCore);
|
|
477
|
-
})();
|
|
375
|
+
})();
|