nodebb-plugin-ezoic-infinite 1.4.99 → 1.5.1
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 +338 -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,400 @@
|
|
|
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
|
-
};
|
|
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
|
+
*/
|
|
31
9
|
|
|
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
|
-
}
|
|
10
|
+
(function () {
|
|
45
11
|
|
|
46
|
-
|
|
47
|
-
|
|
12
|
+
// --- Safety stubs: prevent crashes if Ezoic scripts execute when ads are disabled/excluded ---
|
|
13
|
+
window._ezaq = window._ezaq || [];
|
|
14
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
15
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
16
|
+
if (typeof window.ezstandalone.showAds !== 'function') {
|
|
17
|
+
window.ezstandalone.showAds = function () {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CFG_URL = '/api/plugins/ezoic-infinite/config';
|
|
21
|
+
|
|
22
|
+
// -------------------------
|
|
23
|
+
// Utilities
|
|
24
|
+
// -------------------------
|
|
25
|
+
const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
26
|
+
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
|
|
27
|
+
|
|
28
|
+
function on(ev, fn) { document.addEventListener(ev, fn); }
|
|
29
|
+
function once(ev, fn) {
|
|
30
|
+
const h = (e) => { document.removeEventListener(ev, h); fn(e); };
|
|
31
|
+
document.addEventListener(ev, h);
|
|
48
32
|
}
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
34
|
+
// -------------------------
|
|
35
|
+
// Global state / navigation token
|
|
36
|
+
// -------------------------
|
|
37
|
+
let navToken = 0; // increment on ajaxify.start (page is about to be replaced)
|
|
38
|
+
let rafScheduled = false;
|
|
39
|
+
|
|
40
|
+
// resolved config (or null if failed)
|
|
41
|
+
let cfgPromise = null;
|
|
42
|
+
let cfg = null;
|
|
43
|
+
|
|
44
|
+
// -------------------------
|
|
45
|
+
// Placeholder Registry (keeps IDs alive)
|
|
46
|
+
// -------------------------
|
|
47
|
+
const Pool = {
|
|
48
|
+
el: null,
|
|
49
|
+
ensurePool() {
|
|
50
|
+
if (this.el && document.body.contains(this.el)) return this.el;
|
|
51
|
+
const d = document.createElement('div');
|
|
52
|
+
d.id = 'ezoic-placeholder-pool';
|
|
53
|
+
d.style.display = 'none';
|
|
54
|
+
d.setAttribute('aria-hidden', 'true');
|
|
55
|
+
document.body.appendChild(d);
|
|
56
|
+
this.el = d;
|
|
57
|
+
return d;
|
|
58
|
+
},
|
|
59
|
+
ensurePlaceholder(id) {
|
|
60
|
+
if (!id) return null;
|
|
61
|
+
let ph = document.getElementById(id);
|
|
62
|
+
if (ph) return ph;
|
|
63
|
+
const pool = this.ensurePool();
|
|
64
|
+
ph = document.createElement('div');
|
|
65
|
+
ph.id = id;
|
|
66
|
+
pool.appendChild(ph);
|
|
67
|
+
return ph;
|
|
68
|
+
},
|
|
69
|
+
returnToPool(id) {
|
|
70
|
+
const ph = document.getElementById(id);
|
|
71
|
+
if (!ph) return;
|
|
72
|
+
const pool = this.ensurePool();
|
|
73
|
+
if (ph.parentElement !== pool) {
|
|
74
|
+
pool.appendChild(ph);
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
|
-
|
|
61
|
-
}
|
|
77
|
+
};
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
// -------------------------
|
|
80
|
+
// Ezoic showAds safe wrapper (installed even if defined later)
|
|
81
|
+
// -------------------------
|
|
82
|
+
function normalizeIds(argsLike) {
|
|
83
|
+
const args = Array.from(argsLike);
|
|
84
|
+
if (!args.length) return [];
|
|
85
|
+
// showAds([id1,id2]) or showAds([[...]]) edge
|
|
86
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0].flat().map(String);
|
|
87
|
+
return args.flat().map(String);
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
function
|
|
69
|
-
|
|
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
|
-
}
|
|
90
|
+
function wrapShowAds(original) {
|
|
91
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
const wrapped = function (...args) {
|
|
94
|
+
const ids = normalizeIds(args);
|
|
95
|
+
if (!ids.length) return;
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
// Ensure placeholder exists somewhere (pool), so ez-standalone won't log "does not exist"
|
|
99
|
+
Pool.ensurePlaceholder(id);
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return nodes.filter((el) => {
|
|
91
|
-
if (!el || !el.isConnected) return false;
|
|
92
|
-
if (!el.querySelector(SELECTORS.postContent)) return false;
|
|
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;
|
|
97
|
-
});
|
|
98
|
-
}
|
|
101
|
+
const el = document.getElementById(id);
|
|
102
|
+
if (!el || !document.body.contains(el)) continue;
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
// If the placeholder is still in the hidden pool, DO NOT render into it (would be invisible).
|
|
105
|
+
// We only render when the placeholder has been moved into a visible wrapper.
|
|
106
|
+
const pool = Pool.ensurePool();
|
|
107
|
+
if (pool && pool.contains(el)) continue;
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
try {
|
|
110
|
+
// Call one-by-one to avoid batch logging on missing nodes
|
|
111
|
+
original.call(window.ezstandalone, id);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// swallow: Ezoic can throw if called during transitions
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
119
|
+
return wrapped;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
function installShowAdsHook() {
|
|
123
|
+
// If ezstandalone already exists, wrap now.
|
|
124
|
+
if (window.ezstandalone && window.ezstandalone.showAds) {
|
|
125
|
+
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Hook future assignment (no polling)
|
|
118
129
|
try {
|
|
119
|
-
window.ezstandalone
|
|
130
|
+
if (!window.ezstandalone) window.ezstandalone = {};
|
|
120
131
|
const ez = window.ezstandalone;
|
|
121
|
-
if (ez.__nodebbEzoicPatched) return;
|
|
122
132
|
|
|
123
|
-
// If
|
|
124
|
-
const
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
133
|
+
// If already has a setter installed, do nothing
|
|
134
|
+
const desc = Object.getOwnPropertyDescriptor(ez, 'showAds');
|
|
135
|
+
if (desc && (desc.set || desc.get)) return;
|
|
165
136
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (token !== state.pageToken) return;
|
|
173
|
-
if (getPageKey() !== state.pageKey) return;
|
|
174
|
-
try { fn(); } catch (e) {}
|
|
137
|
+
let _showAds = ez.showAds;
|
|
138
|
+
Object.defineProperty(ez, 'showAds', {
|
|
139
|
+
configurable: true,
|
|
140
|
+
enumerable: true,
|
|
141
|
+
get() { return _showAds; },
|
|
142
|
+
set(v) { _showAds = wrapShowAds(v); }
|
|
175
143
|
});
|
|
176
|
-
|
|
144
|
+
|
|
145
|
+
// Re-assign current value through setter
|
|
146
|
+
ez.showAds = _showAds;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// If defineProperty fails, best-effort wrap is still in place above.
|
|
149
|
+
}
|
|
177
150
|
}
|
|
178
151
|
|
|
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
|
-
}
|
|
152
|
+
function ezCmd(fn) {
|
|
153
|
+
// Tokenize so queued callbacks don't run after navigation
|
|
154
|
+
const tokenAtSchedule = navToken;
|
|
155
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
156
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
157
|
+
window.ezstandalone.cmd.push(function () {
|
|
158
|
+
if (tokenAtSchedule !== navToken) return;
|
|
159
|
+
try { fn(); } catch (e) {}
|
|
194
160
|
});
|
|
195
161
|
}
|
|
196
162
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
163
|
+
// -------------------------
|
|
164
|
+
// Config (hard gate)
|
|
165
|
+
// -------------------------
|
|
166
|
+
function prefetchConfig() {
|
|
167
|
+
if (cfgPromise) return cfgPromise;
|
|
168
|
+
|
|
169
|
+
cfgPromise = fetch(CFG_URL, { credentials: 'same-origin' })
|
|
170
|
+
.then(r => r.ok ? r.json() : Promise.reject(new Error('config http ' + r.status)))
|
|
171
|
+
.then(data => {
|
|
172
|
+
cfg = data;
|
|
173
|
+
// Pre-create placeholders in pool so Ezoic never complains even before first injection
|
|
174
|
+
const ids = parsePlaceholderIds((cfg && cfg.placeholderIds) || '');
|
|
175
|
+
ids.forEach(id => Pool.ensurePlaceholder(id));
|
|
176
|
+
return cfg;
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {
|
|
179
|
+
cfg = null;
|
|
180
|
+
return null;
|
|
181
|
+
});
|
|
212
182
|
|
|
213
|
-
|
|
214
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
215
|
-
wrap.appendChild(ph);
|
|
216
|
-
return wrap;
|
|
183
|
+
return cfgPromise;
|
|
217
184
|
}
|
|
218
185
|
|
|
219
|
-
function
|
|
220
|
-
return
|
|
186
|
+
function parsePlaceholderIds(s) {
|
|
187
|
+
return String(s || '')
|
|
188
|
+
.split(',')
|
|
189
|
+
.map(x => x.trim())
|
|
190
|
+
.filter(Boolean);
|
|
221
191
|
}
|
|
222
192
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const liLike = String(target.tagName).toUpperCase() === 'LI';
|
|
231
|
-
const wrap = buildWrap(id, kindClass, afterPos, liLike);
|
|
193
|
+
// -------------------------
|
|
194
|
+
// Ad insertion logic
|
|
195
|
+
// -------------------------
|
|
196
|
+
const Ad = {
|
|
197
|
+
// throttle showAds per placeholder id
|
|
198
|
+
lastShowAt: new Map(),
|
|
199
|
+
minShowIntervalMs: 1500,
|
|
232
200
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
201
|
+
// Observers
|
|
202
|
+
io: null,
|
|
203
|
+
mo: null,
|
|
237
204
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
205
|
+
// Bookkeeping
|
|
206
|
+
insertedKeys: new Set(), // avoid duplicate injection on re-renders
|
|
207
|
+
|
|
208
|
+
initObservers() {
|
|
209
|
+
if (!this.io) {
|
|
210
|
+
this.io = new IntersectionObserver((entries) => {
|
|
211
|
+
for (const e of entries) {
|
|
212
|
+
if (!e.isIntersecting) continue;
|
|
213
|
+
const id = e.target && e.target.getAttribute('data-ezoic-id');
|
|
214
|
+
if (id) this.requestShow(id);
|
|
215
|
+
}
|
|
216
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0.01 });
|
|
217
|
+
}
|
|
241
218
|
|
|
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) {}
|
|
219
|
+
if (!this.mo) {
|
|
220
|
+
this.mo = new MutationObserver(() => this.scheduleScan());
|
|
221
|
+
const root = document.querySelector('#content, #panel, main, body');
|
|
222
|
+
if (root) this.mo.observe(root, { childList: true, subtree: true });
|
|
256
223
|
}
|
|
257
|
-
},
|
|
258
|
-
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
disconnectObservers() {
|
|
227
|
+
try { this.io && this.io.disconnect(); } catch (e) {}
|
|
228
|
+
try { this.mo && this.mo.disconnect(); } catch (e) {}
|
|
229
|
+
this.io = null;
|
|
230
|
+
this.mo = null;
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
scheduleScan() {
|
|
234
|
+
if (rafScheduled) return;
|
|
235
|
+
rafScheduled = true;
|
|
236
|
+
requestAnimationFrame(() => {
|
|
237
|
+
rafScheduled = false;
|
|
238
|
+
this.scanAndInject();
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async scanAndInject() {
|
|
243
|
+
const c = await prefetchConfig();
|
|
244
|
+
if (!c) return;
|
|
245
|
+
|
|
246
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
247
|
+
|
|
248
|
+
installShowAdsHook();
|
|
249
|
+
this.initObservers();
|
|
250
|
+
|
|
251
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
252
|
+
// Extend: categories ads if you use them
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// NodeBB topic list injection (between li rows)
|
|
256
|
+
injectBetweenTopics(c) {
|
|
257
|
+
// NodeBB 4.x (Harmony) uses: <ul component="category" class="topics-list ...">
|
|
258
|
+
// Older themes may use .topic-list
|
|
259
|
+
const container =
|
|
260
|
+
document.querySelector('ul[component="category"].topics-list') ||
|
|
261
|
+
document.querySelector('ul[component="category"].topic-list') ||
|
|
262
|
+
document.querySelector('ul.topics-list[component="category"]') ||
|
|
263
|
+
document.querySelector('ul.topics-list') ||
|
|
264
|
+
document.querySelector('.category ul.topics-list') ||
|
|
265
|
+
document.querySelector('.category .topic-list') ||
|
|
266
|
+
document.querySelector('.topics .topic-list') ||
|
|
267
|
+
document.querySelector('.topic-list');
|
|
268
|
+
|
|
269
|
+
if (!container) return;
|
|
270
|
+
|
|
271
|
+
let rows = Array.from(container.querySelectorAll(':scope > li')).filter(el => el && el.nodeType === 1);
|
|
272
|
+
if (!rows.length) {
|
|
273
|
+
// Fallback for older markups
|
|
274
|
+
rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item')).filter(el => el && el.nodeType === 1);
|
|
275
|
+
}
|
|
276
|
+
if (!rows.length) return;
|
|
259
277
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (!state.io) return;
|
|
263
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
264
|
-
if (!ph) return;
|
|
265
|
-
try { state.io.observe(ph); } catch (e) {}
|
|
266
|
-
}
|
|
278
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
279
|
+
if (!ids.length) return;
|
|
267
280
|
|
|
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
|
-
}
|
|
281
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
273
282
|
|
|
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;
|
|
283
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
284
|
+
if (c.showFirstTopicAd) {
|
|
285
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
292
286
|
}
|
|
293
|
-
})();
|
|
294
|
-
|
|
295
|
-
return state.cfgPromise;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function getUserGroupNamesFromAjaxify() {
|
|
299
|
-
try {
|
|
300
|
-
const ax = window.ajaxify;
|
|
301
|
-
const u = ax && ax.data && (ax.data.user || ax.data.profile || null);
|
|
302
|
-
if (!u) return [];
|
|
303
|
-
// NodeBB varies by route/theme; handle multiple shapes
|
|
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
|
-
}
|
|
310
287
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
288
|
+
// Between rows
|
|
289
|
+
let idIndex = 0;
|
|
290
|
+
for (let i = interval; i < rows.length; i += interval) {
|
|
291
|
+
idIndex = (idIndex + 1) % ids.length;
|
|
292
|
+
this.insertAdAfterRow(rows[i - 1], ids[idIndex], `between-topic-${i}`);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
317
295
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
296
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
297
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
298
|
+
if (this.insertedKeys.has(key)) return;
|
|
321
299
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
300
|
+
// If already present nearby, skip
|
|
301
|
+
if (rowEl.nextElementSibling && rowEl.nextElementSibling.classList && rowEl.nextElementSibling.classList.contains('ezoic-ad-wrapper')) {
|
|
302
|
+
this.insertedKeys.add(key);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
327
305
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
306
|
+
const isLi = rowEl.tagName && rowEl.tagName.toLowerCase() === 'li';
|
|
307
|
+
const wrapper = document.createElement(isLi ? 'li' : 'div');
|
|
308
|
+
wrapper.className = 'ezoic-ad-wrapper';
|
|
309
|
+
wrapper.setAttribute('role', 'presentation');
|
|
310
|
+
wrapper.setAttribute('data-ezoic-id', placeholderId);
|
|
331
311
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
function computeTargets(count, interval, showFirst) {
|
|
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
|
-
}
|
|
342
|
-
|
|
343
|
-
function injectBetween(kindClass, items, interval, showFirst, pool) {
|
|
344
|
-
if (!items.length || !pool.length) return [];
|
|
345
|
-
|
|
346
|
-
const targets = computeTargets(items.length, interval, showFirst);
|
|
347
|
-
const insertedIds = [];
|
|
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;
|
|
312
|
+
// Theme compatibility: if list uses list-group-item, keep it to avoid layout/JS breaks
|
|
313
|
+
if (isLi && rowEl.classList.contains('list-group-item')) {
|
|
314
|
+
wrapper.classList.add('list-group-item');
|
|
360
315
|
}
|
|
361
316
|
|
|
362
|
-
|
|
317
|
+
// Ensure placeholder exists (in pool) then move it into wrapper
|
|
318
|
+
const ph = Pool.ensurePlaceholder(placeholderId);
|
|
319
|
+
// If placeholder is currently rendered elsewhere, move it (Ezoic uses id lookup)
|
|
320
|
+
try { wrapper.appendChild(ph); } catch (e) {}
|
|
363
321
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (rect && rect.top < (vh * 1.5)) {
|
|
368
|
-
showAd(id);
|
|
369
|
-
} else {
|
|
370
|
-
observePlaceholder(id);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return insertedIds;
|
|
374
|
-
}
|
|
322
|
+
// Insert into DOM
|
|
323
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
324
|
+
this.insertedKeys.add(key);
|
|
375
325
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
326
|
+
// Observe for preloading and request a show
|
|
327
|
+
if (this.io) this.io.observe(wrapper);
|
|
379
328
|
|
|
380
|
-
|
|
381
|
-
|
|
329
|
+
// If above fold, request immediately (no timeout)
|
|
330
|
+
const rect = wrapper.getBoundingClientRect();
|
|
331
|
+
if (rect.top < (window.innerHeight * 1.5)) {
|
|
332
|
+
this.requestShow(placeholderId);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
382
335
|
|
|
383
|
-
|
|
336
|
+
requestShow(placeholderId) {
|
|
337
|
+
if (!placeholderId) return;
|
|
384
338
|
|
|
385
|
-
|
|
386
|
-
|
|
339
|
+
const last = this.lastShowAt.get(placeholderId) || 0;
|
|
340
|
+
const t = now();
|
|
341
|
+
if ((t - last) < this.minShowIntervalMs) return;
|
|
342
|
+
this.lastShowAt.set(placeholderId, t);
|
|
387
343
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
removeAllAds();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
344
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
345
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
393
346
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
getPostContainers(),
|
|
401
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
402
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
403
|
-
pool
|
|
404
|
-
);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
347
|
+
// Use ez cmd queue if available
|
|
348
|
+
const doShow = () => {
|
|
349
|
+
if (!window.ezstandalone || !window.ezstandalone.showAds) return;
|
|
350
|
+
// showAds is wrapped; calling with one id is safest
|
|
351
|
+
window.ezstandalone.showAds(placeholderId);
|
|
352
|
+
};
|
|
407
353
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
354
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
355
|
+
ezCmd(doShow);
|
|
356
|
+
} else {
|
|
357
|
+
doShow();
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
// On navigation, return all placeholders to pool so they still exist
|
|
362
|
+
reclaimPlaceholders() {
|
|
363
|
+
const wrappers = document.querySelectorAll('.ezoic-ad-wrapper[data-ezoic-id]');
|
|
364
|
+
wrappers.forEach(w => {
|
|
365
|
+
const id = w.getAttribute('data-ezoic-id');
|
|
366
|
+
if (id) Pool.returnToPool(id);
|
|
367
|
+
});
|
|
368
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
369
|
+
this.insertedKeys.clear();
|
|
370
|
+
this.lastShowAt.clear();
|
|
418
371
|
}
|
|
372
|
+
};
|
|
419
373
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
pool
|
|
428
|
-
);
|
|
429
|
-
}
|
|
374
|
+
// -------------------------
|
|
375
|
+
// NodeBB hooks
|
|
376
|
+
// -------------------------
|
|
377
|
+
function onAjaxifyStart() {
|
|
378
|
+
navToken++;
|
|
379
|
+
Ad.disconnectObservers();
|
|
380
|
+
Ad.reclaimPlaceholders();
|
|
430
381
|
}
|
|
431
382
|
|
|
432
|
-
function
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
state.cfgPromise = null;
|
|
437
|
-
state.lastShowAt.clear();
|
|
438
|
-
|
|
439
|
-
// Disconnect observers for old DOM
|
|
440
|
-
try { if (state.io) state.io.disconnect(); } catch (e) {}
|
|
441
|
-
state.io = null;
|
|
442
|
-
try { if (state.mo) state.mo.disconnect(); } catch (e) {}
|
|
443
|
-
state.mo = null;
|
|
444
|
-
|
|
445
|
-
removeAllAds();
|
|
383
|
+
function onAjaxifyEnd() {
|
|
384
|
+
// Kick scan for new page
|
|
385
|
+
prefetchConfig();
|
|
386
|
+
Ad.scheduleScan();
|
|
446
387
|
}
|
|
447
388
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
function bind() {
|
|
452
|
-
if (!$) return;
|
|
389
|
+
// NodeBB exposes ajaxify events on document
|
|
390
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
391
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
453
392
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
ensureMO();
|
|
461
|
-
schedule(runCore);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// Infinite scroll hooks
|
|
465
|
-
$(window).on('action:posts.loaded.ezoicInfinite', () => schedule(runCore));
|
|
466
|
-
$(window).on('action:topics.loaded.ezoicInfinite', () => schedule(runCore));
|
|
467
|
-
$(window).on('action:category.loaded.ezoicInfinite', () => schedule(runCore));
|
|
468
|
-
$(window).on('action:topic.loaded.ezoicInfinite', () => schedule(runCore));
|
|
469
|
-
}
|
|
393
|
+
// First load
|
|
394
|
+
once('DOMContentLoaded', () => {
|
|
395
|
+
installShowAdsHook();
|
|
396
|
+
prefetchConfig();
|
|
397
|
+
Ad.scheduleScan();
|
|
398
|
+
});
|
|
470
399
|
|
|
471
|
-
|
|
472
|
-
cleanupForNav();
|
|
473
|
-
bind();
|
|
474
|
-
ensureMO();
|
|
475
|
-
state.pageKey = getPageKey();
|
|
476
|
-
schedule(runCore);
|
|
477
|
-
})();
|
|
400
|
+
})();
|