nodebb-plugin-ezoic-infinite 1.4.72 → 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.
@@ -318,34 +318,33 @@
318
318
  }
319
319
  }
320
320
 
321
-
322
- function forcePlaceholderAutoHeight(wrap, id) {
321
+ function markFilled(wrap) {
323
322
  try {
324
- if (!wrap || !id) return;
325
- const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
326
- if (!ph) return;
327
- // Neutraliser les hauteurs réservées (inline ou CSS) qui créent un espace après la pub
328
- ph.style.setProperty('height', 'auto', 'important');
329
- ph.style.setProperty('min-height', '0px', 'important');
330
- // Eviter le gap baseline sous les iframes/ins
331
- wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => {
332
- try { n.style.setProperty('display', 'block', 'important'); } catch (e) {}
333
- });
334
- requestAnimationFrame(() => {
335
- try {
336
- ph.style.setProperty('height', 'auto', 'important');
337
- ph.style.setProperty('min-height', '0px', 'important');
338
- } catch (e) {}
339
- });
323
+ if (!wrap) return;
324
+ if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
325
+ wrap.setAttribute('data-ezoic-filled', '1');
340
326
  } catch (e) {}
341
327
  }
342
328
 
343
- function markFilled(wrap, id) {
329
+ function forcePlaceholderAutoHeight(wrap, id) {
344
330
  try {
345
- if (!wrap) return;
346
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
347
- wrap.setAttribute('data-ezoic-filled', '1');
348
- try { forcePlaceholderAutoHeight(wrap, id); } catch (e) {}
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) {}
349
348
  } catch (e) {}
350
349
  }
351
350
 
@@ -359,13 +358,15 @@ function markFilled(wrap, id) {
359
358
  if (!ph) return;
360
359
  // Already filled?
361
360
  if (ph.childNodes && ph.childNodes.length > 0) {
362
- markFilled(wrap, id); // Afficher wrapper
361
+ markFilled(wrap); // Afficher wrapper
362
+ forcePlaceholderAutoHeight(wrap, id);
363
363
  sessionDefinedIds.add(id);
364
364
  return;
365
365
  }
366
366
  const obs = new MutationObserver(() => {
367
367
  if (ph.childNodes && ph.childNodes.length > 0) {
368
- markFilled(wrap, id); // CRITIQUE: Afficher wrapper maintenant
368
+ markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
369
+ forcePlaceholderAutoHeight(wrap, id);
369
370
  try { sessionDefinedIds.add(id); } catch (e) {}
370
371
  try { obs.disconnect(); } catch (e) {}
371
372
  }
@@ -385,7 +386,7 @@ function markFilled(wrap, id) {
385
386
  const filled = !!(ph.childNodes && ph.childNodes.length > 0);
386
387
  if (filled) {
387
388
  try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
388
- try { markFilled(wrap, id); } catch (e) {}
389
+ try { markFilled(wrap); } catch (e) {}
389
390
  }
390
391
  return filled;
391
392
  }
@@ -414,18 +415,23 @@ function markFilled(wrap, id) {
414
415
  batchShowAdsTimer = setTimeout(() => {
415
416
  if (pendingShowAdsIds.size === 0) return;
416
417
 
417
- 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
+ });
418
423
  pendingShowAdsIds.clear();
419
424
 
425
+ if (!idsArray.length) return;
426
+
420
427
  // Appeler showAds avec TOUS les IDs en une fois
421
428
  try {
422
429
  window.ezstandalone = window.ezstandalone || {};
423
430
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
424
431
  window.ezstandalone.cmd.push(function() {
425
432
  if (typeof window.ezstandalone.showAds === 'function') {
426
- // Appel batch: showAds(id1, id2, id3...)
427
- const okIds = idsArray.filter(id => { const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`); return el && el.isConnected; });
428
- if (okIds.length) { window.ezstandalone.showAds(...okIds); }
433
+ // Call as array (most compatible with ezstandalone)
434
+ window.ezstandalone.showAds(idsArray);
429
435
  // Tracker tous les IDs
430
436
  idsArray.forEach(id => {
431
437
  state.lastShowById.set(id, Date.now());
@@ -574,6 +580,18 @@ function markFilled(wrap, id) {
574
580
  // Appel immédiat au lieu de 10ms delay
575
581
  callShowAdsWhenReady(id);
576
582
  }
583
+
584
+ liveArr.push({ id, wrap });
585
+ if (wrap && (
586
+ (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
587
+ )) {
588
+ try { wrap.remove(); } catch (e) {}
589
+ if (!(pick.recycled && pick.recycled.wrap)) {
590
+ try { kindPool.unshift(id); } catch (e) {}
591
+ usedSet.delete(id);
592
+ }
593
+ continue;
594
+ }
577
595
  inserted += 1;
578
596
  }
579
597
  return inserted;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.72",
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,31 +0,0 @@
1
-
2
- /* Wrapper: invisible tant que la pub n'est pas réellement insérée */
3
- .ezoic-ad{
4
- display:none;
5
- width:100%;
6
- height:auto !important;
7
- padding:0 !important;
8
- margin:0 !important;
9
- }
10
- .ezoic-ad[data-ezoic-filled="1"]{
11
- display:block;
12
- }
13
-
14
- /* Le placeholder ne doit jamais réserver une hauteur fixe */
15
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"]{
16
- height:auto !important;
17
- min-height:0 !important;
18
- margin:0 !important;
19
- padding:0 !important;
20
- }
21
-
22
- /* Évite le gap baseline sous iframes/ins */
23
- .ezoic-ad iframe,
24
- .ezoic-ad ins{
25
- display:block !important;
26
- }
27
-
28
- /* Supprimer les spacers vides */
29
- .ezoic-ad > div:empty{
30
- display:none !important;
31
- }
File without changes