nodebb-plugin-ezoic-infinite 1.3.0 → 1.4.2
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 +15 -1
- package/package.json +1 -1
- package/public/client.js +147 -82
- package/public/templates/admin/plugins/ezoic-infinite.tpl +26 -0
package/library.js
CHANGED
|
@@ -34,13 +34,21 @@ async function getAllGroups() {
|
|
|
34
34
|
async function getSettings() {
|
|
35
35
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
36
36
|
return {
|
|
37
|
-
// Between-post ads (simple blocks)
|
|
37
|
+
// Between-post ads (simple blocks) in category topic list
|
|
38
38
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
39
|
+
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
39
40
|
placeholderIds: (s.placeholderIds || '').trim(),
|
|
40
41
|
intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
|
|
41
42
|
|
|
43
|
+
// Home/categories list ads (between categories on / or /categories)
|
|
44
|
+
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
45
|
+
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
46
|
+
categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
|
|
47
|
+
intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
|
|
48
|
+
|
|
42
49
|
// "Ad message" between replies (looks like a post)
|
|
43
50
|
enableMessageAds: parseBool(s.enableMessageAds, false),
|
|
51
|
+
showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
|
|
44
52
|
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
45
53
|
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
46
54
|
|
|
@@ -88,9 +96,15 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
88
96
|
res.json({
|
|
89
97
|
excluded,
|
|
90
98
|
enableBetweenAds: settings.enableBetweenAds,
|
|
99
|
+
showFirstTopicAd: settings.showFirstTopicAd,
|
|
91
100
|
placeholderIds: settings.placeholderIds,
|
|
92
101
|
intervalPosts: settings.intervalPosts,
|
|
102
|
+
enableCategoryAds: settings.enableCategoryAds,
|
|
103
|
+
showFirstCategoryAd: settings.showFirstCategoryAd,
|
|
104
|
+
categoryPlaceholderIds: settings.categoryPlaceholderIds,
|
|
105
|
+
intervalCategories: settings.intervalCategories,
|
|
93
106
|
enableMessageAds: settings.enableMessageAds,
|
|
107
|
+
showFirstMessageAd: settings.showFirstMessageAd,
|
|
94
108
|
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
95
109
|
messageIntervalPosts: settings.messageIntervalPosts,
|
|
96
110
|
});
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
(function () {
|
|
3
2
|
'use strict';
|
|
4
3
|
|
|
@@ -7,6 +6,7 @@
|
|
|
7
6
|
const SELECTORS = {
|
|
8
7
|
topicItem: 'li[component="category/topic"]',
|
|
9
8
|
postItem: '[component="post"][data-pid]',
|
|
9
|
+
categoryItem: 'li[component="categories/category"]',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
const WRAP_CLASS = 'ezoic-ad';
|
|
@@ -21,9 +21,17 @@
|
|
|
21
21
|
|
|
22
22
|
poolTopics: [],
|
|
23
23
|
poolPosts: [],
|
|
24
|
+
poolCategories: [],
|
|
24
25
|
|
|
25
26
|
usedTopics: new Set(),
|
|
26
27
|
usedPosts: new Set(),
|
|
28
|
+
usedCategories: new Set(),
|
|
29
|
+
|
|
30
|
+
// wrappers currently on page (FIFO for recycling)
|
|
31
|
+
liveBetween: [],
|
|
32
|
+
liveMessage: [],
|
|
33
|
+
liveCategory: [],
|
|
34
|
+
usedCategories: new Set(),
|
|
27
35
|
|
|
28
36
|
lastShowById: new Map(),
|
|
29
37
|
pendingById: new Set(),
|
|
@@ -72,15 +80,23 @@
|
|
|
72
80
|
function getKind() {
|
|
73
81
|
const p = window.location.pathname || '';
|
|
74
82
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
75
|
-
if (/^\/category\//.test(p)) return '
|
|
76
|
-
if (
|
|
77
|
-
|
|
83
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
84
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
85
|
+
// fallback by DOM
|
|
86
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
87
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
88
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
89
|
+
return 'other';
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
function getTopicItems() {
|
|
81
93
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
function getCategoryItems() {
|
|
97
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
function getPostContainers() {
|
|
85
101
|
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
86
102
|
return nodes.filter((el) => {
|
|
@@ -93,9 +109,63 @@
|
|
|
93
109
|
});
|
|
94
110
|
}
|
|
95
111
|
|
|
96
|
-
|
|
112
|
+
|
|
113
|
+
function safeRect(el) {
|
|
114
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function destroyPlaceholderIds(ids) {
|
|
118
|
+
if (!ids || !ids.length) return;
|
|
119
|
+
const call = () => {
|
|
120
|
+
try {
|
|
121
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
122
|
+
window.ezstandalone.destroyPlaceholders(ids);
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {}
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
128
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
129
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
130
|
+
else window.ezstandalone.cmd.push(call);
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getRecyclable(liveArr) {
|
|
135
|
+
const margin = 1800;
|
|
136
|
+
for (let i = 0; i < liveArr.length; i++) {
|
|
137
|
+
const entry = liveArr[i];
|
|
138
|
+
if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
|
|
139
|
+
const r = safeRect(entry.wrap);
|
|
140
|
+
if (r && r.bottom < -margin) {
|
|
141
|
+
liveArr.splice(i, 1);
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function moveWrapAfter(wrap, target, kindClass, afterPos) {
|
|
149
|
+
try {
|
|
150
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
151
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
152
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
153
|
+
return true;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pickId(pool, liveArr) {
|
|
160
|
+
if (pool.length) return { id: pool.shift(), recycled: null };
|
|
161
|
+
const recycled = getRecyclable(liveArr);
|
|
162
|
+
if (recycled) return { id: recycled.id, recycled };
|
|
163
|
+
return { id: null, recycled: null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
97
167
|
const wrap = document.createElement('div');
|
|
98
|
-
wrap.className = `${WRAP_CLASS} ${
|
|
168
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
99
169
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
100
170
|
wrap.style.width = '100%';
|
|
101
171
|
|
|
@@ -122,15 +192,10 @@
|
|
|
122
192
|
try {
|
|
123
193
|
state.usedTopics.forEach((id) => ids.push(id));
|
|
124
194
|
state.usedPosts.forEach((id) => ids.push(id));
|
|
195
|
+
state.usedCategories && state.usedCategories.forEach((id) => ids.push(id));
|
|
125
196
|
} catch (e) {}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const call = () => {
|
|
130
|
-
try {
|
|
131
|
-
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
132
|
-
window.ezstandalone.destroyPlaceholders(ids);
|
|
133
|
-
}
|
|
197
|
+
destroyPlaceholderIds(ids);
|
|
198
|
+
}
|
|
134
199
|
} catch (e) {}
|
|
135
200
|
};
|
|
136
201
|
|
|
@@ -228,9 +293,9 @@
|
|
|
228
293
|
})();
|
|
229
294
|
}
|
|
230
295
|
|
|
231
|
-
function nextId(
|
|
232
|
-
|
|
233
|
-
if (pool.length) return pool.shift();
|
|
296
|
+
function nextId(pool) {
|
|
297
|
+
// backward compatible: the injector passes the pool array
|
|
298
|
+
if (Array.isArray(pool) && pool.length) return pool.shift();
|
|
234
299
|
return null;
|
|
235
300
|
}
|
|
236
301
|
|
|
@@ -257,6 +322,7 @@
|
|
|
257
322
|
function initPools(cfg) {
|
|
258
323
|
if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
|
|
259
324
|
if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
|
|
325
|
+
if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
260
326
|
}
|
|
261
327
|
|
|
262
328
|
function computeTargets(count, interval, showFirst) {
|
|
@@ -266,77 +332,46 @@
|
|
|
266
332
|
for (let i = 1; i <= count; i++) {
|
|
267
333
|
if (i % interval === 0) out.push(i);
|
|
268
334
|
}
|
|
269
|
-
// uniq + sort
|
|
270
335
|
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
271
336
|
}
|
|
272
337
|
|
|
273
|
-
function
|
|
274
|
-
if (!normalizeBool(cfg.enableBetweenAds)) return 0;
|
|
275
|
-
|
|
276
|
-
const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
|
|
277
|
-
const showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
278
|
-
|
|
279
|
-
const items = getTopicItems();
|
|
338
|
+
function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
|
|
280
339
|
if (!items.length) return 0;
|
|
281
|
-
|
|
282
340
|
const targets = computeTargets(items.length, interval, showFirst);
|
|
283
341
|
|
|
284
342
|
let inserted = 0;
|
|
285
343
|
for (const afterPos of targets) {
|
|
286
344
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
287
345
|
|
|
288
|
-
const
|
|
289
|
-
if (!
|
|
346
|
+
const el = items[afterPos - 1];
|
|
347
|
+
if (!el || !el.isConnected) continue;
|
|
290
348
|
|
|
291
|
-
//
|
|
292
|
-
const prevWrap = findWrap(
|
|
349
|
+
// Prevent back-to-back at load
|
|
350
|
+
const prevWrap = findWrap(kindClass, afterPos - 1);
|
|
293
351
|
if (prevWrap) continue;
|
|
294
352
|
|
|
295
|
-
if (findWrap(
|
|
296
|
-
|
|
297
|
-
const id = nextId('between');
|
|
298
|
-
if (!id) break;
|
|
299
|
-
|
|
300
|
-
state.usedTopics.add(id);
|
|
301
|
-
const wrap = insertAfter(li, id, 'ezoic-ad-between', afterPos);
|
|
302
|
-
if (!wrap) continue;
|
|
303
|
-
|
|
304
|
-
callShowAdsWhenReady(id);
|
|
305
|
-
inserted += 1;
|
|
306
|
-
}
|
|
307
|
-
return inserted;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function injectPosts(cfg) {
|
|
311
|
-
if (!normalizeBool(cfg.enableMessageAds)) return 0;
|
|
312
|
-
|
|
313
|
-
const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
314
|
-
const showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
315
|
-
|
|
316
|
-
const posts = getPostContainers();
|
|
317
|
-
if (!posts.length) return 0;
|
|
318
|
-
|
|
319
|
-
const targets = computeTargets(posts.length, interval, showFirst);
|
|
320
|
-
|
|
321
|
-
let inserted = 0;
|
|
322
|
-
for (const afterPos of targets) {
|
|
323
|
-
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
324
|
-
|
|
325
|
-
const post = posts[afterPos - 1];
|
|
326
|
-
if (!post || !post.isConnected) continue;
|
|
353
|
+
if (findWrap(kindClass, afterPos)) continue;
|
|
327
354
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (findWrap('ezoic-ad-message', afterPos)) continue;
|
|
355
|
+
const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
|
|
356
|
+
: (kindClass === 'ezoic-ad-message') ? state.liveMessage
|
|
357
|
+
: state.liveCategory;
|
|
332
358
|
|
|
333
|
-
const
|
|
359
|
+
const pick = pickId(kindPool, liveArr);
|
|
360
|
+
const id = pick.id;
|
|
334
361
|
if (!id) break;
|
|
335
362
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
363
|
+
let wrap = null;
|
|
364
|
+
if (pick.recycled && pick.recycled.wrap) {
|
|
365
|
+
destroyPlaceholderIds([id]);
|
|
366
|
+
wrap = pick.recycled.wrap;
|
|
367
|
+
if (!moveWrapAfter(wrap, el, kindClass, afterPos)) continue;
|
|
368
|
+
} else {
|
|
369
|
+
usedSet.add(id);
|
|
370
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
371
|
+
if (!wrap) continue;
|
|
372
|
+
}
|
|
339
373
|
|
|
374
|
+
liveArr.push({ id, wrap });
|
|
340
375
|
callShowAdsWhenReady(id);
|
|
341
376
|
inserted += 1;
|
|
342
377
|
}
|
|
@@ -344,7 +379,6 @@
|
|
|
344
379
|
}
|
|
345
380
|
|
|
346
381
|
function enforceNoAdjacentAds() {
|
|
347
|
-
// If DOM changes produce adjacent wrappers (rare but possible), hide the later one.
|
|
348
382
|
const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
|
|
349
383
|
for (let i = 0; i < ads.length; i++) {
|
|
350
384
|
const ad = ads[i];
|
|
@@ -363,8 +397,15 @@
|
|
|
363
397
|
|
|
364
398
|
state.poolTopics = [];
|
|
365
399
|
state.poolPosts = [];
|
|
400
|
+
state.poolCategories = [];
|
|
401
|
+
state.poolCategories = [];
|
|
366
402
|
state.usedTopics.clear();
|
|
367
403
|
state.usedPosts.clear();
|
|
404
|
+
state.usedCategories && state.usedCategories.clear();
|
|
405
|
+
state.liveBetween = [];
|
|
406
|
+
state.liveMessage = [];
|
|
407
|
+
state.liveCategory = [];
|
|
408
|
+
state.usedCategories.clear();
|
|
368
409
|
|
|
369
410
|
state.lastShowById = new Map();
|
|
370
411
|
state.pendingById = new Set();
|
|
@@ -381,7 +422,7 @@
|
|
|
381
422
|
|
|
382
423
|
function ensureObserver() {
|
|
383
424
|
if (state.obs) return;
|
|
384
|
-
state.obs = new MutationObserver(() => scheduleRun('
|
|
425
|
+
state.obs = new MutationObserver(() => scheduleRun('mutation'));
|
|
385
426
|
try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
386
427
|
setTimeout(() => { if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; } }, 15000);
|
|
387
428
|
}
|
|
@@ -397,22 +438,47 @@
|
|
|
397
438
|
const kind = getKind();
|
|
398
439
|
let inserted = 0;
|
|
399
440
|
|
|
400
|
-
if (kind === 'topic')
|
|
401
|
-
|
|
441
|
+
if (kind === 'topic') {
|
|
442
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
443
|
+
inserted = injectBetween('ezoic-ad-message', getPostContainers(),
|
|
444
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
445
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
446
|
+
'message',
|
|
447
|
+
state.usedPosts);
|
|
448
|
+
}
|
|
449
|
+
} else if (kind === 'categoryTopics') {
|
|
450
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
451
|
+
inserted = injectBetween('ezoic-ad-between', getTopicItems(),
|
|
452
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
453
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
454
|
+
'between',
|
|
455
|
+
state.usedTopics);
|
|
456
|
+
}
|
|
457
|
+
} else if (kind === 'categories') {
|
|
458
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
459
|
+
inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
|
|
460
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
461
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
462
|
+
'categories',
|
|
463
|
+
state.usedCategories);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
402
466
|
|
|
403
467
|
enforceNoAdjacentAds();
|
|
404
468
|
|
|
405
|
-
// If nothing inserted and
|
|
406
|
-
|
|
407
|
-
if (
|
|
469
|
+
// If nothing inserted and list isn't in DOM yet (first click), retry a bit
|
|
470
|
+
let count = 0;
|
|
471
|
+
if (kind === 'topic') count = getPostContainers().length;
|
|
472
|
+
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
473
|
+
else if (kind === 'categories') count = getCategoryItems().length;
|
|
474
|
+
|
|
475
|
+
if (count === 0 && state.attempts < 25) {
|
|
408
476
|
state.attempts += 1;
|
|
409
477
|
setTimeout(() => scheduleRun('await-items'), 120);
|
|
410
478
|
return;
|
|
411
479
|
}
|
|
412
480
|
|
|
413
|
-
if (inserted >= MAX_INSERTS_PER_RUN)
|
|
414
|
-
setTimeout(() => scheduleRun('continue-fill'), 140);
|
|
415
|
-
}
|
|
481
|
+
if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
|
|
416
482
|
}
|
|
417
483
|
|
|
418
484
|
function scheduleRun() {
|
|
@@ -468,11 +534,10 @@
|
|
|
468
534
|
});
|
|
469
535
|
}
|
|
470
536
|
|
|
471
|
-
// Boot
|
|
472
537
|
cleanup();
|
|
473
538
|
bind();
|
|
474
539
|
ensureObserver();
|
|
475
540
|
state.pageKey = getPageKey();
|
|
476
541
|
scheduleRun();
|
|
477
542
|
setTimeout(scheduleRun, 250);
|
|
478
|
-
})();
|
|
543
|
+
})();
|
|
@@ -26,6 +26,32 @@
|
|
|
26
26
|
|
|
27
27
|
<hr/>
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
<hr/>
|
|
31
|
+
|
|
32
|
+
<h4 class="mt-3">Pubs entre les catégories (page d’accueil)</h4>
|
|
33
|
+
<p class="form-text">Insère des pubs entre les catégories sur la page d’accueil (liste des catégories).</p>
|
|
34
|
+
|
|
35
|
+
<div class="form-check mb-3">
|
|
36
|
+
<input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
|
|
37
|
+
<label class="form-check-label" for="enableCategoryAds">Activer les pubs entre les catégories</label>
|
|
38
|
+
<div class="form-check mt-2">
|
|
39
|
+
<input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
|
|
40
|
+
<label class="form-check-label">Afficher une pub après la 1ère catégorie</label>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="mb-3">
|
|
45
|
+
<label class="form-label" for="categoryPlaceholderIds">Pool d’IDs Ezoic (catégories)</label>
|
|
46
|
+
<textarea id="categoryPlaceholderIds" name="categoryPlaceholderIds" class="form-control" rows="4">{categoryPlaceholderIds}</textarea>
|
|
47
|
+
<p class="form-text">IDs numériques, un par ligne. Utilise un pool dédié (différent des pools topics/messages).</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="mb-3">
|
|
51
|
+
<label class="form-label" for="intervalCategories">Afficher une pub toutes les N catégories</label>
|
|
52
|
+
<input type="number" id="intervalCategories" name="intervalCategories" class="form-control" value="{intervalCategories}" min="1">
|
|
53
|
+
</div>
|
|
54
|
+
|
|
29
55
|
<h4 class="mt-3">Pubs “message” entre les réponses</h4>
|
|
30
56
|
<p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
|
|
31
57
|
|