nodebb-plugin-ezoic-infinite 1.8.18 → 1.8.20
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 +148 -141
- package/package.json +2 -2
- package/public/admin.js +32 -16
- package/public/client.js +395 -228
- package/public/style.css +9 -15
package/library.js
CHANGED
|
@@ -1,198 +1,205 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const meta
|
|
3
|
+
const meta = require.main.require('./src/meta');
|
|
4
4
|
const groups = require.main.require('./src/groups');
|
|
5
|
-
const db
|
|
5
|
+
const db = require.main.require('./src/database');
|
|
6
6
|
|
|
7
7
|
const SETTINGS_KEY = 'ezoic-infinite';
|
|
8
|
-
const
|
|
8
|
+
const SETTINGS_TTL_MS = 30_000;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
|
|
11
|
+
<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
|
|
12
|
+
<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
15
|
+
ezstandalone.cmd = ezstandalone.cmd || [];
|
|
16
|
+
</script>`;
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const DEFAULTS = Object.freeze({
|
|
19
|
+
enableBetweenAds: true,
|
|
20
|
+
showFirstTopicAd: false,
|
|
21
|
+
placeholderIds: '',
|
|
22
|
+
intervalPosts: 6,
|
|
23
|
+
enableCategoryAds: false,
|
|
24
|
+
showFirstCategoryAd: false,
|
|
25
|
+
categoryPlaceholderIds: '',
|
|
26
|
+
intervalCategories: 4,
|
|
27
|
+
enableMessageAds: false,
|
|
28
|
+
showFirstMessageAd: false,
|
|
29
|
+
messagePlaceholderIds: '',
|
|
30
|
+
messageIntervalPosts: 3,
|
|
31
|
+
excludedGroups: [],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const CONFIG_FIELDS = Object.freeze([
|
|
35
|
+
'enableBetweenAds', 'showFirstTopicAd', 'placeholderIds', 'intervalPosts',
|
|
36
|
+
'enableCategoryAds', 'showFirstCategoryAd', 'categoryPlaceholderIds', 'intervalCategories',
|
|
37
|
+
'enableMessageAds', 'showFirstMessageAd', 'messagePlaceholderIds', 'messageIntervalPosts',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const plugin = {
|
|
41
|
+
_settingsCache: null,
|
|
42
|
+
_settingsCacheAt: 0,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function toBool(value, fallback = false) {
|
|
46
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
47
|
+
if (typeof value === 'boolean') return value;
|
|
48
|
+
switch (String(value).trim().toLowerCase()) {
|
|
49
|
+
case '1':
|
|
50
|
+
case 'true':
|
|
51
|
+
case 'on':
|
|
52
|
+
case 'yes':
|
|
53
|
+
return true;
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
19
56
|
}
|
|
20
|
-
// Fallback : séparation par virgule
|
|
21
|
-
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const s = String(v).toLowerCase();
|
|
28
|
-
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
59
|
+
function toPositiveInt(value, fallback) {
|
|
60
|
+
const parsed = Number.parseInt(value, 10);
|
|
61
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
29
62
|
}
|
|
30
63
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!names || !names.length) {
|
|
34
|
-
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
35
|
-
}
|
|
36
|
-
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
37
|
-
const data = await groups.getGroupsData(filtered);
|
|
38
|
-
const valid = data.filter(g => g && g.name);
|
|
39
|
-
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
40
|
-
return valid;
|
|
64
|
+
function toStringTrim(value) {
|
|
65
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
function normalizeExcludedGroups(value) {
|
|
69
|
+
if (!value) return [];
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.map(String).map(v => v.trim()).filter(Boolean);
|
|
72
|
+
}
|
|
44
73
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const SETTINGS_TTL = 30_000;
|
|
74
|
+
const raw = String(value).trim();
|
|
75
|
+
if (!raw) return [];
|
|
48
76
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
placeholderIds: (s.placeholderIds || '').trim(),
|
|
58
|
-
intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
|
|
59
|
-
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
60
|
-
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
61
|
-
categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
|
|
62
|
-
intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
|
|
63
|
-
enableMessageAds: parseBool(s.enableMessageAds, false),
|
|
64
|
-
showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
|
|
65
|
-
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
66
|
-
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
67
|
-
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
68
|
-
};
|
|
69
|
-
return _settingsCache;
|
|
70
|
-
}
|
|
77
|
+
if (raw.startsWith('[')) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (Array.isArray(parsed)) {
|
|
81
|
+
return parsed.map(String).map(v => v.trim()).filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
71
85
|
|
|
72
|
-
|
|
73
|
-
if (!uid || !excludedGroups.length) return false;
|
|
74
|
-
const userGroups = await groups.getUserGroups([uid]);
|
|
75
|
-
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
86
|
+
return raw.split(',').map(v => v.trim()).filter(Boolean);
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function buildSettings(raw = {}) {
|
|
90
|
+
return {
|
|
91
|
+
enableBetweenAds: toBool(raw.enableBetweenAds, DEFAULTS.enableBetweenAds),
|
|
92
|
+
showFirstTopicAd: toBool(raw.showFirstTopicAd, DEFAULTS.showFirstTopicAd),
|
|
93
|
+
placeholderIds: toStringTrim(raw.placeholderIds),
|
|
94
|
+
intervalPosts: toPositiveInt(raw.intervalPosts, DEFAULTS.intervalPosts),
|
|
95
|
+
enableCategoryAds: toBool(raw.enableCategoryAds, DEFAULTS.enableCategoryAds),
|
|
96
|
+
showFirstCategoryAd: toBool(raw.showFirstCategoryAd, DEFAULTS.showFirstCategoryAd),
|
|
97
|
+
categoryPlaceholderIds: toStringTrim(raw.categoryPlaceholderIds),
|
|
98
|
+
intervalCategories: toPositiveInt(raw.intervalCategories, DEFAULTS.intervalCategories),
|
|
99
|
+
enableMessageAds: toBool(raw.enableMessageAds, DEFAULTS.enableMessageAds),
|
|
100
|
+
showFirstMessageAd: toBool(raw.showFirstMessageAd, DEFAULTS.showFirstMessageAd),
|
|
101
|
+
messagePlaceholderIds: toStringTrim(raw.messagePlaceholderIds),
|
|
102
|
+
messageIntervalPosts: toPositiveInt(raw.messageIntervalPosts, DEFAULTS.messageIntervalPosts),
|
|
103
|
+
excludedGroups: normalizeExcludedGroups(raw.excludedGroups),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
78
106
|
|
|
107
|
+
async function listNonPrivilegeGroups() {
|
|
108
|
+
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
109
|
+
if (!Array.isArray(names) || !names.length) {
|
|
110
|
+
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
111
|
+
}
|
|
79
112
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let _groupsCache = null;
|
|
83
|
-
let _groupsCacheAt = 0;
|
|
84
|
-
const GROUPS_TTL = 5 * 60_000;
|
|
113
|
+
const publicNames = (names || []).filter(name => !groups.isPrivilegeGroup(name));
|
|
114
|
+
const groupData = await groups.getGroupsData(publicNames);
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const g = await getAllGroups();
|
|
90
|
-
_groupsCache = g;
|
|
91
|
-
_groupsCacheAt = Date.now();
|
|
92
|
-
return _groupsCache;
|
|
116
|
+
return (groupData || [])
|
|
117
|
+
.filter(group => group && group.name)
|
|
118
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
async function getSettings() {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (plugin._settingsCache && (now - plugin._settingsCacheAt) < SETTINGS_TTL_MS) {
|
|
124
|
+
return plugin._settingsCache;
|
|
125
|
+
}
|
|
96
126
|
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const EXCLUDED_MAX = 10_000;
|
|
127
|
+
const raw = await meta.settings.get(SETTINGS_KEY);
|
|
128
|
+
const settings = buildSettings(raw);
|
|
100
129
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
130
|
+
plugin._settingsCache = settings;
|
|
131
|
+
plugin._settingsCacheAt = now;
|
|
132
|
+
return settings;
|
|
104
133
|
}
|
|
105
134
|
|
|
106
|
-
async function
|
|
107
|
-
if (!uid || !excludedGroups.length) return false;
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
const sig = excludedSig(excludedGroups);
|
|
110
|
-
const hit = _excludedCache.get(uid);
|
|
111
|
-
if (hit && hit.sig === sig && (now - hit.t) < EXCLUDED_TTL) return hit.v;
|
|
112
|
-
const v = await isUserExcluded(uid, excludedGroups);
|
|
113
|
-
if (_excludedCache.size > EXCLUDED_MAX) _excludedCache.clear();
|
|
114
|
-
_excludedCache.set(uid, { v, t: now, sig });
|
|
115
|
-
return v;
|
|
116
|
-
}
|
|
117
|
-
// ── Scripts Ezoic ──────────────────────────────────────────────────────────
|
|
135
|
+
async function isUserExcluded(uid, excludedGroups) {
|
|
136
|
+
if (!uid || !Array.isArray(excludedGroups) || !excludedGroups.length) return false;
|
|
118
137
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<script>
|
|
123
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
124
|
-
ezstandalone.cmd = ezstandalone.cmd || [];
|
|
125
|
-
</script>`;
|
|
138
|
+
const userGroups = await groups.getUserGroups([uid]);
|
|
139
|
+
const names = (userGroups && userGroups[0]) || [];
|
|
140
|
+
const excludedSet = new Set(excludedGroups);
|
|
126
141
|
|
|
127
|
-
|
|
142
|
+
return names.some(group => group && excludedSet.has(group.name));
|
|
143
|
+
}
|
|
128
144
|
|
|
129
|
-
plugin.onSettingsSet = function (data) {
|
|
130
|
-
if (data && data.hash === SETTINGS_KEY) {
|
|
145
|
+
plugin.onSettingsSet = function onSettingsSet(data) {
|
|
146
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
147
|
+
plugin._settingsCache = null;
|
|
148
|
+
plugin._settingsCacheAt = 0;
|
|
149
|
+
}
|
|
131
150
|
};
|
|
132
151
|
|
|
133
|
-
plugin.addAdminNavigation = async (header)
|
|
152
|
+
plugin.addAdminNavigation = async function addAdminNavigation(header) {
|
|
134
153
|
header.plugins = header.plugins || [];
|
|
135
|
-
header.plugins.push({
|
|
154
|
+
header.plugins.push({
|
|
155
|
+
route: '/plugins/ezoic-infinite',
|
|
156
|
+
icon: 'fa-ad',
|
|
157
|
+
name: 'Ezoic Infinite Ads',
|
|
158
|
+
});
|
|
136
159
|
return header;
|
|
137
160
|
};
|
|
138
161
|
|
|
139
|
-
|
|
140
|
-
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
141
|
-
*
|
|
142
|
-
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
143
|
-
* (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
|
|
144
|
-
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
145
|
-
* et est rendu via req.app.renderAsync('header', hookReturn.templateData).
|
|
146
|
-
* On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
|
|
147
|
-
* tout en préservant ce dernier.
|
|
148
|
-
*/
|
|
149
|
-
plugin.injectEzoicHead = async (data) => {
|
|
162
|
+
plugin.injectEzoicHead = async function injectEzoicHead(data) {
|
|
150
163
|
try {
|
|
151
164
|
const settings = await getSettings();
|
|
152
|
-
const uid
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
156
|
-
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
165
|
+
const uid = data?.req?.uid || 0;
|
|
166
|
+
if (await isUserExcluded(uid, settings.excludedGroups)) {
|
|
167
|
+
return data;
|
|
157
168
|
}
|
|
158
|
-
|
|
169
|
+
|
|
170
|
+
const templateData = data.templateData || (data.templateData = {});
|
|
171
|
+
templateData.customHTML = `${EZOIC_SCRIPTS}${templateData.customHTML || ''}`;
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
159
174
|
return data;
|
|
160
175
|
};
|
|
161
176
|
|
|
162
|
-
plugin.init = async ({ router, middleware })
|
|
163
|
-
async function
|
|
164
|
-
const settings
|
|
165
|
-
|
|
177
|
+
plugin.init = async function init({ router, middleware }) {
|
|
178
|
+
async function renderAdmin(req, res) {
|
|
179
|
+
const [settings, allGroups] = await Promise.all([
|
|
180
|
+
getSettings(),
|
|
181
|
+
listNonPrivilegeGroups(),
|
|
182
|
+
]);
|
|
183
|
+
|
|
166
184
|
res.render('admin/plugins/ezoic-infinite', {
|
|
167
185
|
title: 'Ezoic Infinite Ads',
|
|
168
186
|
...settings,
|
|
169
187
|
enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
|
|
170
|
-
enableMessageAds_checked:
|
|
188
|
+
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
171
189
|
allGroups,
|
|
172
190
|
});
|
|
173
191
|
}
|
|
174
192
|
|
|
175
|
-
router.get('/admin/plugins/ezoic-infinite',
|
|
176
|
-
router.get('/api/admin/plugins/ezoic-infinite',
|
|
193
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
|
|
194
|
+
router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
|
|
177
195
|
|
|
178
196
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
179
197
|
const settings = await getSettings();
|
|
180
|
-
const excluded = await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
placeholderIds: settings.placeholderIds,
|
|
186
|
-
intervalPosts: settings.intervalPosts,
|
|
187
|
-
enableCategoryAds: settings.enableCategoryAds,
|
|
188
|
-
showFirstCategoryAd: settings.showFirstCategoryAd,
|
|
189
|
-
categoryPlaceholderIds: settings.categoryPlaceholderIds,
|
|
190
|
-
intervalCategories: settings.intervalCategories,
|
|
191
|
-
enableMessageAds: settings.enableMessageAds,
|
|
192
|
-
showFirstMessageAd: settings.showFirstMessageAd,
|
|
193
|
-
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
194
|
-
messageIntervalPosts: settings.messageIntervalPosts,
|
|
195
|
-
});
|
|
198
|
+
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
199
|
+
|
|
200
|
+
const payload = { excluded };
|
|
201
|
+
for (const key of CONFIG_FIELDS) payload[key] = settings[key];
|
|
202
|
+
res.json(payload);
|
|
196
203
|
});
|
|
197
204
|
};
|
|
198
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.20",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"compatibility": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"private": false
|
|
21
|
-
}
|
|
21
|
+
}
|
package/public/admin.js
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
/* globals ajaxify */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
(function () {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
(function initAdminEzoicSettings() {
|
|
5
|
+
const FORM_SELECTOR = '.ezoic-infinite-settings';
|
|
6
|
+
const SAVE_SELECTOR = '#save';
|
|
7
|
+
const SETTINGS_KEY = 'ezoic-infinite';
|
|
8
|
+
const EVENT_NS = '.ezoicInfinite';
|
|
9
|
+
|
|
10
|
+
function showSaved(alerts) {
|
|
11
|
+
if (alerts && typeof alerts.success === 'function') {
|
|
12
|
+
alerts.success('[[admin/settings:saved]]');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
16
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
function bind(Settings, alerts, $form) {
|
|
21
|
+
Settings.load(SETTINGS_KEY, $form);
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
$(SAVE_SELECTOR)
|
|
24
|
+
.off(`click${EVENT_NS}`)
|
|
25
|
+
.on(`click${EVENT_NS}`, function onSave(e) {
|
|
13
26
|
e.preventDefault();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (alerts && typeof alerts.success === 'function') {
|
|
17
|
-
alerts.success('[[admin/settings:saved]]');
|
|
18
|
-
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
19
|
-
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
20
|
-
}
|
|
27
|
+
Settings.save(SETTINGS_KEY, $form, function onSaved() {
|
|
28
|
+
showSaved(alerts);
|
|
21
29
|
});
|
|
22
30
|
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function boot() {
|
|
34
|
+
const $form = $(FORM_SELECTOR);
|
|
35
|
+
if (!$form.length) return;
|
|
36
|
+
|
|
37
|
+
require(['settings', 'alerts'], function onModules(Settings, alerts) {
|
|
38
|
+
bind(Settings, alerts, $form);
|
|
23
39
|
});
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
$(document).ready(
|
|
27
|
-
$(window).on('action:ajaxify.end',
|
|
42
|
+
$(document).ready(boot);
|
|
43
|
+
$(window).on('action:ajaxify.end', boot);
|
|
28
44
|
})();
|
package/public/client.js
CHANGED
|
@@ -1,70 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
|
-
*
|
|
4
|
-
* Historique des corrections majeures
|
|
5
|
-
* ────────────────────────────────────
|
|
6
|
-
* v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
|
|
7
|
-
*
|
|
8
|
-
* v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
|
|
9
|
-
* la position dans le batch courant.
|
|
10
|
-
*
|
|
11
|
-
* v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
|
|
12
|
-
* Fix fatal catégories : data-cid au lieu de data-index inexistant.
|
|
13
|
-
* IO fixe (une instance, jamais recréée).
|
|
14
|
-
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
15
|
-
*
|
|
16
|
-
* v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
|
|
17
|
-
*
|
|
18
|
-
* v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
|
|
19
|
-
*
|
|
20
|
-
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB posts).
|
|
21
|
-
*
|
|
22
|
-
* v28 decluster supprimé. Wraps persistants pendant la session.
|
|
23
|
-
*
|
|
24
|
-
* v32 Retour anchorAttr = data-index pour ezoic-ad-between.
|
|
25
|
-
* data-tid peut être absent → clés invalides → wraps empilés.
|
|
26
|
-
* pruneOrphansBetween réactivé uniquement pour topics de catégorie :
|
|
27
|
-
* – NodeBB NE virtualise PAS les topics dans une liste de catégorie,
|
|
28
|
-
* les ancres (data-index) restent en DOM → prune safe et nécessaire
|
|
29
|
-
* pour éviter l'empilement après scroll long.
|
|
30
|
-
* – Toujours désactivé pour les posts : NodeBB virtualise les posts
|
|
31
|
-
* hors-viewport → faux-orphelins → bug réinjection en haut.
|
|
32
|
-
*
|
|
33
|
-
* v34 moveDistantWrap — voir v38.
|
|
34
|
-
*
|
|
35
|
-
* v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
|
|
36
|
-
* après login — filter:middleware.renderHeader re-évalue l'exclusion au
|
|
37
|
-
* rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
|
|
38
|
-
*
|
|
39
|
-
* v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
|
|
40
|
-
*
|
|
41
|
-
* v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
|
|
42
|
-
*
|
|
43
|
-
* v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
|
|
44
|
-
*
|
|
45
|
-
* v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
|
|
46
|
-
* Séquence : destroy → 300ms → define → 300ms → displayMore.
|
|
47
|
-
* Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
|
|
48
|
-
*
|
|
49
|
-
* v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
|
|
50
|
-
* sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
|
|
51
|
-
* déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
|
|
52
|
-
* break propre dans injectBetween. muteConsole : ajout warnings refresh.
|
|
53
|
-
*
|
|
54
|
-
* v36 Optimisations chemin critique (scroll → injectBetween) :
|
|
55
|
-
* – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
|
|
56
|
-
* sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
|
|
57
|
-
* dropWrap et cleanup.
|
|
58
|
-
* – wrapIsLive allégé : pour les voisins immédiats on vérifie les
|
|
59
|
-
* attributs du nœud lui-même sans querySelector global.
|
|
60
|
-
* – MutationObserver : matches() vérifié avant querySelector() pour
|
|
61
|
-
* court-circuiter les sous-arbres entiers ajoutés par NodeBB.
|
|
62
|
-
*
|
|
63
|
-
* v35 Revue complète prod-ready :
|
|
64
|
-
* – initPools protégé contre ré-initialisation inutile (S.poolsReady).
|
|
65
|
-
* – muteConsole élargit à "No valid placeholders for loadMore".
|
|
66
|
-
* – Commentaires et historique nettoyés.
|
|
67
|
-
*/
|
|
68
1
|
(function nbbEzoicInfinite() {
|
|
69
2
|
'use strict';
|
|
70
3
|
|
|
@@ -77,12 +10,18 @@
|
|
|
77
10
|
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
11
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
12
|
|
|
80
|
-
|
|
13
|
+
// Tunables (stables en prod)
|
|
81
14
|
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
|
-
const MAX_INSERTS_RUN =
|
|
83
|
-
const MAX_INFLIGHT =
|
|
84
|
-
const
|
|
85
|
-
const
|
|
15
|
+
const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
|
|
16
|
+
const MAX_INFLIGHT = 2; // ids max simultanés en vol (garde-fou)
|
|
17
|
+
const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
|
|
18
|
+
const SHOW_THROTTLE_MS = 500; // anti-spam showAds() par id (plus réactif)
|
|
19
|
+
const SHOW_RELEASE_MS = 300; // relâche inflight après showAds() batché
|
|
20
|
+
const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
|
|
21
|
+
const BATCH_FLUSH_MS = 30; // micro-buffer pour regrouper les ids proches
|
|
22
|
+
const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
|
|
23
|
+
const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
|
|
24
|
+
const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
|
|
86
25
|
|
|
87
26
|
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
88
27
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
@@ -93,6 +32,9 @@
|
|
|
93
32
|
topic: 'li[component="category/topic"]',
|
|
94
33
|
category: 'li[component="categories/category"]',
|
|
95
34
|
};
|
|
35
|
+
const WRAP_SEL = `.${WRAP_CLASS}`;
|
|
36
|
+
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id]';
|
|
37
|
+
const CONTENT_SEL_LIST = [SEL.post, SEL.topic, SEL.category];
|
|
96
38
|
|
|
97
39
|
/**
|
|
98
40
|
* Table KIND — source de vérité par kindClass.
|
|
@@ -126,7 +68,18 @@
|
|
|
126
68
|
inflight: 0, // showAds() en cours
|
|
127
69
|
pending: [], // ids en attente de slot inflight
|
|
128
70
|
pendingSet: new Set(),
|
|
71
|
+
showBatchTimer: 0,
|
|
72
|
+
destroyBatchTimer: 0,
|
|
73
|
+
destroyPending: [],
|
|
74
|
+
destroyPendingSet: new Set(),
|
|
75
|
+
sweepQueued: false,
|
|
129
76
|
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
77
|
+
ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
|
|
78
|
+
ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
|
|
79
|
+
scrollDir: 1, // 1=bas, -1=haut
|
|
80
|
+
scrollSpeed: 0, // px/s approx (EMA)
|
|
81
|
+
lastScrollY: 0,
|
|
82
|
+
lastScrollTs: 0,
|
|
130
83
|
runQueued: false,
|
|
131
84
|
burstActive: false,
|
|
132
85
|
burstDeadline: 0,
|
|
@@ -140,12 +93,121 @@
|
|
|
140
93
|
const isBlocked = () => ts() < blockedUntil;
|
|
141
94
|
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
142
95
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
143
|
-
const isFilled = n => !!(n?.querySelector?.(
|
|
96
|
+
const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
|
|
97
|
+
|
|
98
|
+
function healFalseEmpty(root = document) {
|
|
99
|
+
try {
|
|
100
|
+
const list = [];
|
|
101
|
+
if (root instanceof Element && root.classList?.contains(WRAP_CLASS)) list.push(root);
|
|
102
|
+
const found = root.querySelectorAll ? root.querySelectorAll(`.${WRAP_CLASS}.is-empty`) : [];
|
|
103
|
+
for (const w of found) list.push(w);
|
|
104
|
+
for (const w of list) {
|
|
105
|
+
if (!w?.classList?.contains('is-empty')) continue;
|
|
106
|
+
if (isFilled(w)) w.classList.remove('is-empty');
|
|
107
|
+
}
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function phEl(id) {
|
|
112
|
+
return document.getElementById(`${PH_PREFIX}${id}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasSinglePlaceholder(id) {
|
|
116
|
+
try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function canShowPlaceholderId(id, now = ts()) {
|
|
120
|
+
const n = parseInt(id, 10);
|
|
121
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
122
|
+
if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
|
|
123
|
+
const ph = phEl(n);
|
|
124
|
+
if (!ph?.isConnected || isFilled(ph)) return false;
|
|
125
|
+
if (!hasSinglePlaceholder(n)) return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function queueSweepDeadWraps() {
|
|
130
|
+
if (S.sweepQueued) return;
|
|
131
|
+
S.sweepQueued = true;
|
|
132
|
+
requestAnimationFrame(() => {
|
|
133
|
+
S.sweepQueued = false;
|
|
134
|
+
sweepDeadWraps();
|
|
135
|
+
healFalseEmpty();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getDynamicShowBatchMax() {
|
|
140
|
+
const speed = S.scrollSpeed || 0;
|
|
141
|
+
const pend = S.pending.length;
|
|
142
|
+
// Scroll très rapide => petits batches (réduit le churn/unused)
|
|
143
|
+
if (speed > 2600) return 2;
|
|
144
|
+
if (speed > 1400) return 3;
|
|
145
|
+
// Peu de candidats => flush plus vite, inutile d'attendre 4
|
|
146
|
+
if (pend <= 1) return 1;
|
|
147
|
+
if (pend <= 3) return 2;
|
|
148
|
+
// Par défaut compromis dynamique
|
|
149
|
+
return 3;
|
|
150
|
+
}
|
|
144
151
|
|
|
145
152
|
function mutate(fn) {
|
|
146
153
|
S.mutGuard++;
|
|
147
154
|
try { fn(); } finally { S.mutGuard--; }
|
|
148
155
|
}
|
|
156
|
+
function scheduleDestroyFlush() {
|
|
157
|
+
if (S.destroyBatchTimer) return;
|
|
158
|
+
S.destroyBatchTimer = setTimeout(() => {
|
|
159
|
+
S.destroyBatchTimer = 0;
|
|
160
|
+
flushDestroyBatch();
|
|
161
|
+
}, DESTROY_FLUSH_MS);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function flushDestroyBatch() {
|
|
165
|
+
if (!S.destroyPending.length) return;
|
|
166
|
+
const ids = [];
|
|
167
|
+
while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
|
|
168
|
+
const id = S.destroyPending.shift();
|
|
169
|
+
S.destroyPendingSet.delete(id);
|
|
170
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
171
|
+
ids.push(id);
|
|
172
|
+
}
|
|
173
|
+
if (ids.length) {
|
|
174
|
+
try {
|
|
175
|
+
const ez = window.ezstandalone;
|
|
176
|
+
const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
|
|
177
|
+
try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
}
|
|
180
|
+
if (S.destroyPending.length) scheduleDestroyFlush();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function destroyEzoicId(id) {
|
|
184
|
+
if (!Number.isFinite(id) || id <= 0) return;
|
|
185
|
+
if (!S.ezActiveIds.has(id)) return;
|
|
186
|
+
S.ezActiveIds.delete(id);
|
|
187
|
+
if (!S.destroyPendingSet.has(id)) {
|
|
188
|
+
S.destroyPending.push(id);
|
|
189
|
+
S.destroyPendingSet.add(id);
|
|
190
|
+
}
|
|
191
|
+
scheduleDestroyFlush();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function destroyBeforeReuse(ids) {
|
|
195
|
+
const out = [];
|
|
196
|
+
const toDestroy = [];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const raw of (ids || [])) {
|
|
199
|
+
const id = parseInt(raw, 10);
|
|
200
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
201
|
+
seen.add(id);
|
|
202
|
+
out.push(id);
|
|
203
|
+
if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
|
|
204
|
+
}
|
|
205
|
+
if (toDestroy.length) {
|
|
206
|
+
try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
|
|
207
|
+
for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
149
211
|
|
|
150
212
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
151
213
|
|
|
@@ -160,7 +222,7 @@
|
|
|
160
222
|
|
|
161
223
|
function parseIds(raw) {
|
|
162
224
|
const out = [], seen = new Set();
|
|
163
|
-
for (const v of String(raw || '').split(/
|
|
225
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
164
226
|
const n = parseInt(v, 10);
|
|
165
227
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
166
228
|
}
|
|
@@ -300,6 +362,27 @@
|
|
|
300
362
|
return null;
|
|
301
363
|
}
|
|
302
364
|
|
|
365
|
+
function sweepDeadWraps() {
|
|
366
|
+
// NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
|
|
367
|
+
// On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
|
|
368
|
+
let changed = false;
|
|
369
|
+
for (const [key, wrap] of S.wrapByKey) {
|
|
370
|
+
if (wrap?.isConnected) continue;
|
|
371
|
+
changed = true;
|
|
372
|
+
S.wrapByKey.delete(key);
|
|
373
|
+
const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
|
|
374
|
+
if (Number.isFinite(id)) {
|
|
375
|
+
S.mountedIds.delete(id);
|
|
376
|
+
S.pendingSet.delete(id);
|
|
377
|
+
S.lastShow.delete(id);
|
|
378
|
+
S.ezActiveIds.delete(id);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (changed && S.pending.length) {
|
|
382
|
+
S.pending = S.pending.filter(id => S.pendingSet.has(id));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
303
386
|
/**
|
|
304
387
|
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
305
388
|
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
@@ -307,60 +390,83 @@
|
|
|
307
390
|
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
308
391
|
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
309
392
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
393
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
394
|
+
const ez = window.ezstandalone;
|
|
395
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
396
|
+
typeof ez?.define !== 'function' ||
|
|
397
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
398
|
+
|
|
399
|
+
const vh = window.innerHeight || 800;
|
|
400
|
+
const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
|
|
401
|
+
const farAbove = -vh;
|
|
402
|
+
const farBelow = vh * 2;
|
|
403
|
+
|
|
404
|
+
let bestPrefEmpty = null, bestPrefMetric = Infinity;
|
|
405
|
+
let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
|
|
406
|
+
let bestAnyEmpty = null, bestAnyMetric = Infinity;
|
|
407
|
+
let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
|
|
408
|
+
|
|
409
|
+
for (const wrap of S.wrapByKey.values()) {
|
|
410
|
+
if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
|
|
411
|
+
try {
|
|
412
|
+
const rect = wrap.getBoundingClientRect();
|
|
413
|
+
const isAbove = rect.bottom <= farAbove;
|
|
414
|
+
const isBelow = rect.top >= farBelow;
|
|
415
|
+
const anyFar = isAbove || isBelow;
|
|
416
|
+
if (!anyFar) continue;
|
|
417
|
+
|
|
418
|
+
const qualifies = preferAbove ? isAbove : isBelow;
|
|
419
|
+
const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
|
|
420
|
+
const filled = isFilled(wrap);
|
|
421
|
+
|
|
422
|
+
if (qualifies) {
|
|
423
|
+
if (!filled) {
|
|
424
|
+
if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
|
|
329
425
|
} else {
|
|
330
|
-
if (
|
|
426
|
+
if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
|
|
331
427
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const oldKey = best.getAttribute(A_ANCHOR);
|
|
341
|
-
// Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
|
|
342
|
-
// parasite si le nœud était encore dans la zone IO_MARGIN.
|
|
343
|
-
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
344
|
-
mutate(() => {
|
|
345
|
-
best.setAttribute(A_ANCHOR, newKey);
|
|
346
|
-
best.setAttribute(A_CREATED, String(ts()));
|
|
347
|
-
best.setAttribute(A_SHOWN, '0');
|
|
348
|
-
best.classList.remove('is-empty');
|
|
349
|
-
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
350
|
-
if (ph) ph.innerHTML = '';
|
|
351
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
352
|
-
});
|
|
353
|
-
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
354
|
-
S.wrapByKey.set(newKey, best);
|
|
428
|
+
}
|
|
429
|
+
if (!filled) {
|
|
430
|
+
if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
|
|
431
|
+
} else {
|
|
432
|
+
if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
|
|
433
|
+
}
|
|
434
|
+
} catch (_) {}
|
|
435
|
+
}
|
|
355
436
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
437
|
+
const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
|
|
438
|
+
if (!best) return null;
|
|
439
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
440
|
+
if (!Number.isFinite(id)) return null;
|
|
441
|
+
|
|
442
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
443
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
444
|
+
mutate(() => {
|
|
445
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
446
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
447
|
+
best.setAttribute(A_SHOWN, '0');
|
|
448
|
+
best.classList.remove('is-empty');
|
|
449
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
450
|
+
if (ph) ph.innerHTML = '';
|
|
451
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
452
|
+
});
|
|
453
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
454
|
+
S.wrapByKey.set(newKey, best);
|
|
455
|
+
|
|
456
|
+
const doDestroy = () => {
|
|
457
|
+
if (S.ezShownSinceDestroy.has(id)) {
|
|
458
|
+
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
459
|
+
S.ezShownSinceDestroy.delete(id);
|
|
460
|
+
}
|
|
461
|
+
S.ezActiveIds.delete(id);
|
|
462
|
+
setTimeout(doDefine, 330);
|
|
463
|
+
};
|
|
464
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
465
|
+
const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
|
|
466
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
361
467
|
|
|
362
|
-
|
|
363
|
-
|
|
468
|
+
return { id, wrap: best };
|
|
469
|
+
}
|
|
364
470
|
|
|
365
471
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
366
472
|
|
|
@@ -396,7 +502,7 @@
|
|
|
396
502
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
397
503
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
398
504
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
399
|
-
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
505
|
+
if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
|
|
400
506
|
const key = w.getAttribute(A_ANCHOR);
|
|
401
507
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
402
508
|
w.remove();
|
|
@@ -422,14 +528,7 @@
|
|
|
422
528
|
const klass = 'ezoic-ad-between';
|
|
423
529
|
const cfg = KIND[klass];
|
|
424
530
|
|
|
425
|
-
|
|
426
|
-
const anchors = new Set();
|
|
427
|
-
document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`).forEach(el => {
|
|
428
|
-
const v = el.getAttribute(cfg.anchorAttr);
|
|
429
|
-
if (v) anchors.add(String(v));
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
531
|
+
document.querySelectorAll(`${WRAP_SEL}.${klass}`).forEach(w => {
|
|
433
532
|
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
434
533
|
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
435
534
|
|
|
@@ -437,7 +536,8 @@
|
|
|
437
536
|
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
438
537
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
439
538
|
|
|
440
|
-
|
|
539
|
+
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
540
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
441
541
|
});
|
|
442
542
|
}
|
|
443
543
|
|
|
@@ -477,7 +577,8 @@
|
|
|
477
577
|
const key = anchorKey(klass, el);
|
|
478
578
|
if (findWrap(key)) continue;
|
|
479
579
|
|
|
480
|
-
|
|
580
|
+
let id = pickId(poolKey);
|
|
581
|
+
if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
|
|
481
582
|
if (id) {
|
|
482
583
|
const w = insertAfter(el, id, klass, key);
|
|
483
584
|
if (w) { observePh(id); inserted++; }
|
|
@@ -508,76 +609,106 @@
|
|
|
508
609
|
}
|
|
509
610
|
|
|
510
611
|
function observePh(id) {
|
|
511
|
-
const ph =
|
|
612
|
+
const ph = phEl(id);
|
|
512
613
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
614
|
+
// Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
|
|
615
|
+
try {
|
|
616
|
+
if (!ph?.isConnected) return;
|
|
617
|
+
const rect = ph.getBoundingClientRect();
|
|
618
|
+
const vh = window.innerHeight || 800;
|
|
619
|
+
const preload = isMobile() ? 1400 : 1000;
|
|
620
|
+
if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
|
|
621
|
+
} catch (_) {}
|
|
513
622
|
}
|
|
514
623
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
624
|
+
function enqueueShow(id) {
|
|
625
|
+
if (!id || isBlocked()) return;
|
|
626
|
+
const n = parseInt(id, 10);
|
|
627
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
628
|
+
if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
629
|
+
if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
|
|
630
|
+
scheduleDrainQueue();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function scheduleDrainQueue() {
|
|
634
|
+
if (isBlocked()) return;
|
|
635
|
+
if (S.showBatchTimer) return;
|
|
636
|
+
S.showBatchTimer = setTimeout(() => {
|
|
637
|
+
S.showBatchTimer = 0;
|
|
638
|
+
drainQueue();
|
|
639
|
+
}, BATCH_FLUSH_MS);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function drainQueue() {
|
|
643
|
+
if (isBlocked()) return;
|
|
644
|
+
const free = Math.max(0, MAX_INFLIGHT - S.inflight);
|
|
645
|
+
if (!free || !S.pending.length) return;
|
|
646
|
+
|
|
647
|
+
const picked = [];
|
|
648
|
+
const seen = new Set();
|
|
649
|
+
const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
|
|
650
|
+
while (S.pending.length && picked.length < batchCap) {
|
|
651
|
+
const id = S.pending.shift();
|
|
652
|
+
S.pendingSet.delete(id);
|
|
653
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
654
|
+
if (!phEl(id)?.isConnected) continue;
|
|
655
|
+
seen.add(id);
|
|
656
|
+
picked.push(id);
|
|
657
|
+
}
|
|
658
|
+
if (picked.length) startShowBatch(picked);
|
|
659
|
+
if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function startShowBatch(ids) {
|
|
663
|
+
if (!ids?.length || isBlocked()) return;
|
|
664
|
+
const reserve = ids.length;
|
|
665
|
+
S.inflight += reserve;
|
|
666
|
+
|
|
667
|
+
let done = false;
|
|
668
|
+
const release = () => {
|
|
669
|
+
if (done) return;
|
|
670
|
+
done = true;
|
|
671
|
+
S.inflight = Math.max(0, S.inflight - reserve);
|
|
672
|
+
drainQueue();
|
|
673
|
+
};
|
|
674
|
+
const timer = setTimeout(release, SHOW_FAILSAFE_MS);
|
|
524
675
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const id = S.pending.shift();
|
|
529
|
-
S.pendingSet.delete(id);
|
|
530
|
-
startShow(id);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
676
|
+
requestAnimationFrame(() => {
|
|
677
|
+
try {
|
|
678
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
533
679
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
S.inflight++;
|
|
537
|
-
let done = false;
|
|
538
|
-
const release = () => {
|
|
539
|
-
if (done) return;
|
|
540
|
-
done = true;
|
|
541
|
-
S.inflight = Math.max(0, S.inflight - 1);
|
|
542
|
-
drainQueue();
|
|
543
|
-
};
|
|
544
|
-
const timer = setTimeout(release, 7000);
|
|
680
|
+
const valid = [];
|
|
681
|
+
const t = ts();
|
|
545
682
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (
|
|
549
|
-
const ph =
|
|
550
|
-
if (!
|
|
683
|
+
for (const raw of ids) {
|
|
684
|
+
const id = parseInt(raw, 10);
|
|
685
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
686
|
+
const ph = phEl(id);
|
|
687
|
+
if (!canShowPlaceholderId(id, t)) continue;
|
|
551
688
|
|
|
552
|
-
const t = ts();
|
|
553
|
-
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
554
689
|
S.lastShow.set(id, t);
|
|
690
|
+
try { ph.closest?.(WRAP_SEL)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
691
|
+
valid.push(id);
|
|
692
|
+
}
|
|
555
693
|
|
|
556
|
-
|
|
694
|
+
if (!valid.length) { clearTimeout(timer); return release(); }
|
|
557
695
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
575
|
-
if (!wrap || !ph?.isConnected) return;
|
|
576
|
-
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
577
|
-
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
578
|
-
} catch (_) {}
|
|
579
|
-
}, EMPTY_CHECK_MS);
|
|
580
|
-
}
|
|
696
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
697
|
+
const ez = window.ezstandalone;
|
|
698
|
+
const doShow = () => {
|
|
699
|
+
const prepared = destroyBeforeReuse(valid);
|
|
700
|
+
if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
|
|
701
|
+
try { ez.showAds(...prepared); } catch (_) {}
|
|
702
|
+
for (const id of prepared) {
|
|
703
|
+
S.ezActiveIds.add(id);
|
|
704
|
+
S.ezShownSinceDestroy.add(id);
|
|
705
|
+
}
|
|
706
|
+
setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
|
|
707
|
+
};
|
|
708
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
709
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
710
|
+
});
|
|
711
|
+
}
|
|
581
712
|
|
|
582
713
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
583
714
|
//
|
|
@@ -595,14 +726,21 @@
|
|
|
595
726
|
const orig = ez.showAds.bind(ez);
|
|
596
727
|
ez.showAds = function (...args) {
|
|
597
728
|
if (isBlocked()) return;
|
|
598
|
-
const ids
|
|
729
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
730
|
+
const valid = [];
|
|
599
731
|
const seen = new Set();
|
|
600
732
|
for (const v of ids) {
|
|
601
733
|
const id = parseInt(v, 10);
|
|
602
734
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
603
|
-
if (!
|
|
735
|
+
if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
|
|
604
736
|
seen.add(id);
|
|
605
|
-
|
|
737
|
+
valid.push(id);
|
|
738
|
+
}
|
|
739
|
+
if (!valid.length) return;
|
|
740
|
+
try { orig(...valid); } catch (_) {
|
|
741
|
+
for (const id of valid) {
|
|
742
|
+
try { orig(id); } catch (_) {}
|
|
743
|
+
}
|
|
606
744
|
}
|
|
607
745
|
};
|
|
608
746
|
} catch (_) {}
|
|
@@ -619,6 +757,7 @@
|
|
|
619
757
|
async function runCore() {
|
|
620
758
|
if (isBlocked()) return 0;
|
|
621
759
|
patchShowAds();
|
|
760
|
+
sweepDeadWraps();
|
|
622
761
|
|
|
623
762
|
const cfg = await fetchConfig();
|
|
624
763
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -685,7 +824,7 @@
|
|
|
685
824
|
S.burstCount++;
|
|
686
825
|
scheduleRun(n => {
|
|
687
826
|
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
688
|
-
setTimeout(step, n > 0 ?
|
|
827
|
+
setTimeout(step, n > 0 ? 80 : 180);
|
|
689
828
|
});
|
|
690
829
|
};
|
|
691
830
|
step();
|
|
@@ -695,7 +834,7 @@
|
|
|
695
834
|
|
|
696
835
|
function cleanup() {
|
|
697
836
|
blockedUntil = ts() + 1500;
|
|
698
|
-
mutate(() => document.querySelectorAll(
|
|
837
|
+
mutate(() => document.querySelectorAll(WRAP_SEL).forEach(dropWrap));
|
|
699
838
|
S.cfg = null;
|
|
700
839
|
S.poolsReady = false;
|
|
701
840
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
@@ -703,26 +842,59 @@
|
|
|
703
842
|
S.mountedIds.clear();
|
|
704
843
|
S.lastShow.clear();
|
|
705
844
|
S.wrapByKey.clear();
|
|
845
|
+
S.ezActiveIds.clear();
|
|
846
|
+
S.ezShownSinceDestroy.clear();
|
|
706
847
|
S.inflight = 0;
|
|
707
848
|
S.pending = [];
|
|
708
849
|
S.pendingSet.clear();
|
|
850
|
+
if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
|
|
851
|
+
if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
|
|
852
|
+
S.destroyPending = [];
|
|
853
|
+
S.destroyPendingSet.clear();
|
|
709
854
|
S.burstActive = false;
|
|
710
855
|
S.runQueued = false;
|
|
856
|
+
S.sweepQueued = false;
|
|
857
|
+
S.scrollSpeed = 0;
|
|
858
|
+
S.lastScrollY = 0;
|
|
859
|
+
S.lastScrollTs = 0;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function nodeMatchesAny(node, selectors) {
|
|
863
|
+
if (!(node instanceof Element)) return false;
|
|
864
|
+
for (const sel of selectors) {
|
|
865
|
+
try { if (node.matches(sel)) return true; } catch (_) {}
|
|
866
|
+
}
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function nodeContainsAny(node, selectors) {
|
|
871
|
+
if (!(node instanceof Element)) return false;
|
|
872
|
+
for (const sel of selectors) {
|
|
873
|
+
try { if (node.querySelector(sel)) return true; } catch (_) {}
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
711
876
|
}
|
|
712
877
|
|
|
713
878
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
714
879
|
|
|
715
880
|
function ensureDomObserver() {
|
|
716
881
|
if (S.domObs) return;
|
|
717
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
718
882
|
S.domObs = new MutationObserver(muts => {
|
|
719
883
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
720
884
|
for (const m of muts) {
|
|
885
|
+
let sawWrapRemoval = false;
|
|
886
|
+
for (const n of m.removedNodes) {
|
|
887
|
+
if (n.nodeType !== 1) continue;
|
|
888
|
+
if (nodeMatchesAny(n, [WRAP_SEL]) || nodeContainsAny(n, [WRAP_SEL])) {
|
|
889
|
+
sawWrapRemoval = true;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (sawWrapRemoval) queueSweepDeadWraps();
|
|
721
893
|
for (const n of m.addedNodes) {
|
|
722
894
|
if (n.nodeType !== 1) continue;
|
|
895
|
+
try { healFalseEmpty(n); } catch (_) {}
|
|
723
896
|
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
724
|
-
if (
|
|
725
|
-
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
897
|
+
if (nodeMatchesAny(n, CONTENT_SEL_LIST) || nodeContainsAny(n, CONTENT_SEL_LIST)) {
|
|
726
898
|
requestBurst(); return;
|
|
727
899
|
}
|
|
728
900
|
}
|
|
@@ -733,27 +905,6 @@
|
|
|
733
905
|
|
|
734
906
|
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
735
907
|
|
|
736
|
-
function muteConsole() {
|
|
737
|
-
if (window.__nbbEzMuted) return;
|
|
738
|
-
window.__nbbEzMuted = true;
|
|
739
|
-
const MUTED = [
|
|
740
|
-
'[EzoicAds JS]: Placeholder Id',
|
|
741
|
-
'No valid placeholders for loadMore',
|
|
742
|
-
'cannot call refresh on the same page',
|
|
743
|
-
'no placeholders are currently defined in Refresh',
|
|
744
|
-
'Debugger iframe already exists',
|
|
745
|
-
`with id ${PH_PREFIX}`,
|
|
746
|
-
];
|
|
747
|
-
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
748
|
-
const orig = console[m];
|
|
749
|
-
if (typeof orig !== 'function') continue;
|
|
750
|
-
console[m] = function (...a) {
|
|
751
|
-
if (typeof a[0] === 'string' && MUTED.some(p => a[0].includes(p))) return;
|
|
752
|
-
orig.apply(console, a);
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
908
|
function ensureTcfLocator() {
|
|
758
909
|
try {
|
|
759
910
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
@@ -775,6 +926,7 @@
|
|
|
775
926
|
function warmNetwork() {
|
|
776
927
|
const head = document.head;
|
|
777
928
|
if (!head) return;
|
|
929
|
+
const frag = document.createDocumentFragment();
|
|
778
930
|
for (const [rel, href, cors] of [
|
|
779
931
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
780
932
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
@@ -789,8 +941,9 @@
|
|
|
789
941
|
const l = document.createElement('link');
|
|
790
942
|
l.rel = rel; l.href = href;
|
|
791
943
|
if (cors) l.crossOrigin = 'anonymous';
|
|
792
|
-
|
|
944
|
+
frag.appendChild(l);
|
|
793
945
|
}
|
|
946
|
+
head.appendChild(frag);
|
|
794
947
|
}
|
|
795
948
|
|
|
796
949
|
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
@@ -804,8 +957,8 @@
|
|
|
804
957
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
805
958
|
S.pageKey = pageKey();
|
|
806
959
|
blockedUntil = 0;
|
|
807
|
-
|
|
808
|
-
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
960
|
+
ensureTcfLocator(); warmNetwork();
|
|
961
|
+
patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
|
|
809
962
|
});
|
|
810
963
|
|
|
811
964
|
const burstEvts = [
|
|
@@ -827,7 +980,22 @@
|
|
|
827
980
|
|
|
828
981
|
function bindScroll() {
|
|
829
982
|
let ticking = false;
|
|
983
|
+
try {
|
|
984
|
+
S.lastScrollY = window.scrollY || window.pageYOffset || 0;
|
|
985
|
+
S.lastScrollTs = ts();
|
|
986
|
+
} catch (_) {}
|
|
830
987
|
window.addEventListener('scroll', () => {
|
|
988
|
+
try {
|
|
989
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
990
|
+
const t = ts();
|
|
991
|
+
const dy = y - (S.lastScrollY || 0);
|
|
992
|
+
const dt = Math.max(1, t - (S.lastScrollTs || t));
|
|
993
|
+
if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
|
|
994
|
+
const inst = Math.abs(dy) * 1000 / dt;
|
|
995
|
+
S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
|
|
996
|
+
S.lastScrollY = y;
|
|
997
|
+
S.lastScrollTs = t;
|
|
998
|
+
} catch (_) {}
|
|
831
999
|
if (ticking) return;
|
|
832
1000
|
ticking = true;
|
|
833
1001
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
@@ -837,7 +1005,6 @@
|
|
|
837
1005
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
838
1006
|
|
|
839
1007
|
S.pageKey = pageKey();
|
|
840
|
-
muteConsole();
|
|
841
1008
|
ensureTcfLocator();
|
|
842
1009
|
warmNetwork();
|
|
843
1010
|
patchShowAds();
|
package/public/style.css
CHANGED
|
@@ -56,23 +56,17 @@
|
|
|
56
56
|
top: auto !important;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/* ── État vide ────────────────────────────────────────────────────────────── */
|
|
60
|
-
/*
|
|
61
|
-
Ajouté 20s après showAds si aucun fill détecté.
|
|
62
|
-
Collapse à 1px (pas 0) : reste observable par l'IO si le fill arrive tard.
|
|
63
|
-
*/
|
|
64
|
-
.nodebb-ezoic-wrap.is-empty {
|
|
65
|
-
display: block !important;
|
|
66
|
-
height: 1px !important;
|
|
67
|
-
min-height: 1px !important;
|
|
68
|
-
max-height: 1px !important;
|
|
69
|
-
margin: 0 !important;
|
|
70
|
-
padding: 0 !important;
|
|
71
|
-
overflow: hidden !important;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
/* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
|
|
75
60
|
.ezoic-ad {
|
|
76
61
|
margin: 0 !important;
|
|
77
62
|
padding: 0 !important;
|
|
78
63
|
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/* Filet anti faux-empty : si la pub est rendue, ne pas laisser le wrap replié */
|
|
67
|
+
.nodebb-ezoic-wrap.is-empty:has(iframe, [data-google-container-id], [id^="google_ads_iframe_"]) {
|
|
68
|
+
height: auto !important;
|
|
69
|
+
min-height: 1px !important;
|
|
70
|
+
max-height: none !important;
|
|
71
|
+
overflow: visible !important;
|
|
72
|
+
}
|