nodebb-plugin-ezoic-infinite 1.7.54 → 1.7.56
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 +29 -13
- package/package.json +1 -1
- package/public/client.js +151 -260
- package/public/style.css +16 -6
package/library.js
CHANGED
|
@@ -15,9 +15,11 @@ function normalizeExcludedGroups(value) {
|
|
|
15
15
|
// NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
|
|
16
16
|
const s = String(value).trim();
|
|
17
17
|
if (s.startsWith('[')) {
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(s);
|
|
20
|
+
if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
|
|
21
|
+
} catch (_) {}
|
|
19
22
|
}
|
|
20
|
-
// Fallback : séparation par virgule
|
|
21
23
|
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -28,7 +30,15 @@ function parseBool(v, def = false) {
|
|
|
28
30
|
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// ── Cache groupes (fix #7 : getAllGroups sans cache sur page ACP) ───────────
|
|
34
|
+
|
|
35
|
+
let _groupsCache = null;
|
|
36
|
+
let _groupsCacheAt = 0;
|
|
37
|
+
const GROUPS_TTL = 60_000; // 1 minute
|
|
38
|
+
|
|
31
39
|
async function getAllGroups() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
32
42
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
33
43
|
if (!names || !names.length) {
|
|
34
44
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
@@ -37,7 +47,9 @@ async function getAllGroups() {
|
|
|
37
47
|
const data = await groups.getGroupsData(filtered);
|
|
38
48
|
const valid = data.filter(g => g && g.name);
|
|
39
49
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
40
|
-
|
|
50
|
+
_groupsCache = valid;
|
|
51
|
+
_groupsCacheAt = now;
|
|
52
|
+
return _groupsCache;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
// ── Settings cache (30s TTL) ────────────────────────────────────────────────
|
|
@@ -88,7 +100,10 @@ ezstandalone.cmd = ezstandalone.cmd || [];
|
|
|
88
100
|
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
89
101
|
|
|
90
102
|
plugin.onSettingsSet = function (data) {
|
|
91
|
-
if (data && data.hash === SETTINGS_KEY)
|
|
103
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
104
|
+
_settingsCache = null;
|
|
105
|
+
_groupsCache = null; // invalider aussi le cache groupes
|
|
106
|
+
}
|
|
92
107
|
};
|
|
93
108
|
|
|
94
109
|
plugin.addAdminNavigation = async (header) => {
|
|
@@ -101,11 +116,11 @@ plugin.addAdminNavigation = async (header) => {
|
|
|
101
116
|
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
102
117
|
*
|
|
103
118
|
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
104
|
-
* (render.js
|
|
105
|
-
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
119
|
+
* (render.js : templateValues.customHTML = meta.config.customHTML).
|
|
120
|
+
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData.
|
|
121
|
+
* On préfixe customHTML pour passer AVANT le customHTML admin tout en le préservant.
|
|
122
|
+
*
|
|
123
|
+
* Fix #3 : erreurs loggées côté serveur plutôt qu'avalées silencieusement.
|
|
109
124
|
*/
|
|
110
125
|
plugin.injectEzoicHead = async (data) => {
|
|
111
126
|
try {
|
|
@@ -113,17 +128,18 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
113
128
|
const uid = data.req?.uid ?? 0;
|
|
114
129
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
115
130
|
if (!excluded) {
|
|
116
|
-
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
117
131
|
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
118
132
|
}
|
|
119
|
-
} catch (
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Log l'erreur mais ne pas planter le rendu de la page
|
|
135
|
+
console.error('[ezoic-infinite] injectEzoicHead error:', err.message || err);
|
|
136
|
+
}
|
|
120
137
|
return data;
|
|
121
138
|
};
|
|
122
139
|
|
|
123
140
|
plugin.init = async ({ router, middleware }) => {
|
|
124
141
|
async function render(req, res) {
|
|
125
|
-
const settings
|
|
126
|
-
const allGroups = await getAllGroups();
|
|
142
|
+
const [settings, allGroups] = await Promise.all([getSettings(), getAllGroups()]);
|
|
127
143
|
res.render('admin/plugins/ezoic-infinite', {
|
|
128
144
|
title: 'Ezoic Infinite Ads',
|
|
129
145
|
...settings,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,90 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v61
|
|
3
3
|
*
|
|
4
|
-
* Historique
|
|
5
|
-
*
|
|
6
|
-
* v18 Ancrage stable par data-pid / data-index
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
4
|
+
* Historique
|
|
5
|
+
* ──────────
|
|
6
|
+
* v18 Ancrage stable par data-pid / data-index.
|
|
7
|
+
* v20 Table KIND. IO fixe. Fix TCF locator.
|
|
8
|
+
* v25 Fix scroll-up / virtualisation NodeBB.
|
|
9
|
+
* v28 Wraps persistants pendant la session.
|
|
10
|
+
* v35 S.poolsReady. muteConsole élargi.
|
|
11
|
+
* v36 S.wrapByKey Map O(1). MutationObserver optimisé.
|
|
12
|
+
* v38 ez.refresh() supprimé. Pool épuisé → break propre.
|
|
13
|
+
* v40 Recyclage destroyPlaceholders+define+displayMore (délais 300ms).
|
|
14
|
+
* v43 Seuil recyclage -1vh + unobserve avant déplacement.
|
|
15
|
+
* v49 Fix normalizeExcludedGroups (JSON.parse tableau NodeBB).
|
|
16
|
+
* v51 fetchConfig backoff 10s. IO recrée au resize. tcfObs dans S.
|
|
17
|
+
* v52 pruneOrphansBetween supprimé (NodeBB virtualise aussi les topics).
|
|
18
|
+
* v53 S.recycling garde double-recyclage. pickId early-exit. cleanup complet.
|
|
19
|
+
* v54 ensureTcfLocator rappelé à chaque ajaxify.end.
|
|
20
|
+
* v56 scheduleEmptyCheck / is-empty supprimés (collapse prématuré).
|
|
21
|
+
* v61 recycleAndMove : ne pas recycler un wrap rempli depuis moins de 30s.
|
|
22
|
+
* Empêche qu'une pub qui vient de charger soit déplacée immédiatement.
|
|
23
|
+
* v59 CSS : min-height 90px sur ezoic-ad-between (anti-CLS AMP ads).
|
|
24
|
+
* v58 tcfObs survit aux navigations : ne plus déconnecter dans cleanup().
|
|
25
|
+
* L'iframe __tcfapiLocator doit exister en permanence pour le CMP —
|
|
26
|
+
* la fenêtre entre cleanup() et ajaxify.end causait des erreurs
|
|
27
|
+
* "Cannot read properties of null (postMessage)" et disparition des pubs.
|
|
28
|
+
* v57 Nettoyage prod : A_SHOWN/A_CREATED supprimés, normBool Set O(1),
|
|
29
|
+
* muteConsole étend aux erreurs CMP getTCData, code nettoyé.
|
|
67
30
|
*/
|
|
68
31
|
(function nbbEzoicInfinite() {
|
|
69
32
|
'use strict';
|
|
70
33
|
|
|
71
34
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
72
35
|
|
|
73
|
-
const WRAP_CLASS
|
|
74
|
-
const PH_PREFIX
|
|
75
|
-
const A_ANCHOR
|
|
76
|
-
const A_WRAPID
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
|
|
86
|
-
|
|
87
|
-
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
36
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
37
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
38
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
39
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
40
|
+
|
|
41
|
+
const EMPTY_CHECK_MS = 60_000;
|
|
42
|
+
const RECYCLE_MIN_AGE_MS = 30_000; // délai minimal avant recyclage d'un wrap rempli // délai avant collapse wrap vide (60s — laisser le temps au CMP/enchères)
|
|
43
|
+
const MAX_INSERTS_RUN = 6; // insertions max par appel runCore
|
|
44
|
+
const MAX_INFLIGHT = 4; // showAds() simultanés max
|
|
45
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
46
|
+
const BURST_COOLDOWN_MS = 200; // délai min entre deux bursts
|
|
47
|
+
|
|
88
48
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
89
49
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
90
50
|
|
|
@@ -96,13 +56,10 @@
|
|
|
96
56
|
|
|
97
57
|
/**
|
|
98
58
|
* Table KIND — source de vérité par kindClass.
|
|
99
|
-
*
|
|
100
|
-
* sel sélecteur CSS complet des éléments cibles
|
|
59
|
+
* sel sélecteur CSS des éléments cibles
|
|
101
60
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
-
* (vide pour posts : le sélecteur commence par '[')
|
|
103
61
|
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
104
|
-
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
105
|
-
* null → fallback positionnel (catégories)
|
|
62
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle (null = fallback positionnel)
|
|
106
63
|
*/
|
|
107
64
|
const KIND = {
|
|
108
65
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -113,34 +70,39 @@
|
|
|
113
70
|
// ── État global ────────────────────────────────────────────────────────────
|
|
114
71
|
|
|
115
72
|
const S = {
|
|
116
|
-
pageKey:
|
|
117
|
-
cfg:
|
|
118
|
-
poolsReady:
|
|
119
|
-
pools:
|
|
120
|
-
cursors:
|
|
121
|
-
mountedIds:
|
|
122
|
-
lastShow:
|
|
123
|
-
io:
|
|
124
|
-
domObs:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
73
|
+
pageKey: null,
|
|
74
|
+
cfg: null,
|
|
75
|
+
poolsReady: false,
|
|
76
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
77
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
78
|
+
mountedIds: new Set(),
|
|
79
|
+
lastShow: new Map(),
|
|
80
|
+
io: null,
|
|
81
|
+
domObs: null,
|
|
82
|
+
tcfObs: null,
|
|
83
|
+
mutGuard: 0,
|
|
84
|
+
inflight: 0,
|
|
85
|
+
pending: [],
|
|
86
|
+
pendingSet: new Set(),
|
|
87
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
88
|
+
recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
|
|
89
|
+
runQueued: false,
|
|
90
|
+
burstActive: false,
|
|
132
91
|
burstDeadline: 0,
|
|
133
|
-
burstCount:
|
|
134
|
-
lastBurstTs:
|
|
92
|
+
burstCount: 0,
|
|
93
|
+
lastBurstTs: 0,
|
|
135
94
|
};
|
|
136
95
|
|
|
137
|
-
let blockedUntil
|
|
96
|
+
let blockedUntil = 0;
|
|
97
|
+
let _cfgErrorUntil = 0;
|
|
98
|
+
let _ioMobile = null;
|
|
138
99
|
|
|
139
|
-
const ts
|
|
140
|
-
const isBlocked
|
|
141
|
-
const isMobile
|
|
142
|
-
const
|
|
143
|
-
const
|
|
100
|
+
const ts = () => Date.now();
|
|
101
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
102
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
103
|
+
const _BOOL_TRUE = new Set([true, 'true', 1, '1', 'on']);
|
|
104
|
+
const normBool = v => _BOOL_TRUE.has(v);
|
|
105
|
+
const isFilled = n => !!(n?.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
144
106
|
|
|
145
107
|
function mutate(fn) {
|
|
146
108
|
S.mutGuard++;
|
|
@@ -151,10 +113,12 @@
|
|
|
151
113
|
|
|
152
114
|
async function fetchConfig() {
|
|
153
115
|
if (S.cfg) return S.cfg;
|
|
116
|
+
if (Date.now() < _cfgErrorUntil) return null;
|
|
154
117
|
try {
|
|
155
118
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
156
|
-
if (r.ok) S.cfg = await r.json();
|
|
157
|
-
|
|
119
|
+
if (r.ok) { S.cfg = await r.json(); }
|
|
120
|
+
else { _cfgErrorUntil = Date.now() + 10_000; }
|
|
121
|
+
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
158
122
|
return S.cfg;
|
|
159
123
|
}
|
|
160
124
|
|
|
@@ -214,53 +178,37 @@
|
|
|
214
178
|
|
|
215
179
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
216
180
|
|
|
217
|
-
/**
|
|
218
|
-
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
219
|
-
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
220
|
-
*/
|
|
221
181
|
function wrapIsLive(wrap) {
|
|
222
182
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
223
183
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
224
184
|
if (!key) return false;
|
|
225
|
-
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
226
|
-
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
227
185
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
-
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
229
186
|
const colonIdx = key.indexOf(':');
|
|
230
187
|
const klass = key.slice(0, colonIdx);
|
|
231
188
|
const anchorId = key.slice(colonIdx + 1);
|
|
232
189
|
const cfg = KIND[klass];
|
|
233
190
|
if (!cfg) return false;
|
|
234
|
-
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
235
|
-
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
236
191
|
const parent = wrap.parentElement;
|
|
237
192
|
if (parent) {
|
|
238
193
|
for (const sib of parent.children) {
|
|
239
194
|
if (sib === wrap) continue;
|
|
240
195
|
try {
|
|
241
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
196
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
242
197
|
return sib.isConnected;
|
|
243
|
-
}
|
|
244
198
|
} catch (_) {}
|
|
245
199
|
}
|
|
246
200
|
}
|
|
247
|
-
// Dernier recours : querySelector global
|
|
248
201
|
try {
|
|
249
202
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
250
203
|
return !!(found?.isConnected);
|
|
251
204
|
} catch (_) { return false; }
|
|
252
205
|
}
|
|
253
206
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
207
|
+
const adjacentWrap = el =>
|
|
208
|
+
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
257
209
|
|
|
258
210
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
259
211
|
|
|
260
|
-
/**
|
|
261
|
-
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
262
|
-
* ou un fallback positionnel si l'attribut est absent.
|
|
263
|
-
*/
|
|
264
212
|
function stableId(klass, el) {
|
|
265
213
|
const attr = KIND[klass]?.anchorAttr;
|
|
266
214
|
if (attr) {
|
|
@@ -279,18 +227,15 @@
|
|
|
279
227
|
|
|
280
228
|
function findWrap(key) {
|
|
281
229
|
const w = S.wrapByKey.get(key);
|
|
282
|
-
return
|
|
230
|
+
return w?.isConnected ? w : null;
|
|
283
231
|
}
|
|
284
232
|
|
|
285
233
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
286
234
|
|
|
287
|
-
/**
|
|
288
|
-
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
-
* ou null si tous les ids sont montés.
|
|
290
|
-
*/
|
|
291
235
|
function pickId(poolKey) {
|
|
292
236
|
const pool = S.pools[poolKey];
|
|
293
237
|
if (!pool.length) return null;
|
|
238
|
+
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
294
239
|
for (let t = 0; t < pool.length; t++) {
|
|
295
240
|
const i = S.cursors[poolKey] % pool.length;
|
|
296
241
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -302,9 +247,7 @@
|
|
|
302
247
|
|
|
303
248
|
/**
|
|
304
249
|
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
305
|
-
* Séquence
|
|
306
|
-
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
307
|
-
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
250
|
+
* Séquence : destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
308
251
|
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
309
252
|
*/
|
|
310
253
|
function recycleAndMove(klass, targetEl, newKey) {
|
|
@@ -313,10 +256,7 @@
|
|
|
313
256
|
typeof ez?.define !== 'function' ||
|
|
314
257
|
typeof ez?.displayMore !== 'function') return null;
|
|
315
258
|
|
|
316
|
-
const
|
|
317
|
-
// Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
|
|
318
|
-
// après pour neutraliser l'IO — plus de showAds parasite possible.
|
|
319
|
-
const threshold = -vh;
|
|
259
|
+
const threshold = -(window.innerHeight || 800);
|
|
320
260
|
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
321
261
|
let bestFilled = null, bestFilledBottom = Infinity;
|
|
322
262
|
|
|
@@ -336,16 +276,18 @@
|
|
|
336
276
|
if (!best) return null;
|
|
337
277
|
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
338
278
|
if (!Number.isFinite(id)) return null;
|
|
279
|
+
if (S.recycling.has(id)) return null;
|
|
280
|
+
// Ne pas recycler un wrap rempli depuis moins de RECYCLE_MIN_AGE_MS
|
|
281
|
+
if (best === bestFilled) {
|
|
282
|
+
const filledAt = parseInt(best.getAttribute('data-ezoic-filled') || '0', 10);
|
|
283
|
+
if (ts() - filledAt < RECYCLE_MIN_AGE_MS) return null;
|
|
284
|
+
}
|
|
285
|
+
S.recycling.add(id);
|
|
339
286
|
|
|
340
287
|
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
288
|
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
344
289
|
mutate(() => {
|
|
345
|
-
best.setAttribute(A_ANCHOR,
|
|
346
|
-
best.setAttribute(A_CREATED, String(ts()));
|
|
347
|
-
best.setAttribute(A_SHOWN, '0');
|
|
348
|
-
best.classList.remove('is-empty');
|
|
290
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
349
291
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
350
292
|
if (ph) ph.innerHTML = '';
|
|
351
293
|
targetEl.insertAdjacentElement('afterend', best);
|
|
@@ -353,11 +295,14 @@
|
|
|
353
295
|
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
354
296
|
S.wrapByKey.set(newKey, best);
|
|
355
297
|
|
|
356
|
-
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
357
298
|
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
358
299
|
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
359
|
-
const doDisplay = () => {
|
|
360
|
-
|
|
300
|
+
const doDisplay = () => {
|
|
301
|
+
try { ez.displayMore([id]); } catch (_) {}
|
|
302
|
+
S.recycling.delete(id);
|
|
303
|
+
observePh(id);
|
|
304
|
+
};
|
|
305
|
+
try { typeof ez.cmd?.push === 'function' ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
361
306
|
|
|
362
307
|
return { id, wrap: best };
|
|
363
308
|
}
|
|
@@ -367,10 +312,8 @@
|
|
|
367
312
|
function makeWrap(id, klass, key) {
|
|
368
313
|
const w = document.createElement('div');
|
|
369
314
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
370
|
-
w.setAttribute(A_ANCHOR,
|
|
371
|
-
w.setAttribute(A_WRAPID,
|
|
372
|
-
w.setAttribute(A_CREATED, String(ts()));
|
|
373
|
-
w.setAttribute(A_SHOWN, '0');
|
|
315
|
+
w.setAttribute(A_ANCHOR, key);
|
|
316
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
374
317
|
w.style.cssText = 'width:100%;display:block;';
|
|
375
318
|
const ph = document.createElement('div');
|
|
376
319
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -403,44 +346,8 @@
|
|
|
403
346
|
} catch (_) {}
|
|
404
347
|
}
|
|
405
348
|
|
|
406
|
-
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
407
|
-
//
|
|
408
|
-
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
409
|
-
//
|
|
410
|
-
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
411
|
-
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
412
|
-
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
413
|
-
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
414
|
-
// liste après un long scroll et bloquent les nouvelles injections.
|
|
415
|
-
//
|
|
416
|
-
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
417
|
-
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
418
|
-
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
419
|
-
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
420
|
-
|
|
421
|
-
function pruneOrphansBetween() {
|
|
422
|
-
const klass = 'ezoic-ad-between';
|
|
423
|
-
const cfg = KIND[klass];
|
|
424
|
-
|
|
425
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
426
|
-
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
427
|
-
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
428
|
-
|
|
429
|
-
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
430
|
-
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
431
|
-
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
432
|
-
|
|
433
|
-
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
434
|
-
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
349
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
439
350
|
|
|
440
|
-
/**
|
|
441
|
-
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
442
|
-
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
443
|
-
*/
|
|
444
351
|
function ordinal(klass, el) {
|
|
445
352
|
const attr = KIND[klass]?.ordinalAttr;
|
|
446
353
|
if (attr) {
|
|
@@ -459,18 +366,14 @@
|
|
|
459
366
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
460
367
|
if (!items.length) return 0;
|
|
461
368
|
let inserted = 0;
|
|
462
|
-
|
|
463
369
|
for (const el of items) {
|
|
464
370
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
465
371
|
if (!el?.isConnected) continue;
|
|
466
|
-
|
|
467
372
|
const ord = ordinal(klass, el);
|
|
468
373
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
469
374
|
if (adjacentWrap(el)) continue;
|
|
470
|
-
|
|
471
375
|
const key = anchorKey(klass, el);
|
|
472
376
|
if (findWrap(key)) continue;
|
|
473
|
-
|
|
474
377
|
const id = pickId(poolKey);
|
|
475
378
|
if (id) {
|
|
476
379
|
const w = insertAfter(el, id, klass, key);
|
|
@@ -487,7 +390,10 @@
|
|
|
487
390
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
488
391
|
|
|
489
392
|
function getIO() {
|
|
490
|
-
|
|
393
|
+
const mobile = isMobile();
|
|
394
|
+
if (S.io && _ioMobile === mobile) return S.io;
|
|
395
|
+
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
396
|
+
_ioMobile = mobile;
|
|
491
397
|
try {
|
|
492
398
|
S.io = new IntersectionObserver(entries => {
|
|
493
399
|
for (const e of entries) {
|
|
@@ -496,7 +402,7 @@
|
|
|
496
402
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
497
403
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
498
404
|
}
|
|
499
|
-
}, { root: null, rootMargin:
|
|
405
|
+
}, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
500
406
|
} catch (_) { S.io = null; }
|
|
501
407
|
return S.io;
|
|
502
408
|
}
|
|
@@ -516,6 +422,19 @@
|
|
|
516
422
|
startShow(id);
|
|
517
423
|
}
|
|
518
424
|
|
|
425
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
426
|
+
setTimeout(() => {
|
|
427
|
+
try {
|
|
428
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
429
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
430
|
+
if (!wrap || !ph?.isConnected) return;
|
|
431
|
+
// Ne pas écraser un showAds plus récent
|
|
432
|
+
if ((S.lastShow.get(id) ?? 0) > showTs) return;
|
|
433
|
+
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
434
|
+
} catch (_) {}
|
|
435
|
+
}, EMPTY_CHECK_MS);
|
|
436
|
+
}
|
|
437
|
+
|
|
519
438
|
function drainQueue() {
|
|
520
439
|
if (isBlocked()) return;
|
|
521
440
|
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
@@ -536,19 +455,16 @@
|
|
|
536
455
|
drainQueue();
|
|
537
456
|
};
|
|
538
457
|
const timer = setTimeout(release, 7000);
|
|
539
|
-
|
|
540
458
|
requestAnimationFrame(() => {
|
|
541
459
|
try {
|
|
542
460
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
543
461
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
544
462
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
-
|
|
546
463
|
const t = ts();
|
|
547
464
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
548
465
|
S.lastShow.set(id, t);
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
|
|
466
|
+
// Marquer le wrap avec le timestamp de fill pour bloquer le recyclage
|
|
467
|
+
try { const w = document.getElementById(`${PH_PREFIX}${id}`)?.closest?.(`.${WRAP_CLASS}`); if (w) w.setAttribute('data-ezoic-filled', String(t)); } catch (_) {}
|
|
552
468
|
window.ezstandalone = window.ezstandalone || {};
|
|
553
469
|
const ez = window.ezstandalone;
|
|
554
470
|
const doShow = () => {
|
|
@@ -561,23 +477,10 @@
|
|
|
561
477
|
});
|
|
562
478
|
}
|
|
563
479
|
|
|
564
|
-
function scheduleEmptyCheck(id, showTs) {
|
|
565
|
-
setTimeout(() => {
|
|
566
|
-
try {
|
|
567
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
568
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
569
|
-
if (!wrap || !ph?.isConnected) return;
|
|
570
|
-
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
571
|
-
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
572
|
-
} catch (_) {}
|
|
573
|
-
}, EMPTY_CHECK_MS);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
480
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
577
481
|
//
|
|
578
|
-
// Intercepte ez.showAds() pour
|
|
579
|
-
//
|
|
580
|
-
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
482
|
+
// Intercepte ez.showAds() pour ignorer les appels pendant blockedUntil
|
|
483
|
+
// et filtrer les ids dont le placeholder n'est pas connecté au DOM.
|
|
581
484
|
|
|
582
485
|
function patchShowAds() {
|
|
583
486
|
const apply = () => {
|
|
@@ -613,37 +516,19 @@
|
|
|
613
516
|
async function runCore() {
|
|
614
517
|
if (isBlocked()) return 0;
|
|
615
518
|
patchShowAds();
|
|
616
|
-
|
|
617
519
|
const cfg = await fetchConfig();
|
|
618
520
|
if (!cfg || cfg.excluded) return 0;
|
|
619
521
|
initPools(cfg);
|
|
620
|
-
|
|
621
522
|
const kind = getKind();
|
|
622
523
|
if (kind === 'other') return 0;
|
|
623
|
-
|
|
624
524
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
625
525
|
if (!normBool(cfgEnable)) return 0;
|
|
626
526
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
627
527
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
628
528
|
};
|
|
629
|
-
|
|
630
|
-
if (kind === '
|
|
631
|
-
|
|
632
|
-
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
633
|
-
);
|
|
634
|
-
|
|
635
|
-
if (kind === 'categoryTopics') {
|
|
636
|
-
pruneOrphansBetween();
|
|
637
|
-
return exec(
|
|
638
|
-
'ezoic-ad-between', getTopics,
|
|
639
|
-
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return exec(
|
|
644
|
-
'ezoic-ad-categories', getCategories,
|
|
645
|
-
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
646
|
-
);
|
|
529
|
+
if (kind === 'topic') return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
530
|
+
if (kind === 'categoryTopics') return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
531
|
+
return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
647
532
|
}
|
|
648
533
|
|
|
649
534
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -667,11 +552,9 @@
|
|
|
667
552
|
S.lastBurstTs = t;
|
|
668
553
|
S.pageKey = pageKey();
|
|
669
554
|
S.burstDeadline = t + 2000;
|
|
670
|
-
|
|
671
555
|
if (S.burstActive) return;
|
|
672
556
|
S.burstActive = true;
|
|
673
557
|
S.burstCount = 0;
|
|
674
|
-
|
|
675
558
|
const step = () => {
|
|
676
559
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
677
560
|
S.burstActive = false; return;
|
|
@@ -690,21 +573,27 @@
|
|
|
690
573
|
function cleanup() {
|
|
691
574
|
blockedUntil = ts() + 1500;
|
|
692
575
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
693
|
-
S.cfg
|
|
694
|
-
|
|
695
|
-
S.
|
|
696
|
-
S.
|
|
576
|
+
S.cfg = null;
|
|
577
|
+
_cfgErrorUntil = 0;
|
|
578
|
+
S.poolsReady = false;
|
|
579
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
580
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
697
581
|
S.mountedIds.clear();
|
|
698
582
|
S.lastShow.clear();
|
|
699
583
|
S.wrapByKey.clear();
|
|
700
|
-
S.
|
|
701
|
-
S.
|
|
584
|
+
S.recycling.clear();
|
|
585
|
+
S.inflight = 0;
|
|
586
|
+
S.pending = [];
|
|
702
587
|
S.pendingSet.clear();
|
|
703
|
-
S.burstActive
|
|
704
|
-
S.runQueued
|
|
588
|
+
S.burstActive = false;
|
|
589
|
+
S.runQueued = false;
|
|
590
|
+
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
591
|
+
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
592
|
+
// exister en permanence — la déconnecter pendant la navigation cause
|
|
593
|
+
// des erreurs CMP postMessage et la disparition des pubs.
|
|
705
594
|
}
|
|
706
595
|
|
|
707
|
-
// ── MutationObserver
|
|
596
|
+
// ── MutationObserver DOM ───────────────────────────────────────────────────
|
|
708
597
|
|
|
709
598
|
function ensureDomObserver() {
|
|
710
599
|
if (S.domObs) return;
|
|
@@ -714,9 +603,8 @@
|
|
|
714
603
|
for (const m of muts) {
|
|
715
604
|
for (const n of m.addedNodes) {
|
|
716
605
|
if (n.nodeType !== 1) continue;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
606
|
+
if (allSel.some(s => { try { return n.matches(s); } catch (_) { return false; } }) ||
|
|
607
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch (_) { return false; } })) {
|
|
720
608
|
requestBurst(); return;
|
|
721
609
|
}
|
|
722
610
|
}
|
|
@@ -736,6 +624,7 @@
|
|
|
736
624
|
'cannot call refresh on the same page',
|
|
737
625
|
'no placeholders are currently defined in Refresh',
|
|
738
626
|
'Debugger iframe already exists',
|
|
627
|
+
'[CMP] Error in custom getTCData',
|
|
739
628
|
`with id ${PH_PREFIX}`,
|
|
740
629
|
];
|
|
741
630
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -758,9 +647,9 @@
|
|
|
758
647
|
(document.body || document.documentElement).appendChild(f);
|
|
759
648
|
};
|
|
760
649
|
inject();
|
|
761
|
-
if (!
|
|
762
|
-
|
|
763
|
-
|
|
650
|
+
if (!S.tcfObs) {
|
|
651
|
+
S.tcfObs = new MutationObserver(inject);
|
|
652
|
+
S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
764
653
|
}
|
|
765
654
|
} catch (_) {}
|
|
766
655
|
}
|
|
@@ -792,22 +681,19 @@
|
|
|
792
681
|
function bindNodeBB() {
|
|
793
682
|
const $ = window.jQuery;
|
|
794
683
|
if (!$) return;
|
|
795
|
-
|
|
796
684
|
$(window).off('.nbbEzoic');
|
|
797
685
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
798
686
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
799
687
|
S.pageKey = pageKey();
|
|
800
688
|
blockedUntil = 0;
|
|
801
|
-
muteConsole(); ensureTcfLocator();
|
|
689
|
+
muteConsole(); ensureTcfLocator();
|
|
802
690
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
803
691
|
});
|
|
804
|
-
|
|
805
692
|
const burstEvts = [
|
|
806
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
807
|
-
'action:categories.loaded',
|
|
693
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
694
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
808
695
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
809
696
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
-
|
|
811
697
|
try {
|
|
812
698
|
require(['hooks'], hooks => {
|
|
813
699
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -826,6 +712,11 @@
|
|
|
826
712
|
ticking = true;
|
|
827
713
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
828
714
|
}, { passive: true });
|
|
715
|
+
let resizeTimer = 0;
|
|
716
|
+
window.addEventListener('resize', () => {
|
|
717
|
+
clearTimeout(resizeTimer);
|
|
718
|
+
resizeTimer = setTimeout(getIO, 500);
|
|
719
|
+
}, { passive: true });
|
|
829
720
|
}
|
|
830
721
|
|
|
831
722
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
package/public/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — style.css (
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — style.css (v59)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* ── Wrapper ──────────────────────────────────────────────────────────────── */
|
|
@@ -56,16 +56,26 @@
|
|
|
56
56
|
top: auto !important;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/* ── Réservation d'espace anti-CLS ───────────────────────────────────────── */
|
|
60
|
+
/*
|
|
61
|
+
Réserve 90px avant que la pub charge (hauteur standard leaderboard).
|
|
62
|
+
Évite le saut de layout (CLS) quand AMP/Ezoic redimensionne l'iframe.
|
|
63
|
+
*/
|
|
64
|
+
.nodebb-ezoic-wrap.ezoic-ad-between {
|
|
65
|
+
min-height: 90px;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
/* ── État vide ────────────────────────────────────────────────────────────── */
|
|
60
69
|
/*
|
|
61
|
-
Ajouté
|
|
62
|
-
Collapse à
|
|
70
|
+
Ajouté 60s après showAds si aucun fill détecté (délai généreux pour CMP/enchères).
|
|
71
|
+
Collapse à 0 : évite la ligne de quelques pixels visible quand Ezoic
|
|
72
|
+
injecte un conteneur vide mais ne sert pas de pub.
|
|
63
73
|
*/
|
|
64
74
|
.nodebb-ezoic-wrap.is-empty {
|
|
65
75
|
display: block !important;
|
|
66
|
-
height:
|
|
67
|
-
min-height:
|
|
68
|
-
max-height:
|
|
76
|
+
height: 0 !important;
|
|
77
|
+
min-height: 0 !important;
|
|
78
|
+
max-height: 0 !important;
|
|
69
79
|
margin: 0 !important;
|
|
70
80
|
padding: 0 !important;
|
|
71
81
|
overflow: hidden !important;
|