nodebb-plugin-ezoic-infinite 1.7.58 → 1.7.60
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 +13 -29
- package/package.json +1 -1
- package/public/client.js +261 -148
- package/public/style.css +6 -16
package/library.js
CHANGED
|
@@ -15,11 +15,9 @@ 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 {
|
|
19
|
-
const parsed = JSON.parse(s);
|
|
20
|
-
if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
|
|
21
|
-
} catch (_) {}
|
|
18
|
+
try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
|
|
22
19
|
}
|
|
20
|
+
// Fallback : séparation par virgule
|
|
23
21
|
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
24
22
|
}
|
|
25
23
|
|
|
@@ -30,15 +28,7 @@ function parseBool(v, def = false) {
|
|
|
30
28
|
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
31
29
|
}
|
|
32
30
|
|
|
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
|
-
|
|
39
31
|
async function getAllGroups() {
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
42
32
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
43
33
|
if (!names || !names.length) {
|
|
44
34
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
@@ -47,9 +37,7 @@ async function getAllGroups() {
|
|
|
47
37
|
const data = await groups.getGroupsData(filtered);
|
|
48
38
|
const valid = data.filter(g => g && g.name);
|
|
49
39
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
50
|
-
|
|
51
|
-
_groupsCacheAt = now;
|
|
52
|
-
return _groupsCache;
|
|
40
|
+
return valid;
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
// ── Settings cache (30s TTL) ────────────────────────────────────────────────
|
|
@@ -100,10 +88,7 @@ ezstandalone.cmd = ezstandalone.cmd || [];
|
|
|
100
88
|
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
101
89
|
|
|
102
90
|
plugin.onSettingsSet = function (data) {
|
|
103
|
-
if (data && data.hash === SETTINGS_KEY)
|
|
104
|
-
_settingsCache = null;
|
|
105
|
-
_groupsCache = null; // invalider aussi le cache groupes
|
|
106
|
-
}
|
|
91
|
+
if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
|
|
107
92
|
};
|
|
108
93
|
|
|
109
94
|
plugin.addAdminNavigation = async (header) => {
|
|
@@ -116,11 +101,11 @@ plugin.addAdminNavigation = async (header) => {
|
|
|
116
101
|
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
117
102
|
*
|
|
118
103
|
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
119
|
-
* (render.js : templateValues.customHTML = meta.config.customHTML).
|
|
120
|
-
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
104
|
+
* (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
|
|
105
|
+
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
106
|
+
* et est rendu via req.app.renderAsync('header', hookReturn.templateData).
|
|
107
|
+
* On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
|
|
108
|
+
* tout en préservant ce dernier.
|
|
124
109
|
*/
|
|
125
110
|
plugin.injectEzoicHead = async (data) => {
|
|
126
111
|
try {
|
|
@@ -128,18 +113,17 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
128
113
|
const uid = data.req?.uid ?? 0;
|
|
129
114
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
130
115
|
if (!excluded) {
|
|
116
|
+
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
131
117
|
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
132
118
|
}
|
|
133
|
-
} catch (
|
|
134
|
-
// Log l'erreur mais ne pas planter le rendu de la page
|
|
135
|
-
console.error('[ezoic-infinite] injectEzoicHead error:', err.message || err);
|
|
136
|
-
}
|
|
119
|
+
} catch (_) {}
|
|
137
120
|
return data;
|
|
138
121
|
};
|
|
139
122
|
|
|
140
123
|
plugin.init = async ({ router, middleware }) => {
|
|
141
124
|
async function render(req, res) {
|
|
142
|
-
const
|
|
125
|
+
const settings = await getSettings();
|
|
126
|
+
const allGroups = await getAllGroups();
|
|
143
127
|
res.render('admin/plugins/ezoic-infinite', {
|
|
144
128
|
title: 'Ezoic Infinite Ads',
|
|
145
129
|
...settings,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,51 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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.
|
|
32
67
|
*/
|
|
33
68
|
(function nbbEzoicInfinite() {
|
|
34
69
|
'use strict';
|
|
35
70
|
|
|
36
71
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
37
72
|
|
|
38
|
-
const WRAP_CLASS
|
|
39
|
-
const PH_PREFIX
|
|
40
|
-
const A_ANCHOR
|
|
41
|
-
const A_WRAPID
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
73
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
74
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
75
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
76
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
77
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
|
+
|
|
80
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
|
|
81
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
|
+
const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
|
|
83
|
+
const MAX_INFLIGHT = 4; // max showAds() simultanés
|
|
84
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
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
|
|
49
88
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
50
89
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
51
90
|
|
|
@@ -57,10 +96,13 @@
|
|
|
57
96
|
|
|
58
97
|
/**
|
|
59
98
|
* Table KIND — source de vérité par kindClass.
|
|
60
|
-
*
|
|
99
|
+
*
|
|
100
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
61
101
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
62
103
|
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
63
|
-
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
104
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
105
|
+
* null → fallback positionnel (catégories)
|
|
64
106
|
*/
|
|
65
107
|
const KIND = {
|
|
66
108
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -71,39 +113,34 @@
|
|
|
71
113
|
// ── État global ────────────────────────────────────────────────────────────
|
|
72
114
|
|
|
73
115
|
const S = {
|
|
74
|
-
pageKey:
|
|
75
|
-
cfg:
|
|
76
|
-
poolsReady:
|
|
77
|
-
pools:
|
|
78
|
-
cursors:
|
|
79
|
-
mountedIds:
|
|
80
|
-
lastShow:
|
|
81
|
-
io:
|
|
82
|
-
domObs:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
runQueued: false,
|
|
91
|
-
burstActive: false,
|
|
116
|
+
pageKey: null,
|
|
117
|
+
cfg: null,
|
|
118
|
+
poolsReady: false,
|
|
119
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
120
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
121
|
+
mountedIds: new Set(),
|
|
122
|
+
lastShow: new Map(),
|
|
123
|
+
io: null,
|
|
124
|
+
domObs: null,
|
|
125
|
+
mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
|
|
126
|
+
inflight: 0, // showAds() en cours
|
|
127
|
+
pending: [], // ids en attente de slot inflight
|
|
128
|
+
pendingSet: new Set(),
|
|
129
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
130
|
+
runQueued: false,
|
|
131
|
+
burstActive: false,
|
|
92
132
|
burstDeadline: 0,
|
|
93
|
-
burstCount:
|
|
94
|
-
lastBurstTs:
|
|
133
|
+
burstCount: 0,
|
|
134
|
+
lastBurstTs: 0,
|
|
95
135
|
};
|
|
96
136
|
|
|
97
|
-
let blockedUntil
|
|
98
|
-
let _cfgErrorUntil = 0;
|
|
99
|
-
let _ioMobile = null;
|
|
137
|
+
let blockedUntil = 0;
|
|
100
138
|
|
|
101
|
-
const ts
|
|
102
|
-
const isBlocked
|
|
103
|
-
const isMobile
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const isFilled = n => !!(n?.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
139
|
+
const ts = () => Date.now();
|
|
140
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
141
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
142
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
143
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
144
|
|
|
108
145
|
function mutate(fn) {
|
|
109
146
|
S.mutGuard++;
|
|
@@ -114,12 +151,10 @@
|
|
|
114
151
|
|
|
115
152
|
async function fetchConfig() {
|
|
116
153
|
if (S.cfg) return S.cfg;
|
|
117
|
-
if (Date.now() < _cfgErrorUntil) return null;
|
|
118
154
|
try {
|
|
119
155
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
120
|
-
if (r.ok)
|
|
121
|
-
|
|
122
|
-
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
156
|
+
if (r.ok) S.cfg = await r.json();
|
|
157
|
+
} catch (_) {}
|
|
123
158
|
return S.cfg;
|
|
124
159
|
}
|
|
125
160
|
|
|
@@ -179,37 +214,53 @@
|
|
|
179
214
|
|
|
180
215
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
181
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
219
|
+
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
220
|
+
*/
|
|
182
221
|
function wrapIsLive(wrap) {
|
|
183
222
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
184
223
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
185
224
|
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).
|
|
186
227
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
187
229
|
const colonIdx = key.indexOf(':');
|
|
188
230
|
const klass = key.slice(0, colonIdx);
|
|
189
231
|
const anchorId = key.slice(colonIdx + 1);
|
|
190
232
|
const cfg = KIND[klass];
|
|
191
233
|
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.
|
|
192
236
|
const parent = wrap.parentElement;
|
|
193
237
|
if (parent) {
|
|
194
238
|
for (const sib of parent.children) {
|
|
195
239
|
if (sib === wrap) continue;
|
|
196
240
|
try {
|
|
197
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
241
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
198
242
|
return sib.isConnected;
|
|
243
|
+
}
|
|
199
244
|
} catch (_) {}
|
|
200
245
|
}
|
|
201
246
|
}
|
|
247
|
+
// Dernier recours : querySelector global
|
|
202
248
|
try {
|
|
203
249
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
204
250
|
return !!(found?.isConnected);
|
|
205
251
|
} catch (_) { return false; }
|
|
206
252
|
}
|
|
207
253
|
|
|
208
|
-
|
|
209
|
-
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
254
|
+
function adjacentWrap(el) {
|
|
255
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
256
|
+
}
|
|
210
257
|
|
|
211
258
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
212
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
262
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
263
|
+
*/
|
|
213
264
|
function stableId(klass, el) {
|
|
214
265
|
const attr = KIND[klass]?.anchorAttr;
|
|
215
266
|
if (attr) {
|
|
@@ -228,15 +279,18 @@
|
|
|
228
279
|
|
|
229
280
|
function findWrap(key) {
|
|
230
281
|
const w = S.wrapByKey.get(key);
|
|
231
|
-
return w?.isConnected ? w : null;
|
|
282
|
+
return (w?.isConnected) ? w : null;
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
235
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
+
* ou null si tous les ids sont montés.
|
|
290
|
+
*/
|
|
236
291
|
function pickId(poolKey) {
|
|
237
292
|
const pool = S.pools[poolKey];
|
|
238
293
|
if (!pool.length) return null;
|
|
239
|
-
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
240
294
|
for (let t = 0; t < pool.length; t++) {
|
|
241
295
|
const i = S.cursors[poolKey] % pool.length;
|
|
242
296
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -248,7 +302,9 @@
|
|
|
248
302
|
|
|
249
303
|
/**
|
|
250
304
|
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
251
|
-
* Séquence
|
|
305
|
+
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
306
|
+
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
307
|
+
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
252
308
|
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
253
309
|
*/
|
|
254
310
|
function recycleAndMove(klass, targetEl, newKey) {
|
|
@@ -257,7 +313,10 @@
|
|
|
257
313
|
typeof ez?.define !== 'function' ||
|
|
258
314
|
typeof ez?.displayMore !== 'function') return null;
|
|
259
315
|
|
|
260
|
-
const
|
|
316
|
+
const vh = window.innerHeight || 800;
|
|
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;
|
|
261
320
|
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
262
321
|
let bestFilled = null, bestFilledBottom = Infinity;
|
|
263
322
|
|
|
@@ -277,13 +336,16 @@
|
|
|
277
336
|
if (!best) return null;
|
|
278
337
|
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
279
338
|
if (!Number.isFinite(id)) return null;
|
|
280
|
-
if (S.recycling.has(id)) return null;
|
|
281
|
-
S.recycling.add(id);
|
|
282
339
|
|
|
283
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.
|
|
284
343
|
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
285
344
|
mutate(() => {
|
|
286
|
-
best.setAttribute(A_ANCHOR,
|
|
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');
|
|
287
349
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
288
350
|
if (ph) ph.innerHTML = '';
|
|
289
351
|
targetEl.insertAdjacentElement('afterend', best);
|
|
@@ -291,14 +353,11 @@
|
|
|
291
353
|
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
292
354
|
S.wrapByKey.set(newKey, best);
|
|
293
355
|
|
|
356
|
+
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
294
357
|
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
295
358
|
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
296
|
-
const doDisplay = () => {
|
|
297
|
-
|
|
298
|
-
S.recycling.delete(id);
|
|
299
|
-
observePh(id);
|
|
300
|
-
};
|
|
301
|
-
try { typeof ez.cmd?.push === 'function' ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
359
|
+
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
360
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
302
361
|
|
|
303
362
|
return { id, wrap: best };
|
|
304
363
|
}
|
|
@@ -308,8 +367,10 @@
|
|
|
308
367
|
function makeWrap(id, klass, key) {
|
|
309
368
|
const w = document.createElement('div');
|
|
310
369
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
311
|
-
w.setAttribute(A_ANCHOR,
|
|
312
|
-
w.setAttribute(A_WRAPID,
|
|
370
|
+
w.setAttribute(A_ANCHOR, key);
|
|
371
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
372
|
+
w.setAttribute(A_CREATED, String(ts()));
|
|
373
|
+
w.setAttribute(A_SHOWN, '0');
|
|
313
374
|
w.style.cssText = 'width:100%;display:block;';
|
|
314
375
|
const ph = document.createElement('div');
|
|
315
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -327,7 +388,6 @@
|
|
|
327
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
328
389
|
S.mountedIds.add(id);
|
|
329
390
|
S.wrapByKey.set(key, w);
|
|
330
|
-
scheduleEmptyCheck(id);
|
|
331
391
|
return w;
|
|
332
392
|
}
|
|
333
393
|
|
|
@@ -343,8 +403,44 @@
|
|
|
343
403
|
} catch (_) {}
|
|
344
404
|
}
|
|
345
405
|
|
|
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
|
+
|
|
346
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
347
439
|
|
|
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
|
+
*/
|
|
348
444
|
function ordinal(klass, el) {
|
|
349
445
|
const attr = KIND[klass]?.ordinalAttr;
|
|
350
446
|
if (attr) {
|
|
@@ -363,14 +459,18 @@
|
|
|
363
459
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
364
460
|
if (!items.length) return 0;
|
|
365
461
|
let inserted = 0;
|
|
462
|
+
|
|
366
463
|
for (const el of items) {
|
|
367
464
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
368
465
|
if (!el?.isConnected) continue;
|
|
466
|
+
|
|
369
467
|
const ord = ordinal(klass, el);
|
|
370
468
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
371
469
|
if (adjacentWrap(el)) continue;
|
|
470
|
+
|
|
372
471
|
const key = anchorKey(klass, el);
|
|
373
472
|
if (findWrap(key)) continue;
|
|
473
|
+
|
|
374
474
|
const id = pickId(poolKey);
|
|
375
475
|
if (id) {
|
|
376
476
|
const w = insertAfter(el, id, klass, key);
|
|
@@ -387,10 +487,7 @@
|
|
|
387
487
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
388
488
|
|
|
389
489
|
function getIO() {
|
|
390
|
-
|
|
391
|
-
if (S.io && _ioMobile === mobile) return S.io;
|
|
392
|
-
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
393
|
-
_ioMobile = mobile;
|
|
490
|
+
if (S.io) return S.io;
|
|
394
491
|
try {
|
|
395
492
|
S.io = new IntersectionObserver(entries => {
|
|
396
493
|
for (const e of entries) {
|
|
@@ -399,7 +496,7 @@
|
|
|
399
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
400
497
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
401
498
|
}
|
|
402
|
-
}, { root: null, rootMargin:
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
403
500
|
} catch (_) { S.io = null; }
|
|
404
501
|
return S.io;
|
|
405
502
|
}
|
|
@@ -419,18 +516,6 @@
|
|
|
419
516
|
startShow(id);
|
|
420
517
|
}
|
|
421
518
|
|
|
422
|
-
function scheduleEmptyCheck(id) {
|
|
423
|
-
setTimeout(() => {
|
|
424
|
-
try {
|
|
425
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
426
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
427
|
-
if (!wrap || !ph?.isConnected) return;
|
|
428
|
-
// Collapse uniquement si vraiment vide après 60s
|
|
429
|
-
if (!isFilled(wrap)) wrap.classList.add('is-empty');
|
|
430
|
-
} catch (_) {}
|
|
431
|
-
}, EMPTY_CHECK_MS);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
519
|
function drainQueue() {
|
|
435
520
|
if (isBlocked()) return;
|
|
436
521
|
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
@@ -451,21 +536,24 @@
|
|
|
451
536
|
drainQueue();
|
|
452
537
|
};
|
|
453
538
|
const timer = setTimeout(release, 7000);
|
|
539
|
+
|
|
454
540
|
requestAnimationFrame(() => {
|
|
455
541
|
try {
|
|
456
542
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
457
543
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
458
544
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
+
|
|
459
546
|
const t = ts();
|
|
460
547
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
461
548
|
S.lastShow.set(id, t);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
549
|
+
|
|
550
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
551
|
+
|
|
465
552
|
window.ezstandalone = window.ezstandalone || {};
|
|
466
553
|
const ez = window.ezstandalone;
|
|
467
554
|
const doShow = () => {
|
|
468
555
|
try { ez.showAds(id); } catch (_) {}
|
|
556
|
+
scheduleEmptyCheck(id, t);
|
|
469
557
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
470
558
|
};
|
|
471
559
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
@@ -473,10 +561,23 @@
|
|
|
473
561
|
});
|
|
474
562
|
}
|
|
475
563
|
|
|
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
|
+
|
|
476
576
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
477
577
|
//
|
|
478
|
-
// Intercepte ez.showAds() pour
|
|
479
|
-
//
|
|
578
|
+
// Intercepte ez.showAds() pour :
|
|
579
|
+
// – ignorer les appels pendant blockedUntil
|
|
580
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
480
581
|
|
|
481
582
|
function patchShowAds() {
|
|
482
583
|
const apply = () => {
|
|
@@ -512,19 +613,37 @@
|
|
|
512
613
|
async function runCore() {
|
|
513
614
|
if (isBlocked()) return 0;
|
|
514
615
|
patchShowAds();
|
|
616
|
+
|
|
515
617
|
const cfg = await fetchConfig();
|
|
516
618
|
if (!cfg || cfg.excluded) return 0;
|
|
517
619
|
initPools(cfg);
|
|
620
|
+
|
|
518
621
|
const kind = getKind();
|
|
519
622
|
if (kind === 'other') return 0;
|
|
623
|
+
|
|
520
624
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
521
625
|
if (!normBool(cfgEnable)) return 0;
|
|
522
626
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
523
627
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
524
628
|
};
|
|
525
|
-
|
|
526
|
-
if (kind === '
|
|
527
|
-
|
|
629
|
+
|
|
630
|
+
if (kind === 'topic') return exec(
|
|
631
|
+
'ezoic-ad-message', getPosts,
|
|
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
|
+
);
|
|
528
647
|
}
|
|
529
648
|
|
|
530
649
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -548,9 +667,11 @@
|
|
|
548
667
|
S.lastBurstTs = t;
|
|
549
668
|
S.pageKey = pageKey();
|
|
550
669
|
S.burstDeadline = t + 2000;
|
|
670
|
+
|
|
551
671
|
if (S.burstActive) return;
|
|
552
672
|
S.burstActive = true;
|
|
553
673
|
S.burstCount = 0;
|
|
674
|
+
|
|
554
675
|
const step = () => {
|
|
555
676
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
556
677
|
S.burstActive = false; return;
|
|
@@ -569,27 +690,21 @@
|
|
|
569
690
|
function cleanup() {
|
|
570
691
|
blockedUntil = ts() + 1500;
|
|
571
692
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
572
|
-
S.cfg
|
|
573
|
-
|
|
574
|
-
S.
|
|
575
|
-
S.
|
|
576
|
-
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
693
|
+
S.cfg = null;
|
|
694
|
+
S.poolsReady = false;
|
|
695
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
696
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
577
697
|
S.mountedIds.clear();
|
|
578
698
|
S.lastShow.clear();
|
|
579
699
|
S.wrapByKey.clear();
|
|
580
|
-
S.
|
|
581
|
-
S.
|
|
582
|
-
S.pending = [];
|
|
700
|
+
S.inflight = 0;
|
|
701
|
+
S.pending = [];
|
|
583
702
|
S.pendingSet.clear();
|
|
584
|
-
S.burstActive
|
|
585
|
-
S.runQueued
|
|
586
|
-
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
587
|
-
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
588
|
-
// exister en permanence — la déconnecter pendant la navigation cause
|
|
589
|
-
// des erreurs CMP postMessage et la disparition des pubs.
|
|
703
|
+
S.burstActive = false;
|
|
704
|
+
S.runQueued = false;
|
|
590
705
|
}
|
|
591
706
|
|
|
592
|
-
// ── MutationObserver
|
|
707
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
593
708
|
|
|
594
709
|
function ensureDomObserver() {
|
|
595
710
|
if (S.domObs) return;
|
|
@@ -599,8 +714,9 @@
|
|
|
599
714
|
for (const m of muts) {
|
|
600
715
|
for (const n of m.addedNodes) {
|
|
601
716
|
if (n.nodeType !== 1) continue;
|
|
602
|
-
|
|
603
|
-
|
|
717
|
+
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
718
|
+
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
719
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
604
720
|
requestBurst(); return;
|
|
605
721
|
}
|
|
606
722
|
}
|
|
@@ -620,7 +736,6 @@
|
|
|
620
736
|
'cannot call refresh on the same page',
|
|
621
737
|
'no placeholders are currently defined in Refresh',
|
|
622
738
|
'Debugger iframe already exists',
|
|
623
|
-
'[CMP] Error in custom getTCData',
|
|
624
739
|
`with id ${PH_PREFIX}`,
|
|
625
740
|
];
|
|
626
741
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -643,9 +758,9 @@
|
|
|
643
758
|
(document.body || document.documentElement).appendChild(f);
|
|
644
759
|
};
|
|
645
760
|
inject();
|
|
646
|
-
if (!
|
|
647
|
-
|
|
648
|
-
|
|
761
|
+
if (!window.__nbbTcfObs) {
|
|
762
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
763
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
649
764
|
}
|
|
650
765
|
} catch (_) {}
|
|
651
766
|
}
|
|
@@ -677,19 +792,22 @@
|
|
|
677
792
|
function bindNodeBB() {
|
|
678
793
|
const $ = window.jQuery;
|
|
679
794
|
if (!$) return;
|
|
795
|
+
|
|
680
796
|
$(window).off('.nbbEzoic');
|
|
681
797
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
682
798
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
683
799
|
S.pageKey = pageKey();
|
|
684
800
|
blockedUntil = 0;
|
|
685
|
-
muteConsole(); ensureTcfLocator();
|
|
801
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
686
802
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
687
803
|
});
|
|
804
|
+
|
|
688
805
|
const burstEvts = [
|
|
689
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
690
|
-
'action:categories.loaded',
|
|
806
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
807
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
691
808
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
692
809
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
+
|
|
693
811
|
try {
|
|
694
812
|
require(['hooks'], hooks => {
|
|
695
813
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -708,11 +826,6 @@
|
|
|
708
826
|
ticking = true;
|
|
709
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
710
828
|
}, { passive: true });
|
|
711
|
-
let resizeTimer = 0;
|
|
712
|
-
window.addEventListener('resize', () => {
|
|
713
|
-
clearTimeout(resizeTimer);
|
|
714
|
-
resizeTimer = setTimeout(getIO, 500);
|
|
715
|
-
}, { passive: true });
|
|
716
829
|
}
|
|
717
830
|
|
|
718
831
|
// ── 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 (v20)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* ── Wrapper ──────────────────────────────────────────────────────────────── */
|
|
@@ -56,26 +56,16 @@
|
|
|
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
|
-
|
|
68
59
|
/* ── État vide ────────────────────────────────────────────────────────────── */
|
|
69
60
|
/*
|
|
70
|
-
Ajouté
|
|
71
|
-
Collapse à 0 :
|
|
72
|
-
injecte un conteneur vide mais ne sert pas de pub.
|
|
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.
|
|
73
63
|
*/
|
|
74
64
|
.nodebb-ezoic-wrap.is-empty {
|
|
75
65
|
display: block !important;
|
|
76
|
-
height:
|
|
77
|
-
min-height:
|
|
78
|
-
max-height:
|
|
66
|
+
height: 1px !important;
|
|
67
|
+
min-height: 1px !important;
|
|
68
|
+
max-height: 1px !important;
|
|
79
69
|
margin: 0 !important;
|
|
80
70
|
padding: 0 !important;
|
|
81
71
|
overflow: hidden !important;
|