nodebb-plugin-ezoic-infinite 1.4.71 → 1.4.73

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.
@@ -238,25 +238,17 @@
238
238
  return false;
239
239
  }
240
240
 
241
- function buildWrap(target, id, kindClass, afterPos) {
242
- // Use <li> when inserting inside a <ul>/<ol> list (NodeBB topic/category lists),
243
- // otherwise fall back to <div>. This prevents DOM "repair" that can drop/move placeholders.
244
- const parentTag = (target && target.parentElement && target.parentElement.tagName) ? target.parentElement.tagName.toUpperCase() : '';
245
- const useLi = target && target.tagName && target.tagName.toUpperCase() === 'LI' && (parentTag === 'UL' || parentTag === 'OL');
246
-
247
- const wrap = document.createElement(useLi ? 'li' : 'div');
241
+ function buildWrap(id, kindClass, afterPos) {
242
+ const wrap = document.createElement('div');
248
243
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
249
244
  wrap.setAttribute('data-ezoic-after', String(afterPos));
250
-
251
- // Ensure it behaves like a full-width block inside list layouts
252
245
  wrap.style.width = '100%';
253
- if (useLi) wrap.style.listStyle = 'none';
254
246
 
255
247
  const ph = document.createElement('div');
256
248
  ph.id = `${PLACEHOLDER_PREFIX}${id}`;
257
249
  wrap.appendChild(ph);
258
250
  return wrap;
259
- }
251
+ }
260
252
 
