nodebb-plugin-ezoic-infinite 0.8.0 → 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 +153 -59
- 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.8.
|
|
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,53 +115,119 @@ 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;
|
|
170
|
+
|
|
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;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function setupAdAutoHeight() {
|
|
188
|
+
if (window.__ezoicAutoHeightAttached) return;
|
|
189
|
+
window.__ezoicAutoHeightAttached = true;
|
|
190
|
+
|
|
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
|
+
});
|
|
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
|
+
}
|
|
137
206
|
|
|
138
|
-
|
|
139
|
-
|
|
207
|
+
attach();
|
|
208
|
+
setTimeout(attach, 600);
|
|
209
|
+
setTimeout(attach, 1600);
|
|
210
|
+
} catch (e) {}
|
|
140
211
|
}
|
|
141
212
|
|
|
142
213
|
function callEzoic(ids) {
|
|
143
|
-
// ids optional; if omitted, we will scan DOM for unrendered placeholders
|
|
144
214
|
window.ezstandalone = window.ezstandalone || {};
|
|
145
215
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
146
216
|
|
|
217
|
+
// Collect unrendered wrappers (prevents duplicates even if events fire multiple times)
|
|
147
218
|
const collect = function () {
|
|
148
219
|
const list = [];
|
|
149
|
-
document.querySelectorAll('.ezoic-ad
|
|
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;
|
|
150
224
|
const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
|
|
151
225
|
const id = parseInt(idStr, 10);
|
|
152
226
|
if (!Number.isFinite(id) || id <= 0) return;
|
|
153
227
|
|
|
154
|
-
const wrap = ph.closest('.ezoic-ad');
|
|
155
|
-
if (!wrap) return;
|
|
156
|
-
if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
|
|
157
|
-
|
|
158
|
-
list.push(id);
|
|
159
228
|
wrap.setAttribute('data-ezoic-rendered', '1');
|
|
229
|
+
list.push(id);
|
|
160
230
|
});
|
|
161
|
-
// de-dupe
|
|
162
231
|
return Array.from(new Set(list));
|
|
163
232
|
};
|
|
164
233
|
|
|
@@ -178,16 +247,13 @@ function callEzoic(ids) {
|
|
|
178
247
|
setupAdAutoHeight();
|
|
179
248
|
window.ezstandalone.cmd.push(function () { run(); });
|
|
180
249
|
|
|
181
|
-
// Retry
|
|
250
|
+
// Retry if Ezoic loads late
|
|
182
251
|
let tries = 0;
|
|
183
252
|
const maxTries = 6;
|
|
184
253
|
const retry = function () {
|
|
185
254
|
tries++;
|
|
186
255
|
const ok = run();
|
|
187
256
|
if (ok) return;
|
|
188
|
-
try {
|
|
189
|
-
if (typeof window.ezstandalone.showAds === 'function') return;
|
|
190
|
-
} catch (e) {}
|
|
191
257
|
if (tries < maxTries) setTimeout(retry, 800);
|
|
192
258
|
};
|
|
193
259
|
|
|
@@ -201,13 +267,15 @@ function callEzoic(ids) {
|
|
|
201
267
|
}
|
|
202
268
|
|
|
203
269
|
function getPostNumber($post) {
|
|
204
|
-
|
|
205
|
-
if (Number.isFinite(di) && di > 0) return di;
|
|
206
|
-
|
|
270
|
+
// Harmony: <a class="post-index">#72</a>
|
|
207
271
|
const txt = ($post.find('a.post-index').first().text() || '').trim();
|
|
208
272
|
const m = txt.match(/#\s*(\d+)/);
|
|
209
273
|
if (m) return parseInt(m[1], 10);
|
|
210
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
|
+
|
|
211
279
|
return NaN;
|
|
212
280
|
}
|
|
213
281
|
|
|
@@ -224,8 +292,9 @@ function injectTopicMessageAds($posts, pool, interval) {
|
|
|
224
292
|
|
|
225
293
|
$posts.each(function () {
|
|
226
294
|
const $p = $(this);
|
|
227
|
-
// Never insert after the last
|
|
295
|
+
// Never insert after the last post (sentinel)
|
|
228
296
|
if ($p.is($posts.last())) return;
|
|
297
|
+
|
|
229
298
|
const postNo = getPostNumber($p);
|
|
230
299
|
if (!Number.isFinite(postNo) || postNo <= 0) return;
|
|
231
300
|
|
|
@@ -233,7 +302,10 @@ function injectTopicMessageAds($posts, pool, interval) {
|
|
|
233
302
|
if (seenAfterPostNo.has(postNo)) return;
|
|
234
303
|
|
|
235
304
|
let id = pickNextId(pool);
|
|
236
|
-
if (!id) {
|
|
305
|
+
if (!id) {
|
|
306
|
+
id = recycleOldestTopicId($posts);
|
|
307
|
+
if (!id) return;
|
|
308
|
+
}
|
|
237
309
|
|
|
238
310
|
const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
|
|
239
311
|
const html = makeWrapperLike(
|
|
@@ -259,8 +331,9 @@ function injectCategoryBetweenAds($items, pool, interval) {
|
|
|
259
331
|
|
|
260
332
|
$items.each(function () {
|
|
261
333
|
const $it = $(this);
|
|
262
|
-
// Never insert after the last
|
|
334
|
+
// Never insert after the last topic item (sentinel)
|
|
263
335
|
if ($it.is($items.last())) return;
|
|
336
|
+
|
|
264
337
|
const pos = getTopicPos($it);
|
|
265
338
|
if (!Number.isFinite(pos) || pos <= 0) return;
|
|
266
339
|
|
|
@@ -268,7 +341,10 @@ function injectCategoryBetweenAds($items, pool, interval) {
|
|
|
268
341
|
if (seenAfterTopicPos.has(pos)) return;
|
|
269
342
|
|
|
270
343
|
let id = pickNextId(pool);
|
|
271
|
-
if (!id) {
|
|
344
|
+
if (!id) {
|
|
345
|
+
id = recycleOldestCategoryId($items);
|
|
346
|
+
if (!id) return;
|
|
347
|
+
}
|
|
272
348
|
|
|
273
349
|
const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
|
|
274
350
|
const html = makeWrapperLike(
|
|
@@ -310,17 +386,20 @@ async function refreshAds() {
|
|
|
310
386
|
const messagePool = parsePool(cfg.messagePlaceholderIds);
|
|
311
387
|
const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
312
388
|
|
|
389
|
+
const hasTopicList = isCategoryTopicListPage();
|
|
313
390
|
const onTopic = isTopicPage();
|
|
314
|
-
const onCategory =
|
|
391
|
+
const onCategory = hasTopicList && !onTopic;
|
|
315
392
|
|
|
316
393
|
const newIds = [];
|
|
317
394
|
|
|
395
|
+
// Rule: topic list => between only ; topic page => message only
|
|
318
396
|
if (onCategory) {
|
|
319
397
|
const $items = getCategoryTopicItems();
|
|
320
398
|
if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
|
|
321
399
|
newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
|
|
322
400
|
}
|
|
323
401
|
callEzoic(newIds);
|
|
402
|
+
// also ensure any unrendered placeholders are rendered
|
|
324
403
|
callEzoic();
|
|
325
404
|
return;
|
|
326
405
|
}
|
|
@@ -347,28 +426,29 @@ function debounceRefresh() {
|
|
|
347
426
|
debounceTimer = setTimeout(refreshAds, 220);
|
|
348
427
|
}
|
|
349
428
|
|
|
350
|
-
$(document).ready(debounceRefresh);
|
|
429
|
+
$(document).ready(function () { setupAdAutoHeight(); debounceRefresh(); });
|
|
430
|
+
|
|
351
431
|
$(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
|
|
352
|
-
setTimeout(debounceRefresh, 2200);
|
|
353
432
|
|
|
354
|
-
|
|
355
|
-
|
|
433
|
+
setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
|
|
434
|
+
|
|
435
|
+
// Observer + poller: ensure we rerun when NodeBB injects new items
|
|
356
436
|
(function setupEzoicObserver() {
|
|
357
437
|
if (window.__ezoicInfiniteObserver) return;
|
|
358
438
|
try {
|
|
439
|
+
const trigger = function () { debounceRefresh(); };
|
|
440
|
+
|
|
359
441
|
const obs = new MutationObserver(function (mutations) {
|
|
360
442
|
for (const m of mutations) {
|
|
361
443
|
if (!m.addedNodes || !m.addedNodes.length) continue;
|
|
362
444
|
for (const n of m.addedNodes) {
|
|
363
445
|
if (!n || n.nodeType !== 1) continue;
|
|
364
|
-
// direct match
|
|
365
446
|
if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
|
|
366
|
-
|
|
447
|
+
trigger();
|
|
367
448
|
return;
|
|
368
449
|
}
|
|
369
|
-
// descendant match
|
|
370
450
|
if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
|
|
371
|
-
|
|
451
|
+
trigger();
|
|
372
452
|
return;
|
|
373
453
|
}
|
|
374
454
|
}
|
|
@@ -376,5 +456,19 @@ setTimeout(debounceRefresh, 2200);
|
|
|
376
456
|
});
|
|
377
457
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
378
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);
|
|
379
473
|
} catch (e) {}
|
|
380
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;}
|