nodebb-plugin-ezoic-infinite 1.6.81 → 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
@@ -29,7 +29,6 @@ plugin.init = async ({ router, middleware }) => {
29
29
  res.render('admin/plugins/ezoic-infinite', {
30
30
  ...settings,
31
31
  allGroups,
32
- // Helper pour le tpl
33
32
  enableBetweenAds_checked: settings.enableBetweenAds === 'on' ? 'checked' : '',
34
33
  enableMessageAds_checked: settings.enableMessageAds === 'on' ? 'checked' : ''
35
34
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.81",
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,237 +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 ezInitialized = false; // Flag crucial pour éviter l'erreur de "refresh"
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 || !wrap.parentNode) 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
- // Anti-pillup : évite que deux pubs se suivent
52
- function decluster(container) {
53
- const wraps = Array.from(container.querySelectorAll(`.${WRAP_CLASS}`));
54
- wraps.forEach(wrap => {
55
- let next = wrap.nextElementSibling;
56
- if (next && next.classList.contains(WRAP_CLASS)) {
57
- withInternalDomChange(() => releaseWrapNode(next));
58
- }
59
- });
60
- }
61
-
62
- // Nettoyage des pubs orphelines lors de la virtualisation
63
- function pruneOrphanWraps() {
64
- const wraps = document.querySelectorAll(`.${WRAP_CLASS}`);
65
- const items = document.querySelectorAll('[component="category/topic"], [component="post"]');
66
- const itemSet = new Set(items);
67
-
68
- wraps.forEach(wrap => {
69
- if (wrap.parentElement && wrap.parentElement.id === POOL_ID) return;
70
- let hasNeighbor = false;
71
- let prev = wrap.previousElementSibling;
72
- for (let i = 0; i < 3 && prev; i++) {
73
- if (itemSet.has(prev)) { hasNeighbor = true; break; }
74
- prev = prev.previousElementSibling;
75
- }
76
- if (!hasNeighbor) {
77
- let next = wrap.nextElementSibling;
78
- for (let i = 0; i < 3 && next; i++) {
79
- if (itemSet.has(next)) { hasNeighbor = true; break; }
80
- next = next.nextElementSibling;
81
- }
82
- }
83
28
 
84
- if (!hasNeighbor) {
85
- if (scrollDir === -1) {
86
- withInternalDomChange(() => releaseWrapNode(wrap));
87
- } else {
88
- 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);
89
36
  }
90
- } else {
37
+ return p;
38
+ }
39
+
40
+ function releaseWrapNode(wrap) {
41
+ if (!wrap || !wrap.parentNode) return;
42
+ const pool = getPool();
91
43
  wrap.classList.remove('ez-orphan-hidden');
92
- }
93
- });
94
- }
95
-
96
- function callEzoic(placeholderId) {
97
- if (typeof window.ezstandalone === 'undefined') return;
98
- const pid = parseInt(placeholderId, 10);
99
- if (isNaN(pid)) return;
100
-
101
- try {
102
- if (!ezInitialized) {
103
- // PREMIER APPEL : define + enable + display
104
- window.ezstandalone.define(pid);
105
- window.ezstandalone.enable();
106
- window.ezstandalone.display();
107
- ezInitialized = true;
108
- } else {
109
- // APPELS SUIVANTS (Scroll) : define + refresh
110
- window.ezstandalone.define(pid);
111
- // On attend que le DOM soit stable pour le refresh
112
- setTimeout(() => {
113
- if (document.getElementById('ezoic-pub-ad-placeholder-' + pid)) {
114
- window.ezstandalone.refresh();
115
- }
116
- }, 150);
117
- }
118
- } catch (e) {
119
- console.warn('[Ezoic-Infinite] Ez Error:', e.message);
44
+ if (wrap.parentNode !== pool) {
45
+ pool.appendChild(wrap);
46
+ }
120
47
  }
121
- }
122
48
 
123
- function redistribute(container) {
124
- if (!container || !config || config.excluded) return;
125
-
126
- pruneOrphanWraps();
127
- decluster(container);
128
-
129
- const isTopicList = container.getAttribute('component') === 'category' || container.classList.contains('topic-list');
130
- const isPostList = container.getAttribute('component') === 'topic' || container.querySelectorAll('[component="post"]').length > 0;
131
-
132
- let enabled = false;
133
- let interval = 10;
134
- let kind = '';
135
- let items = [];
136
- let showFirst = false;
137
-
138
- if (isTopicList && config.enableBetweenAds) {
139
- enabled = true;
140
- interval = parseInt(config.intervalPosts, 10) || 10;
141
- kind = 'between';
142
- items = Array.from(container.querySelectorAll('[component="category/topic"]'));
143
- showFirst = config.showFirstTopicAd;
144
- } else if (isPostList && config.enableMessageAds) {
145
- enabled = true;
146
- interval = parseInt(config.messageIntervalPosts, 10) || 10;
147
- kind = 'message';
148
- items = Array.from(container.querySelectorAll('[component="post"]'));
149
- 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
+ });
150
59
  }
