nodebb-plugin-ezoic-infinite 1.6.80 → 1.6.82

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
@@ -8,12 +8,12 @@ const SETTINGS_KEY = 'ezoic-infinite';
8
8
  const plugin = {};
9
9
 
10
10
  async function getSettings() {
11
- return await meta.settings.get(SETTINGS_KEY);
11
+ const settings = await meta.settings.get(SETTINGS_KEY);
12
+ return settings || {};
12
13
  }
13
14
 
14
15
  async function isUserExcluded(uid, excludedGroups) {
15
16
  if (!uid || !excludedGroups) return false;
16
- // Gérer le format multiple de NodeBB
17
17
  const groupsList = Array.isArray(excludedGroups) ? excludedGroups : [excludedGroups];
18
18
  if (!groupsList.length) return false;
19
19
  return await groups.isMemberOfGroups(uid, groupsList);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.80",
3
+ "version": "1.6.82",
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,229 +1,220 @@
1
1
  (function () {
2
- 'use strict';
2
+ 'use strict';
3
3
 
4
- let lastScrollY = 0;
5
- let scrollDir = 1;
6
- try {
7
- lastScrollY = window.scrollY || 0;
4
+ // Singleton pour éviter les conflits lors de la navigation AJAX de NodeBB
5
+ if (window.ezInfiniteInjected) return;
6
+ window.ezInfiniteInjected = true;
7
+
8
+ const WRAP_CLASS = 'nodebb-ezoic-wrap';
9
+ const POOL_ID = 'nodebb-ezoic-placeholder-pool';
10
+
11
+ let config = null;
12
+ let isInternalChange = false;
13
+ let ezEnabledInternally = false;
14
+
15
+ // Détection du sens du scroll pour le nettoyage (Pillup prevention)
16
+ let lastScrollY = window.scrollY;
17
+ let scrollDir = 1;
8
18
  window.addEventListener('scroll', () => {
9
- const y = window.scrollY || 0;
10
- const d = y - lastScrollY;
11
- if (Math.abs(d) > 4) {
12
- scrollDir = d > 0 ? 1 : -1;
19
+ const y = window.scrollY;
20
+ scrollDir = y > lastScrollY ? 1 : -1;
13
21
  lastScrollY = y;
14
- }
15
22
  }, { passive: true });
16
- } catch (e) {}
17
-
18
- const WRAP_CLASS = 'nodebb-ezoic-wrap';
19
- const POOL_ID = 'nodebb-ezoic-placeholder-pool';
20
- const MAX_INSERTS_PER_RUN = 8;
21
-
22
- let config = null;
23
- let isInternalChange = false;
24
- let ezEnabled = false; // Flag pour savoir si ezstandalone.enable() a été appelé
25
-
26
- function withInternalDomChange(fn) {
27
- isInternalChange = true;
28
- try { fn(); } finally { isInternalChange = false; }
29
- }
30
-
31
- function getPool() {
32
- let p = document.getElementById(POOL_ID);
33
- if (!p) {
34
- p = document.createElement('div');
35
- p.id = POOL_ID;
36
- p.style.display = 'none';
37
- document.body.appendChild(p);
38
- }
39
- return p;
40
- }
41
-
42
- function releaseWrapNode(wrap) {
43
- if (!wrap) return;
44
- const pool = getPool();
45
- wrap.classList.remove('ez-orphan-hidden');
46
- if (wrap.parentNode !== pool) {
47
- pool.appendChild(wrap);
23
+
24
+ function withInternalDomChange(fn) {
25
+ isInternalChange = true;
26
+ try { fn(); } finally { isInternalChange = false; }
48
27
  }
49
- }
50
-
51
- function decluster(container) {
52
- const wraps = Array.from(container.querySelectorAll(`.${WRAP_CLASS}`));
53
- wraps.forEach(wrap => {
54
- let next = wrap.nextElementSibling;
55
- if (next && next.classList.contains(WRAP_CLASS)) {
56
- withInternalDomChange(() => releaseWrapNode(next));
57
- }
58
- });
59
- }
60
-
61
- function pruneOrphanWraps() {
62
- const wraps = document.querySelectorAll(`.${WRAP_CLASS}`);
63
- const items = document.querySelectorAll('[component="category/topic"], [component="post"]');
64
- const itemSet = new Set(items);
65
-
66
- wraps.forEach(wrap => {
67
- if (wrap.parentElement && wrap.parentElement.id === POOL_ID) return;
68
- let hasNeighbor = false;
69
- let prev = wrap.previousElementSibling;
70
- for (let i = 0; i < 3 && prev; i++) {
71
- if (itemSet.has(prev)) { hasNeighbor = true; break; }
72
- prev = prev.previousElementSibling;
73
- }
74
- if (!hasNeighbor) {
75
- let next = wrap.nextElementSibling;
76
- for (let i = 0; i < 3 && next; i++) {
77
- if (itemSet.has(next)) { hasNeighbor = true; break; }
78
- next = next.nextElementSibling;
79
- }
80
- }
81
28
 
82
- if (!hasNeighbor) {
83
- if (scrollDir === -1) {
84
- withInternalDomChange(() => releaseWrapNode(wrap));
85
- } else {
86
- wrap.classList.add('ez-orphan-hidden');
29
+ function getPool() {
30
+ let p = document.getElementById(POOL_ID);
31
+ if (!p) {
32
+ p = document.createElement('div');
33
+ p.id = POOL_ID;
34
+ p.style.display = 'none';
35
+ document.body.appendChild(p);
87
36
  }
88
- } else {
89
- wrap.classList.remove('ez-orphan-hidden');
90
- }
91
- });
92
- }
37
+ return p;
38
+ }
93
39
 
94
- function callEzoic(placeholderId) {
95
- if (typeof window.ezstandalone === 'undefined') return;
96
-
97
- const pid = parseInt(placeholderId, 10);
98
- try {
99
- if (!ezEnabled) {
100
- window.ezstandalone.define(pid);
101
- window.ezstandalone.enable();
102
- window.ezstandalone.display();
103
- ezEnabled = true;
104
- } else {
105
- window.ezstandalone.define(pid);
106
- // Utiliser une petite pause pour être sûr que le DOM est prêt
107
- setTimeout(() => {
108
- if (document.getElementById('ezoic-pub-ad-placeholder-' + pid)) {
109
- window.ezstandalone.refresh();
110
- }
111
- }, 100);
112
- }
113
- } catch (e) {
114
- console.warn('[Ezoic-Infinite] Error refreshing ad:', e);
40
+ function releaseWrapNode(wrap) {
41
+ if (!wrap || !wrap.parentNode) return;
42
+ const pool = getPool();
43
+ wrap.classList.remove('ez-orphan-hidden');
44
+ if (wrap.parentNode !== pool) {
45
+ pool.appendChild(wrap);
46
+ }
115
47
  }
116
- }
117
48
 
118
- function redistribute(container) {
119
- if (!container || !config || config.excluded) return;
120
-
121
- pruneOrphanWraps();
122
- decluster(container);
123
-
124
- const isTopicList = container.getAttribute('component') === 'category' || container.classList.contains('topic-list');
125
- const isPostList = container.getAttribute('component') === 'topic' || container.querySelectorAll('[component="post"]').length > 0;
126
-
127
- let enabled = false;
128
- let interval = 10;
129
- let kind = '';
130
- let items = [];
131
- let showFirst = false;
132
-
133
- if (isTopicList && config.enableBetweenAds) {
134
- enabled = true;
135
- interval = parseInt(config.intervalPosts, 10) || 10;
136
- kind = 'between';
137
- items = Array.from(container.querySelectorAll('[component="category/topic"]'));
138
- showFirst = config.showFirstTopicAd;
139
- } else if (isPostList && config.enableMessageAds) {
140
- enabled = true;
141
- interval = parseInt(config.messageIntervalPosts, 10) || 10;
142
- kind = 'message';
143
- items = Array.from(container.querySelectorAll('[component="post"]'));
144
- showFirst = config.showFirstMessageAd;
49
+ // ANTI-PILLUP : Supprime les pubs si elles se touchent
50
+ function decluster(container) {
51
+ const wraps = Array.from(container.querySelectorAll(`.${WRAP_CLASS}`));
52
+ wraps.forEach(wrap => {
53
+ let next = wrap.nextElementSibling;
54
+ // Si l'élément suivant est une autre pub, on la remet au pool
55
+ if (next && next.classList.contains(WRAP_CLASS)) {
56
+ withInternalDomChange(() => releaseWrapNode(next));
57
+ }
58
+ });
145
59
  }
146
60
 
147
- if (!enabled || items.length === 0) return;
61
+ // Nettoyage des orphelins (Virtualisation NodeBB)
62
+ function pruneOrphanWraps() {
63
+ const wraps = document.querySelectorAll(`.${WRAP_CLASS}`);
64
+ const items = document.querySelectorAll('[component="category/topic"], [component="post"]');
65
+ const itemSet = new Set(items);
66
+
67
+ wraps.forEach(wrap => {
68
+ if (wrap.parentElement && wrap.parentElement.id === POOL_ID) return;
69
+
70
+ let hasNeighbor = false;
71
+ let p = wrap.previousElementSibling;
72
+ // On vérifie sur 2 voisins pour être sûr
73
+ for (let i = 0; i < 2 && p; i++) {
74
+ if (itemSet.has(p)) { hasNeighbor = true; break; }
75
+ p = p.previousElementSibling;
76
+ }
77
+ if (!hasNeighbor) {
78
+ let n = wrap.nextElementSibling;
79
+ for (let i = 0; i < 2 && n; i++) {
80
+ if (itemSet.has(n)) { hasNeighbor = true; break; }
81
+ n = n.nextElementSibling;
82
+ }
83
+ }
148
84
 
149
- let inserts = 0;
150
- items.forEach((item, index) => {
151
- if (inserts >= MAX_INSERTS_PER_RUN) return;
152
- const pos = index + 1;
153
- let shouldHaveAd = (pos % interval === 0);
154
- if (pos === 1 && showFirst) shouldHaveAd = true;
85
+ if (!hasNeighbor) {
86
+ // En remontée (Up), on recycle immédiatement pour éviter l'empilement
87
+ if (scrollDir === -1) {
88
+ withInternalDomChange(() => releaseWrapNode(wrap));
89
+ } else {
90
+ wrap.classList.add('ez-orphan-hidden');
91
+ }
92
+ } else {
93
+ wrap.classList.remove('ez-orphan-hidden');
94
+ }
95
+ });
96
+ }
155
97
 
156
- const next = item.nextElementSibling;
157
- const hasAdAfter = next && next.classList.contains(WRAP_CLASS);
98
+ // Appel à la bibliothèque Ezoic déjà initialisée
99
+ function callEzoicRefresh(placeholderId) {
100
+ if (typeof window.ezstandalone === 'undefined') return;
101
+ const pid = parseInt(placeholderId, 10);
102
+
103
+ try {
104
+ // On définit l'ID
105
+ window.ezstandalone.define(pid);
106
+
107
+ // Si c'est le tout premier, on tente un display
108
+ if (!ezEnabledInternally) {
109
+ window.ezstandalone.enable();
110
+ window.ezstandalone.display();
111
+ ezEnabledInternally = true;
112
+ } else {
113
+ // Sinon on rafraîchit (avec un petit délai pour le rendu DOM)
114
+ setTimeout(() => {
115
+ const el = document.getElementById('ezoic-pub-ad-placeholder-' + pid);
116
+ if (el && el.offsetParent !== null) {
117
+ window.ezstandalone.refresh();
118
+ }
119
+ }, 250);
120
+ }
121
+ } catch (e) {
122
+ console.warn('[Ezoic-Infinite] Refresh skip:', e.message);
123
+ }
124
+ }
158
125
 
159
- if (shouldHaveAd && !hasAdAfter) {
160
- const pool = getPool();
161
- const available = pool.querySelector(`.${WRAP_CLASS}[data-kind="${kind}"]`);
162
- if (available) {
163
- withInternalDomChange(() => {
164
- item.parentNode.insertBefore(available, item.nextSibling);
165
- callEzoic(available.getAttribute('data-placeholder-id'));
166
- });
167
- inserts++;
126
+ function redistribute(container) {
127
+ if (!container || !config || config.excluded) return;
128
+
129
+ pruneOrphanWraps();
130
+ decluster(container);
131
+
132
+ const isTopicList = container.getAttribute('component') === 'category' || container.classList.contains('topic-list');
133
+ const isPostList = container.getAttribute('component') === 'topic' || container.querySelectorAll('[component="post"]').length > 0;
134
+
135
+ let interval = 10;
136
+ let kind = '';
137
+ let items = [];
138
+ let showFirst = false;
139
+
140
+ if (isTopicList && config.enableBetweenAds) {
141
+ interval = parseInt(config.intervalPosts, 10) || 10;
142
+ kind = 'between';
143
+ items = Array.from(container.querySelectorAll('[component="category/topic"]'));
144
+ showFirst = config.showFirstTopicAd;
145
+ } else if (isPostList && config.enableMessageAds) {
146
+ interval = parseInt(config.messageIntervalPosts, 10) || 10;
147
+ kind = 'message';
148
+ items = Array.from(container.querySelectorAll('[component="post"]'));
149
+ showFirst = config.showFirstMessageAd;
168
150
  }
169
- }
170
- });
171
- }
172
-
173
- function fetchConfig(cb) {
174
- fetch('/api/plugins/ezoic-infinite/config')
175
- .then(r => r.json())
176
- .then(data => {
177
- config = data;
178
- const pool = getPool();
179
- const setupPool = (idsRaw, kind) => {
180
- if (!idsRaw) return;
181
- const ids = idsRaw.split(/[\s,]+/).filter(Boolean);
182
- ids.forEach(id => {
183
- if (!document.querySelector(`[data-placeholder-id="${id}"]`)) {
184
- const d = document.createElement('div');
185
- d.className = WRAP_CLASS + ' nodebb-ezoic-ad-' + kind;
186
- d.setAttribute('data-kind', kind);
187
- d.setAttribute('data-placeholder-id', id);
188
- const inner = document.createElement('div');
189
- inner.id = 'ezoic-pub-ad-placeholder-' + id;
190
- inner.className = 'ezoic-ad';
191
- d.appendChild(inner);
192
- pool.appendChild(d);
151
+
152
+ if (items.length === 0) return;
153
+
154
+ items.forEach((item, index) => {
155
+ const pos = index + 1;
156
+ let shouldHaveAd = (pos % interval === 0);
157
+ if (pos === 1 && showFirst) shouldHaveAd = true;
158
+
159
+ const next = item.nextElementSibling;
160
+ if (shouldHaveAd && !(next && next.classList.contains(WRAP_CLASS))) {
161
+ const pool = getPool();
162
+ const available = pool.querySelector(`.${WRAP_CLASS}[data-kind="${kind}"]`);
163
+ if (available) {
164
+ withInternalDomChange(() => {
165
+ item.parentNode.insertBefore(available, item.nextSibling);
166
+ callEzoicRefresh(available.getAttribute('data-placeholder-id'));
167
+ });
168
+ }
193
169
  }
194
- });
195
- };
196
- setupPool(config.placeholderIds, 'between');
197
- setupPool(config.messagePlaceholderIds, 'message');
198
- cb();
199
- });
200
- }
201
-
202
- let timer = null;
203
- function schedule() {
204
- if (timer) clearTimeout(timer);
205
- timer = setTimeout(() => {
206
- const lists = document.querySelectorAll('[component="category"], .topic-list, [component="topic"], [component="category/topic/list"]');
207
- lists.forEach(redistribute);
208
- }, 150); // Un peu plus de délai pour NodeBB 4.x
209
- }
210
-
211
- function init() {
212
- fetchConfig(() => {
213
- schedule();
214
- if (typeof MutationObserver !== 'undefined') {
215
- const mo = new MutationObserver((muts) => {
216
- if (isInternalChange) return;
217
- schedule();
218
170
  });
219
- mo.observe(document.body, { childList: true, subtree: true });
220
- }
221
- });
222
- }
223
-
224
- if (document.readyState === 'loading') {
225
- document.addEventListener('DOMContentLoaded', init);
226
- } else {
227
- init();
228
- }
171
+ }
172
+
173
+ let timer = null;
174
+ function schedule() {
175
+ if (timer) clearTimeout(timer);
176
+ timer = setTimeout(() => {
177
+ const targets = document.querySelectorAll('[component="category"], .topic-list, [component="topic"], [component="category/topic/list"]');
178
+ targets.forEach(redistribute);
179
+ }, 300);
180
+ }
181
+
182
+ function init() {
183
+ fetch('/api/plugins/ezoic-infinite/config')
184
+ .then(r => r.json())
185
+ .then(data => {
186
+ config = data;
187
+ const pool = getPool();
188
+ const setup = (raw, kind) => {
189
+ if (!raw) return;
190
+ raw.split(/[\s,]+/).filter(Boolean).forEach(id => {
191
+ if (!document.querySelector(`[data-placeholder-id="${id}"]`)) {
192
+ const d = document.createElement('div');
193
+ d.className = WRAP_CLASS;
194
+ d.setAttribute('data-kind', kind);
195
+ d.setAttribute('data-placeholder-id', id);
196
+ d.innerHTML = `<div id="ezoic-pub-ad-placeholder-${id}" class="ezoic-ad"></div>`;
197
+ pool.appendChild(d);
198
+ }
199
+ });
200
+ };
201
+ setup(config.placeholderIds, 'between');
202
+ setup(config.messagePlaceholderIds, 'message');
203
+
204
+ schedule();
205
+
206
+ if (typeof MutationObserver !== 'undefined') {
207
+ const mo = new MutationObserver(() => {
208
+ if (!isInternalChange) schedule();
209
+ });
210
+ mo.observe(document.body, { childList: true, subtree: true });
211
+ }
212
+ });
213
+ }
214
+
215
+ if (document.readyState === 'loading') {
216
+ document.addEventListener('DOMContentLoaded', init);
217
+ } else {
218
+ init();
219
+ }
229
220
  })();
package/public/style.css CHANGED
@@ -1,37 +1,37 @@
1
+ /* Container de base */
1
2
  .nodebb-ezoic-wrap {
2
- display: block;
3
- width: 100%;
4
- margin: 20px 0 !important;
5
- padding: 0 !important;
6
- clear: both;
7
- overflow: visible;
8
- min-height: 50px; /* Aide Ezoic à détecter l'élément */
3
+ display: block;
4
+ width: 100%;
5
+ margin: 30px auto !important;
6
+ padding: 0 !important;
7
+ clear: both;
8
+ min-height: 100px; /* Important pour Ezoic */
9
9
  }
10
10
 
11
- /* Cache les pubs orphelines proprement */
12
- .nodebb-ezoic-wrap.ez-orphan-hidden {
13
- display: none !important;
14
- height: 0 !important;
15
- min-height: 0 !important;
16
- margin: 0 !important;
17
- padding: 0 !important;
11
+ /* ANTI-PILLUP RADICAL : Si deux pubs se suivent, on cache la 2ème immédiatement par CSS */
12
+ .nodebb-ezoic-wrap + .nodebb-ezoic-wrap {
13
+ display: none !important;
14
+ height: 0 !important;
15
+ margin: 0 !important;
18
16
  }
19
17
 
20
- /* Neutralisation du style interne Ezoic pour éviter les énormes espaces blancs */
21
- .nodebb-ezoic-wrap .ezoic-ad {
22
- margin: 0 auto !important;
23
- padding: 0 !important;
24
- min-height: 1px !important;
25
- height: auto !important;
26
- line-height: normal !important;
18
+ /* Cache les blocs orphelins (ceux qui ont perdu leurs posts voisins au scroll up) */
19
+ .nodebb-ezoic-wrap.ez-orphan-hidden {
20
+ display: none !important;
21
+ height: 0 !important;
22
+ margin: 0 !important;
23
+ padding: 0 !important;
27
24
  }
28
25
 
29
- /* Enlève les marges doubles si plusieurs pubs se suivent malgré le script */
30
- .nodebb-ezoic-wrap + .nodebb-ezoic-wrap {
31
- margin-top: 0 !important;
26
+ /* Évite les espaces blancs inutiles à l'intérieur */
27
+ .nodebb-ezoic-wrap .ezoic-ad {
28
+ margin: 0 auto !important;
29
+ min-height: 1px !important;
32
30
  }
33
31
 
34
- /* Assurer la visibilité pour l'IntersectionObserver d'Ezoic */
35
- .ezoic-ad iframe {
36
- max-width: 100%;
32
+ /* Harmonisation NodeBB 4.x / Harmony */
33
+ [component="category"] .nodebb-ezoic-wrap,
34
+ .topic-list .nodebb-ezoic-wrap {
35
+ border-top: 1px solid rgba(0,0,0,0.05);
36
+ padding-top: 10px !important;
37
37
  }