nodebb-plugin-ezoic-infinite 1.7.59 → 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 +315 -139
- 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;
|
|
@@ -246,13 +300,77 @@
|
|
|
246
300
|
return null;
|
|
247
301
|
}
|
|
248
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
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.
|
|
308
|
+
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
309
|
+
*/
|
|
310
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
311
|
+
const ez = window.ezstandalone;
|
|
312
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
313
|
+
typeof ez?.define !== 'function' ||
|
|
314
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
315
|
+
|
|
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;
|
|
320
|
+
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
321
|
+
let bestFilled = null, bestFilledBottom = Infinity;
|
|
322
|
+
|
|
323
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
324
|
+
try {
|
|
325
|
+
const rect = wrap.getBoundingClientRect();
|
|
326
|
+
if (rect.bottom > threshold) return;
|
|
327
|
+
if (!isFilled(wrap)) {
|
|
328
|
+
if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
|
|
329
|
+
} else {
|
|
330
|
+
if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
|
|
331
|
+
}
|
|
332
|
+
} catch (_) {}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const best = bestEmpty ?? bestFilled;
|
|
336
|
+
if (!best) return null;
|
|
337
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
338
|
+
if (!Number.isFinite(id)) return null;
|
|
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);
|
|
355
|
+
|
|
356
|
+
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
357
|
+
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
358
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
359
|
+
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
360
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
361
|
+
|
|
362
|
+
return { id, wrap: best };
|
|
363
|
+
}
|
|
364
|
+
|
|
249
365
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
250
366
|
|
|
251
367
|
function makeWrap(id, klass, key) {
|
|
252
368
|
const w = document.createElement('div');
|
|
253
369
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
254
|
-
w.setAttribute(A_ANCHOR,
|
|
255
|
-
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');
|
|
256
374
|
w.style.cssText = 'width:100%;display:block;';
|
|
257
375
|
const ph = document.createElement('div');
|
|
258
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -270,7 +388,6 @@
|
|
|
270
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
271
389
|
S.mountedIds.add(id);
|
|
272
390
|
S.wrapByKey.set(key, w);
|
|
273
|
-
scheduleEmptyCheck(id);
|
|
274
391
|
return w;
|
|
275
392
|
}
|
|
276
393
|
|
|
@@ -286,8 +403,44 @@
|
|
|
286
403
|
} catch (_) {}
|
|
287
404
|
}
|
|
288
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
|
+
|
|
289
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
290
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
|
+
*/
|
|
291
444
|
function ordinal(klass, el) {
|
|
292
445
|
const attr = KIND[klass]?.ordinalAttr;
|
|
293
446
|
if (attr) {
|
|
@@ -306,18 +459,27 @@
|
|
|
306
459
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
307
460
|
if (!items.length) return 0;
|
|
308
461
|
let inserted = 0;
|
|
462
|
+
|
|
309
463
|
for (const el of items) {
|
|
310
464
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
311
465
|
if (!el?.isConnected) continue;
|
|
466
|
+
|
|
312
467
|
const ord = ordinal(klass, el);
|
|
313
468
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
314
469
|
if (adjacentWrap(el)) continue;
|
|
470
|
+
|
|
315
471
|
const key = anchorKey(klass, el);
|
|
316
472
|
if (findWrap(key)) continue;
|
|
473
|
+
|
|
317
474
|
const id = pickId(poolKey);
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
475
|
+
if (id) {
|
|
476
|
+
const w = insertAfter(el, id, klass, key);
|
|
477
|
+
if (w) { observePh(id); inserted++; }
|
|
478
|
+
} else {
|
|
479
|
+
const recycled = recycleAndMove(klass, el, key);
|
|
480
|
+
if (!recycled) break;
|
|
481
|
+
inserted++;
|
|
482
|
+
}
|
|
321
483
|
}
|
|
322
484
|
return inserted;
|
|
323
485
|
}
|
|
@@ -325,10 +487,7 @@
|
|
|
325
487
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
326
488
|
|
|
327
489
|
function getIO() {
|
|
328
|
-
|
|
329
|
-
if (S.io && _ioMobile === mobile) return S.io;
|
|
330
|
-
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
331
|
-
_ioMobile = mobile;
|
|
490
|
+
if (S.io) return S.io;
|
|
332
491
|
try {
|
|
333
492
|
S.io = new IntersectionObserver(entries => {
|
|
334
493
|
for (const e of entries) {
|
|
@@ -337,7 +496,7 @@
|
|
|
337
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
338
497
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
339
498
|
}
|
|
340
|
-
}, { root: null, rootMargin:
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
341
500
|
} catch (_) { S.io = null; }
|
|
342
501
|
return S.io;
|
|
343
502
|
}
|
|
@@ -357,18 +516,6 @@
|
|
|
357
516
|
startShow(id);
|
|
358
517
|
}
|
|
359
518
|
|
|
360
|
-
function scheduleEmptyCheck(id) {
|
|
361
|
-
setTimeout(() => {
|
|
362
|
-
try {
|
|
363
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
364
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
365
|
-
if (!wrap || !ph?.isConnected) return;
|
|
366
|
-
// Collapse uniquement si vraiment vide après 60s
|
|
367
|
-
if (!isFilled(wrap)) wrap.classList.add('is-empty');
|
|
368
|
-
} catch (_) {}
|
|
369
|
-
}, EMPTY_CHECK_MS);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
519
|
function drainQueue() {
|
|
373
520
|
if (isBlocked()) return;
|
|
374
521
|
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
@@ -389,21 +536,24 @@
|
|
|
389
536
|
drainQueue();
|
|
390
537
|
};
|
|
391
538
|
const timer = setTimeout(release, 7000);
|
|
539
|
+
|
|
392
540
|
requestAnimationFrame(() => {
|
|
393
541
|
try {
|
|
394
542
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
395
543
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
396
544
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
+
|
|
397
546
|
const t = ts();
|
|
398
547
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
399
548
|
S.lastShow.set(id, t);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
549
|
+
|
|
550
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
551
|
+
|
|
403
552
|
window.ezstandalone = window.ezstandalone || {};
|
|
404
553
|
const ez = window.ezstandalone;
|
|
405
554
|
const doShow = () => {
|
|
406
555
|
try { ez.showAds(id); } catch (_) {}
|
|
556
|
+
scheduleEmptyCheck(id, t);
|
|
407
557
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
408
558
|
};
|
|
409
559
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
@@ -411,10 +561,23 @@
|
|
|
411
561
|
});
|
|
412
562
|
}
|
|
413
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
|
+
|
|
414
576
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
415
577
|
//
|
|
416
|
-
// Intercepte ez.showAds() pour
|
|
417
|
-
//
|
|
578
|
+
// Intercepte ez.showAds() pour :
|
|
579
|
+
// – ignorer les appels pendant blockedUntil
|
|
580
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
418
581
|
|
|
419
582
|
function patchShowAds() {
|
|
420
583
|
const apply = () => {
|
|
@@ -450,19 +613,37 @@
|
|
|
450
613
|
async function runCore() {
|
|
451
614
|
if (isBlocked()) return 0;
|
|
452
615
|
patchShowAds();
|
|
616
|
+
|
|
453
617
|
const cfg = await fetchConfig();
|
|
454
618
|
if (!cfg || cfg.excluded) return 0;
|
|
455
619
|
initPools(cfg);
|
|
620
|
+
|
|
456
621
|
const kind = getKind();
|
|
457
622
|
if (kind === 'other') return 0;
|
|
623
|
+
|
|
458
624
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
459
625
|
if (!normBool(cfgEnable)) return 0;
|
|
460
626
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
461
627
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
462
628
|
};
|
|
463
|
-
|
|
464
|
-
if (kind === '
|
|
465
|
-
|
|
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
|
+
);
|
|
466
647
|
}
|
|
467
648
|
|
|
468
649
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -486,9 +667,11 @@
|
|
|
486
667
|
S.lastBurstTs = t;
|
|
487
668
|
S.pageKey = pageKey();
|
|
488
669
|
S.burstDeadline = t + 2000;
|
|
670
|
+
|
|
489
671
|
if (S.burstActive) return;
|
|
490
672
|
S.burstActive = true;
|
|
491
673
|
S.burstCount = 0;
|
|
674
|
+
|
|
492
675
|
const step = () => {
|
|
493
676
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
494
677
|
S.burstActive = false; return;
|
|
@@ -507,26 +690,21 @@
|
|
|
507
690
|
function cleanup() {
|
|
508
691
|
blockedUntil = ts() + 1500;
|
|
509
692
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
510
|
-
S.cfg
|
|
511
|
-
|
|
512
|
-
S.
|
|
513
|
-
S.
|
|
514
|
-
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 };
|
|
515
697
|
S.mountedIds.clear();
|
|
516
698
|
S.lastShow.clear();
|
|
517
699
|
S.wrapByKey.clear();
|
|
518
|
-
S.inflight
|
|
519
|
-
S.pending
|
|
700
|
+
S.inflight = 0;
|
|
701
|
+
S.pending = [];
|
|
520
702
|
S.pendingSet.clear();
|
|
521
|
-
S.burstActive
|
|
522
|
-
S.runQueued
|
|
523
|
-
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
524
|
-
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
525
|
-
// exister en permanence — la déconnecter pendant la navigation cause
|
|
526
|
-
// des erreurs CMP postMessage et la disparition des pubs.
|
|
703
|
+
S.burstActive = false;
|
|
704
|
+
S.runQueued = false;
|
|
527
705
|
}
|
|
528
706
|
|
|
529
|
-
// ── MutationObserver
|
|
707
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
530
708
|
|
|
531
709
|
function ensureDomObserver() {
|
|
532
710
|
if (S.domObs) return;
|
|
@@ -536,8 +714,9 @@
|
|
|
536
714
|
for (const m of muts) {
|
|
537
715
|
for (const n of m.addedNodes) {
|
|
538
716
|
if (n.nodeType !== 1) continue;
|
|
539
|
-
|
|
540
|
-
|
|
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;} })) {
|
|
541
720
|
requestBurst(); return;
|
|
542
721
|
}
|
|
543
722
|
}
|
|
@@ -557,7 +736,6 @@
|
|
|
557
736
|
'cannot call refresh on the same page',
|
|
558
737
|
'no placeholders are currently defined in Refresh',
|
|
559
738
|
'Debugger iframe already exists',
|
|
560
|
-
'[CMP] Error in custom getTCData',
|
|
561
739
|
`with id ${PH_PREFIX}`,
|
|
562
740
|
];
|
|
563
741
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -580,9 +758,9 @@
|
|
|
580
758
|
(document.body || document.documentElement).appendChild(f);
|
|
581
759
|
};
|
|
582
760
|
inject();
|
|
583
|
-
if (!
|
|
584
|
-
|
|
585
|
-
|
|
761
|
+
if (!window.__nbbTcfObs) {
|
|
762
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
763
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
586
764
|
}
|
|
587
765
|
} catch (_) {}
|
|
588
766
|
}
|
|
@@ -614,19 +792,22 @@
|
|
|
614
792
|
function bindNodeBB() {
|
|
615
793
|
const $ = window.jQuery;
|
|
616
794
|
if (!$) return;
|
|
795
|
+
|
|
617
796
|
$(window).off('.nbbEzoic');
|
|
618
797
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
619
798
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
620
799
|
S.pageKey = pageKey();
|
|
621
800
|
blockedUntil = 0;
|
|
622
|
-
muteConsole(); ensureTcfLocator();
|
|
801
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
623
802
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
624
803
|
});
|
|
804
|
+
|
|
625
805
|
const burstEvts = [
|
|
626
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
627
|
-
'action:categories.loaded',
|
|
806
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
807
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
628
808
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
629
809
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
+
|
|
630
811
|
try {
|
|
631
812
|
require(['hooks'], hooks => {
|
|
632
813
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -645,11 +826,6 @@
|
|
|
645
826
|
ticking = true;
|
|
646
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
647
828
|
}, { passive: true });
|
|
648
|
-
let resizeTimer = 0;
|
|
649
|
-
window.addEventListener('resize', () => {
|
|
650
|
-
clearTimeout(resizeTimer);
|
|
651
|
-
resizeTimer = setTimeout(getIO, 500);
|
|
652
|
-
}, { passive: true });
|
|
653
829
|
}
|
|
654
830
|
|
|
655
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;
|