nodebb-plugin-ezoic-infinite 0.8.7 → 0.8.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -22,5 +22,8 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "templates": "public/templates"
25
+ "templates": "public/templates",
26
+ "css": [
27
+ "public/style.css"
28
+ ]
26
29
  }
package/public/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
- /* globals ajaxify */
3
2
 
3
+ /* globals ajaxify */
4
4
  window.ezoicInfiniteLoaded = true;
5
5
 
6
6
  let cachedConfig;
@@ -10,23 +10,22 @@ let debounceTimer;
10
10
  let inFlight = false;
11
11
  let rerunRequested = false;
12
12
 
13
- // Per-page state
13
+ // Per-page state (keyed by tid/cid)
14
14
  let pageKey = null;
15
15
 
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}]
16
+ // Topic page state: anchor ads to absolute post number (not DOM index)
17
+ let seenAfterPostNo = new Set(); // post numbers we've already inserted an ad after
18
+ let usedIds = new Set(); // ids currently in DOM
19
+ let fifo = []; // [{afterPostNo, id}]
20
20
 
21
- // Category topic list page: anchor ads to absolute position
22
- let seenAfterTopicPos = new Set();
23
- let fifoCat = []; // [{afterPos, id}]
21
+ // Category page state: anchor to absolute topic position if available
22
+ let seenAfterTopicPos = new Set(); // topic positions we've inserted after
23
+ let fifoCat = []; // [{afterPos, id}]
24
24
 
25
25
  function resetState() {
26
26
  seenAfterPostNo = new Set();
27
27
  seenAfterTopicPos = new Set();
28
- usedIdsMsg = new Set();
29
- usedIdsBetween = new Set();
28
+ usedIds = new Set();
30
29
  fifo = [];
31
30
  fifoCat = [];
32
31
  }
@@ -51,7 +50,7 @@ function parsePool(raw) {
51
50
  }
52
51
 