151
60
 
152
- 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
+ }
153
84
 
154
- let inserts = 0;
155
- items.forEach((item, index) => {
156
- if (inserts >= MAX_INSERTS_PER_RUN) return;
157
- const pos = index + 1;
158
- let shouldHaveAd = (pos % interval === 0);
159
- 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
+ }
160
97
 
161
- const next = item.nextElementSibling;
162
- 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
+ }
163
125
 
164
- if (shouldHaveAd && !hasAdAfter) {
165
- const pool = getPool();
166
- const available = pool.querySelector(`.${WRAP_CLASS}[data-kind="${kind}"]`);
167
- if (available) {
168
- withInternalDomChange(() => {
169
- item.parentNode.insertBefore(available, item.nextSibling);
170
- callEzoic(available.getAttribute('data-placeholder-id'));
171
- });
172
- 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;
173
150
  }
174
- }
175
- });
176
- }
177
-
178
- function fetchConfig(cb) {
179
- fetch('/api/plugins/ezoic-infinite/config')
180
- .then(r => r.json())
181
- .then(data => {
182
- config = data;
183
- const pool = getPool();
184
- const setupPool = (idsRaw, kind) => {
185
- if (!idsRaw) return;
186
- const ids = idsRaw.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
187
- ids.forEach(id => {
188
- if (!document.querySelector(`[data-placeholder-id="${id}"]`)) {
189
- const d = document.createElement('div');
190
- d.className = WRAP_CLASS + ' nodebb-ezoic-ad-' + kind;
191
- d.setAttribute('data-kind', kind);
192
- d.setAttribute('data-placeholder-id', id);
193
- const inner = document.createElement('div');
194
- inner.id = 'ezoic-pub-ad-placeholder-' + id;
195
- inner.className = 'ezoic-ad';
196
- d.appendChild(inner);
197
- 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
+ }
198
169
  }
199
- });
200
- };
201
- setupPool(config.placeholderIds, 'between');
202
- setupPool(config.messagePlaceholderIds, 'message');
203
- cb();
204
- }).catch(e => console.error('[Ezoic] Config load error', e));
205
- }
206
-
207
- let timer = null;
208
- function schedule() {
209
- if (timer) clearTimeout(timer);
210
- timer = setTimeout(() => {
211
- // Sélecteurs pour NodeBB 4.x / Harmony
212
- const lists = document.querySelectorAll('[component="category"], .topic-list, [component="topic"], [component="category/topic/list"]');
213
- lists.forEach(redistribute);
214
- }, 200);
215
- }
216
-
217
- function init() {
218
- fetchConfig(() => {
219
- schedule();
220
- if (typeof MutationObserver !== 'undefined') {
221
- const mo = new MutationObserver((muts) => {
222
- if (isInternalChange) return;
223
- // On vérifie si un élément structurel a été ajouté
224
- const shouldRun = muts.some(m => m.addedNodes.length > 0);
225
- if (shouldRun) schedule();
226
170
  });
227
- mo.observe(document.body, { childList: true, subtree: true });
228
- }
229
- });
230
- }
231
-
232
- if (document.readyState === 'loading') {
233
- document.addEventListener('DOMContentLoaded', init);
234
- } else {
235
- init();
236
- }
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
+ }
237
220
  })();
package/public/style.css CHANGED
@@ -1,35 +1,37 @@
1
- /* Container principal */
1
+ /* Container de base */
2
2
  .nodebb-ezoic-wrap {
3
- display: block;
4
- width: 100%;
5
- margin: 20px 0 !important;
6
- padding: 0 !important;
7
- clear: both;
8
- min-height: 50px; /* Important pour que l'ad-tester détecte le bloc */
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 sans les supprimer du DOM */
12
- .nodebb-ezoic-wrap.ez-orphan-hidden {
13
- display: none !important;
14
- height: 0 !important;
15
- min-height: 0 !important;
16
- margin: 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;
17
16
  }
18
17
 
19
- /* Nettoyage des styles internes Ezoic */
20
- .nodebb-ezoic-wrap .ezoic-ad {
21
- margin: 0 auto !important;
22
- padding: 0 !important;
23
- min-height: 1px !important;
24
- height: auto !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;
25
24
  }
26
25
 
27
- /* Neutralise les marges doubles */
28
- .nodebb-ezoic-wrap + .nodebb-ezoic-wrap {
29
- 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;
30
30
  }
31
31
 
32
- /* Évite les débordements sur mobile */
33
- .ezoic-ad iframe {
34
- max-width: 100% !important;
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;
35
37
  }