nodebb-plugin-ezoic-infinite 1.4.73 → 1.4.75
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/README.md +15 -0
- package/library.js +130 -0
- package/package.json +1 -1
- package/plugin.json +33 -0
- package/{client.js → public/client.js} +11 -56
- package/public/style.css +21 -0
- package/style.css +0 -43
- /package/{admin.js → public/admin.js} +0 -0
- /package/{templates → public/templates}/admin/plugins/ezoic-infinite.tpl +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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/package.json
CHANGED
package/plugin.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -142,32 +142,17 @@
|
|
|
142
142
|
const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
|
|
143
143
|
if (!ph) return;
|
|
144
144
|
|
|
145
|
-
// Supprimer
|
|
146
|
-
// qui créent de l'espace vertical
|
|
147
|
-
let found = false;
|
|
145
|
+
// ULTRA-AGRESSIF: Supprimer TOUT sauf le placeholder
|
|
148
146
|
Array.from(wrapper.children).forEach(child => {
|
|
149
|
-
if (child === ph || child.contains(ph))
|
|
150
|
-
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Si élément APRÈS le placeholder
|
|
155
|
-
if (found) {
|
|
156
|
-
const rect = child.getBoundingClientRect();
|
|
157
|
-
const computed = window.getComputedStyle(child);
|
|
158
|
-
|
|
159
|
-
// Supprimer si:
|
|
160
|
-
// 1. Height > 0 mais pas de texte/image visible
|
|
161
|
-
// 2. Ou opacity: 0
|
|
162
|
-
// 3. Ou visibility: hidden
|
|
163
|
-
const hasContent = child.textContent.trim().length > 0 ||
|
|
164
|
-
child.querySelector('img, iframe, video');
|
|
165
|
-
|
|
166
|
-
if (!hasContent || computed.opacity === '0' || computed.visibility === 'hidden') {
|
|
147
|
+
if (child === ph || child.contains(ph)) return;
|
|
148
|
+
// Supprimer TOUT le reste
|
|
167
149
|
child.remove();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
150
|
});
|
|
151
|
+
|
|
152
|
+
// Forcer wrapper sans espace
|
|
153
|
+
wrapper.style.height = 'auto';
|
|
154
|
+
wrapper.style.overflow = 'hidden';
|
|
155
|
+
wrapper.style.lineHeight = '0';
|
|
171
156
|
});
|
|
172
157
|
} catch (e) {}
|
|
173
158
|
}
|
|
@@ -326,28 +311,6 @@
|
|
|
326
311
|
} catch (e) {}
|
|
327
312
|
}
|
|
328
313
|
|
|
329
|
-
function forcePlaceholderAutoHeight(wrap, id) {
|
|
330
|
-
try {
|
|
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) {}
|
|
348
|
-
} catch (e) {}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
314
|
function isWrapMarkedFilled(wrap) {
|
|
352
315
|
try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
|
|
353
316
|
}
|
|
@@ -359,14 +322,12 @@
|
|
|
359
322
|
// Already filled?
|
|
360
323
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
361
324
|
markFilled(wrap); // Afficher wrapper
|
|
362
|
-
forcePlaceholderAutoHeight(wrap, id);
|
|
363
325
|
sessionDefinedIds.add(id);
|
|
364
326
|
return;
|
|
365
327
|
}
|
|
366
328
|
const obs = new MutationObserver(() => {
|
|
367
329
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
368
330
|
markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
|
|
369
|
-
forcePlaceholderAutoHeight(wrap, id);
|
|
370
331
|
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
371
332
|
try { obs.disconnect(); } catch (e) {}
|
|
372
333
|
}
|
|
@@ -415,23 +376,17 @@
|
|
|
415
376
|
batchShowAdsTimer = setTimeout(() => {
|
|
416
377
|
if (pendingShowAdsIds.size === 0) return;
|
|
417
378
|
|
|
418
|
-
|
|
419
|
-
const idsArray = Array.from(pendingShowAdsIds).filter((id) => {
|
|
420
|
-
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
421
|
-
return !!(el && el.isConnected);
|
|
422
|
-
});
|
|
379
|
+
const idsArray = Array.from(pendingShowAdsIds);
|
|
423
380
|
pendingShowAdsIds.clear();
|
|
424
381
|
|
|
425
|
-
if (!idsArray.length) return;
|
|
426
|
-
|
|
427
382
|
// Appeler showAds avec TOUS les IDs en une fois
|
|
428
383
|
try {
|
|
429
384
|
window.ezstandalone = window.ezstandalone || {};
|
|
430
385
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
431
386
|
window.ezstandalone.cmd.push(function() {
|
|
432
387
|
if (typeof window.ezstandalone.showAds === 'function') {
|
|
433
|
-
//
|
|
434
|
-
window.ezstandalone.showAds(idsArray);
|
|
388
|
+
// Appel batch: showAds(id1, id2, id3...)
|
|
389
|
+
window.ezstandalone.showAds(...idsArray);
|
|
435
390
|
// Tracker tous les IDs
|
|
436
391
|
idsArray.forEach(id => {
|
|
437
392
|
state.lastShowById.set(id, Date.now());
|
package/public/style.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.ezoic-ad {
|
|
2
|
+
height: auto !important;
|
|
3
|
+
padding: 0 !important;
|
|
4
|
+
margin: 0 !important;
|
|
5
|
+
border: 0 !important;
|
|
6
|
+
overflow: hidden !important;
|
|
7
|
+
line-height: 0 !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ezoic-ad * {
|
|
11
|
+
margin: 0 !important;
|
|
12
|
+
padding: 0 !important;
|
|
13
|
+
border: 0 !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.ezoic-ad::after {
|
|
17
|
+
content: '';
|
|
18
|
+
display: block;
|
|
19
|
+
height: 0 !important;
|
|
20
|
+
margin: 0 !important;
|
|
21
|
+
}
|
package/style.css
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|