nodebb-plugin-ezoic-infinite 1.0.18 → 1.1.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/README.md +15 -0
- package/package.json +5 -4
- package/public/client.js +286 -382
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# NodeBB Plugin – Ezoic Infinite (Production)
|
|
2
|
+
|
|
3
|
+
This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
|
|
4
|
+
with full support for infinite scroll.
|
|
5
|
+
|
|
6
|
+
## Key guarantees
|
|
7
|
+
- No duplicate ads back-to-back
|
|
8
|
+
- One showAds call per placeholder
|
|
9
|
+
- Fast reveal (MutationObserver on first child)
|
|
10
|
+
- Safe with ajaxify navigation
|
|
11
|
+
- Works with NodeBB 4.x + Harmony
|
|
12
|
+
|
|
13
|
+
## Notes
|
|
14
|
+
- Placeholders must exist and be selected in Ezoic
|
|
15
|
+
- Use separate ID pools for topics vs messages
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Ezoic
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
"infinite-scroll"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"
|
|
15
|
+
"nodebb": ">=4.0.0"
|
|
16
16
|
},
|
|
17
17
|
"nbbpm": {
|
|
18
18
|
"compatibility": "^4.0.0"
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
"private": false
|
|
20
21
|
}
|
package/public/client.js
CHANGED
|
@@ -1,251 +1,150 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/* globals ajaxify */
|
|
1
|
+
/* eslint-disable no-console */
|
|
4
2
|
(function () {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (ax.data.cid) return 'cid:' + ax.data.cid + ':' + window.location.pathname;
|
|
36
|
-
}
|
|
37
|
-
} catch (e) {}
|
|
38
|
-
return window.location.pathname;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isTopicPage() {
|
|
42
|
-
try {
|
|
43
|
-
const ax = window.ajaxify;
|
|
44
|
-
return !!(ax && ax.data && ax.data.tid);
|
|
45
|
-
} catch (e) {}
|
|
46
|
-
return /^\/topic\//.test(window.location.pathname);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function isCategoryTopicList() {
|
|
50
|
-
return document.querySelectorAll('li[component="category/topic"]').length > 0 && !isTopicPage();
|
|
51
|
-
}
|
|
52
|
-
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// NodeBB client env provides jQuery. We keep it optional.
|
|
6
|
+
const $w = (typeof window.jQuery === 'function') ? window.jQuery(window) : null;
|
|
7
|
+
|
|
8
|
+
const SELECTORS = {
|
|
9
|
+
topicListItem: 'li[component="category/topic"]',
|
|
10
|
+
postItem: '[component="post"][data-pid]',
|
|
11
|
+
postContent: '[component="post/content"]',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const WRAP_CLASS = 'ezoic-ad';
|
|
15
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
16
|
+
|
|
17
|
+
const state = {
|
|
18
|
+
pageKey: null,
|
|
19
|
+
cfg: null,
|
|
20
|
+
cfgPromise: null,
|
|
21
|
+
// pools and used ids
|
|
22
|
+
usedBetween: new Set(),
|
|
23
|
+
usedMessage: new Set(),
|
|
24
|
+
fifoBetween: [],
|
|
25
|
+
fifoMessage: [],
|
|
26
|
+
// showAds anti-double
|
|
27
|
+
lastShowById: {},
|
|
28
|
+
pendingById: {},
|
|
29
|
+
// retry bookkeeping
|
|
30
|
+
retryCount: {},
|
|
31
|
+
observers: {},
|
|
32
|
+
};
|
|
53
33
|
|
|
54
34
|
function normalizeBool(v) {
|
|
55
|
-
return v === true || v ===
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normalizeInterval(v, fallback) {
|
|
59
|
-
const n = parseInt(v, 10);
|
|
60
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getPoolValue(v) {
|
|
64
|
-
// Accept string, array of numbers/strings
|
|
65
|
-
if (Array.isArray(v)) return v.join('\n');
|
|
66
|
-
return v;
|
|
35
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
67
36
|
}
|
|
68
37
|
|
|
69
38
|
function parsePool(raw) {
|
|
70
|
-
raw = getPoolValue(raw);
|
|
71
|
-
|
|
72
|
-
raw = getPoolValue(raw);
|
|
73
|
-
|
|
74
39
|
if (!raw) return [];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.map(
|
|
78
|
-
.filter(
|
|
79
|
-
// unique while preserving order
|
|
80
|
-
return Array.from(new Set(arr));
|
|
81
|
-
}
|
|
40
|
+
const lines = String(raw)
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.map(s => s.trim())
|
|
43
|
+
.filter(Boolean);
|
|
82
44
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const res = await fetch(base + '/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
87
|
-
const json = await res.json();
|
|
88
|
-
cachedConfig = json;
|
|
89
|
-
lastFetch = Date.now();
|
|
90
|
-
return json;
|
|
91
|
-
}
|
|
45
|
+
const ids = lines
|
|
46
|
+
.map(s => parseInt(s, 10))
|
|
47
|
+
.filter(n => Number.isFinite(n) && n > 0);
|
|
92
48
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
if (typeof queues !== 'undefined') { queues.between.length = 0; queues.message.length = 0; }
|
|
102
|
-
if (typeof queueState !== 'undefined') { queueState.between = false; queueState.message = false; }
|
|
103
|
-
} catch (e) {}
|
|
49
|
+
// unique preserve order
|
|
50
|
+
const out = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (const id of ids) {
|
|
53
|
+
if (!seen.has(id)) { seen.add(id); out.push(id); }
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
104
56
|
}
|
|
105
57
|
|
|
106
|
-
function
|
|
58
|
+
function getPageKey() {
|
|
107
59
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
window.
|
|
112
|
-
return;
|
|
60
|
+
const ax = window.ajaxify;
|
|
61
|
+
if (ax && ax.data) {
|
|
62
|
+
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
63
|
+
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
113
64
|
}
|
|
114
|
-
window.ezstandalone.cmd.push(function () {
|
|
115
|
-
try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
|
|
116
|
-
});
|
|
117
65
|
} catch (e) {}
|
|
66
|
+
return window.location.pathname;
|
|
118
67
|
}
|
|
119
68
|
|
|
120
|
-
function
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
69
|
+
function getPageKind() {
|
|
70
|
+
const p = window.location.pathname || '';
|
|
71
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
72
|
+
if (/^\/category\//.test(p)) return 'category';
|
|
73
|
+
// fallback hints
|
|
74
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
75
|
+
if (document.querySelector(SELECTORS.topicListItem)) return 'category';
|
|
76
|
+
return 'category';
|
|
127
77
|
}
|
|
128
78
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const queues = { between: [], message: [] };
|
|
132
|
-
const queueState = { between: false, message: false };
|
|
133
|
-
|
|
134
|
-
function isFilled(placeholderEl) {
|
|
135
|
-
if (!placeholderEl) return false;
|
|
136
|
-
if (placeholderEl.children && placeholderEl.children.length) return true;
|
|
137
|
-
const html = placeholderEl.innerHTML || '';
|
|
138
|
-
if (html.trim().length > 0) return true;
|
|
139
|
-
const r = placeholderEl.getBoundingClientRect ? placeholderEl.getBoundingClientRect() : null;
|
|
140
|
-
if (r && r.height > 10) return true;
|
|
141
|
-
return false;
|
|
79
|
+
function safeGetRect(el) {
|
|
80
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
142
81
|
}
|
|
143
82
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let done = false;
|
|
150
|
-
const finish = (ok) => {
|
|
151
|
-
if (done) return;
|
|
152
|
-
done = true;
|
|
153
|
-
try { obs.disconnect(); } catch (e) {}
|
|
154
|
-
resolve(ok);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const obs = new MutationObserver(() => {
|
|
158
|
-
if (isFilled(placeholderEl)) finish(true);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
obs.observe(placeholderEl, { childList: true, subtree: true, attributes: true });
|
|
163
|
-
} catch (e) {}
|
|
164
|
-
|
|
165
|
-
setTimeout(() => finish(isFilled(placeholderEl)), timeoutMs);
|
|
166
|
-
});
|
|
83
|
+
function hasAdImmediatelyAfter(targetEl) {
|
|
84
|
+
if (!targetEl) return false;
|
|
85
|
+
const next = targetEl.nextElementSibling;
|
|
86
|
+
return !!(next && next.classList && next.classList.contains(WRAP_CLASS));
|
|
167
87
|
}
|
|
168
88
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const item = queues[kind][0];
|
|
176
|
-
const wrap = item.wrapper;
|
|
177
|
-
const id = item.id;
|
|
178
|
-
|
|
179
|
-
// if removed (recycled) before fill, drop it
|
|
180
|
-
if (!wrap || !wrap.isConnected) {
|
|
181
|
-
queues[kind].shift();
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// call showAds once
|
|
186
|
-
callShowAdsSingle(id);
|
|
89
|
+
function buildWrap(id, kind, afterPos) {
|
|
90
|
+
const wrap = document.createElement('div');
|
|
91
|
+
wrap.className = `${WRAP_CLASS} ${kind}`;
|
|
92
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
93
|
+
wrap.setAttribute('data-ezoic-kind', kind);
|
|
94
|
+
wrap.style.width = '100%';
|
|
187
95
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
96
|
+
const ph = document.createElement('div');
|
|
97
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
98
|
+
wrap.appendChild(ph);
|
|
191
99
|
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
} finally {
|
|
195
|
-
queueState[kind] = false;
|
|
196
|
-
}
|
|
100
|
+
return wrap;
|
|
197
101
|
}
|
|
198
102
|
|
|
199
|
-
function
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
processQueue(kind);
|
|
103
|
+
function insertAfter(targetEl, id, kind, afterPos) {
|
|
104
|
+
if (!targetEl || !targetEl.insertAdjacentElement) return null;
|
|
105
|
+
const wrap = buildWrap(id, kind, afterPos);
|
|
106
|
+
targetEl.insertAdjacentElement('afterend', wrap);
|
|
107
|
+
return wrap;
|
|
205
108
|
}
|
|
206
109
|
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
110
|
+
function destroyPlaceholder(id) {
|
|
111
|
+
try {
|
|
112
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
113
|
+
window.ezstandalone.destroyPlaceholders([id]);
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {}
|
|
212
116
|
}
|
|
213
117
|
|
|
214
118
|
function callShowAdsSingle(id) {
|
|
215
119
|
if (!id) return;
|
|
216
|
-
|
|
217
|
-
// Normalize id key
|
|
218
120
|
const key = String(id);
|
|
219
121
|
|
|
220
|
-
// Ensure we never call showAds twice for the same id within a short window
|
|
221
122
|
const now = Date.now();
|
|
222
|
-
|
|
223
|
-
const last = window.__ezoicLastSingle[key] || 0;
|
|
123
|
+
const last = state.lastShowById[key] || 0;
|
|
224
124
|
if (now - last < 4000) return;
|
|
225
125
|
|
|
226
|
-
// If showAds is ready
|
|
126
|
+
// If showAds is ready, call once and return
|
|
227
127
|
try {
|
|
228
128
|
window.ezstandalone = window.ezstandalone || {};
|
|
229
129
|
if (typeof window.ezstandalone.showAds === 'function') {
|
|
230
|
-
|
|
130
|
+
state.lastShowById[key] = now;
|
|
231
131
|
window.ezstandalone.showAds(id);
|
|
232
132
|
return;
|
|
233
133
|
}
|
|
234
134
|
} catch (e) {}
|
|
235
135
|
|
|
236
|
-
// Otherwise
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
window.__ezoicPending[key] = true;
|
|
136
|
+
// Otherwise, queue a single pending attempt (per id)
|
|
137
|
+
if (state.pendingById[key]) return;
|
|
138
|
+
state.pendingById[key] = true;
|
|
240
139
|
|
|
241
140
|
window.ezstandalone = window.ezstandalone || {};
|
|
242
141
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
243
142
|
|
|
244
|
-
const tryRun =
|
|
143
|
+
const tryRun = () => {
|
|
245
144
|
try {
|
|
246
145
|
if (typeof window.ezstandalone.showAds === 'function') {
|
|
247
|
-
|
|
248
|
-
delete
|
|
146
|
+
state.lastShowById[key] = Date.now();
|
|
147
|
+
delete state.pendingById[key];
|
|
249
148
|
window.ezstandalone.showAds(id);
|
|
250
149
|
return true;
|
|
251
150
|
}
|
|
@@ -253,272 +152,277 @@
|
|
|
253
152
|
return false;
|
|
254
153
|
};
|
|
255
154
|
|
|
256
|
-
|
|
257
|
-
window.ezstandalone.cmd.push(function () { tryRun(); });
|
|
155
|
+
window.ezstandalone.cmd.push(() => { tryRun(); });
|
|
258
156
|
|
|
259
|
-
// Retry a few times in case cmd doesn't fire (race conditions)
|
|
260
157
|
let tries = 0;
|
|
261
158
|
(function tick() {
|
|
262
|
-
tries
|
|
159
|
+
tries += 1;
|
|
263
160
|
if (tryRun() || tries >= 8) {
|
|
264
|
-
if (tries >= 8) delete
|
|
161
|
+
if (tries >= 8) delete state.pendingById[key];
|
|
265
162
|
return;
|
|
266
163
|
}
|
|
267
164
|
setTimeout(tick, 800);
|
|
268
165
|
})();
|
|
269
166
|
}
|
|
270
167
|
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function recycle(fifo, usedSet, selector) {
|
|
277
|
-
// Recycle only ads that are safely outside the viewport (to avoid ads “disappearing” while reading)
|
|
278
|
-
const margin = 1200; // px
|
|
168
|
+
function recycleIfNeeded(pool, usedSet, fifo, selectorFn) {
|
|
169
|
+
// only recycle ads far above the viewport, so they don't "disappear" while reading
|
|
170
|
+
const margin = 1200;
|
|
279
171
|
const vpTop = -margin;
|
|
280
172
|
|
|
281
173
|
fifo.sort((a, b) => a.after - b.after);
|
|
282
174
|
|
|
283
|
-
// Prefer recycling ads far ABOVE the current viewport (user has already passed them)
|
|
284
175
|
for (let i = 0; i < fifo.length; i++) {
|
|
285
176
|
const old = fifo[i];
|
|
286
|
-
const el = document.querySelector(
|
|
177
|
+
const el = document.querySelector(selectorFn(old));
|
|
287
178
|
if (!el) {
|
|
288
|
-
fifo.splice(i, 1);
|
|
289
|
-
i--;
|
|
179
|
+
fifo.splice(i, 1); i--;
|
|
290
180
|
continue;
|
|
291
181
|
}
|
|
292
|
-
const r = el
|
|
293
|
-
if (r.bottom < vpTop) {
|
|
182
|
+
const r = safeGetRect(el);
|
|
183
|
+
if (r && r.bottom < vpTop) {
|
|
294
184
|
fifo.splice(i, 1);
|
|
295
185
|
el.remove();
|
|
296
186
|
usedSet.delete(old.id);
|
|
297
187
|
destroyPlaceholder(old.id);
|
|
298
|
-
|
|
188
|
+
pool.push(old.id);
|
|
189
|
+
return true;
|
|
299
190
|
}
|
|
300
191
|
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
301
194
|
|
|
302
|
-
|
|
195
|
+
function nextId(pool, usedSet, fifo, selectorFn) {
|
|
196
|
+
if (pool.length) return pool.shift();
|
|
197
|
+
// try recycling one and then use
|
|
198
|
+
const recycled = recycleIfNeeded(pool, usedSet, fifo, selectorFn);
|
|
199
|
+
if (recycled && pool.length) return pool.shift();
|
|
303
200
|
return null;
|
|
304
201
|
}
|
|
305
202
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
const wrap = document.createElement('div');
|
|
309
|
-
wrap.className = 'ezoic-ad ' + cls;
|
|
310
|
-
wrap.setAttribute('data-ezoic-id', String(id));
|
|
311
|
-
wrap.setAttribute('data-ezoic-after', String(afterVal));
|
|
312
|
-
wrap.innerHTML = '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
|
|
313
|
-
targetEl.insertAdjacentElement('afterend', wrap);
|
|
314
|
-
return wrap;
|
|
203
|
+
function getTopicItems() {
|
|
204
|
+
return Array.from(document.querySelectorAll(SELECTORS.topicListItem));
|
|
315
205
|
}
|
|
316
206
|
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const pool = parsePool(cfg.placeholderIds);
|
|
321
|
-
if (!pool.length) return;
|
|
322
|
-
|
|
323
|
-
const items = Array.from(document.querySelectorAll('li[component="category/topic"]'));
|
|
324
|
-
if (!items.length) return;
|
|
325
|
-
|
|
326
|
-
items.forEach((li, idx) => {
|
|
327
|
-
const pos = idx + 1;
|
|
328
|
-
const firstEnabled = normalizeBool(cfg.showFirstTopicAd);
|
|
329
|
-
if (!(firstEnabled && pos === 1) && (pos % interval !== 0)) return;
|
|
330
|
-
if (idx === items.length - 1) return;
|
|
331
|
-
|
|
332
|
-
const next = li.nextElementSibling;
|
|
333
|
-
if (next && next.classList && next.classList.contains('ezoic-ad-between')) return;
|
|
334
|
-
|
|
335
|
-
let id = pickNextId(pool, usedBetween);
|
|
336
|
-
if (!id) {
|
|
337
|
-
id = recycle(fifoBetween, usedBetween, (old) => '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
|
|
338
|
-
if (!id) return; // pool empty and nothing recyclable => stop
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
usedBetween.add(id);
|
|
342
|
-
fifoBetween.push({ id, after: pos });
|
|
343
|
-
|
|
344
|
-
const wrap = insertAfter(li, id, 'ezoic-ad-between', pos);
|
|
345
|
-
enqueueShowAds('between', wrap, id);
|
|
346
|
-
});
|
|
207
|
+
function getPostItems() {
|
|
208
|
+
// NodeBB harmony: component=post with data-pid is stable
|
|
209
|
+
return Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
347
210
|
}
|
|
348
211
|
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!pool.length) return;
|
|
212
|
+
function cleanupForNewPage() {
|
|
213
|
+
state.pageKey = getPageKey();
|
|
214
|
+
state.cfg = null;
|
|
215
|
+
state.cfgPromise = null;
|
|
354
216
|
|
|
355
|
-
|
|
356
|
-
|
|
217
|
+
state.usedBetween.clear();
|
|
218
|
+
state.usedMessage.clear();
|
|
219
|
+
state.fifoBetween = [];
|
|
220
|
+
state.fifoMessage = [];
|
|
357
221
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const firstEnabled = normalizeBool(cfg.showFirstMessageAd);
|
|
361
|
-
if (!(firstEnabled && no === 1) && (no % interval !== 0)) return;
|
|
362
|
-
if (idx === posts.length - 1) return;
|
|
222
|
+
state.lastShowById = {};
|
|
223
|
+
state.pendingById = {};
|
|
363
224
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
let id = pickNextId(pool, usedMessage);
|
|
368
|
-
if (!id) {
|
|
369
|
-
id = recycle(fifoMessage, usedMessage, (old) => '.ezoic-ad-message[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
|
|
370
|
-
if (!id) return;
|
|
371
|
-
}
|
|
225
|
+
// Remove injected wrappers
|
|
226
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach(el => el.remove());
|
|
372
227
|
|
|
373
|
-
|
|
374
|
-
|
|
228
|
+
// Disconnect observers
|
|
229
|
+
Object.values(state.observers).forEach(obs => { try { obs.disconnect(); } catch (e) {} });
|
|
230
|
+
state.observers = {};
|
|
375
231
|
|
|
376
|
-
|
|
377
|
-
enqueueShowAds('between', wrap, id);
|
|
378
|
-
});
|
|
232
|
+
state.retryCount = {};
|
|
379
233
|
}
|
|
380
234
|
|
|
235
|
+
async function fetchConfig() {
|
|
236
|
+
if (state.cfg) return state.cfg;
|
|
237
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
381
238
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
239
|
+
state.cfgPromise = (async () => {
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
242
|
+
if (!res.ok) return null;
|
|
243
|
+
const cfg = await res.json();
|
|
244
|
+
state.cfg = cfg;
|
|
245
|
+
return cfg;
|
|
246
|
+
} catch (e) {
|
|
247
|
+
return null;
|
|
248
|
+
} finally {
|
|
249
|
+
state.cfgPromise = null;
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
385
252
|
|
|
386
|
-
|
|
387
|
-
return document.querySelectorAll('[component="post"][data-pid]').length;
|
|
253
|
+
return state.cfgPromise;
|
|
388
254
|
}
|
|
389
255
|
|
|
390
|
-
function observeUntilTargets(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
window.__ezoicObs = window.__ezoicObs || {};
|
|
394
|
-
if (window.__ezoicObs[key]) return;
|
|
256
|
+
function observeUntilTargets(kind, cb) {
|
|
257
|
+
const key = `${kind}:${getPageKey()}`;
|
|
258
|
+
if (state.observers[key]) return;
|
|
395
259
|
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
return
|
|
260
|
+
const hasTargets = () => {
|
|
261
|
+
if (kind === 'topic') return document.querySelectorAll(SELECTORS.postItem).length > 0;
|
|
262
|
+
return document.querySelectorAll(SELECTORS.topicListItem).length > 0;
|
|
399
263
|
};
|
|
400
264
|
|
|
401
|
-
if (
|
|
265
|
+
if (hasTargets()) {
|
|
402
266
|
cb();
|
|
403
267
|
return;
|
|
404
268
|
}
|
|
405
269
|
|
|
406
|
-
const obs = new MutationObserver(
|
|
407
|
-
if (
|
|
270
|
+
const obs = new MutationObserver(() => {
|
|
271
|
+
if (hasTargets()) {
|
|
408
272
|
try { obs.disconnect(); } catch (e) {}
|
|
409
|
-
delete
|
|
273
|
+
delete state.observers[key];
|
|
410
274
|
cb();
|
|
411
275
|
}
|
|
412
276
|
});
|
|
413
277
|
|
|
414
|
-
|
|
278
|
+
state.observers[key] = obs;
|
|
415
279
|
try {
|
|
416
280
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
417
|
-
} catch (e) {
|
|
418
|
-
// ignore
|
|
419
|
-
}
|
|
281
|
+
} catch (e) {}
|
|
420
282
|
|
|
421
|
-
|
|
422
|
-
setTimeout(function () {
|
|
283
|
+
setTimeout(() => {
|
|
423
284
|
try { obs.disconnect(); } catch (e) {}
|
|
424
|
-
delete
|
|
285
|
+
delete state.observers[key];
|
|
425
286
|
}, 6000);
|
|
426
287
|
}
|
|
427
288
|
|
|
428
|
-
function scheduleRetry(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
289
|
+
function scheduleRetry(kind) {
|
|
290
|
+
const key = `${kind}:${getPageKey()}`;
|
|
291
|
+
state.retryCount[key] = state.retryCount[key] || 0;
|
|
292
|
+
if (state.retryCount[key] >= 24) return;
|
|
293
|
+
state.retryCount[key] += 1;
|
|
433
294
|
setTimeout(run, 250);
|
|
434
295
|
}
|
|
435
296
|
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
inFlight = true;
|
|
297
|
+
function injectBetweenTopics(cfg) {
|
|
298
|
+
if (!normalizeBool(cfg.enableBetweenAds)) return;
|
|
439
299
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
300
|
+
const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
|
|
301
|
+
const firstEnabled = normalizeBool(cfg.showFirstTopicAd);
|
|
302
|
+
|
|
303
|
+
const pool = parsePool(cfg.placeholderIds);
|
|
304
|
+
if (!pool.length) return;
|
|
305
|
+
|
|
306
|
+
const items = getTopicItems();
|
|
307
|
+
if (!items.length) return;
|
|
308
|
+
|
|
309
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
310
|
+
const li = items[idx];
|
|
311
|
+
const pos = idx + 1;
|
|
447
312
|
|
|
313
|
+
if (!li || !li.isConnected) continue;
|
|
314
|
+
|
|
315
|
+
// position rule
|
|
316
|
+
const ok = (firstEnabled && pos === 1) || (pos % interval === 0);
|
|
317
|
+
if (!ok) continue;
|
|
318
|
+
|
|
319
|
+
if (hasAdImmediatelyAfter(li)) continue;
|
|
320
|
+
|
|
321
|
+
const id = nextId(pool, state.usedBetween, state.fifoBetween, (old) => `.${WRAP_CLASS}.ezoic-ad-between[data-ezoic-after="${old.after}"]`);
|
|
322
|
+
if (!id) break;
|
|
323
|
+
|
|
324
|
+
state.usedBetween.add(id);
|
|
325
|
+
const wrap = insertAfter(li, id, 'ezoic-ad-between', pos);
|
|
326
|
+
if (!wrap) continue;
|
|
327
|
+
|
|
328
|
+
state.fifoBetween.push({ id, after: pos });
|
|
329
|
+
|
|
330
|
+
callShowAdsSingle(id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function injectBetweenMessages(cfg) {
|
|
335
|
+
if (!normalizeBool(cfg.enableMessageAds)) return;
|
|
336
|
+
|
|
337
|
+
const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
338
|
+
const firstEnabled = normalizeBool(cfg.showFirstMessageAd);
|
|
339
|
+
|
|
340
|
+
const pool = parsePool(cfg.messagePlaceholderIds);
|
|
341
|
+
if (!pool.length) return;
|
|
342
|
+
|
|
343
|
+
const posts = getPostItems();
|
|
344
|
+
if (!posts.length) return;
|
|
345
|
+
|
|
346
|
+
for (let idx = 0; idx < posts.length; idx++) {
|
|
347
|
+
const post = posts[idx];
|
|
348
|
+
const no = idx + 1;
|
|
349
|
+
if (!post || !post.isConnected) continue;
|
|
350
|
+
|
|
351
|
+
const ok = (firstEnabled && no === 1) || (no % interval === 0);
|
|
352
|
+
if (!ok) continue;
|
|
353
|
+
|
|
354
|
+
if (hasAdImmediatelyAfter(post)) continue;
|
|
355
|
+
|
|
356
|
+
const id = nextId(pool, state.usedMessage, state.fifoMessage, (old) => `.${WRAP_CLASS}.ezoic-ad-message[data-ezoic-after="${old.after}"]`);
|
|
357
|
+
if (!id) break;
|
|
358
|
+
|
|
359
|
+
state.usedMessage.add(id);
|
|
360
|
+
const wrap = insertAfter(post, id, 'ezoic-ad-message', no);
|
|
361
|
+
if (!wrap) continue;
|
|
362
|
+
|
|
363
|
+
state.fifoMessage.push({ id, after: no });
|
|
364
|
+
|
|
365
|
+
callShowAdsSingle(id);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function run() {
|
|
370
|
+
try {
|
|
448
371
|
const cfg = await fetchConfig();
|
|
449
372
|
if (!cfg || cfg.excluded) return;
|
|
450
373
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
observeUntilTargets('topic', function () { run(); });
|
|
458
|
-
scheduleRetry('topic:' + getPageKey());
|
|
459
|
-
}
|
|
374
|
+
const kind = getPageKind();
|
|
375
|
+
|
|
376
|
+
if (kind === 'topic') {
|
|
377
|
+
const hasPosts = document.querySelectorAll(SELECTORS.postItem).length > 0;
|
|
378
|
+
if (hasPosts) injectBetweenMessages(cfg);
|
|
379
|
+
else { observeUntilTargets('topic', run); scheduleRetry('topic'); }
|
|
460
380
|
} else {
|
|
461
|
-
const hasList =
|
|
462
|
-
if (hasList)
|
|
463
|
-
|
|
464
|
-
} else {
|
|
465
|
-
observeUntilTargets('category', function () { run(); });
|
|
466
|
-
scheduleRetry('category:' + getPageKey());
|
|
467
|
-
}
|
|
381
|
+
const hasList = document.querySelectorAll(SELECTORS.topicListItem).length > 0;
|
|
382
|
+
if (hasList) injectBetweenTopics(cfg);
|
|
383
|
+
else { observeUntilTargets('category', run); scheduleRetry('category'); }
|
|
468
384
|
}
|
|
469
385
|
} catch (e) {
|
|
470
|
-
//
|
|
471
|
-
} finally {
|
|
472
|
-
inFlight = false;
|
|
473
|
-
if (rerunRequested) {
|
|
474
|
-
rerunRequested = false;
|
|
475
|
-
setTimeout(run, 50);
|
|
476
|
-
}
|
|
386
|
+
// Never break NodeBB UI
|
|
477
387
|
}
|
|
478
388
|
}
|
|
479
389
|
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
debounceTimer = setTimeout(run, 150);
|
|
483
|
-
}
|
|
390
|
+
function bindNodeBBEvents() {
|
|
391
|
+
if (!$w) return;
|
|
484
392
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (window.jQuery) {
|
|
488
|
-
const $w = window.jQuery(window);
|
|
489
|
-
$w.off('.ezoicInfinite');
|
|
490
|
-
$w.on('action:ajaxify.end.ezoicInfinite', function(){ scheduleRun(); setTimeout(scheduleRun, 600); });
|
|
491
|
-
$w.on('action:posts.loaded.ezoicInfinite', scheduleRun);
|
|
492
|
-
$w.on('action:topic.loaded.ezoicInfinite', scheduleRun);
|
|
493
|
-
$w.on('action:topics.loaded.ezoicInfinite', scheduleRun);
|
|
494
|
-
$w.on('action:category.loaded.ezoicInfinite', scheduleRun);
|
|
495
|
-
$w.on('action:ajaxify.start.ezoicInfinite', function () {
|
|
496
|
-
pageKey = null;
|
|
497
|
-
cleanupForNewPage();
|
|
498
|
-
window.__ezoicCatRetry = 0;
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
}
|
|
393
|
+
// Prevent duplicate binding
|
|
394
|
+
$w.off('.ezoicInfinite');
|
|
502
395
|
|
|
503
|
-
|
|
396
|
+
$w.on('action:ajaxify.start.ezoicInfinite', () => {
|
|
397
|
+
cleanupForNewPage();
|
|
398
|
+
});
|
|
504
399
|
|
|
505
|
-
|
|
506
|
-
run();
|
|
507
|
-
setTimeout(function(){
|
|
508
|
-
// only retry if nothing was injected yet
|
|
509
|
-
if (!document.querySelector('.ezoic-ad')) run();
|
|
510
|
-
}, 1400);
|
|
511
|
-
|
|
512
|
-
// Also run on hard-refresh initial load
|
|
513
|
-
if (document.readyState === 'loading') {
|
|
514
|
-
document.addEventListener('DOMContentLoaded', function () {
|
|
515
|
-
bindNodeBBEvents();
|
|
400
|
+
$w.on('action:ajaxify.end.ezoicInfinite', () => {
|
|
516
401
|
run();
|
|
517
|
-
setTimeout(run,
|
|
402
|
+
setTimeout(run, 200);
|
|
403
|
+
setTimeout(run, 800);
|
|
518
404
|
});
|
|
405
|
+
|
|
406
|
+
$w.on('action:category.loaded.ezoicInfinite', () => {
|
|
407
|
+
run();
|
|
408
|
+
setTimeout(run, 300);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
$w.on('action:topic.loaded.ezoicInfinite', () => {
|
|
412
|
+
run();
|
|
413
|
+
setTimeout(run, 300);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Infinite scroll events (varies by route)
|
|
417
|
+
$w.on('action:topics.loaded.ezoicInfinite', run);
|
|
418
|
+
$w.on('action:posts.loaded.ezoicInfinite', run);
|
|
519
419
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
420
|
+
|
|
421
|
+
// Boot
|
|
422
|
+
cleanupForNewPage();
|
|
423
|
+
bindNodeBBEvents();
|
|
424
|
+
|
|
425
|
+
// First load (non-ajaxify)
|
|
426
|
+
run();
|
|
427
|
+
setTimeout(run, 250);
|
|
524
428
|
})();
|