261
253
  function findWrap(kindClass, afterPos) {
262
254
  return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
@@ -276,7 +268,7 @@
276
268
  insertingIds.add(id);
277
269
 
278
270
  try {
279
- const wrap = buildWrap(target, id, kindClass, afterPos);
271
+ const wrap = buildWrap(id, kindClass, afterPos);
280
272
  target.insertAdjacentElement('afterend', wrap);
281
273
  attachFillObserver(wrap, id);
282
274
  return wrap;
@@ -290,24 +282,7 @@
290
282
  if (ids.length) destroyPlaceholderIds(ids);
291
283
  }
292
284
 
293
-
294
- function forcePlaceholderAutoHeight(wrap, id) {
295
- try {
296
- if (!wrap) return;
297
- const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
298
- if (!ph) return;
299
- ph.style.setProperty('height', 'auto', 'important');
300
- ph.style.setProperty('min-height', '0px', 'important');
301
- requestAnimationFrame(() => {
302
- try {
303
- ph.style.setProperty('height', 'auto', 'important');
304
- ph.style.setProperty('min-height', '0px', 'important');
305
- } catch (e) {}
306
- });
307
- } catch (e) {}
308
- }
309
-
310
- function patchShowAds() {
285
+ function patchShowAds() {
311
286
  const applyPatch = () => {
312
287
  try {
313
288
  window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
@@ -343,12 +318,33 @@ function patchShowAds() {
343
318
  }
344
319
  }
345
320
 
346
- function markFilled(wrap, id) {
321
+ function markFilled(wrap) {
347
322
  try {
348
323
  if (!wrap) return;
349
324
  if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
350
325
  wrap.setAttribute('data-ezoic-filled', '1');
351
- if (id) forcePlaceholderAutoHeight(wrap, id);
326
+ } catch (e) {}
327
+ }
328
+
329
+ function forcePlaceholderAutoHeight(wrap, id) {
330
+ try {
331
+ if (!wrap || !id) return;
332
+ const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
333
+ if (!ph) return;
334
+ // Override any reserved height/min-height injected by ad scripts
335
+ try {
336
+ ph.style.setProperty('height', 'auto', 'important');
337
+ ph.style.setProperty('min-height', '0px', 'important');
338
+ } catch (e) {}
339
+ // Some creatives adjust after a frame
340
+ try {
341
+ requestAnimationFrame(() => {
342
+ try {
343
+ ph.style.setProperty('height', 'auto', 'important');
344
+ ph.style.setProperty('min-height', '0px', 'important');
345
+ } catch (e) {}
346
+ });
347
+ } catch (e) {}
352
348
  } catch (e) {}
353
349
  }
354
350
 
@@ -362,13 +358,15 @@ function patchShowAds() {
362
358
  if (!ph) return;
363
359
  // Already filled?
364
360
  if (ph.childNodes && ph.childNodes.length > 0) {
365
- markFilled(wrap, id); // Afficher wrapper
361
+ markFilled(wrap); // Afficher wrapper
362
+ forcePlaceholderAutoHeight(wrap, id);
366
363
  sessionDefinedIds.add(id);
367
364
  return;
368
365
  }
369
366
  const obs = new MutationObserver(() => {
370
367
  if (ph.childNodes && ph.childNodes.length > 0) {
371
- markFilled(wrap, id); // CRITIQUE: Afficher wrapper maintenant
368
+ markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
369
+ forcePlaceholderAutoHeight(wrap, id);
372
370
  try { sessionDefinedIds.add(id); } catch (e) {}
373
371
  try { obs.disconnect(); } catch (e) {}
374
372
  }
@@ -388,7 +386,7 @@ function patchShowAds() {
388
386
  const filled = !!(ph.childNodes && ph.childNodes.length > 0);
389
387
  if (filled) {
390
388
  try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
391
- try { markFilled(wrap, id); } catch (e) {}
389
+ try { markFilled(wrap); } catch (e) {}
392
390
  }
393
391
  return filled;
394
392
  }
@@ -417,17 +415,23 @@ function patchShowAds() {
417
415
  batchShowAdsTimer = setTimeout(() => {
418
416
  if (pendingShowAdsIds.size === 0) return;
419
417
 
420
- const idsArray = Array.from(pendingShowAdsIds);
418
+ // Only keep IDs whose placeholders still exist at execution time
419
+ const idsArray = Array.from(pendingShowAdsIds).filter((id) => {
420
+ const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
421
+ return !!(el && el.isConnected);
422
+ });
421
423
  pendingShowAdsIds.clear();
422
424
 
425
+ if (!idsArray.length) return;
426
+
423
427
  // Appeler showAds avec TOUS les IDs en une fois
424
428
  try {
425
429
  window.ezstandalone = window.ezstandalone || {};
426
430
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
427
431
  window.ezstandalone.cmd.push(function() {
428
432
  if (typeof window.ezstandalone.showAds === 'function') {
429
- // Appel batch: showAds(id1, id2, id3...)
430
- window.ezstandalone.showAds(...idsArray);
433
+ // Call as array (most compatible with ezstandalone)
434
+ window.ezstandalone.showAds(idsArray);
431
435
  // Tracker tous les IDs
432
436
  idsArray.forEach(id => {
433
437
  state.lastShowById.set(id, Date.now());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.71",
3
+ "version": "1.4.73",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/style.css ADDED
@@ -0,0 +1,43 @@
1
+ /*
2
+ NodeBB + Ezoic (standalone)
3
+ Goal: never reserve space until the ad is truly filled, and never keep a fixed/min height.
4
+ */
5
+
6
+ /* Hide wrappers until we detect the placeholder has content (prevents empty gaps if CMP/ads are blocked) */
7
+ .ezoic-ad {
8
+ display: none;
9
+ width: 100%;
10
+ height: auto !important;
11
+ min-height: 0 !important;
12
+ padding: 0 !important;
13
+ margin: 0 !important;
14
+ }
15
+
16
+ .ezoic-ad[data-ezoic-filled="1"] {
17
+ display: block;
18
+ }
19
+
20
+ /* The placeholder must not reserve space */
21
+ .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
22
+ height: auto !important;
23
+ min-height: 0 !important;
24
+ margin: 0 !important;
25
+ padding: 0 !important;
26
+ }
27
+
28
+ /* Avoid baseline gap under iframes/ins */
29
+ .ezoic-ad iframe,
30
+ .ezoic-ad ins {
31
+ display: block !important;
32
+ }
33
+
34
+ /* Remove empty spacer divs that sometimes get injected */
35
+ .ezoic-ad > div:empty {
36
+ display: none !important;
37
+ }
38
+
39
+ /* Keep internal margins/paddings from creating vertical gaps */
40
+ .ezoic-ad * {
41
+ margin: 0 !important;
42
+ padding: 0 !important;
43
+ }
package/README.md DELETED
@@ -1,15 +0,0 @@
1
- # NodeBB Plugin – Ezoic Infinite (Production)
2
-
3
- This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
4
- with full support for infinite scroll.
5
-
6
- ## Key guarantees
7
- - No duplicate ads back-to-back
8
- - One showAds call per placeholder
9
- - Fast reveal (MutationObserver on first child)
10
- - Safe with ajaxify navigation
11
- - Works with NodeBB 4.x + Harmony
12
-
13
- ## Notes
14
- - Placeholders must exist and be selected in Ezoic
15
- - Use separate ID pools for topics vs messages
package/library.js DELETED
@@ -1,130 +0,0 @@
1
- 'use strict';
2
-
3
- const meta = require.main.require('./src/meta');
4
- const groups = require.main.require('./src/groups');
5
- const db = require.main.require('./src/database');
6
-
7
- const SETTINGS_KEY = 'ezoic-infinite';
8
- const plugin = {};
9
-
10
- function normalizeExcludedGroups(value) {
11
- if (!value) return [];
12
- if (Array.isArray(value)) return value;
13
- return String(value).split(',').map(s => s.trim()).filter(Boolean);
14
- }
15
-
16
- function parseBool(v, def = false) {
17
- if (v === undefined || v === null || v === '') return def;
18
- if (typeof v === 'boolean') return v;
19
- const s = String(v).toLowerCase();
20
- return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
- }
22
-
23
- async function getAllGroups() {
24
- let names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
- if (!names || !names.length) {
26
- names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
27
- }
28
- const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
- const data = await groups.getGroupsData(filtered);
30
- // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
- const valid = data.filter(g => g && g.name);
32
- valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
- return valid;
34
- }
35
- let _settingsCache = null;
36
- let _settingsCacheAt = 0;
37
- const SETTINGS_TTL = 30000; // 30s
38
-
39
- async function getSettings() {
40
- const now = Date.now();
41
- if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
42
- const s = await meta.settings.get(SETTINGS_KEY);
43
- _settingsCacheAt = Date.now();
44
- _settingsCache = {
45
- // Between-post ads (simple blocks) in category topic list
46
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
47
- showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
48
- placeholderIds: (s.placeholderIds || '').trim(),
49
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
50
-
51
- // Home/categories list ads (between categories on / or /categories)
52
- enableCategoryAds: parseBool(s.enableCategoryAds, false),
53
- showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
54
- categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
55
- intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
56
-
57
- // "Ad message" between replies (looks like a post)
58
- enableMessageAds: parseBool(s.enableMessageAds, false),
59
- showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
60
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
61
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
62
-
63
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
- };
65
- return _settingsCache;
66
- }
67
-
68
- async function isUserExcluded(uid, excludedGroups) {
69
- if (!uid || !excludedGroups.length) return false;
70
- const userGroups = await groups.getUserGroups([uid]);
71
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
72
- }
73
-
74
- plugin.onSettingsSet = function (data) {
75
- // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
76
- if (data && data.hash === SETTINGS_KEY) {
77
- _settingsCache = null;
78
- }
79
- };
80
-
81
- plugin.addAdminNavigation = async (header) => {
82
- header.plugins = header.plugins || [];
83
- header.plugins.push({
84
- route: '/plugins/ezoic-infinite',
85
- icon: 'fa-ad',
86
- name: 'Ezoic Infinite Ads'
87
- });
88
- return header;
89
- };
90
-
91
- plugin.init = async ({ router, middleware }) => {
92
- async function render(req, res) {
93
- const settings = await getSettings();
94
- const allGroups = await getAllGroups();
95
-
96
- res.render('admin/plugins/ezoic-infinite', {
97
- title: 'Ezoic Infinite Ads',
98
- ...settings,
99
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
100
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
101
- allGroups,
102
- });
103
- }
104
-
105
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
- router.get('/api/admin/plugins/ezoic-infinite', render);
107
-
108
- router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
109
- const settings = await getSettings();
110
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
111
-
112
- res.json({
113
- excluded,
114
- enableBetweenAds: settings.enableBetweenAds,
115
- showFirstTopicAd: settings.showFirstTopicAd,
116
- placeholderIds: settings.placeholderIds,
117
- intervalPosts: settings.intervalPosts,
118
- enableCategoryAds: settings.enableCategoryAds,
119
- showFirstCategoryAd: settings.showFirstCategoryAd,
120
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
121
- intervalCategories: settings.intervalCategories,
122
- enableMessageAds: settings.enableMessageAds,
123
- showFirstMessageAd: settings.showFirstMessageAd,
124
- messagePlaceholderIds: settings.messagePlaceholderIds,
125
- messageIntervalPosts: settings.messageIntervalPosts,
126
- });
127
- });
128
- };
129
-
130
- module.exports = plugin;
package/plugin.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "id": "nodebb-plugin-ezoic-infinite",
3
- "name": "NodeBB Ezoic Infinite Ads",
4
- "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
- "library": "./library.js",
6
- "hooks": [
7
- {
8
- "hook": "static:app.load",
9
- "method": "init"
10
- },
11
- {
12
- "hook": "filter:admin.header.build",
13
- "method": "addAdminNavigation"
14
- },
15
- {
16
- "hook": "action:settings.set",
17
- "method": "onSettingsSet"
18
- }
19
- ],
20
- "staticDirs": {
21
- "public": "public"
22
- },
23
- "acpScripts": [
24
- "public/admin.js"
25
- ],
26
- "scripts": [
27
- "public/client.js"
28
- ],
29
- "templates": "public/templates",
30
- "css": [
31
- "public/style.css"
32
- ]
33
- }
package/public/style.css DELETED
@@ -1,43 +0,0 @@
1
- .ezoic-ad {
2
- height: auto !important;
3
- padding: 0 !important;
4
- margin: 0 !important;
5
- }
6
-
7
- .ezoic-ad * {
8
- margin: 0 !important;
9
- padding: 0 !important;
10
- }
11
-
12
- /* --- Ezoic anti-blank-space fixes (NodeBB + Ajaxify) --- */
13
- .ezoic-ad {
14
- display: none; /* shown only when filled */
15
- width: 100% !important;
16
- height: auto !important;
17
- min-height: 0 !important;
18
- padding: 0 !important;
19
- margin: 0 !important;
20
- }
21
-
22
- .ezoic-ad[data-ezoic-filled="1"]{
23
- display: block;
24
- }
25
-
26
- /* Ezoic placeholder should not reserve fixed height */
27
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"]{
28
- height: auto !important;
29
- min-height: 0 !important;
30
- margin: 0 !important;
31
- padding: 0 !important;
32
- }
33
-
34
- /* prevent baseline gaps under iframes/ins */
35
- .ezoic-ad iframe,
36
- .ezoic-ad ins{
37
- display: block !important;
38
- }
39
-
40
- /* neutralize empty spacer divs if any */
41
- .ezoic-ad > div:empty{
42
- display:none !important;
43
- }
File without changes