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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.7.9",
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
@@ -22,8 +22,5 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "templates": "public/templates",
26
- "css": [
27
- "public/style.css"
28
- ]
25
+ "templates": "public/templates"
29
26
  }
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 (keyed by tid/cid)
13
+ // Per-page state
14
14
  let pageKey = null;
15
15
 
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}]
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 state: anchor to absolute topic position if available
22
- let seenAfterTopicPos = new Set(); // topic positions we've inserted after
23
- let fifoCat = []; // [{afterPos, id}]
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 < 10000) return cachedConfig;
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
- return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
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
- // fallback: top-level with post/content
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 MUST be LI (otherwise browser may move it to top)
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, .ezoic-ad-between').remove();
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 removeOldestTopicAd() {
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
- 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();
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
- usedIds.delete(old.id);
125
- // DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
126
- return true;
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 removeOldestCategoryAd() {
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
- 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();
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
- usedIds.delete(old.id);
139
- return true;
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 callEzoic(ids) {
143
- if (!ids || !ids.length) return;
187
+ function setupAdAutoHeight() {
188
+ if (window.__ezoicAutoHeightAttached) return;
189
+ window.__ezoicAutoHeightAttached = true;
144
190
 
145
- // Prevent duplicate calls (observer/poller can fire multiple times)
146
- const key = Array.isArray(ids) ? ids.join(',') : String(ids);
147
- const now = Date.now();
148
- if (window.__ezoicLastShowKey === key && (now - (window.__ezoicLastShowAt || 0)) < 2500) {
149
- return;
150
- }
151
- window.__ezoicLastShowKey = key;
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, ids);
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 only if showAds isn't available yet
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
- const di = parseInt($post.attr('data-index'), 10);
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 real post: it can break NodeBB infinite scroll
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) { return; }
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 real topic item (keeps NodeBB infinite scroll working)
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) { return; }
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 = !onTopic && isCategoryTopicListPage();
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
- // Fallback: some themes/devices don't emit the expected events for infinite scroll.
347
- // Observe DOM additions and trigger a refresh when new posts/topics are appended/prepended.
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
- debounceRefresh();
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
- debounceRefresh();
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-post{margin:0.75rem 0;}
2
- .ezoic-ad-message-inner{padding:0.75rem 0;}
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;}