nodebb-plugin-ezoic-infinite 0.7.9 → 0.8.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/package.json +4 -1
- package/plugin.json +1 -4
- package/public/client.js +169 -67
- package/public/style.css +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,5 +13,8 @@
|
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"nodebb": ">=4.0.0"
|
|
16
19
|
}
|
|
17
20
|
}
|
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
3
2
|
/* globals ajaxify */
|
|
3
|
+
|
|
4
4
|
window.ezoicInfiniteLoaded = true;
|
|
5
5
|
|
|
6
6
|
let cachedConfig;
|
|
@@ -10,17 +10,17 @@ let debounceTimer;
|
|
|
10
10
|
let inFlight = false;
|
|
11
11
|
let rerunRequested = false;
|
|
12
12
|
|
|
13
|
-
// Per-page state
|
|
13
|
+
// Per-page state
|
|
14
14
|
let pageKey = null;
|
|
15
15
|
|
|
16
|
-
// Topic page
|
|
17
|
-
let seenAfterPostNo = new Set();
|
|
18
|
-
let usedIds = new Set();
|
|
19
|
-
let fifo = [];
|
|
16
|
+
// Topic page: anchor ads to absolute post number
|
|
17
|
+
let seenAfterPostNo = new Set(); // post numbers we've inserted after
|
|
18
|
+
let usedIds = new Set(); // ids currently present in DOM
|
|
19
|
+
let fifo = []; // [{afterPostNo, id}]
|
|
20
20
|
|
|
21
|
-
// Category page
|
|
22
|
-
let seenAfterTopicPos = new Set();
|
|
23
|
-
let fifoCat = [];
|
|
21
|
+
// Category topic list page: anchor ads to absolute position
|
|
22
|
+
let seenAfterTopicPos = new Set();
|
|
23
|
+
let fifoCat = []; // [{afterPos, id}]
|
|
24
24
|
|
|
25
25
|
function resetState() {
|
|
26
26
|
seenAfterPostNo = new Set();
|
|
@@ -50,7 +50,7 @@ function parsePool(raw) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async function fetchConfig() {
|
|
53
|
-
if (cachedConfig && Date.now() - lastFetch <
|
|
53
|
+
if (cachedConfig && Date.now() - lastFetch < 8000) return cachedConfig;
|
|
54
54
|
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
55
55
|
cachedConfig = await res.json();
|
|
56
56
|
lastFetch = Date.now();
|
|
@@ -58,7 +58,10 @@ async function fetchConfig() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function isTopicPage() {
|
|
61
|
-
|
|
61
|
+
try {
|
|
62
|
+
if (ajaxify && ajaxify.data && ajaxify.data.tid) return true;
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
return /^\/topic\//.test(window.location.pathname);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function isCategoryTopicListPage() {
|
|
@@ -69,7 +72,7 @@ function getTopicPosts() {
|
|
|
69
72
|
const $primary = $('[component="post"][data-pid]');
|
|
70
73
|
if ($primary.length) return $primary;
|
|
71
74
|
|
|
72
|
-
//
|
|
75
|
+
// Fallback: top-level nodes with post/content (avoid nested)
|
|
73
76
|
return $('[data-pid]').filter(function () {
|
|
74
77
|
const $el = $(this);
|
|
75
78
|
const hasContent = $el.find('[component="post/content"]').length > 0;
|
|
@@ -82,7 +85,7 @@ function getCategoryTopicItems() {
|
|
|
82
85
|
return $('li[component="category/topic"]');
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
// If target's parent is UL/OL, wrapper
|
|
88
|
+
// If target's parent is UL/OL, wrapper must be LI
|
|
86
89
|
function wrapperTagFor($target) {
|
|
87
90
|
if (!$target || !$target.length) return 'div';
|
|
88
91
|
const parentTag = ($target.parent().prop('tagName') || '').toUpperCase();
|
|
@@ -102,7 +105,7 @@ function makeWrapperLike($target, classes, innerHtml, attrs) {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
function cleanupOnNav() {
|
|
105
|
-
$('.ezoic-ad-post, .ezoic-ad-topic
|
|
108
|
+
$('.ezoic-ad-post, .ezoic-ad-topic').remove();
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
function pickNextId(pool) {
|
|
@@ -112,52 +115,129 @@ function pickNextId(pool) {
|
|
|
112
115
|
return null;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
function
|
|
118
|
+
function destroyEzoicId(id) {
|
|
119
|
+
try {
|
|
120
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
121
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
122
|
+
const fn = function () {
|
|
123
|
+
try {
|
|
124
|
+
if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
125
|
+
window.ezstandalone.destroyPlaceholders(id);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {}
|
|
128
|
+
};
|
|
129
|
+
if (typeof window.ezstandalone.destroyPlaceholders === 'function') fn();
|
|
130
|
+
else window.ezstandalone.cmd.push(fn);
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function recycleOldestTopicId($posts) {
|
|
135
|
+
if (!fifo.length) return null;
|
|
116
136
|
fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
|
|
117
|
-
const old = fifo.shift();
|
|
118
|
-
if (!old) return false;
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
while (fifo.length) {
|
|
139
|
+
const old = fifo.shift();
|
|
140
|
+
const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
|
|
141
|
+
const $el = $(sel);
|
|
142
|
+
if (!$el.length) continue;
|
|
143
|
+
|
|
144
|
+
// Never recycle the ad that is right after the last real post (sentinel)
|
|
145
|
+
try {
|
|
146
|
+
const $last = $posts.last();
|
|
147
|
+
if ($last.length && $el.prev().is($last)) {
|
|
148
|
+
fifo.push(old);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {}
|
|
123
152
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
$el.remove();
|
|
154
|
+
usedIds.delete(old.id);
|
|
155
|
+
destroyEzoicId(old.id);
|
|
156
|
+
return old.id;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
127
159
|
}
|
|
128
160
|
|
|
129
|
-
function
|
|
161
|
+
function recycleOldestCategoryId($items) {
|
|
162
|
+
if (!fifoCat.length) return null;
|
|
130
163
|
fifoCat.sort((a, b) => a.afterPos - b.afterPos);
|
|
131
|
-
const old = fifoCat.shift();
|
|
132
|
-
if (!old) return false;
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
165
|
+
while (fifoCat.length) {
|
|
166
|
+
const old = fifoCat.shift();
|
|
167
|
+
const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
|
|
168
|
+
const $el = $(sel);
|
|
169
|
+
if (!$el.length) continue;
|
|
137
170
|
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
try {
|
|
172
|
+
const $last = $items.last();
|
|
173
|
+
if ($last.length && $el.prev().is($last)) {
|
|
174
|
+
fifoCat.push(old);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
|
|
179
|
+
$el.remove();
|
|
180
|
+
usedIds.delete(old.id);
|
|
181
|
+
destroyEzoicId(old.id);
|
|
182
|
+
return old.id;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
140
185
|
}
|
|
141
186
|
|
|
142
|
-
function
|
|
143
|
-
if (
|
|
187
|
+
function setupAdAutoHeight() {
|
|
188
|
+
if (window.__ezoicAutoHeightAttached) return;
|
|
189
|
+
window.__ezoicAutoHeightAttached = true;
|
|
144
190
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
window.__ezoicLastShowAt = now;
|
|
191
|
+
try {
|
|
192
|
+
const ro = new ResizeObserver(function () {
|
|
193
|
+
document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
|
|
194
|
+
wrap.style.minHeight = '0px';
|
|
195
|
+
wrap.style.height = 'auto';
|
|
196
|
+
});
|
|
197
|
+
});
|
|
153
198
|
|
|
199
|
+
function attach() {
|
|
200
|
+
document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
|
|
201
|
+
if (wrap.__ezoicRO) return;
|
|
202
|
+
wrap.__ezoicRO = true;
|
|
203
|
+
ro.observe(wrap);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
attach();
|
|
208
|
+
setTimeout(attach, 600);
|
|
209
|
+
setTimeout(attach, 1600);
|
|
210
|
+
} catch (e) {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function callEzoic(ids) {
|
|
154
214
|
window.ezstandalone = window.ezstandalone || {};
|
|
155
215
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
156
216
|
|
|
217
|
+
// Collect unrendered wrappers (prevents duplicates even if events fire multiple times)
|
|
218
|
+
const collect = function () {
|
|
219
|
+
const list = [];
|
|
220
|
+
document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
|
|
221
|
+
if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
|
|
222
|
+
const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
|
|
223
|
+
if (!ph) return;
|
|
224
|
+
const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
|
|
225
|
+
const id = parseInt(idStr, 10);
|
|
226
|
+
if (!Number.isFinite(id) || id <= 0) return;
|
|
227
|
+
|
|
228
|
+
wrap.setAttribute('data-ezoic-rendered', '1');
|
|
229
|
+
list.push(id);
|
|
230
|
+
});
|
|
231
|
+
return Array.from(new Set(list));
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const toShow = (ids && ids.length) ? Array.from(new Set(ids)) : collect();
|
|
235
|
+
if (!toShow.length) return;
|
|
236
|
+
|
|
157
237
|
const run = function () {
|
|
158
238
|
try {
|
|
159
239
|
if (typeof window.ezstandalone.showAds === 'function') {
|
|
160
|
-
window.ezstandalone.showAds.apply(window.ezstandalone,
|
|
240
|
+
window.ezstandalone.showAds.apply(window.ezstandalone, toShow);
|
|
161
241
|
return true;
|
|
162
242
|
}
|
|
163
243
|
} catch (e) {}
|
|
@@ -167,16 +247,13 @@ function callEzoic(ids) {
|
|
|
167
247
|
setupAdAutoHeight();
|
|
168
248
|
window.ezstandalone.cmd.push(function () { run(); });
|
|
169
249
|
|
|
170
|
-
// Retry
|
|
250
|
+
// Retry if Ezoic loads late
|
|
171
251
|
let tries = 0;
|
|
172
252
|
const maxTries = 6;
|
|
173
253
|
const retry = function () {
|
|
174
254
|
tries++;
|
|
175
255
|
const ok = run();
|
|
176
256
|
if (ok) return;
|
|
177
|
-
try {
|
|
178
|
-
if (typeof window.ezstandalone.showAds === 'function') return;
|
|
179
|
-
} catch (e) {}
|
|
180
257
|
if (tries < maxTries) setTimeout(retry, 800);
|
|
181
258
|
};
|
|
182
259
|
|
|
@@ -190,13 +267,15 @@ function callEzoic(ids) {
|
|
|
190
267
|
}
|
|
191
268
|
|
|
192
269
|
function getPostNumber($post) {
|
|
193
|
-
|
|
194
|
-
if (Number.isFinite(di) && di > 0) return di;
|
|
195
|
-
|
|
270
|
+
// Harmony: <a class="post-index">#72</a>
|
|
196
271
|
const txt = ($post.find('a.post-index').first().text() || '').trim();
|
|
197
272
|
const m = txt.match(/#\s*(\d+)/);
|
|
198
273
|
if (m) return parseInt(m[1], 10);
|
|
199
274
|
|
|
275
|
+
// fallback data-index (often 0/1 based)
|
|
276
|
+
const di = parseInt($post.attr('data-index'), 10);
|
|
277
|
+
if (Number.isFinite(di) && di >= 0) return di + 1;
|
|
278
|
+
|
|
200
279
|
return NaN;
|
|
201
280
|
}
|
|
202
281
|
|
|
@@ -213,8 +292,9 @@ function injectTopicMessageAds($posts, pool, interval) {
|
|
|
213
292
|
|
|
214
293
|
$posts.each(function () {
|
|
215
294
|
const $p = $(this);
|
|
216
|
-
// Never insert after the last
|
|
295
|
+
// Never insert after the last post (sentinel)
|
|
217
296
|
if ($p.is($posts.last())) return;
|
|
297
|
+
|
|
218
298
|
const postNo = getPostNumber($p);
|
|
219
299
|
if (!Number.isFinite(postNo) || postNo <= 0) return;
|
|
220
300
|
|
|
@@ -222,7 +302,10 @@ function injectTopicMessageAds($posts, pool, interval) {
|
|
|
222
302
|
if (seenAfterPostNo.has(postNo)) return;
|
|
223
303
|
|
|
224
304
|
let id = pickNextId(pool);
|
|
225
|
-
if (!id) {
|
|
305
|
+
if (!id) {
|
|
306
|
+
id = recycleOldestTopicId($posts);
|
|
307
|
+
if (!id) return;
|
|
308
|
+
}
|
|
226
309
|
|
|
227
310
|
const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
|
|
228
311
|
const html = makeWrapperLike(
|
|
@@ -248,8 +331,9 @@ function injectCategoryBetweenAds($items, pool, interval) {
|
|
|
248
331
|
|
|
249
332
|
$items.each(function () {
|
|
250
333
|
const $it = $(this);
|
|
251
|
-
// Never insert after the last
|
|
334
|
+
// Never insert after the last topic item (sentinel)
|
|
252
335
|
if ($it.is($items.last())) return;
|
|
336
|
+
|
|
253
337
|
const pos = getTopicPos($it);
|
|
254
338
|
if (!Number.isFinite(pos) || pos <= 0) return;
|
|
255
339
|
|
|
@@ -257,7 +341,10 @@ function injectCategoryBetweenAds($items, pool, interval) {
|
|
|
257
341
|
if (seenAfterTopicPos.has(pos)) return;
|
|
258
342
|
|
|
259
343
|
let id = pickNextId(pool);
|
|
260
|
-
if (!id) {
|
|
344
|
+
if (!id) {
|
|
345
|
+
id = recycleOldestCategoryId($items);
|
|
346
|
+
if (!id) return;
|
|
347
|
+
}
|
|
261
348
|
|
|
262
349
|
const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
|
|
263
350
|
const html = makeWrapperLike(
|
|
@@ -279,11 +366,6 @@ function injectCategoryBetweenAds($items, pool, interval) {
|
|
|
279
366
|
}
|
|
280
367
|
|
|
281
368
|
async function refreshAds() {
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
if (window.__ezoicLastRefreshAt && now - window.__ezoicLastRefreshAt < 400) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
window.__ezoicLastRefreshAt = now;
|
|
287
369
|
const key = getPageKey();
|
|
288
370
|
if (pageKey !== key) {
|
|
289
371
|
pageKey = key;
|
|
@@ -304,17 +386,21 @@ async function refreshAds() {
|
|
|
304
386
|
const messagePool = parsePool(cfg.messagePlaceholderIds);
|
|
305
387
|
const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
306
388
|
|
|
389
|
+
const hasTopicList = isCategoryTopicListPage();
|
|
307
390
|
const onTopic = isTopicPage();
|
|
308
|
-
const onCategory =
|
|
391
|
+
const onCategory = hasTopicList && !onTopic;
|
|
309
392
|
|
|
310
393
|
const newIds = [];
|
|
311
394
|
|
|
395
|
+
// Rule: topic list => between only ; topic page => message only
|
|
312
396
|
if (onCategory) {
|
|
313
397
|
const $items = getCategoryTopicItems();
|
|
314
398
|
if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
|
|
315
399
|
newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
|
|
316
400
|
}
|
|
317
401
|
callEzoic(newIds);
|
|
402
|
+
// also ensure any unrendered placeholders are rendered
|
|
403
|
+
callEzoic();
|
|
318
404
|
return;
|
|
319
405
|
}
|
|
320
406
|
|
|
@@ -324,6 +410,7 @@ async function refreshAds() {
|
|
|
324
410
|
newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
|
|
325
411
|
}
|
|
326
412
|
callEzoic(newIds);
|
|
413
|
+
callEzoic();
|
|
327
414
|
}
|
|
328
415
|
} finally {
|
|
329
416
|
inFlight = false;
|
|
@@ -339,28 +426,29 @@ function debounceRefresh() {
|
|
|
339
426
|
debounceTimer = setTimeout(refreshAds, 220);
|
|
340
427
|
}
|
|
341
428
|
|
|
342
|
-
$(document).ready(debounceRefresh);
|
|
429
|
+
$(document).ready(function () { setupAdAutoHeight(); debounceRefresh(); });
|
|
430
|
+
|
|
343
431
|
$(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
|
|
344
|
-
setTimeout(debounceRefresh, 2200);
|
|
345
432
|
|
|
346
|
-
|
|
347
|
-
|
|
433
|
+
setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
|
|
434
|
+
|
|
435
|
+
// Observer + poller: ensure we rerun when NodeBB injects new items
|
|
348
436
|
(function setupEzoicObserver() {
|
|
349
437
|
if (window.__ezoicInfiniteObserver) return;
|
|
350
438
|
try {
|
|
439
|
+
const trigger = function () { debounceRefresh(); };
|
|
440
|
+
|
|
351
441
|
const obs = new MutationObserver(function (mutations) {
|
|
352
442
|
for (const m of mutations) {
|
|
353
443
|
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
354
444
|
for (const n of m.addedNodes) {
|
|
355
445
|
if (!n || n.nodeType !== 1) continue;
|
|
356
|
-
// direct match
|
|
357
446
|
if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
|
|
358
|
-
|
|
447
|
+
trigger();
|
|
359
448
|
return;
|
|
360
449
|
}
|
|
361
|
-
// descendant match
|
|
362
450
|
if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
|
|
363
|
-
|
|
451
|
+
trigger();
|
|
364
452
|
return;
|
|
365
453
|
}
|
|
366
454
|
}
|
|
@@ -368,5 +456,19 @@ setTimeout(debounceRefresh, 2200);
|
|
|
368
456
|
});
|
|
369
457
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
370
458
|
window.__ezoicInfiniteObserver = obs;
|
|
459
|
+
|
|
460
|
+
let lastPostCount = 0;
|
|
461
|
+
let lastTopicCount = 0;
|
|
462
|
+
setInterval(function () {
|
|
463
|
+
try {
|
|
464
|
+
const posts = document.querySelectorAll('[component="post"][data-pid]').length;
|
|
465
|
+
const topics = document.querySelectorAll('li[component="category/topic"]').length;
|
|
466
|
+
if (posts !== lastPostCount || topics !== lastTopicCount) {
|
|
467
|
+
lastPostCount = posts;
|
|
468
|
+
lastTopicCount = topics;
|
|
469
|
+
trigger();
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {}
|
|
472
|
+
}, 1500);
|
|
371
473
|
} catch (e) {}
|
|
372
474
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
.ezoic-ad-
|
|
2
|
-
.ezoic-ad-
|
|
1
|
+
.ezoic-ad{min-height:0!important;height:auto!important;padding:0!important;}
|
|
2
|
+
.ezoic-ad-post,.ezoic-ad-topic{margin:0.5rem 0;}
|
|
3
|
+
.ezoic-ad-message-inner{padding:0;margin:0;}
|