nodebb-plugin-ezoic-infinite 1.4.98 → 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 +3 -2
- package/package.json +1 -1
- package/public/client.js +309 -538
- package/public/style.css +7 -19
package/library.js
CHANGED
|
@@ -68,7 +68,7 @@ async function getSettings() {
|
|
|
68
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
69
69
|
if (!uid || !excludedGroups.length) return false;
|
|
70
70
|
const userGroups = await groups.getUserGroups([uid]);
|
|
71
|
-
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : g));
|
|
71
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : String(g)));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
plugin.onSettingsSet = function (data) {
|
|
@@ -107,10 +107,11 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
107
107
|
|
|
108
108
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
109
|
const settings = await getSettings();
|
|
110
|
-
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
110
|
+
const excluded = await isUserExcluded((req.uid || (req.user && req.user.uid)), settings.excludedGroups);
|
|
111
111
|
|
|
112
112
|
res.json({
|
|
113
113
|
excluded,
|
|
114
|
+
excludedGroups: settings.excludedGroups,
|
|
114
115
|
enableBetweenAds: settings.enableBetweenAds,
|
|
115
116
|
showFirstTopicAd: settings.showFirstTopicAd,
|
|
116
117
|
placeholderIds: settings.placeholderIds,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,604 +1,375 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Insert at most N ads per run to keep the UI smooth on infinite scroll
|
|
11
|
-
const MAX_INSERTS_PER_RUN = 3;
|
|
12
|
-
|
|
13
|
-
// Preload before viewport (tune if you want even earlier)
|
|
14
|
-
const PRELOAD_ROOT_MARGIN = '1200px 0px';
|
|
15
|
-
|
|
16
|
-
const SELECTORS = {
|
|
17
|
-
topicItem: 'li[component="category/topic"]',
|
|
18
|
-
postItem: '[component="post"][data-pid]',
|
|
19
|
-
categoryItem: 'li[component="categories/category"]',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Hard block during navigation to avoid “placeholder does not exist” spam
|
|
23
|
-
let EZOIC_BLOCKED = false;
|
|
24
|
-
|
|
25
|
-
const state = {
|
|
26
|
-
pageKey: null,
|
|
27
|
-
cfg: null,
|
|
28
|
-
|
|
29
|
-
poolTopics: [],
|
|
30
|
-
poolPosts: [],
|
|
31
|
-
poolCategories: [],
|
|
32
|
-
|
|
33
|
-
usedTopics: new Set(),
|
|
34
|
-
usedPosts: new Set(),
|
|
35
|
-
usedCategories: new Set(),
|
|
36
|
-
|
|
37
|
-
// throttle per placeholder id
|
|
38
|
-
lastShowById: new Map(),
|
|
39
|
-
|
|
40
|
-
// observers / schedulers
|
|
41
|
-
domObs: null,
|
|
42
|
-
io: null,
|
|
43
|
-
runQueued: false,
|
|
44
|
-
|
|
45
|
-
// hero
|
|
46
|
-
heroDoneForPage: false,
|
|
47
|
-
};
|
|
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
|
+
*/
|
|
48
9
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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);
|
|
56
23
|
}
|
|
57
24
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parsePool(raw) {
|
|
72
|
-
if (!raw) return [];
|
|
73
|
-
const lines = String(raw)
|
|
74
|
-
.split(/\r?\n/)
|
|
75
|
-
.map(s => s.trim())
|
|
76
|
-
.filter(Boolean);
|
|
77
|
-
return uniqInts(lines);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getPageKey() {
|
|
81
|
-
try {
|
|
82
|
-
const ax = window.ajaxify;
|
|
83
|
-
if (ax && ax.data) {
|
|
84
|
-
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
85
|
-
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
86
|
-
}
|
|
87
|
-
} catch (e) {}
|
|
88
|
-
return window.location.pathname;
|
|
89
|
-
}
|
|
68
|
+
};
|
|
90
69
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
101
|
-
return 'other';
|
|
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);
|
|
102
79
|
}
|
|
103
80
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
}
|
|
81
|
+
function wrapShowAds(original) {
|
|
82
|
+
if (!original || original.__nodebbSafeWrapped) return original;
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
84
|
+
const wrapped = function (...args) {
|
|
85
|
+
const ids = normalizeIds(args);
|
|
86
|
+
if (!ids.length) return;
|
|
111
87
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!el || !el.isConnected) return false;
|
|
116
|
-
if (!el.querySelector('[component="post/content"]')) return false;
|
|
117
|
-
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
118
|
-
if (parentPost && parentPost !== el) return false;
|
|
119
|
-
if (el.getAttribute('component') === 'post/parent') return false;
|
|
120
|
-
return true;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
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);
|
|
123
91
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const links = [
|
|
132
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
133
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
134
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
135
|
-
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
136
|
-
];
|
|
137
|
-
for (const [rel, href, cors] of links) {
|
|
138
|
-
const key = `${rel}|${href}`;
|
|
139
|
-
if (_warmLinksDone.has(key)) continue;
|
|
140
|
-
_warmLinksDone.add(key);
|
|
141
|
-
const link = document.createElement('link');
|
|
142
|
-
link.rel = rel;
|
|
143
|
-
link.href = href;
|
|
144
|
-
if (cors) link.crossOrigin = 'anonymous';
|
|
145
|
-
head.appendChild(link);
|
|
146
|
-
}
|
|
147
|
-
} catch (e) {}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
|
|
151
|
-
function patchShowAds() {
|
|
152
|
-
const applyPatch = () => {
|
|
153
|
-
try {
|
|
154
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
155
|
-
const ez = window.ezstandalone;
|
|
156
|
-
if (window.__nodebbEzoicPatched) return;
|
|
157
|
-
if (typeof ez.showAds !== 'function') return;
|
|
158
|
-
|
|
159
|
-
window.__nodebbEzoicPatched = true;
|
|
160
|
-
const orig = ez.showAds;
|
|
161
|
-
|
|
162
|
-
ez.showAds = function (...args) {
|
|
163
|
-
if (EZOIC_BLOCKED) return;
|
|
164
|
-
|
|
165
|
-
let ids = [];
|
|
166
|
-
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
167
|
-
else ids = args;
|
|
168
|
-
|
|
169
|
-
const seen = new Set();
|
|
170
|
-
for (const v of ids) {
|
|
171
|
-
const id = parseInt(v, 10);
|
|
172
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
173
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
174
|
-
if (!ph || !ph.isConnected) continue;
|
|
175
|
-
seen.add(id);
|
|
176
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
92
|
+
const el = document.getElementById(id);
|
|
93
|
+
if (el && document.body.contains(el)) {
|
|
94
|
+
try {
|
|
95
|
+
// Call one-by-one to avoid batch logging on missing nodes
|
|
96
|
+
original.call(window.ezstandalone, id);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// swallow: Ezoic can throw if called during transitions
|
|
177
99
|
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
180
102
|
};
|
|
181
103
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
186
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
187
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
188
|
-
} catch (e) {}
|
|
189
|
-
}
|
|
104
|
+
wrapped.__nodebbSafeWrapped = true;
|
|
105
|
+
return wrapped;
|
|
190
106
|
}
|
|
191
107
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
198
|
-
if (!res.ok) return null;
|
|
199
|
-
state.cfg = await res.json();
|
|
200
|
-
return state.cfg;
|
|
201
|
-
} catch (e) {
|
|
202
|
-
return null;
|
|
108
|
+
function installShowAdsHook() {
|
|
109
|
+
// If ezstandalone already exists, wrap now.
|
|
110
|
+
if (window.ezstandalone && window.ezstandalone.showAds) {
|
|
111
|
+
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
203
112
|
}
|
|
204
|
-
}
|
|
205
113
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
211
|
-
}
|
|
114
|
+
// Hook future assignment (no polling)
|
|
115
|
+
try {
|
|
116
|
+
if (!window.ezstandalone) window.ezstandalone = {};
|
|
117
|
+
const ez = window.ezstandalone;
|
|
212
118
|
|
|
213
|
-
|
|
119
|
+
// If already has a setter installed, do nothing
|
|
120
|
+
const desc = Object.getOwnPropertyDescriptor(ez, 'showAds');
|
|
121
|
+
if (desc && (desc.set || desc.get)) return;
|
|
214
122
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
123
|
+
let _showAds = ez.showAds;
|
|
124
|
+
Object.defineProperty(ez, 'showAds', {
|
|
125
|
+
configurable: true,
|
|
126
|
+
enumerable: true,
|
|
127
|
+
get() { return _showAds; },
|
|
128
|
+
set(v) { _showAds = wrapShowAds(v); }
|
|
129
|
+
});
|
|
223
130
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
wrap.style.listStyle = 'none';
|
|
229
|
-
// preserve common NodeBB list styling
|
|
230
|
-
if (target && target.classList && target.classList.contains('list-group-item')) wrap.classList.add('list-group-item');
|
|
231
|
-
}
|
|
232
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
233
|
-
if (wrap.tagName === 'LI') {
|
|
234
|
-
wrap.setAttribute('role', 'presentation');
|
|
235
|
-
wrap.setAttribute('aria-hidden', 'true');
|
|
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.
|
|
236
135
|
}
|
|
237
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
238
|
-
wrap.style.width = '100%';
|
|
239
|
-
|
|
240
|
-
const ph = document.createElement('div');
|
|
241
|
-
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
242
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
243
|
-
wrap.appendChild(ph);
|
|
244
|
-
|
|
245
|
-
return wrap;
|
|
246
136
|
}
|
|
247
137
|
|
|
248
|
-
function
|
|
249
|
-
|
|
138
|
+
function ezCmd(fn) {
|
|
139
|
+
// Tokenize so queued callbacks don't run after navigation
|
|
140
|
+
const tokenAtSchedule = navToken;
|
|
141
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
142
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
143
|
+
window.ezstandalone.cmd.push(function () {
|
|
144
|
+
if (tokenAtSchedule !== navToken) return;
|
|
145
|
+
try { fn(); } catch (e) {}
|
|
146
|
+
});
|
|
250
147
|
}
|
|
251
148
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
});
|
|
259
168
|
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
const wrap = buildWrap(target, id, kindClass, afterPos);
|
|
263
|
-
target.insertAdjacentElement('afterend', wrap);
|
|
264
|
-
return wrap;
|
|
265
|
-
} finally {
|
|
266
|
-
insertingIds.delete(id);
|
|
267
|
-
}
|
|
169
|
+
return cfgPromise;
|
|
268
170
|
}
|
|
269
171
|
|
|
270
|
-
function
|
|
271
|
-
return
|
|
172
|
+
function parsePlaceholderIds(s) {
|
|
173
|
+
return String(s || '')
|
|
174
|
+
.split(',')
|
|
175
|
+
.map(x => x.trim())
|
|
176
|
+
.filter(Boolean);
|
|
272
177
|
}
|
|
273
178
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
282
|
-
if (!ph || !ph.isConnected) return;
|
|
283
|
-
|
|
284
|
-
state.lastShowById.set(id, now);
|
|
179
|
+
// -------------------------
|
|
180
|
+
// Ad insertion logic
|
|
181
|
+
// -------------------------
|
|
182
|
+
const Ad = {
|
|
183
|
+
// throttle showAds per placeholder id
|
|
184
|
+
lastShowAt: new Map(),
|
|
185
|
+
minShowIntervalMs: 1500,
|
|
285
186
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
187
|
+
// Observers
|
|
188
|
+
io: null,
|
|
189
|
+
mo: null,
|
|
190
|
+
|
|
191
|
+
// Bookkeeping
|
|
192
|
+
insertedKeys: new Set(), // avoid duplicate injection on re-renders
|
|
193
|
+
|
|
194
|
+
initObservers() {
|
|
195
|
+
if (!this.io) {
|
|
196
|
+
this.io = new IntersectionObserver((entries) => {
|
|
197
|
+
for (const e of entries) {
|
|
198
|
+
if (!e.isIntersecting) continue;
|
|
199
|
+
const id = e.target && e.target.getAttribute('data-ezoic-id');
|
|
200
|
+
if (id) this.requestShow(id);
|
|
201
|
+
}
|
|
202
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0.01 });
|
|
295
203
|
}
|
|
296
204
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
ez.cmd.push(() => {
|
|
302
|
-
try {
|
|
303
|
-
if (EZOIC_BLOCKED) return;
|
|
304
|
-
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
305
|
-
if (!el || !el.isConnected) return;
|
|
306
|
-
window.ezstandalone.showAds(id);
|
|
307
|
-
sessionDefinedIds.add(id);
|
|
308
|
-
} catch (e) {}
|
|
309
|
-
});
|
|
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 });
|
|
310
209
|
}
|
|
311
|
-
}
|
|
312
|
-
|
|
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
|
+
},
|
|
313
227
|
|
|
314
|
-
|
|
228
|
+
async scanAndInject() {
|
|
229
|
+
const c = await prefetchConfig();
|
|
230
|
+
if (!c) return;
|
|
315
231
|
|
|
316
|
-
|
|
317
|
-
if (state.io) return state.io;
|
|
318
|
-
try {
|
|
319
|
-
state.io = new IntersectionObserver((entries) => {
|
|
320
|
-
for (const ent of entries) {
|
|
321
|
-
if (!ent.isIntersecting) continue;
|
|
322
|
-
const el = ent.target;
|
|
323
|
-
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
324
|
-
|
|
325
|
-
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
326
|
-
const id = parseInt(idAttr, 10);
|
|
327
|
-
if (Number.isFinite(id) && id > 0) showAd(id);
|
|
328
|
-
}
|
|
329
|
-
}, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
|
|
330
|
-
} catch (e) {
|
|
331
|
-
state.io = null;
|
|
332
|
-
}
|
|
333
|
-
return state.io;
|
|
334
|
-
}
|
|
232
|
+
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
335
233
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (!ph || !ph.isConnected) return;
|
|
339
|
-
const io = ensurePreloadObserver();
|
|
340
|
-
try { io && io.observe(ph); } catch (e) {}
|
|
234
|
+
installShowAdsHook();
|
|
235
|
+
this.initObservers();
|
|
341
236
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
|
|
346
|
-
} catch (e) {}
|
|
347
|
-
}
|
|
237
|
+
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
238
|
+
// Extend: categories ads if you use them
|
|
239
|
+
},
|
|
348
240
|
|
|
349
|
-
|
|
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');
|
|
350
247
|
|
|
351
|
-
|
|
352
|
-
const out = [];
|
|
353
|
-
if (count <= 0) return out;
|
|
354
|
-
if (showFirst) out.push(1);
|
|
355
|
-
for (let i = 1; i <= count; i++) {
|
|
356
|
-
if (i % interval === 0) out.push(i);
|
|
357
|
-
}
|
|
358
|
-
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
|
|
362
|
-
if (!items.length) return 0;
|
|
363
|
-
|
|
364
|
-
const targets = computeTargets(items.length, interval, showFirst);
|
|
365
|
-
let inserted = 0;
|
|
248
|
+
if (!container) return;
|
|
366
249
|
|
|
367
|
-
|
|
368
|
-
if (
|
|
250
|
+
const rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item'));
|
|
251
|
+
if (!rows.length) return;
|
|
369
252
|
|
|
370
|
-
const
|
|
371
|
-
if (!
|
|
372
|
-
if (isAdjacentAd(el)) continue;
|
|
373
|
-
if (findWrap(kindClass, afterPos)) continue;
|
|
253
|
+
const ids = parsePlaceholderIds(c.placeholderIds);
|
|
254
|
+
if (!ids.length) return;
|
|
374
255
|
|
|
375
|
-
const
|
|
376
|
-
if (!id) break;
|
|
256
|
+
const interval = clamp(parseInt(c.intervalPosts, 10) || 6, 1, 50);
|
|
377
257
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
usedSet.delete(id);
|
|
382
|
-
pool.unshift(id);
|
|
383
|
-
continue;
|
|
258
|
+
// HERO: early, above-the-fold (first eligible insertion point)
|
|
259
|
+
if (c.showFirstTopicAd) {
|
|
260
|
+
this.insertAdAfterRow(rows[0], ids[0], 'hero-topic-0');
|
|
384
261
|
}
|
|
385
262
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
async function insertHeroAdEarly() {
|
|
394
|
-
if (state.heroDoneForPage) return;
|
|
395
|
-
const cfg = await fetchConfigOnce();
|
|
396
|
-
if (!cfg || cfg.excluded) return;
|
|
397
|
-
|
|
398
|
-
initPools(cfg);
|
|
399
|
-
|
|
400
|
-
const kind = getKind();
|
|
401
|
-
let items = [];
|
|
402
|
-
let pool = null;
|
|
403
|
-
let usedSet = null;
|
|
404
|
-
let kindClass = '';
|
|
405
|
-
|
|
406
|
-
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
407
|
-
items = getPostContainers();
|
|
408
|
-
pool = state.poolPosts;
|
|
409
|
-
usedSet = state.usedPosts;
|
|
410
|
-
kindClass = 'ezoic-ad-message';
|
|
411
|
-
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
412
|
-
items = getTopicItems();
|
|
413
|
-
pool = state.poolTopics;
|
|
414
|
-
usedSet = state.usedTopics;
|
|
415
|
-
kindClass = 'ezoic-ad-between';
|
|
416
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
417
|
-
items = getCategoryItems();
|
|
418
|
-
pool = state.poolCategories;
|
|
419
|
-
usedSet = state.usedCategories;
|
|
420
|
-
kindClass = 'ezoic-ad-categories';
|
|
421
|
-
} else {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!items.length) return;
|
|
426
|
-
|
|
427
|
-
// Insert after the very first item (above-the-fold)
|
|
428
|
-
const afterPos = 1;
|
|
429
|
-
const el = items[afterPos - 1];
|
|
430
|
-
if (!el || !el.isConnected) return;
|
|
431
|
-
if (isAdjacentAd(el)) return;
|
|
432
|
-
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
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}`);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
433
270
|
|
|
434
|
-
|
|
435
|
-
|
|
271
|
+
insertAdAfterRow(rowEl, placeholderId, key) {
|
|
272
|
+
if (!rowEl || !placeholderId || !key) return;
|
|
273
|
+
if (this.insertedKeys.has(key)) return;
|
|
436
274
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
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;
|
|
279
|
+
}
|
|
444
280
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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);
|
|
448
286
|
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
}
|
|
451
291
|
|
|
452
|
-
|
|
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) {}
|
|
453
296
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
297
|
+
// Insert into DOM
|
|
298
|
+
rowEl.insertAdjacentElement('afterend', wrapper);
|
|
299
|
+
this.insertedKeys.add(key);
|
|
457
300
|
|
|
458
|
-
|
|
301
|
+
// Observe for preloading and request a show
|
|
302
|
+
if (this.io) this.io.observe(wrapper);
|
|
459
303
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
getPostContainers(),
|
|
465
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
466
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
467
|
-
state.poolPosts,
|
|
468
|
-
state.usedPosts
|
|
469
|
-
);
|
|
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);
|
|
470
308
|
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
requestShow(placeholderId) {
|
|
312
|
+
if (!placeholderId) return;
|
|
313
|
+
|
|
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);
|
|
318
|
+
|
|
319
|
+
// Ensure placeholder exists in DOM (pool or wrapper)
|
|
320
|
+
Pool.ensurePlaceholder(placeholderId);
|
|
321
|
+
|
|
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
|
+
};
|
|
328
|
+
|
|
329
|
+
if (window.ezstandalone && Array.isArray(window.ezstandalone.cmd)) {
|
|
330
|
+
ezCmd(doShow);
|
|
331
|
+
} else {
|
|
332
|
+
doShow();
|
|
481
333
|
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
state.usedCategories
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function scheduleRun() {
|
|
497
|
-
if (state.runQueued) return;
|
|
498
|
-
state.runQueued = true;
|
|
499
|
-
window.requestAnimationFrame(() => {
|
|
500
|
-
state.runQueued = false;
|
|
501
|
-
const pk = getPageKey();
|
|
502
|
-
if (state.pageKey && pk !== state.pageKey) return;
|
|
503
|
-
runCore().catch(() => {});
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// ---------- observers / lifecycle ----------
|
|
508
|
-
|
|
509
|
-
function cleanup() {
|
|
510
|
-
EZOIC_BLOCKED = true;
|
|
511
|
-
|
|
512
|
-
// remove all wrappers
|
|
513
|
-
try {
|
|
514
|
-
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
515
|
-
try { el.remove(); } catch (e) {}
|
|
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);
|
|
516
342
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
state.poolPosts = [];
|
|
523
|
-
state.poolCategories = [];
|
|
524
|
-
state.usedTopics.clear();
|
|
525
|
-
state.usedPosts.clear();
|
|
526
|
-
state.usedCategories.clear();
|
|
527
|
-
state.lastShowById.clear();
|
|
528
|
-
state.heroDoneForPage = false;
|
|
529
|
-
|
|
530
|
-
sessionDefinedIds.clear();
|
|
531
|
-
|
|
532
|
-
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function ensureDomObserver() {
|
|
536
|
-
if (state.domObs) return;
|
|
537
|
-
state.domObs = new MutationObserver(() => {
|
|
538
|
-
if (!EZOIC_BLOCKED) scheduleRun();
|
|
539
|
-
});
|
|
540
|
-
try {
|
|
541
|
-
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
542
|
-
} catch (e) {}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function bindNodeBB() {
|
|
546
|
-
if (!$) return;
|
|
547
|
-
|
|
548
|
-
$(window).off('.ezoicInfinite');
|
|
549
|
-
|
|
550
|
-
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
551
|
-
cleanup();
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
555
|
-
state.pageKey = getPageKey();
|
|
556
|
-
EZOIC_BLOCKED = false;
|
|
557
|
-
|
|
558
|
-
warmUpNetwork();
|
|
559
|
-
patchShowAds();
|
|
560
|
-
ensurePreloadObserver();
|
|
561
|
-
ensureDomObserver();
|
|
562
|
-
|
|
563
|
-
// Ultra-fast above-the-fold first
|
|
564
|
-
insertHeroAdEarly().catch(() => {});
|
|
565
|
-
|
|
566
|
-
// Then normal insertion
|
|
567
|
-
scheduleRun();
|
|
568
|
-
});
|
|
343
|
+
// Let NodeBB wipe DOM; we do not aggressively remove nodes here.
|
|
344
|
+
this.insertedKeys.clear();
|
|
345
|
+
this.lastShowAt.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
569
348
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
349
|
+
// -------------------------
|
|
350
|
+
// NodeBB hooks
|
|
351
|
+
// -------------------------
|
|
352
|
+
function onAjaxifyStart() {
|
|
353
|
+
navToken++;
|
|
354
|
+
Ad.disconnectObservers();
|
|
355
|
+
Ad.reclaimPlaceholders();
|
|
575
356
|
}
|
|
576
357
|
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
ticking = true;
|
|
582
|
-
window.requestAnimationFrame(() => {
|
|
583
|
-
ticking = false;
|
|
584
|
-
if (!EZOIC_BLOCKED) scheduleRun();
|
|
585
|
-
});
|
|
586
|
-
}, { passive: true });
|
|
358
|
+
function onAjaxifyEnd() {
|
|
359
|
+
// Kick scan for new page
|
|
360
|
+
prefetchConfig();
|
|
361
|
+
Ad.scheduleScan();
|
|
587
362
|
}
|
|
588
363
|
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
warmUpNetwork();
|
|
593
|
-
patchShowAds();
|
|
594
|
-
ensurePreloadObserver();
|
|
595
|
-
ensureDomObserver();
|
|
364
|
+
// NodeBB exposes ajaxify events on document
|
|
365
|
+
on('ajaxify.start', onAjaxifyStart);
|
|
366
|
+
on('ajaxify.end', onAjaxifyEnd);
|
|
596
367
|
|
|
597
|
-
|
|
598
|
-
|
|
368
|
+
// First load
|
|
369
|
+
once('DOMContentLoaded', () => {
|
|
370
|
+
installShowAdsHook();
|
|
371
|
+
prefetchConfig();
|
|
372
|
+
Ad.scheduleScan();
|
|
373
|
+
});
|
|
599
374
|
|
|
600
|
-
// First paint: try hero + run
|
|
601
|
-
EZOIC_BLOCKED = false;
|
|
602
|
-
insertHeroAdEarly().catch(() => {});
|
|
603
|
-
scheduleRun();
|
|
604
375
|
})();
|
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;}
|