53
52
  async function fetchConfig() {
54
- if (cachedConfig && Date.now() - lastFetch < 8000) return cachedConfig;
53
+ if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
55
54
  const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
56
55
  cachedConfig = await res.json();
57
56
  lastFetch = Date.now();
@@ -59,10 +58,7 @@ async function fetchConfig() {
59
58
  }
60
59
 
61
60
  function isTopicPage() {
62
- try {
63
- if (ajaxify && ajaxify.data && ajaxify.data.tid) return true;
64
- } catch (e) {}
65
- return /^\/topic\//.test(window.location.pathname);
61
+ return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
66
62
  }
67
63
 
68
64
  function isCategoryTopicListPage() {
@@ -73,7 +69,7 @@ function getTopicPosts() {
73
69
  const $primary = $('[component="post"][data-pid]');
74
70
  if ($primary.length) return $primary;
75
71
 
76
- // Fallback: top-level nodes with post/content (avoid nested)
72
+ // fallback: top-level with post/content
77
73
  return $('[data-pid]').filter(function () {
78
74
  const $el = $(this);
79
75
  const hasContent = $el.find('[component="post/content"]').length > 0;
@@ -86,7 +82,7 @@ function getCategoryTopicItems() {
86
82
  return $('li[component="category/topic"]');
87
83
  }
88
84
 
89
- // If target's parent is UL/OL, wrapper must be LI
85
+ // If target's parent is UL/OL, wrapper MUST be LI (otherwise browser may move it to top)
90
86
  function wrapperTagFor($target) {
91
87
  if (!$target || !$target.length) return 'div';
92
88
  const parentTag = ($target.parent().prop('tagName') || '').toUpperCase();
@@ -106,202 +102,69 @@ function makeWrapperLike($target, classes, innerHtml, attrs) {
106
102
  }
107
103
 
108
104
  function cleanupOnNav() {
109
- $('.ezoic-ad-post, .ezoic-ad-topic').remove();
105
+ $('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad-between').remove();
110
106
  }
111
107
 
112
- function pickNextId(pool, usedSet) {
108
+ function pickNextId(pool) {
113
109
  for (const id of pool) {
114
- if (!usedSet.has(id)) return id;
110
+ if (!usedIds.has(id)) return id;
115
111
  }
116
112
  return null;
117
- } finally {
118
- window.__ezoicRecycling = false;
119
- }
120
113
  }
121
114
 
122
- function destroyEzoicId(id) {
123
- window.__ezoicLastDestroy = window.__ezoicLastDestroy || {};
124
- const now = Date.now();
125
- if (window.__ezoicLastDestroy[id] && now - window.__ezoicLastDestroy[id] < 2000) {
126
- return;
127
- }
128
- window.__ezoicLastDestroy[id] = now;
129
- try {
130
- window.ezstandalone = window.ezstandalone || {};
131
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
132
- const fn = function () {
133
- try {
134
- if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
135
- window.ezstandalone.destroyPlaceholders(id);
136
- }
137
- } catch (e) {}
138
- };
139
- if (typeof window.ezstandalone.destroyPlaceholders === 'function') fn();
140
- else window.ezstandalone.cmd.push(fn);
141
- } catch (e) {}
142
- }
143
-
144
- function recycleOldestTopicId($posts) {
145
- if (window.__ezoicRecycling) return null;
146
- window.__ezoicRecycling = true;
147
- try {
148
- if (!fifo.length) return null;
115
+ function removeOldestTopicAd() {
149
116
  fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
117
+ const old = fifo.shift();
118
+ if (!old) return false;
150
119
 
151
- while (fifo.length) {
152
- const old = fifo.shift();
153
- const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
154
- const $el = $(sel);
155
- if (!$el.length) continue;
120
+ const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
121
+ const $el = $(sel);
122
+ if ($el.length) $el.remove();
156
123
 
157
- // Never recycle the ad that is right after the last real post (sentinel)
158
- try {
159
- const $last = $posts.last();
160
- if ($last.length && $el.prev().is($last)) {
161
- fifo.push(old);
162
- return null;
163
- }
164
- } catch (e) {}
165
-
166
- $el.remove();
167
- usedIdsMsg.delete(old.id);
168
- destroyEzoicId(old.id);
169
- return old.id;
170
- }
171
- return null;
172
- } finally {
173
- window.__ezoicRecycling = false;
174
- }
124
+ usedIds.delete(old.id);
125
+ // DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
126
+ return true;
175
127
  }
176
128
 
177
- function recycleOldestCategoryId($items) {
178
- if (window.__ezoicRecycling) return null;
179
- window.__ezoicRecycling = true;
180
- try {
181
- if (!fifoCat.length) return null;
129
+ function removeOldestCategoryAd() {
182
130
  fifoCat.sort((a, b) => a.afterPos - b.afterPos);
131
+ const old = fifoCat.shift();
132
+ if (!old) return false;
183
133
 
184
- while (fifoCat.length) {
185
- const old = fifoCat.shift();
186
- const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
187
- const $el = $(sel);
188
- if (!$el.length) continue;
134
+ const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
135
+ const $el = $(sel);
136
+ if ($el.length) $el.remove();
189
137
 
190
- try {
191
- const $last = $items.last();
192
- if ($last.length && $el.prev().is($last)) {
193
- fifoCat.push(old);
194
- return null;
195
- }
196
- } catch (e) {}
197
-
198
- $el.remove();
199
- usedIdsBetween.delete(old.id);
200
- destroyEzoicId(old.id);
201
- return old.id;
202
- }
203
- return null;
204
- }
205
-
206
- function setupAdAutoHeight() {
207
- if (window.__ezoicAutoHeightAttached) return;
208
- window.__ezoicAutoHeightAttached = true;
209
-
210
- try {
211
- const ro = new ResizeObserver(function () {
212
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
213
- // Collapse until content is injected
214
- if (!wrap.classList.contains('ezoic-filled')) {
215
- wrap.style.display = 'none';
216
- }
217
- wrap.style.minHeight = '0px';
218
- wrap.style.height = 'auto';
219
- });
220
- });
221
-
222
- function attach() {
223
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
224
- // Collapse until content is injected
225
- if (!wrap.classList.contains('ezoic-filled')) {
226
- wrap.style.display = 'none';
227
- }
228
- if (wrap.__ezoicRO) return;
229
- wrap.__ezoicRO = true;
230
- ro.observe(wrap);
231
- });
232
- }
233
-
234
- attach();
235
- setTimeout(attach, 600);
236
- setTimeout(attach, 1600);
237
- } catch (e) {}
138
+ usedIds.delete(old.id);
139
+ return true;
238
140
  }
239
141
 
240
142
  function callEzoic(ids) {
143
+ // ids optional; if omitted, we will scan DOM for unrendered placeholders
241
144
  window.ezstandalone = window.ezstandalone || {};
242
145
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
243
146
 
244
- const markAndFilterByIds = function (list) {
245
- const out = [];
246
- list.forEach(function (id) {
247
- const sel = '.ezoic-ad[data-ezoic-id="' + id + '"]';
248
- const wraps = document.querySelectorAll(sel);
249
- let anyUnrendered = false;
250
- wraps.forEach(function (w) {
251
- if (w.getAttribute('data-ezoic-rendered') !== '1') {
252
- anyUnrendered = true;
253
- }
254
- });
255
- if (!wraps.length) {
256
- // fallback: if wrapper is missing, allow showAds (Ezoic might still handle placeholder)
257
- out.push(id);
258
- return;
259
- }
260
- if (anyUnrendered) {
261
- wraps.forEach(function (w) { w.setAttribute('data-ezoic-rendered', '1'); });
262
- out.push(id);
263
- }
264
- });
265
- return out;
266
- };
267
-
268
147
  const collect = function () {
269
148
  const list = [];
270
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
271
- // Collapse until content is injected
272
- if (!wrap.classList.contains('ezoic-filled')) {
273
- wrap.style.display = 'none';
274
- }
275
- if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
276
- const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
277
- if (ph && ph.children && ph.children.length) {
278
- // Content injected
279
- wrap.classList.add('ezoic-filled');
280
- wrap.style.display = '';
281
- }
282
- if (!ph) return;
149
+ document.querySelectorAll('.ezoic-ad [id^="ezoic-pub-ad-placeholder-"]').forEach(function (ph) {
283
150
  const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
284
151
  const id = parseInt(idStr, 10);
285
152
  if (!Number.isFinite(id) || id <= 0) return;
286
- wrap.setAttribute('data-ezoic-rendered', '1');
153
+
154
+ const wrap = ph.closest('.ezoic-ad');
155
+ if (!wrap) return;
156
+ if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
157
+
287
158
  list.push(id);
159
+ wrap.setAttribute('data-ezoic-rendered', '1');
288
160
  });
161
+ // de-dupe
289
162
  return Array.from(new Set(list));
290
163
  };
291
164
 
292
- const input = (ids && ids.length) ? Array.from(new Set(ids)) : null;
293
- const toShow = input ? markAndFilterByIds(input) : collect();
165
+ const toShow = (ids && ids.length) ? Array.from(new Set(ids)) : collect();
294
166
  if (!toShow.length) return;
295
167
 
296
- // De-dupe rapid duplicate calls (observer/poller can fire twice)
297
- const key = toShow.join(',');
298
- const now = Date.now();
299
- if (window.__ezoicLastShowKey === key && (now - (window.__ezoicLastShowAt || 0)) < 1200) {
300
- return;
301
- }
302
- window.__ezoicLastShowKey = key;
303
- window.__ezoicLastShowAt = now;
304
-
305
168
  const run = function () {
306
169
  try {
307
170
  if (typeof window.ezstandalone.showAds === 'function') {
@@ -338,15 +201,13 @@ function callEzoic(ids) {
338
201
  }
339
202
 
340
203
  function getPostNumber($post) {
341
- // Harmony: <a class="post-index">#72</a>
204
+ const di = parseInt($post.attr('data-index'), 10);
205
+ if (Number.isFinite(di) && di > 0) return di;
206
+
342
207
  const txt = ($post.find('a.post-index').first().text() || '').trim();
343
208
  const m = txt.match(/#\s*(\d+)/);
344
209
  if (m) return parseInt(m[1], 10);
345
210
 
346
- // fallback data-index (often 0/1 based)
347
- const di = parseInt($post.attr('data-index'), 10);
348
- if (Number.isFinite(di) && di >= 0) return di + 1;
349
-
350
211
  return NaN;
351
212
  }
352
213
 
@@ -363,20 +224,16 @@ function injectTopicMessageAds($posts, pool, interval) {
363
224
 
364
225
  $posts.each(function () {
365
226
  const $p = $(this);
366
- // Never insert after the last post (sentinel)
227
+ // Never insert after the last real post: it can break NodeBB infinite scroll
367
228
  if ($p.is($posts.last())) return;
368
-
369
229
  const postNo = getPostNumber($p);
370
230
  if (!Number.isFinite(postNo) || postNo <= 0) return;
371
231
 
372
232
  if (postNo % interval !== 0) return;
373
233
  if (seenAfterPostNo.has(postNo)) return;
374
234
 
375
- let id = pickNextId(pool, usedIdsMsg);
376
- if (!id) {
377
- id = recycleOldestTopicId($posts);
378
- if (!id) return;
379
- }
235
+ let id = pickNextId(pool);
236
+ if (!id) { return; }
380
237
 
381
238
  const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
382
239
  const html = makeWrapperLike(
@@ -389,7 +246,7 @@ function injectTopicMessageAds($posts, pool, interval) {
389
246
  $p.after(html);
390
247
 
391
248
  seenAfterPostNo.add(postNo);
392
- usedIdsMsg.add(id);
249
+ usedIds.add(id);
393
250
  fifo.push({ afterPostNo: postNo, id: id });
394
251
  newIds.push(id);
395
252
  });
@@ -402,20 +259,16 @@ function injectCategoryBetweenAds($items, pool, interval) {
402
259
 
403
260
  $items.each(function () {
404
261
  const $it = $(this);
405
- // Never insert after the last topic item (sentinel)
262
+ // Never insert after the last real topic item (keeps NodeBB infinite scroll working)
406
263
  if ($it.is($items.last())) return;
407
-
408
264
  const pos = getTopicPos($it);
409
265
  if (!Number.isFinite(pos) || pos <= 0) return;
410
266
 
411
267
  if (pos % interval !== 0) return;
412
268
  if (seenAfterTopicPos.has(pos)) return;
413
269
 
414
- let id = pickNextId(pool, usedIdsBetween);
415
- if (!id) {
416
- id = recycleOldestCategoryId($items);
417
- if (!id) return;
418
- }
270
+ let id = pickNextId(pool);
271
+ if (!id) { return; }
419
272
 
420
273
  const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
421
274
  const html = makeWrapperLike(
@@ -428,7 +281,7 @@ function injectCategoryBetweenAds($items, pool, interval) {
428
281
  $it.after(html);
429
282
 
430
283
  seenAfterTopicPos.add(pos);
431
- usedIdsBetween.add(id);
284
+ usedIds.add(id);
432
285
  fifoCat.push({ afterPos: pos, id: id });
433
286
  newIds.push(id);
434
287
  });
@@ -437,9 +290,6 @@ function injectCategoryBetweenAds($items, pool, interval) {
437
290
  }
438
291
 
439
292
  async function refreshAds() {
440
- const now = Date.now();
441
- if (window.__ezoicRefreshThrottle && now - window.__ezoicRefreshThrottle < 250) return;
442
- window.__ezoicRefreshThrottle = now;
443
293
  const key = getPageKey();
444
294
  if (pageKey !== key) {
445
295
  pageKey = key;
@@ -460,20 +310,17 @@ async function refreshAds() {
460
310
  const messagePool = parsePool(cfg.messagePlaceholderIds);
461
311
  const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
462
312
 
463
- const hasTopicList = isCategoryTopicListPage();
464
313
  const onTopic = isTopicPage();
465
- const onCategory = hasTopicList && !onTopic;
314
+ const onCategory = !onTopic && isCategoryTopicListPage();
466
315
 
467
316
  const newIds = [];
468
317
 
469
- // Rule: topic list => between only ; topic page => message only
470
318
  if (onCategory) {
471
319
  const $items = getCategoryTopicItems();
472
320
  if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
473
321
  newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
474
322
  }
475
323
  callEzoic(newIds);
476
- // also ensure any unrendered placeholders are rendered
477
324
  callEzoic();
478
325
  return;
479
326
  }
@@ -484,6 +331,7 @@ async function refreshAds() {
484
331
  newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
485
332
  }
486
333
  callEzoic(newIds);
334
+ callEzoic();
487
335
  }
488
336
  } finally {
489
337
  inFlight = false;
@@ -499,47 +347,28 @@ function debounceRefresh() {
499
347
  debounceTimer = setTimeout(refreshAds, 220);
500
348
  }
501
349
 
502
- $(document).ready(function () { setupAdAutoHeight(); debounceRefresh(); });
503
-
504
- $(window).on('action:ajaxify.start', function (ev, data) {
505
- // IMPORTANT: do not cleanup on infinite scroll loads; only when navigating to a different page.
506
- try {
507
- const targetUrl = (data && (data.url || data.href)) ? String(data.url || data.href) : '';
508
- if (targetUrl) {
509
- const a = document.createElement('a');
510
- a.href = targetUrl;
511
- const targetPath = a.pathname || targetUrl;
512
- // If we're staying on the same logical page (same path), don't wipe ads/state
513
- if (targetPath === window.location.pathname) {
514
- return;
515
- }
516
- }
517
- } catch (e) {}
518
- pageKey = null;
519
- resetState();
520
- cleanupOnNav();
521
- });
350
+ $(document).ready(debounceRefresh);
522
351
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
352
+ setTimeout(debounceRefresh, 2200);
523
353
 
524
- setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
525
-
526
- // Observer + poller: ensure we rerun when NodeBB injects new items
354
+ // Fallback: some themes/devices don't emit the expected events for infinite scroll.
355
+ // Observe DOM additions and trigger a refresh when new posts/topics are appended/prepended.
527
356
  (function setupEzoicObserver() {
528
357
  if (window.__ezoicInfiniteObserver) return;
529
358
  try {
530
- const trigger = function () { debounceRefresh(); };
531
-
532
359
  const obs = new MutationObserver(function (mutations) {
533
360
  for (const m of mutations) {
534
361
  if (!m.addedNodes || !m.addedNodes.length) continue;
535
362
  for (const n of m.addedNodes) {
536
363
  if (!n || n.nodeType !== 1) continue;
364
+ // direct match
537
365
  if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
538
- trigger();
366
+ debounceRefresh();
539
367
  return;
540
368
  }
369
+ // descendant match
541
370
  if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
542
- trigger();
371
+ debounceRefresh();
543
372
  return;
544
373
  }
545
374
  }
@@ -547,19 +376,5 @@ setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
547
376
  });
548
377
  obs.observe(document.body, { childList: true, subtree: true });
549
378
  window.__ezoicInfiniteObserver = obs;
550
-
551
- let lastPostCount = 0;
552
- let lastTopicCount = 0;
553
- setInterval(function () {
554
- try {
555
- const posts = document.querySelectorAll('[component="post"][data-pid]').length;
556
- const topics = document.querySelectorAll('li[component="category/topic"]').length;
557
- if (posts !== lastPostCount || topics !== lastTopicCount) {
558
- lastPostCount = posts;
559
- lastTopicCount = topics;
560
- trigger();
561
- }
562
- } catch (e) {}
563
- }, 1500);
564
379
  } catch (e) {}
565
380
  })();
package/public/style.css CHANGED
@@ -1,4 +1,2 @@
1
- .ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
2
- .ezoic-ad:not(.ezoic-filled){display:none !important;}
3
- .ezoic-ad-message-inner{padding:0;margin:0;}
4
- .ezoic-ad-message-inner > div{margin:0;padding:0;}
1
+ .ezoic-ad-post{margin:0.75rem 0;}
2
+ .ezoic-ad-message-inner{padding:0.75rem 0;}