nodebb-plugin-ezoic-infinite 0.9.16 → 1.0.1

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
@@ -1,52 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
- const db = require.main.require('./src/database');
5
4
  const groups = require.main.require('./src/groups');
6
5
 
7
6
  const Plugin = {};
8
7
 
9
- Plugin.init = async function (params) {
10
- const { router, middleware } = params;
11
-
8
+ Plugin.init = async function ({ router, middleware }) {
12
9
  router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
13
10
  router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
14
11
  };
15
12
 
16
- async function getAllGroupNames() {
17
- const keys = [
18
- 'groups:createtime',
19
- 'groups:visible:createtime',
20
- 'groups:system:createtime',
21
- 'groups:alphabetical',
22
- 'groups:visible:alphabetical',
23
- ];
24
-
25
- for (const key of keys) {
26
- try {
27
- const names = await db.getSortedSetRange(key, 0, -1);
28
- if (Array.isArray(names) && names.length) {
29
- return names;
30
- }
31
- } catch (e) {}
32
- }
13
+ async function renderAdmin(req, res) {
14
+ const settings = await meta.settings.get('ezoic-infinite');
33
15
 
34
- // Last resort: ask groups module for any known set
16
+ let groupNames = [];
35
17
  const candidates = ['groups:createtime', 'groups:visible:createtime'];
36
18
  for (const set of candidates) {
37
19
  try {
38
- const names = await groups.getGroupsFromSet(set, 0, -1);
39
- if (Array.isArray(names) && names.length) return names;
20
+ groupNames = await groups.getGroupsFromSet(set, 0, -1);
21
+ if (Array.isArray(groupNames) && groupNames.length) break;
40
22
  } catch (e) {}
41
23
  }
42
24
 
43
- return [];
44
- }
45
-
46
- async function renderAdmin(req, res) {
47
- const settings = await meta.settings.get('ezoic-infinite');
48
-
49
- const groupNames = await getAllGroupNames();
50
25
  let groupList = [];
51
26
  try {
52
27
  groupList = await groups.getGroupsData(groupNames);
@@ -55,7 +30,7 @@ async function renderAdmin(req, res) {
55
30
  }
56
31
 
57
32
  groupList = (groupList || [])
58
- .filter(g => g && g.name && typeof g.name === 'string')
33
+ .filter(g => g && g.name)
59
34
  .sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr', { sensitivity: 'base' }));
60
35
 
61
36
  res.render('admin/plugins/ezoic-infinite', {
@@ -75,4 +50,11 @@ Plugin.addAdminNavigation = async function (header) {
75
50
  return header;
76
51
  };
77
52
 
53
+ // Expose settings to client without any admin API calls
54
+ Plugin.addConfig = async function (config) {
55
+ const settings = await meta.settings.get('ezoic-infinite');
56
+ config.ezoicInfinite = settings || {};
57
+ return config;
58
+ };
59
+
78
60
  module.exports = Plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.9.16",
4
- "description": "Injection de publicités Ezoic entre les topics et entre les messages avec infinite scroll (NodeBB 4.x).",
3
+ "version": "1.0.1",
4
+ "description": "Ezoic ads injection for NodeBB 4.x (rebased to 0.8.3 style).",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
package/plugin.json CHANGED
@@ -11,6 +11,10 @@
11
11
  {
12
12
  "hook": "filter:admin.header.build",
13
13
  "method": "addAdminNavigation"
14
+ },
15
+ {
16
+ "hook": "filter:config.get",
17
+ "method": "addConfig"
14
18
  }
15
19
  ],
16
20
  "staticDirs": {
package/public/admin.js CHANGED
@@ -17,12 +17,12 @@ $(document).ready(function () {
17
17
 
18
18
  form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
19
19
  form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
20
- form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || ''); }
20
+ form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
21
21
 
22
- const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
23
- $select.find('option').each(function () {
24
- $(this).prop('selected', selected.includes($(this).val()));
25
- });
22
+ // Do NOT clear groups; server rendered
23
+ const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
24
+ form.find('[name="excludedGroups"] option').each(function () {
25
+ $(this).prop('selected', selected.includes($(this).val()));
26
26
  });
27
27
  });
28
28
  }
@@ -42,10 +42,7 @@ $(document).ready(function () {
42
42
  };
43
43
 
44
44
  socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
45
- if (err) {
46
- app.alertError(err.message || err);
47
- return;
48
- }
45
+ if (err) { app.alertError(err.message || err); return; }
49
46
  app.alertSuccess('Paramètres enregistrés');
50
47
  });
51
48
  }
package/public/client.js CHANGED
@@ -1,25 +1,25 @@
1
- /* global $, ajaxify, app, socket */
1
+ /* global $, ajaxify, app, config */
2
2
  'use strict';
3
3
 
4
4
  (function () {
5
5
  if (window.ezoicInfiniteLoaded) return;
6
6
  window.ezoicInfiniteLoaded = true;
7
7
 
8
- const SETTINGS_NS = 'ezoic-infinite';
9
-
10
- let settings = null;
8
+ let settings = (window.config && window.config.ezoicInfinite) ? window.config.ezoicInfinite : {};
11
9
  let pageKey = null;
12
10
 
13
- // State per page
14
- let usedTopic = new Set(); // ids currently in DOM (topic)
15
- let usedCat = new Set(); // ids currently in DOM (category)
16
- let fifoTopic = []; // [{id, afterNo}]
17
- let fifoCat = []; // [{id, afterPos}]
11
+ let usedTopic = new Set();
12
+ let usedCat = new Set();
13
+ let fifoTopic = [];
14
+ let fifoCat = [];
18
15
 
19
- // Refresh single-flight
20
16
  let refreshInFlight = false;
21
17
  let refreshQueued = false;
22
18
 
19
+ function reloadSettingsFromConfig() {
20
+ settings = (window.config && window.config.ezoicInfinite) ? window.config.ezoicInfinite : (settings || {});
21
+ }
22
+
23
23
  function parsePool(text) {
24
24
  return String(text || '')
25
25
  .split(/\r?\n/)
@@ -31,15 +31,13 @@
31
31
 
32
32
  function userExcluded() {
33
33
  try {
34
- const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
34
+ const raw = settings && settings.excludedGroups ? String(settings.excludedGroups) : '';
35
35
  if (!raw) return false;
36
36
  const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
37
37
  if (!excluded.length) return false;
38
38
  const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
39
39
  return excluded.some(g => myGroups.includes(g));
40
- } catch (e) {
41
- return false;
42
- }
40
+ } catch (e) { return false; }
43
41
  }
44
42
 
45
43
  function getPageKey() {
@@ -63,16 +61,12 @@
63
61
 
64
62
  function cleanupForNewPage() {
65
63
  $('.ezoic-ad').remove();
66
- usedTopic = new Set();
67
- usedCat = new Set();
68
- fifoTopic = [];
69
- fifoCat = [];
64
+ usedTopic = new Set(); usedCat = new Set();
65
+ fifoTopic = []; fifoCat = [];
70
66
  }
71
67
 
72
68
  function pickNextId(pool, usedSet) {
73
- for (const id of pool) {
74
- if (!usedSet.has(id)) return id;
75
- }
69
+ for (const id of pool) if (!usedSet.has(id)) return id;
76
70
  return null;
77
71
  }
78
72
 
@@ -93,77 +87,49 @@
93
87
  function ensureUniquePlaceholder(id) {
94
88
  const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
95
89
  if (!existing) return;
96
-
97
90
  const wrap = existing.closest('.ezoic-ad');
98
- if (wrap) {
99
- try { $(wrap).remove(); } catch (e) { wrap.remove(); }
100
- } else {
101
- existing.remove();
102
- }
91
+ if (wrap) $(wrap).remove(); else existing.remove();
103
92
  destroyPlaceholder(id);
104
93
  }
105
94
 
106
- // IMPORTANT: showAds is called ONLY for a single newly injected placeholder id (never a batch)
107
95
  function callEzoicSingle(id) {
108
96
  if (!id) return;
109
-
110
97
  const now = Date.now();
111
98
  window.__ezoicLastSingle = window.__ezoicLastSingle || {};
112
- const last = window.__ezoicLastSingle[id] || 0;
113
- if (now - last < 800) return;
99
+ if (now - (window.__ezoicLastSingle[id] || 0) < 1200) return;
114
100
  window.__ezoicLastSingle[id] = now;
115
101
 
116
- try {
117
- window.ezstandalone = window.ezstandalone || {};
118
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
102
+ window.ezstandalone = window.ezstandalone || {};
103
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
104
+ const run = function () {
105
+ if (typeof window.ezstandalone.showAds === 'function') {
106
+ window.ezstandalone.showAds(id);
107
+ return true;
108
+ }
109
+ return false;
110
+ };
111
+ window.ezstandalone.cmd.push(function () { try { run(); } catch (e) {} });
119
112
 
120
- const run = function () {
121
- try {
122
- if (typeof window.ezstandalone.showAds === 'function') {
123
- window.ezstandalone.showAds(id);
124
- return true;
125
- }
126
- } catch (e) {}
127
- return false;
128
- };
129
-
130
- window.ezstandalone.cmd.push(function () { run(); });
131
-
132
- // Retry a few times if ez loads late
133
- let tries = 0;
134
- const tick = function () {
135
- tries++;
136
- if (run() || tries >= 8) return;
137
- setTimeout(tick, 800);
138
- };
113
+ let tries = 0;
114
+ (function tick() {
115
+ tries++;
116
+ try { if (run() || tries >= 8) return; } catch (e) {}
139
117
  setTimeout(tick, 800);
140
- } catch (e) {}
118
+ })();
141
119
  }
142
120
 
143
- // Auto height: wrapper visible only when placeholder gets children
144
121
  function setupAutoHeightOnce() {
145
122
  if (window.__ezoicAutoHeight) return;
146
123
  window.__ezoicAutoHeight = true;
147
-
148
- const mark = function (wrap) {
149
- if (!wrap) return;
150
- const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
151
- if (ph && ph.children && ph.children.length) {
152
- wrap.classList.add('ezoic-filled');
153
- }
154
- };
155
-
156
124
  const scan = function () {
157
- document.querySelectorAll('.ezoic-ad').forEach(mark);
125
+ document.querySelectorAll('.ezoic-ad').forEach((wrap) => {
126
+ const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
127
+ if (ph && ph.children && ph.children.length) wrap.classList.add('ezoic-filled');
128
+ });
158
129
  };
159
-
160
130
  scan();
161
131
  setInterval(scan, 1000);
162
-
163
- try {
164
- const mo = new MutationObserver(scan);
165
- mo.observe(document.body, { childList: true, subtree: true });
166
- } catch (e) {}
132
+ try { new MutationObserver(scan).observe(document.body, { childList: true, subtree: true }); } catch (e) {}
167
133
  }
168
134
 
169
135
  function insertAfter($target, id, cls, afterVal) {
@@ -174,52 +140,27 @@
174
140
  '</div>'
175
141
  );
176
142
  $target.after(wrap);
177
- return wrap;
178
143
  }
179
144
 
180
145
  function recycleTopic($posts) {
181
- fifoTopic.sort((a, b) => a.afterNo - b.afterNo);
146
+ fifoTopic.sort((a,b)=>a.afterNo-b.afterNo);
182
147
  while (fifoTopic.length) {
183
- const old = fifoTopic.shift();
184
- const sel = '.ezoic-ad-topic[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterNo + '"]';
185
- const $el = $(sel);
148
+ const old=fifoTopic.shift();
149
+ const $el=$('.ezoic-ad-topic[data-ezoic-id="'+old.id+'"][data-ezoic-after="'+old.afterNo+'"]');
186
150
  if (!$el.length) continue;
187
-
188
- try {
189
- const $last = $posts.last();
190
- if ($last.length && $el.prev().is($last)) {
191
- fifoTopic.push(old);
192
- return null;
193
- }
194
- } catch (e) {}
195
-
196
- $el.remove();
197
- usedTopic.delete(old.id);
198
- destroyPlaceholder(old.id);
151
+ $el.remove(); usedTopic.delete(old.id); destroyPlaceholder(old.id);
199
152
  return old.id;
200
153
  }
201
154
  return null;
202
155
  }
203
156
 
204
157
  function recycleCat($items) {
205
- fifoCat.sort((a, b) => a.afterPos - b.afterPos);
158
+ fifoCat.sort((a,b)=>a.afterPos-b.afterPos);
206
159
  while (fifoCat.length) {
207
- const old = fifoCat.shift();
208
- const sel = '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterPos + '"]';
209
- const $el = $(sel);
160
+ const old=fifoCat.shift();
161
+ const $el=$('.ezoic-ad-between[data-ezoic-id="'+old.id+'"][data-ezoic-after="'+old.afterPos+'"]');
210
162
  if (!$el.length) continue;
211
-
212
- try {
213
- const $last = $items.last();
214
- if ($last.length && $el.prev().is($last)) {
215
- fifoCat.push(old);
216
- return null;
217
- }
218
- } catch (e) {}
219
-
220
- $el.remove();
221
- usedCat.delete(old.id);
222
- destroyPlaceholder(old.id);
163
+ $el.remove(); usedCat.delete(old.id); destroyPlaceholder(old.id);
223
164
  return old.id;
224
165
  }
225
166
  return null;
@@ -227,118 +168,82 @@
227
168
 
228
169
  function injectInTopic() {
229
170
  if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
230
-
231
- const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
171
+ const interval = parseInt(settings.messageIntervalPosts,10) || 3;
232
172
  const pool = parsePool(settings.messagePlaceholderIds);
233
173
  if (!pool.length) return;
234
174
 
235
175
  const $posts = $('[component="post"][data-pid]');
236
176
  if (!$posts.length) return;
237
177
 
238
- $posts.each(function (idx) {
239
- const postNo = idx + 1;
240
- if (postNo % interval !== 0) return;
241
- if (idx === $posts.length - 1) return;
178
+ $posts.each(function(idx){
179
+ const no=idx+1;
180
+ if (no % interval !== 0) return;
181
+ if (idx === $posts.length-1) return;
242
182
 
243
- const $post = $(this);
244
- const existing = $post.next('.ezoic-ad-topic');
245
- if (existing.length) return;
183
+ const $post=$(this);
184
+ if ($post.next('.ezoic-ad-topic').length) return;
246
185
 
247
- let id = pickNextId(pool, usedTopic);
248
- if (!id) {
249
- id = recycleTopic($posts);
250
- if (!id) return;
251
- }
252
-
253
- usedTopic.add(id);
254
- fifoTopic.push({ id, afterNo: postNo });
186
+ let id=pickNextId(pool, usedTopic);
187
+ if (!id) { id=recycleTopic($posts); if (!id) return; }
255
188
 
256
- insertAfter($post, id, 'ezoic-ad-topic', postNo);
189
+ usedTopic.add(id); fifoTopic.push({id, afterNo:no});
190
+ insertAfter($post, id, 'ezoic-ad-topic', no);
257
191
  callEzoicSingle(id);
258
192
  });
259
193
  }
260
194
 
261
195
  function injectInCategory() {
262
196
  if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
263
-
264
- const interval = parseInt(settings.intervalTopics, 10) || 6;
197
+ const interval = parseInt(settings.intervalTopics,10) || 6;
265
198
  const pool = parsePool(settings.placeholderIds);
266
199
  if (!pool.length) return;
267
200
 
268
201
  const $items = $('li[component="category/topic"]');
269
202
  if (!$items.length) return;
270
203
 
271
- $items.each(function (idx) {
272
- const pos = idx + 1;
204
+ $items.each(function(idx){
205
+ const pos=idx+1;
273
206
  if (pos % interval !== 0) return;
274
- if (idx === $items.length - 1) return;
207
+ if (idx === $items.length-1) return;
275
208
 
276
- const $li = $(this);
277
- const existing = $li.next('.ezoic-ad-between');
278
- if (existing.length) return;
279
-
280
- let id = pickNextId(pool, usedCat);
281
- if (!id) {
282
- id = recycleCat($items);
283
- if (!id) return;
284
- }
209
+ const $li=$(this);
210
+ if ($li.next('.ezoic-ad-between').length) return;
285
211
 
286
- usedCat.add(id);
287
- fifoCat.push({ id, afterPos: pos });
212
+ let id=pickNextId(pool, usedCat);
213
+ if (!id) { id=recycleCat($items); if (!id) return; }
288
214
 
215
+ usedCat.add(id); fifoCat.push({id, afterPos:pos});
289
216
  insertAfter($li, id, 'ezoic-ad-between', pos);
290
217
  callEzoicSingle(id);
291
218
  });
292
219
  }
293
220
 
294
221
  function refresh() {
222
+ reloadSettingsFromConfig();
295
223
  if (!settings) return;
296
224
  if (userExcluded()) return;
297
225
 
298
226
  if (refreshInFlight) { refreshQueued = true; return; }
299
227
  refreshInFlight = true;
300
228
  try {
301
- const key = getPageKey();
302
- if (pageKey !== key) {
303
- pageKey = key;
304
- cleanupForNewPage();
305
- }
306
-
229
+ const key=getPageKey();
230
+ if (pageKey !== key) { pageKey=key; cleanupForNewPage(); }
307
231
  setupAutoHeightOnce();
308
-
309
232
  if (isTopicPage()) injectInTopic();
310
233
  else if (isCategoryTopicList()) injectInCategory();
311
234
  } finally {
312
- refreshInFlight = false;
313
- if (refreshQueued) { refreshQueued = false; setTimeout(refresh, 50); }
235
+ refreshInFlight=false;
236
+ if (refreshQueued) { refreshQueued=false; setTimeout(refresh, 50); }
314
237
  }
315
238
  }
316
239
 
317
- function loadSettings(cb) {
318
- socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
319
- settings = data || {};
320
- cb && cb();
321
- });
322
- }
323
-
324
- function boot() {
325
- loadSettings(function () {
326
- refresh();
327
- setTimeout(refresh, 1200);
328
- });
329
- }
240
+ function boot() { refresh(); setTimeout(refresh, 1200); }
330
241
 
331
242
  $(document).ready(boot);
332
243
  $(window).on('action:ajaxify.end', boot);
333
-
334
- $(window).on('action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', function () {
335
- refresh();
336
- setTimeout(refresh, 600);
337
- });
338
-
339
- $(window).on('action:ajaxify.start', function () {
340
- pageKey = null;
341
- cleanupForNewPage();
244
+ $(window).on('action:posts.loaded action:topics.loaded action:topic.loaded action:category.loaded', function(){
245
+ refresh(); setTimeout(refresh, 600);
342
246
  });
247
+ $(window).on('action:ajaxify.start', function(){ pageKey=null; cleanupForNewPage(); });
343
248
 
344
249
  })();
package/public/style.css CHANGED
@@ -1,5 +1,3 @@
1
- /* Le conteneur est caché tant qu'Ezoic n'a pas injecté de contenu */
2
1
  .ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
3
2
  .ezoic-ad:not(.ezoic-filled){display:none !important;}
4
3
  .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
5
- .ezoic-ad .ezoic-ad-inner > div{margin:0;padding:0;}
@@ -1,56 +1,47 @@
1
1
  <div class="acp-page-container">
2
2
  <h1 class="mb-3">Ezoic - Publicités Infinite Scroll</h1>
3
3
 
4
- <div class="alert alert-info">
5
- Format placeholder&nbsp;: <code>&lt;div id="ezoic-pub-ad-placeholder-XXX"&gt;&lt;/div&gt;</code>
6
- </div>
7
-
8
4
  <form role="form" class="ezoic-infinite-settings">
9
5
  <div class="mb-3">
10
6
  <label class="form-label">Groupes exclus (pas de pubs pour ces groupes)</label>
11
7
  <select multiple class="form-select" name="excludedGroups">
12
8
  <!-- BEGIN groups -->
13
9
  <option value="{groups.name}">{groups.name}</option>
14
- <!-- END groups --></select>
15
- <div class="form-text">Maintenez Ctrl/Cmd pour sélectionner plusieurs groupes. Liste triée par ordre alphabétique.</div>
10
+ <!-- END groups -->
11
+ </select>
12
+ <div class="form-text">Liste triée par ordre alphabétique.</div>
16
13
  </div>
17
14
 
18
15
  <hr/>
19
16
 
20
17
  <h3>Entre les topics dans une catégorie (liste des sujets)</h3>
21
-
22
18
  <div class="form-check form-switch mb-2">
23
19
  <input class="form-check-input" type="checkbox" name="enableBetweenAds">
24
- <label class="form-check-label">Activer les pubs entre les topics</label>
20
+ <label class="form-check-label">Activer</label>
25
21
  </div>
26
-
27
22
  <div class="mb-3">
28
- <label class="form-label">Intervalle (insérer après chaque N topics)</label>
23
+ <label class="form-label">Intervalle (après chaque N topics)</label>
29
24
  <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
30
25
  </div>
31
-
32
26
  <div class="mb-3">
33
- <label class="form-label">Pool d'IDs de placeholder (un par ligne)</label>
34
- <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
27
+ <label class="form-label">Pool d'IDs placeholder (un par ligne)</label>
28
+ <textarea class="form-control" name="placeholderIds" rows="5"></textarea>
35
29
  </div>
36
30
 
37
31
  <hr/>
38
32
 
39
33
  <h3>Dans les topics (entre les messages)</h3>
40
-
41
34
  <div class="form-check form-switch mb-2">
42
35
  <input class="form-check-input" type="checkbox" name="enableMessageAds">
43
- <label class="form-check-label">Activer les pubs entre les messages</label>
36
+ <label class="form-check-label">Activer</label>
44
37
  </div>
45
-
46
38
  <div class="mb-3">
47
- <label class="form-label">Intervalle (insérer après chaque N messages)</label>
39
+ <label class="form-label">Intervalle (après chaque N messages)</label>
48
40
  <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
49
41
  </div>
50
-
51
42
  <div class="mb-3">
52
- <label class="form-label">Pool d'IDs de placeholder pour messages (un par ligne)</label>
53
- <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
43
+ <label class="form-label">Pool d'IDs placeholder messages (un par ligne)</label>
44
+ <textarea class="form-control" name="messagePlaceholderIds" rows="5"></textarea>
54
45
  </div>
55
46
 
56
47
  <button type="button" class="btn btn-primary ezoic-infinite-save">Enregistrer</button>