nodebb-plugin-ezoic-infinite 1.3.0 → 1.4.2

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/library.js CHANGED
@@ -34,13 +34,21 @@ async function getAllGroups() {
34
34
  async function getSettings() {
35
35
  const s = await meta.settings.get(SETTINGS_KEY);
36
36
  return {
37
- // Between-post ads (simple blocks)
37
+ // Between-post ads (simple blocks) in category topic list
38
38
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
39
40
  placeholderIds: (s.placeholderIds || '').trim(),
40
41
  intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
41
42
 
43
+ // Home/categories list ads (between categories on / or /categories)
44
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
45
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
46
+ categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
47
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
48
+
42
49
  // "Ad message" between replies (looks like a post)
43
50
  enableMessageAds: parseBool(s.enableMessageAds, false),
51
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
44
52
  messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
45
53
  messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
46
54
 
@@ -88,9 +96,15 @@ plugin.init = async ({ router, middleware }) => {
88
96
  res.json({
89
97
  excluded,
90
98
  enableBetweenAds: settings.enableBetweenAds,
99
+ showFirstTopicAd: settings.showFirstTopicAd,
91
100
  placeholderIds: settings.placeholderIds,
92
101
  intervalPosts: settings.intervalPosts,
102
+ enableCategoryAds: settings.enableCategoryAds,
103
+ showFirstCategoryAd: settings.showFirstCategoryAd,
104
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
105
+ intervalCategories: settings.intervalCategories,
93
106
  enableMessageAds: settings.enableMessageAds,
107
+ showFirstMessageAd: settings.showFirstMessageAd,
94
108
  messagePlaceholderIds: settings.messagePlaceholderIds,
95
109
  messageIntervalPosts: settings.messageIntervalPosts,
96
110
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.3.0",
3
+ "version": "1.4.2",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  (function () {
3
2
  'use strict';
4
3
 
@@ -7,6 +6,7 @@
7
6
  const SELECTORS = {
8
7
  topicItem: 'li[component="category/topic"]',
9
8
  postItem: '[component="post"][data-pid]',
9
+ categoryItem: 'li[component="categories/category"]',
10
10
  };
11
11
 
12
12
  const WRAP_CLASS = 'ezoic-ad';
@@ -21,9 +21,17 @@
21
21
 
22
22
  poolTopics: [],
23
23
  poolPosts: [],
24
+ poolCategories: [],
24
25
 
25
26
  usedTopics: new Set(),
26
27
  usedPosts: new Set(),
28
+ usedCategories: new Set(),
29
+
30
+ // wrappers currently on page (FIFO for recycling)
31
+ liveBetween: [],
32
+ liveMessage: [],
33
+ liveCategory: [],
34
+ usedCategories: new Set(),
27
35
 
28
36
  lastShowById: new Map(),
29
37
  pendingById: new Set(),
@@ -72,15 +80,23 @@
72
80
  function getKind() {
73
81
  const p = window.location.pathname || '';
74
82
  if (/^\/topic\//.test(p)) return 'topic';
75
- if (/^\/category\//.test(p)) return 'category';
76
- if (document.querySelector('[component="topic"]') || document.querySelector('[component="post"][data-pid]')) return 'topic';
77
- return 'category';
83
+ if (/^\/category\//.test(p)) return 'categoryTopics';
84
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
85
+ // fallback by DOM
86
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
87
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
88
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
89
+ return 'other';
78
90
  }
79
91
 
80
92
  function getTopicItems() {
81
93
  return Array.from(document.querySelectorAll(SELECTORS.topicItem));
82
94
  }
83
95
 
96
+ function getCategoryItems() {
97
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
98
+ }
99
+
84
100
  function getPostContainers() {
85
101
  const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
86
102
  return nodes.filter((el) => {
@@ -93,9 +109,63 @@
93
109
  });
94
110
  }
95
111
 
96
- function buildWrap(id, kind, afterPos) {
112
+
113
+ function safeRect(el) {
114
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
115
+ }
116
+
117
+ function destroyPlaceholderIds(ids) {
118
+ if (!ids || !ids.length) return;
119
+ const call = () => {
120
+ try {
121
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
122
+ window.ezstandalone.destroyPlaceholders(ids);
123
+ }
124
+ } catch (e) {}
125
+ };
126
+ try {
127
+ window.ezstandalone = window.ezstandalone || {};
128
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
129
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
130
+ else window.ezstandalone.cmd.push(call);
131
+ } catch (e) {}
132
+ }
133
+
134
+ function getRecyclable(liveArr) {
135
+ const margin = 1800;
136
+ for (let i = 0; i < liveArr.length; i++) {
137
+ const entry = liveArr[i];
138
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
139
+ const r = safeRect(entry.wrap);
140
+ if (r && r.bottom < -margin) {
141
+ liveArr.splice(i, 1);
142
+ return entry;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function moveWrapAfter(wrap, target, kindClass, afterPos) {
149
+ try {
150
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
151
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
152
+ target.insertAdjacentElement('afterend', wrap);
153
+ return true;
154
+ } catch (e) {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function pickId(pool, liveArr) {
160
+ if (pool.length) return { id: pool.shift(), recycled: null };
161
+ const recycled = getRecyclable(liveArr);
162
+ if (recycled) return { id: recycled.id, recycled };
163
+ return { id: null, recycled: null };
164
+ }
165
+
166
+ function buildWrap(id, kindClass, afterPos) {
97
167
  const wrap = document.createElement('div');
98
- wrap.className = `${WRAP_CLASS} ${kind}`;
168
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
99
169
  wrap.setAttribute('data-ezoic-after', String(afterPos));
100
170
  wrap.style.width = '100%';
101
171
 
@@ -122,15 +192,10 @@
122
192
  try {
123
193
  state.usedTopics.forEach((id) => ids.push(id));
124
194
  state.usedPosts.forEach((id) => ids.push(id));
195
+ state.usedCategories && state.usedCategories.forEach((id) => ids.push(id));
125
196
  } catch (e) {}
126
-
127
- if (!ids.length) return;
128
-
129
- const call = () => {
130
- try {
131
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
132
- window.ezstandalone.destroyPlaceholders(ids);
133
- }
197
+ destroyPlaceholderIds(ids);
198
+ }
134
199
  } catch (e) {}
135
200
  };
136
201
 
@@ -228,9 +293,9 @@
228
293
  })();
229
294
  }
230
295
 
231
- function nextId(kind) {
232
- const pool = (kind === 'between') ? state.poolTopics : state.poolPosts;
233
- if (pool.length) return pool.shift();
296
+ function nextId(pool) {
297
+ // backward compatible: the injector passes the pool array
298
+ if (Array.isArray(pool) && pool.length) return pool.shift();
234
299
  return null;
235
300
  }
236
301
 
@@ -257,6 +322,7 @@
257
322
  function initPools(cfg) {
258
323
  if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
259
324
  if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
325
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
260
326
  }
261
327
 
262
328
  function computeTargets(count, interval, showFirst) {
@@ -266,77 +332,46 @@
266
332
  for (let i = 1; i <= count; i++) {
267
333
  if (i % interval === 0) out.push(i);
268
334
  }
269
- // uniq + sort
270
335
  return Array.from(new Set(out)).sort((a, b) => a - b);
271
336
  }
272
337
 
273
- function injectTopics(cfg) {
274
- if (!normalizeBool(cfg.enableBetweenAds)) return 0;
275
-
276
- const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
277
- const showFirst = normalizeBool(cfg.showFirstTopicAd);
278
-
279
- const items = getTopicItems();
338
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
280
339
  if (!items.length) return 0;
281
-
282
340
  const targets = computeTargets(items.length, interval, showFirst);
283
341
 
284
342
  let inserted = 0;
285
343
  for (const afterPos of targets) {
286
344
  if (inserted >= MAX_INSERTS_PER_RUN) break;
287
345
 
288
- const li = items[afterPos - 1];
289
- if (!li || !li.isConnected) continue;
346
+ const el = items[afterPos - 1];
347
+ if (!el || !el.isConnected) continue;
290
348
 
291
- // Avoid back-to-back ads at load: if afterPos-1 already has an ad wrapper, skip
292
- const prevWrap = findWrap('ezoic-ad-between', afterPos - 1);
349
+ // Prevent back-to-back at load
350
+ const prevWrap = findWrap(kindClass, afterPos - 1);
293
351
  if (prevWrap) continue;
294
352
 
295
- if (findWrap('ezoic-ad-between', afterPos)) continue;
296
-
297
- const id = nextId('between');
298
- if (!id) break;
299
-
300
- state.usedTopics.add(id);
301
- const wrap = insertAfter(li, id, 'ezoic-ad-between', afterPos);
302
- if (!wrap) continue;
303
-
304
- callShowAdsWhenReady(id);
305
- inserted += 1;
306
- }
307
- return inserted;
308
- }
309
-
310
- function injectPosts(cfg) {
311
- if (!normalizeBool(cfg.enableMessageAds)) return 0;
312
-
313
- const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
314
- const showFirst = normalizeBool(cfg.showFirstMessageAd);
315
-
316
- const posts = getPostContainers();
317
- if (!posts.length) return 0;
318
-
319
- const targets = computeTargets(posts.length, interval, showFirst);
320
-
321
- let inserted = 0;
322
- for (const afterPos of targets) {
323
- if (inserted >= MAX_INSERTS_PER_RUN) break;
324
-
325
- const post = posts[afterPos - 1];
326
- if (!post || !post.isConnected) continue;
353
+ if (findWrap(kindClass, afterPos)) continue;
327
354
 
328
- const prevWrap = findWrap('ezoic-ad-message', afterPos - 1);
329
- if (prevWrap) continue;
330
-
331
- if (findWrap('ezoic-ad-message', afterPos)) continue;
355
+ const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
356
+ : (kindClass === 'ezoic-ad-message') ? state.liveMessage
357
+ : state.liveCategory;
332
358
 
333
- const id = nextId('message');
359
+ const pick = pickId(kindPool, liveArr);
360
+ const id = pick.id;
334
361
  if (!id) break;
335
362
 
336
- state.usedPosts.add(id);
337
- const wrap = insertAfter(post, id, 'ezoic-ad-message', afterPos);
338
- if (!wrap) continue;
363
+ let wrap = null;
364
+ if (pick.recycled && pick.recycled.wrap) {
365
+ destroyPlaceholderIds([id]);
366
+ wrap = pick.recycled.wrap;
367
+ if (!moveWrapAfter(wrap, el, kindClass, afterPos)) continue;
368
+ } else {
369
+ usedSet.add(id);
370
+ wrap = insertAfter(el, id, kindClass, afterPos);
371
+ if (!wrap) continue;
372
+ }
339
373
 
374
+ liveArr.push({ id, wrap });
340
375
  callShowAdsWhenReady(id);
341
376
  inserted += 1;
342
377
  }
@@ -344,7 +379,6 @@
344
379
  }
345
380
 
346
381
  function enforceNoAdjacentAds() {
347
- // If DOM changes produce adjacent wrappers (rare but possible), hide the later one.
348
382
  const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
349
383
  for (let i = 0; i < ads.length; i++) {
350
384
  const ad = ads[i];
@@ -363,8 +397,15 @@
363
397
 
364
398
  state.poolTopics = [];
365
399
  state.poolPosts = [];
400
+ state.poolCategories = [];
401
+ state.poolCategories = [];
366
402
  state.usedTopics.clear();
367
403
  state.usedPosts.clear();
404
+ state.usedCategories && state.usedCategories.clear();
405
+ state.liveBetween = [];
406
+ state.liveMessage = [];
407
+ state.liveCategory = [];
408
+ state.usedCategories.clear();
368
409
 
369
410
  state.lastShowById = new Map();
370
411
  state.pendingById = new Set();
@@ -381,7 +422,7 @@
381
422
 
382
423
  function ensureObserver() {
383
424
  if (state.obs) return;
384
- state.obs = new MutationObserver(() => scheduleRun('dom-mutation'));
425
+ state.obs = new MutationObserver(() => scheduleRun('mutation'));
385
426
  try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
386
427
  setTimeout(() => { if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; } }, 15000);
387
428
  }
@@ -397,22 +438,47 @@
397
438
  const kind = getKind();
398
439
  let inserted = 0;
399
440
 
400
- if (kind === 'topic') inserted = injectPosts(cfg);
401
- else inserted = injectTopics(cfg);
441
+ if (kind === 'topic') {
442
+ if (normalizeBool(cfg.enableMessageAds)) {
443
+ inserted = injectBetween('ezoic-ad-message', getPostContainers(),
444
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
445
+ normalizeBool(cfg.showFirstMessageAd),
446
+ 'message',
447
+ state.usedPosts);
448
+ }
449
+ } else if (kind === 'categoryTopics') {
450
+ if (normalizeBool(cfg.enableBetweenAds)) {
451
+ inserted = injectBetween('ezoic-ad-between', getTopicItems(),
452
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
453
+ normalizeBool(cfg.showFirstTopicAd),
454
+ 'between',
455
+ state.usedTopics);
456
+ }
457
+ } else if (kind === 'categories') {
458
+ if (normalizeBool(cfg.enableCategoryAds)) {
459
+ inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
460
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
461
+ normalizeBool(cfg.showFirstCategoryAd),
462
+ 'categories',
463
+ state.usedCategories);
464
+ }
465
+ }
402
466
 
403
467
  enforceNoAdjacentAds();
404
468
 
405
- // If nothing inserted and we have no items yet (first click), retry a few times
406
- const itemCount = (kind === 'topic') ? getPostContainers().length : getTopicItems().length;
407
- if (itemCount === 0 && state.attempts < 25) {
469
+ // If nothing inserted and list isn't in DOM yet (first click), retry a bit
470
+ let count = 0;
471
+ if (kind === 'topic') count = getPostContainers().length;
472
+ else if (kind === 'categoryTopics') count = getTopicItems().length;
473
+ else if (kind === 'categories') count = getCategoryItems().length;
474
+
475
+ if (count === 0 && state.attempts < 25) {
408
476
  state.attempts += 1;
409
477
  setTimeout(() => scheduleRun('await-items'), 120);
410
478
  return;
411
479
  }
412
480
 
413
- if (inserted >= MAX_INSERTS_PER_RUN) {
414
- setTimeout(() => scheduleRun('continue-fill'), 140);
415
- }
481
+ if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
416
482
  }
417
483
 
418
484
  function scheduleRun() {
@@ -468,11 +534,10 @@
468
534
  });
469
535
  }
470
536
 
471
- // Boot
472
537
  cleanup();
473
538
  bind();
474
539
  ensureObserver();
475
540
  state.pageKey = getPageKey();
476
541
  scheduleRun();
477
542
  setTimeout(scheduleRun, 250);
478
- })();
543
+ })();
@@ -26,6 +26,32 @@
26
26
 
27
27
  <hr/>
28
28
 
29
+
30
+ <hr/>
31
+
32
+ <h4 class="mt-3">Pubs entre les catégories (page d’accueil)</h4>
33
+ <p class="form-text">Insère des pubs entre les catégories sur la page d’accueil (liste des catégories).</p>
34
+
35
+ <div class="form-check mb-3">
36
+ <input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
37
+ <label class="form-check-label" for="enableCategoryAds">Activer les pubs entre les catégories</label>
38
+ <div class="form-check mt-2">
39
+ <input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
40
+ <label class="form-check-label">Afficher une pub après la 1ère catégorie</label>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="mb-3">
45
+ <label class="form-label" for="categoryPlaceholderIds">Pool d’IDs Ezoic (catégories)</label>
46
+ <textarea id="categoryPlaceholderIds" name="categoryPlaceholderIds" class="form-control" rows="4">{categoryPlaceholderIds}</textarea>
47
+ <p class="form-text">IDs numériques, un par ligne. Utilise un pool dédié (différent des pools topics/messages).</p>
48
+ </div>
49
+
50
+ <div class="mb-3">
51
+ <label class="form-label" for="intervalCategories">Afficher une pub toutes les N catégories</label>
52
+ <input type="number" id="intervalCategories" name="intervalCategories" class="form-control" value="{intervalCategories}" min="1">
53
+ </div>
54
+
29
55
  <h4 class="mt-3">Pubs “message” entre les réponses</h4>
30
56
  <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
31
57