nodebb-plugin-ezoic-infinite 0.8.6 → 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.6",
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,185 +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
- }
121
-
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
113
  }
143
114
 
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
- wrap.style.minHeight = '0px';
214
- wrap.style.height = 'auto';
215
- });
216
- });
217
-
218
- function attach() {
219
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
220
- if (wrap.__ezoicRO) return;
221
- wrap.__ezoicRO = true;
222
- ro.observe(wrap);
223
- });
224
- }
225
-
226
- attach();
227
- setTimeout(attach, 600);
228
- setTimeout(attach, 1600);
229
- } catch (e) {}
138
+ usedIds.delete(old.id);
139
+ return true;
230
140
  }
231
141
 
232
142
  function callEzoic(ids) {
143
+ // ids optional; if omitted, we will scan DOM for unrendered placeholders
233
144
  window.ezstandalone = window.ezstandalone || {};
234
145
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
235
146
 
236
- const markAndFilterByIds = function (list) {
237
- const out = [];
238
- list.forEach(function (id) {
239
- const sel = '.ezoic-ad[data-ezoic-id="' + id + '"]';
240
- const wraps = document.querySelectorAll(sel);
241
- let anyUnrendered = false;
242
- wraps.forEach(function (w) {
243
- if (w.getAttribute('data-ezoic-rendered') !== '1') {
244
- anyUnrendered = true;
245
- }
246
- });
247
- if (!wraps.length) {
248
- // fallback: if wrapper is missing, allow showAds (Ezoic might still handle placeholder)
249
- out.push(id);
250
- return;
251
- }
252
- if (anyUnrendered) {
253
- wraps.forEach(function (w) { w.setAttribute('data-ezoic-rendered', '1'); });
254
- out.push(id);
255
- }
256
- });
257
- return out;
258
- };
259
-
260
147
  const collect = function () {
261
148
  const list = [];
262
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
263
- if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
264
- const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
265
- if (!ph) return;
149
+ document.querySelectorAll('.ezoic-ad [id^="ezoic-pub-ad-placeholder-"]').forEach(function (ph) {
266
150
  const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
267
151
  const id = parseInt(idStr, 10);
268
152
  if (!Number.isFinite(id) || id <= 0) return;
269
- 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
+
270
158
  list.push(id);
159
+ wrap.setAttribute('data-ezoic-rendered', '1');
271
160
  });
161
+ // de-dupe
272
162
  return Array.from(new Set(list));
273
163
  };
274
164
 
275
- const input = (ids && ids.length) ? Array.from(new Set(ids)) : null;
276
- const toShow = input ? markAndFilterByIds(input) : collect();
165
+ const toShow = (ids && ids.length) ? Array.from(new Set(ids)) : collect();
277
166
  if (!toShow.length) return;
278
167
 
279
- // De-dupe rapid duplicate calls (observer/poller can fire twice)
280
- const key = toShow.join(',');
281
- const now = Date.now();
282
- if (window.__ezoicLastShowKey === key && (now - (window.__ezoicLastShowAt || 0)) < 1200) {
283
- return;
284
- }
285
- window.__ezoicLastShowKey = key;
286
- window.__ezoicLastShowAt = now;
287
-
288
168
  const run = function () {
289
169
  try {
290
170
  if (typeof window.ezstandalone.showAds === 'function') {
@@ -321,15 +201,13 @@ function callEzoic(ids) {
321
201
  }
322
202
 
323
203
  function getPostNumber($post) {
324
- // 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
+
325
207
  const txt = ($post.find('a.post-index').first().text() || '').trim();
326
208
  const m = txt.match(/#\s*(\d+)/);
327
209
  if (m) return parseInt(m[1], 10);
328
210
 
329
- // fallback data-index (often 0/1 based)
330
- const di = parseInt($post.attr('data-index'), 10);
331
- if (Number.isFinite(di) && di >= 0) return di + 1;
332
-
333
211
  return NaN;
334
212
  }
335
213
 
@@ -346,20 +224,16 @@ function injectTopicMessageAds($posts, pool, interval) {
346
224
 
347
225
  $posts.each(function () {
348
226
  const $p = $(this);
349
- // Never insert after the last post (sentinel)
227
+ // Never insert after the last real post: it can break NodeBB infinite scroll
350
228
  if ($p.is($posts.last())) return;
351
-
352
229
  const postNo = getPostNumber($p);
353
230
  if (!Number.isFinite(postNo) || postNo <= 0) return;
354
231
 
355
232
  if (postNo % interval !== 0) return;
356
233
  if (seenAfterPostNo.has(postNo)) return;
357
234
 
358
- let id = pickNextId(pool, usedIdsMsg);
359
- if (!id) {
360
- id = recycleOldestTopicId($posts);
361
- if (!id) return;
362
- }
235
+ let id = pickNextId(pool);
236
+ if (!id) { return; }
363
237
 
364
238
  const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
365
239
  const html = makeWrapperLike(
@@ -372,7 +246,7 @@ function injectTopicMessageAds($posts, pool, interval) {
372
246
  $p.after(html);
373
247
 
374
248
  seenAfterPostNo.add(postNo);
375
- usedIdsMsg.add(id);
249
+ usedIds.add(id);
376
250
  fifo.push({ afterPostNo: postNo, id: id });
377
251
  newIds.push(id);
378
252
  });
@@ -385,20 +259,16 @@ function injectCategoryBetweenAds($items, pool, interval) {
385
259
 
386
260
  $items.each(function () {
387
261
  const $it = $(this);
388
- // Never insert after the last topic item (sentinel)
262
+ // Never insert after the last real topic item (keeps NodeBB infinite scroll working)
389
263
  if ($it.is($items.last())) return;
390
-
391
264
  const pos = getTopicPos($it);
392
265
  if (!Number.isFinite(pos) || pos <= 0) return;
393
266
 
394
267
  if (pos % interval !== 0) return;
395
268
  if (seenAfterTopicPos.has(pos)) return;
396
269
 
397
- let id = pickNextId(pool, usedIdsBetween);
398
- if (!id) {
399
- id = recycleOldestCategoryId($items);
400
- if (!id) return;
401
- }
270
+ let id = pickNextId(pool);
271
+ if (!id) { return; }
402
272
 
403
273
  const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
404
274
  const html = makeWrapperLike(
@@ -411,7 +281,7 @@ function injectCategoryBetweenAds($items, pool, interval) {
411
281
  $it.after(html);
412
282
 
413
283
  seenAfterTopicPos.add(pos);
414
- usedIdsBetween.add(id);
284
+ usedIds.add(id);
415
285
  fifoCat.push({ afterPos: pos, id: id });
416
286
  newIds.push(id);
417
287
  });
@@ -420,9 +290,6 @@ function injectCategoryBetweenAds($items, pool, interval) {
420
290
  }
421
291
 
422
292
  async function refreshAds() {
423
- const now = Date.now();
424
- if (window.__ezoicRefreshThrottle && now - window.__ezoicRefreshThrottle < 250) return;
425
- window.__ezoicRefreshThrottle = now;
426
293
  const key = getPageKey();
427
294
  if (pageKey !== key) {
428
295
  pageKey = key;
@@ -443,20 +310,17 @@ async function refreshAds() {
443
310
  const messagePool = parsePool(cfg.messagePlaceholderIds);
444
311
  const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
445
312
 
446
- const hasTopicList = isCategoryTopicListPage();
447
313
  const onTopic = isTopicPage();
448
- const onCategory = hasTopicList && !onTopic;
314
+ const onCategory = !onTopic && isCategoryTopicListPage();
449
315
 
450
316
  const newIds = [];
451
317
 
452
- // Rule: topic list => between only ; topic page => message only
453
318
  if (onCategory) {
454
319
  const $items = getCategoryTopicItems();
455
320
  if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
456
321
  newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
457
322
  }
458
323
  callEzoic(newIds);
459
- // also ensure any unrendered placeholders are rendered
460
324
  callEzoic();
461
325
  return;
462
326
  }
@@ -467,6 +331,7 @@ async function refreshAds() {
467
331
  newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
468
332
  }
469
333
  callEzoic(newIds);
334
+ callEzoic();
470
335
  }
471
336
  } finally {
472
337
  inFlight = false;
@@ -482,47 +347,28 @@ function debounceRefresh() {
482
347
  debounceTimer = setTimeout(refreshAds, 220);
483
348
  }
484
349
 
485
- $(document).ready(function () { setupAdAutoHeight(); debounceRefresh(); });
486
-
487
- $(window).on('action:ajaxify.start', function (ev, data) {
488
- // IMPORTANT: do not cleanup on infinite scroll loads; only when navigating to a different page.
489
- try {
490
- const targetUrl = (data && (data.url || data.href)) ? String(data.url || data.href) : '';
491
- if (targetUrl) {
492
- const a = document.createElement('a');
493
- a.href = targetUrl;
494
- const targetPath = a.pathname || targetUrl;
495
- // If we're staying on the same logical page (same path), don't wipe ads/state
496
- if (targetPath === window.location.pathname) {
497
- return;
498
- }
499
- }
500
- } catch (e) {}
501
- pageKey = null;
502
- resetState();
503
- cleanupOnNav();
504
- });
350
+ $(document).ready(debounceRefresh);
505
351
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
352
+ setTimeout(debounceRefresh, 2200);
506
353
 
507
- setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
508
-
509
- // 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.
510
356
  (function setupEzoicObserver() {
511
357
  if (window.__ezoicInfiniteObserver) return;
512
358
  try {
513
- const trigger = function () { debounceRefresh(); };
514
-
515
359
  const obs = new MutationObserver(function (mutations) {
516
360
  for (const m of mutations) {
517
361
  if (!m.addedNodes || !m.addedNodes.length) continue;
518
362
  for (const n of m.addedNodes) {
519
363
  if (!n || n.nodeType !== 1) continue;
364
+ // direct match
520
365
  if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
521
- trigger();
366
+ debounceRefresh();
522
367
  return;
523
368
  }
369
+ // descendant match
524
370
  if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
525
- trigger();
371
+ debounceRefresh();
526
372
  return;
527
373
  }
528
374
  }
@@ -530,19 +376,5 @@ setTimeout(function () { setupAdAutoHeight(); debounceRefresh(); }, 2200);
530
376
  });
531
377
  obs.observe(document.body, { childList: true, subtree: true });
532
378
  window.__ezoicInfiniteObserver = obs;
533
-
534
- let lastPostCount = 0;
535
- let lastTopicCount = 0;
536
- setInterval(function () {
537
- try {
538
- const posts = document.querySelectorAll('[component="post"][data-pid]').length;
539
- const topics = document.querySelectorAll('li[component="category/topic"]').length;
540
- if (posts !== lastPostCount || topics !== lastTopicCount) {
541
- lastPostCount = posts;
542
- lastTopicCount = topics;
543
- trigger();
544
- }
545
- } catch (e) {}
546
- }, 1500);
547
379
  } catch (e) {}
548
380
  })();
package/public/style.css CHANGED
@@ -1,3 +1,2 @@
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;}
1
+ .ezoic-ad-post{margin:0.75rem 0;}
2
+ .ezoic-ad-message-inner{padding:0.75rem 0;}