nodebb-plugin-ezoic-infinite 1.4.71 → 1.4.73
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/{public/client.js → client.js} +42 -38
- package/package.json +1 -1
- package/style.css +43 -0
- package/README.md +0 -15
- package/library.js +0 -130
- package/plugin.json +0 -33
- package/public/style.css +0 -43
- /package/{public/admin.js → admin.js} +0 -0
- /package/{public/templates → templates}/admin/plugins/ezoic-infinite.tpl +0 -0
|
@@ -238,25 +238,17 @@
|
|
|
238
238
|
return false;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
function buildWrap(
|
|
242
|
-
|
|
243
|
-
// otherwise fall back to <div>. This prevents DOM "repair" that can drop/move placeholders.
|
|
244
|
-
const parentTag = (target && target.parentElement && target.parentElement.tagName) ? target.parentElement.tagName.toUpperCase() : '';
|
|
245
|
-
const useLi = target && target.tagName && target.tagName.toUpperCase() === 'LI' && (parentTag === 'UL' || parentTag === 'OL');
|
|
246
|
-
|
|
247
|
-
const wrap = document.createElement(useLi ? 'li' : 'div');
|
|
241
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
242
|
+
const wrap = document.createElement('div');
|
|
248
243
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
249
244
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
250
|
-
|
|
251
|
-
// Ensure it behaves like a full-width block inside list layouts
|
|
252
245
|
wrap.style.width = '100%';
|
|
253
|
-
if (useLi) wrap.style.listStyle = 'none';
|
|
254
246
|
|
|
255
247
|
const ph = document.createElement('div');
|
|
256
248
|
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
257
249
|
wrap.appendChild(ph);
|
|
258
250
|
return wrap;
|
|
259
|
-
}
|
|
251
|
+
}
|
|
260
252
|
|
|
261
253
|
function findWrap(kindClass, afterPos) {
|
|
262
254
|
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
@@ -276,7 +268,7 @@
|
|
|
276
268
|
insertingIds.add(id);
|
|
277
269
|
|
|
278
270
|
try {
|
|
279
|
-
const wrap = buildWrap(
|
|
271
|
+
const wrap = buildWrap(id, kindClass, afterPos);
|
|
280
272
|
target.insertAdjacentElement('afterend', wrap);
|
|
281
273
|
attachFillObserver(wrap, id);
|
|
282
274
|
return wrap;
|
|
@@ -290,24 +282,7 @@
|
|
|
290
282
|
if (ids.length) destroyPlaceholderIds(ids);
|
|
291
283
|
}
|
|
292
284
|
|
|
293
|
-
|
|
294
|
-
function forcePlaceholderAutoHeight(wrap, id) {
|
|
295
|
-
try {
|
|
296
|
-
if (!wrap) return;
|
|
297
|
-
const ph = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
|
|
298
|
-
if (!ph) return;
|
|
299
|
-
ph.style.setProperty('height', 'auto', 'important');
|
|
300
|
-
ph.style.setProperty('min-height', '0px', 'important');
|
|
301
|
-
requestAnimationFrame(() => {
|
|
302
|
-
try {
|
|
303
|
-
ph.style.setProperty('height', 'auto', 'important');
|
|
304
|
-
ph.style.setProperty('min-height', '0px', 'important');
|
|
305
|
-
} catch (e) {}
|
|
306
|
-
});
|
|
307
|
-
} catch (e) {}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function patchShowAds() {
|
|
285
|
+
function patchShowAds() {
|
|
311
286
|
const applyPatch = () => {
|
|
312
287
|
try {
|
|
313
288
|
window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
|
|
@@ -343,12 +318,33 @@ function patchShowAds() {
|
|
|
343
318
|
}
|
|
344
319
|
}
|
|
345
320
|
|
|
346
|
-
function markFilled(wrap
|
|
321
|
+
function markFilled(wrap) {
|
|
347
322
|
try {
|
|
348
323
|
if (!wrap) return;
|
|
349
324
|
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
350
325
|
wrap.setAttribute('data-ezoic-filled', '1');
|
|
351
|
-
|
|
326
|
+
} catch (e) {}
|
|
327
|
+
}
|
|
328
|
+
|
|
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) {}
|
|
352
348
|
} catch (e) {}
|
|
353
349
|
}
|
|
354
350
|
|
|
@@ -362,13 +358,15 @@ function patchShowAds() {
|
|
|
362
358
|
if (!ph) return;
|
|
363
359
|
// Already filled?
|
|
364
360
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
365
|
-
markFilled(wrap
|
|
361
|
+
markFilled(wrap); // Afficher wrapper
|
|
362
|
+
forcePlaceholderAutoHeight(wrap, id);
|
|
366
363
|
sessionDefinedIds.add(id);
|
|
367
364
|
return;
|
|
368
365
|
}
|
|
369
366
|
const obs = new MutationObserver(() => {
|
|
370
367
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
371
|
-
markFilled(wrap
|
|
368
|
+
markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
|
|
369
|
+
forcePlaceholderAutoHeight(wrap, id);
|
|
372
370
|
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
373
371
|
try { obs.disconnect(); } catch (e) {}
|
|
374
372
|
}
|
|
@@ -388,7 +386,7 @@ function patchShowAds() {
|
|
|
388
386
|
const filled = !!(ph.childNodes && ph.childNodes.length > 0);
|
|
389
387
|
if (filled) {
|
|
390
388
|
try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
|
|
391
|
-
try { markFilled(wrap
|
|
389
|
+
try { markFilled(wrap); } catch (e) {}
|
|
392
390
|
}
|
|
393
391
|
return filled;
|
|
394
392
|
}
|
|
@@ -417,17 +415,23 @@ function patchShowAds() {
|
|
|
417
415
|
batchShowAdsTimer = setTimeout(() => {
|
|
418
416
|
if (pendingShowAdsIds.size === 0) return;
|
|
419
417
|
|
|
420
|
-
|
|
418
|
+
// Only keep IDs whose placeholders still exist at execution time
|
|
419
|
+
const idsArray = Array.from(pendingShowAdsIds).filter((id) => {
|
|
420
|
+
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
421
|
+
return !!(el && el.isConnected);
|
|
422
|
+
});
|
|
421
423
|
pendingShowAdsIds.clear();
|
|
422
424
|
|
|
425
|
+
if (!idsArray.length) return;
|
|
426
|
+
|
|
423
427
|
// Appeler showAds avec TOUS les IDs en une fois
|
|
424
428
|
try {
|
|
425
429
|
window.ezstandalone = window.ezstandalone || {};
|
|
426
430
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
427
431
|
window.ezstandalone.cmd.push(function() {
|
|
428
432
|
if (typeof window.ezstandalone.showAds === 'function') {
|
|
429
|
-
//
|
|
430
|
-
window.ezstandalone.showAds(
|
|
433
|
+
// Call as array (most compatible with ezstandalone)
|
|
434
|
+
window.ezstandalone.showAds(idsArray);
|
|
431
435
|
// Tracker tous les IDs
|
|
432
436
|
idsArray.forEach(id => {
|
|
433
437
|
state.lastShowById.set(id, Date.now());
|
package/package.json
CHANGED
package/style.css
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
package/README.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
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/plugin.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
}
|
package/public/style.css
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
.ezoic-ad {
|
|
2
|
-
height: auto !important;
|
|
3
|
-
padding: 0 !important;
|
|
4
|
-
margin: 0 !important;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.ezoic-ad * {
|
|
8
|
-
margin: 0 !important;
|
|
9
|
-
padding: 0 !important;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/* --- Ezoic anti-blank-space fixes (NodeBB + Ajaxify) --- */
|
|
13
|
-
.ezoic-ad {
|
|
14
|
-
display: none; /* shown only when filled */
|
|
15
|
-
width: 100% !important;
|
|
16
|
-
height: auto !important;
|
|
17
|
-
min-height: 0 !important;
|
|
18
|
-
padding: 0 !important;
|
|
19
|
-
margin: 0 !important;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.ezoic-ad[data-ezoic-filled="1"]{
|
|
23
|
-
display: block;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* Ezoic placeholder should not reserve fixed height */
|
|
27
|
-
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"]{
|
|
28
|
-
height: auto !important;
|
|
29
|
-
min-height: 0 !important;
|
|
30
|
-
margin: 0 !important;
|
|
31
|
-
padding: 0 !important;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* prevent baseline gaps under iframes/ins */
|
|
35
|
-
.ezoic-ad iframe,
|
|
36
|
-
.ezoic-ad ins{
|
|
37
|
-
display: block !important;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* neutralize empty spacer divs if any */
|
|
41
|
-
.ezoic-ad > div:empty{
|
|
42
|
-
display:none !important;
|
|
43
|
-
}
|
|
File without changes
|
|
File without changes
|