nodebb-plugin-ezoic-infinite 1.7.60 → 1.7.61
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 +148 -317
- 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,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v62
|
|
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
|
-
*
|
|
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
|
+
* v62 is-empty réintroduit : collapse 60s après insertion du wrap (pas après
|
|
22
|
+
* showAds) si isFilled est toujours false. Évite les trous permanents.
|
|
23
|
+
* v61 recycleAndMove : ne pas recycler un wrap rempli depuis moins de 30s.
|
|
24
|
+
* Empêche qu'une pub qui vient de charger soit déplacée immédiatement.
|
|
25
|
+
* v59 CSS : min-height 90px sur ezoic-ad-between (anti-CLS AMP ads).
|
|
26
|
+
* v58 tcfObs survit aux navigations : ne plus déconnecter dans cleanup().
|
|
27
|
+
* L'iframe __tcfapiLocator doit exister en permanence pour le CMP —
|
|
28
|
+
* la fenêtre entre cleanup() et ajaxify.end causait des erreurs
|
|
29
|
+
* "Cannot read properties of null (postMessage)" et disparition des pubs.
|
|
30
|
+
* v57 Nettoyage prod : A_SHOWN/A_CREATED supprimés, normBool Set O(1),
|
|
31
|
+
* muteConsole étend aux erreurs CMP getTCData, code nettoyé.
|
|
67
32
|
*/
|
|
68
33
|
(function nbbEzoicInfinite() {
|
|
69
34
|
'use strict';
|
|
70
35
|
|
|
71
36
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
72
37
|
|
|
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
|
-
|
|
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
|
|
38
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
39
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
40
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
41
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
42
|
+
|
|
43
|
+
const EMPTY_CHECK_MS = 60_000;
|
|
44
|
+
const MAX_INSERTS_RUN = 6; // insertions max par appel runCore
|
|
45
|
+
const MAX_INFLIGHT = 4; // showAds() simultanés max
|
|
46
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
47
|
+
const BURST_COOLDOWN_MS = 200; // délai min entre deux bursts
|
|
48
|
+
|
|
88
49
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
89
50
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
90
51
|
|
|
@@ -96,13 +57,10 @@
|
|
|
96
57
|
|
|
97
58
|
/**
|
|
98
59
|
* Table KIND — source de vérité par kindClass.
|
|
99
|
-
*
|
|
100
|
-
* sel sélecteur CSS complet des éléments cibles
|
|
60
|
+
* sel sélecteur CSS des éléments cibles
|
|
101
61
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
-
* (vide pour posts : le sélecteur commence par '[')
|
|
103
62
|
* 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)
|
|
63
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle (null = fallback positionnel)
|
|
106
64
|
*/
|
|
107
65
|
const KIND = {
|
|
108
66
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -113,34 +71,39 @@
|
|
|
113
71
|
// ── État global ────────────────────────────────────────────────────────────
|
|
114
72
|
|
|
115
73
|
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
|
-
|
|
74
|
+
pageKey: null,
|
|
75
|
+
cfg: null,
|
|
76
|
+
poolsReady: false,
|
|
77
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
78
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
79
|
+
mountedIds: new Set(),
|
|
80
|
+
lastShow: new Map(),
|
|
81
|
+
io: null,
|
|
82
|
+
domObs: null,
|
|
83
|
+
tcfObs: null,
|
|
84
|
+
mutGuard: 0,
|
|
85
|
+
inflight: 0,
|
|
86
|
+
pending: [],
|
|
87
|
+
pendingSet: new Set(),
|
|
88
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
89
|
+
recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
|
|
90
|
+
runQueued: false,
|
|
91
|
+
burstActive: false,
|
|
132
92
|
burstDeadline: 0,
|
|
133
|
-
burstCount:
|
|
134
|
-
lastBurstTs:
|
|
93
|
+
burstCount: 0,
|
|
94
|
+
lastBurstTs: 0,
|
|
135
95
|
};
|
|
136
96
|
|
|
137
|
-
let blockedUntil
|
|
97
|
+
let blockedUntil = 0;
|
|
98
|
+
let _cfgErrorUntil = 0;
|
|
99
|
+
let _ioMobile = null;
|
|
138
100
|
|
|
139
|
-
const ts
|
|
140
|
-
const isBlocked
|
|
141
|
-
const isMobile
|
|
142
|
-
const
|
|
143
|
-
const
|
|
101
|
+
const ts = () => Date.now();
|
|
102
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
103
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
104
|
+
const _BOOL_TRUE = new Set([true, 'true', 1, '1', 'on']);
|
|
105
|
+
const normBool = v => _BOOL_TRUE.has(v);
|
|
106
|
+
const isFilled = n => !!(n?.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
144
107
|
|
|
145
108
|
function mutate(fn) {
|
|
146
109
|
S.mutGuard++;
|
|
@@ -151,10 +114,12 @@
|
|
|
151
114
|
|
|
152
115
|
async function fetchConfig() {
|
|
153
116
|
if (S.cfg) return S.cfg;
|
|
117
|
+
if (Date.now() < _cfgErrorUntil) return null;
|
|
154
118
|
try {
|
|
155
119
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
156
|
-
if (r.ok) S.cfg = await r.json();
|
|
157
|
-
|
|
120
|
+
if (r.ok) { S.cfg = await r.json(); }
|
|
121
|
+
else { _cfgErrorUntil = Date.now() + 10_000; }
|
|
122
|
+
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
158
123
|
return S.cfg;
|
|
159
124
|
}
|
|
160
125
|
|
|
@@ -214,53 +179,37 @@
|
|
|
214
179
|
|
|
215
180
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
216
181
|
|
|
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
182
|
function wrapIsLive(wrap) {
|
|
222
183
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
223
184
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
224
185
|
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
186
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
-
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
229
187
|
const colonIdx = key.indexOf(':');
|
|
230
188
|
const klass = key.slice(0, colonIdx);
|
|
231
189
|
const anchorId = key.slice(colonIdx + 1);
|
|
232
190
|
const cfg = KIND[klass];
|
|
233
191
|
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
192
|
const parent = wrap.parentElement;
|
|
237
193
|
if (parent) {
|
|
238
194
|
for (const sib of parent.children) {
|
|
239
195
|
if (sib === wrap) continue;
|
|
240
196
|
try {
|
|
241
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
197
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
242
198
|
return sib.isConnected;
|
|
243
|
-
}
|
|
244
199
|
} catch (_) {}
|
|
245
200
|
}
|
|
246
201
|
}
|
|
247
|
-
// Dernier recours : querySelector global
|
|
248
202
|
try {
|
|
249
203
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
250
204
|
return !!(found?.isConnected);
|
|
251
205
|
} catch (_) { return false; }
|
|
252
206
|
}
|
|
253
207
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
208
|
+
const adjacentWrap = el =>
|
|
209
|
+
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
257
210
|
|
|
258
211
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
259
212
|
|
|
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
213
|
function stableId(klass, el) {
|
|
265
214
|
const attr = KIND[klass]?.anchorAttr;
|
|
266
215
|
if (attr) {
|
|
@@ -279,18 +228,15 @@
|
|
|
279
228
|
|
|
280
229
|
function findWrap(key) {
|
|
281
230
|
const w = S.wrapByKey.get(key);
|
|
282
|
-
return
|
|
231
|
+
return w?.isConnected ? w : null;
|
|
283
232
|
}
|
|
284
233
|
|
|
285
234
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
286
235
|
|
|
287
|
-
/**
|
|
288
|
-
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
-
* ou null si tous les ids sont montés.
|
|
290
|
-
*/
|
|
291
236
|
function pickId(poolKey) {
|
|
292
237
|
const pool = S.pools[poolKey];
|
|
293
238
|
if (!pool.length) return null;
|
|
239
|
+
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
294
240
|
for (let t = 0; t < pool.length; t++) {
|
|
295
241
|
const i = S.cursors[poolKey] % pool.length;
|
|
296
242
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -300,77 +246,13 @@
|
|
|
300
246
|
return null;
|
|
301
247
|
}
|
|
302
248
|
|
|
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
|
-
|
|
365
249
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
366
250
|
|
|
367
251
|
function makeWrap(id, klass, key) {
|
|
368
252
|
const w = document.createElement('div');
|
|
369
253
|
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');
|
|
254
|
+
w.setAttribute(A_ANCHOR, key);
|
|
255
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
374
256
|
w.style.cssText = 'width:100%;display:block;';
|
|
375
257
|
const ph = document.createElement('div');
|
|
376
258
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -388,6 +270,7 @@
|
|
|
388
270
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
389
271
|
S.mountedIds.add(id);
|
|
390
272
|
S.wrapByKey.set(key, w);
|
|
273
|
+
scheduleEmptyCheck(id);
|
|
391
274
|
return w;
|
|
392
275
|
}
|
|
393
276
|
|
|
@@ -403,44 +286,8 @@
|
|
|
403
286
|
} catch (_) {}
|
|
404
287
|
}
|
|
405
288
|
|
|
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
289
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
439
290
|
|
|
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
291
|
function ordinal(klass, el) {
|
|
445
292
|
const attr = KIND[klass]?.ordinalAttr;
|
|
446
293
|
if (attr) {
|
|
@@ -459,27 +306,18 @@
|
|
|
459
306
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
460
307
|
if (!items.length) return 0;
|
|
461
308
|
let inserted = 0;
|
|
462
|
-
|
|
463
309
|
for (const el of items) {
|
|
464
310
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
465
311
|
if (!el?.isConnected) continue;
|
|
466
|
-
|
|
467
312
|
const ord = ordinal(klass, el);
|
|
468
313
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
469
314
|
if (adjacentWrap(el)) continue;
|
|
470
|
-
|
|
471
315
|
const key = anchorKey(klass, el);
|
|
472
316
|
if (findWrap(key)) continue;
|
|
473
|
-
|
|
474
317
|
const id = pickId(poolKey);
|
|
475
|
-
if (id)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
} else {
|
|
479
|
-
const recycled = recycleAndMove(klass, el, key);
|
|
480
|
-
if (!recycled) break;
|
|
481
|
-
inserted++;
|
|
482
|
-
}
|
|
318
|
+
if (!id) break;
|
|
319
|
+
const w = insertAfter(el, id, klass, key);
|
|
320
|
+
if (w) { observePh(id); inserted++; }
|
|
483
321
|
}
|
|
484
322
|
return inserted;
|
|
485
323
|
}
|
|
@@ -487,7 +325,10 @@
|
|
|
487
325
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
488
326
|
|
|
489
327
|
function getIO() {
|
|
490
|
-
|
|
328
|
+
const mobile = isMobile();
|
|
329
|
+
if (S.io && _ioMobile === mobile) return S.io;
|
|
330
|
+
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
331
|
+
_ioMobile = mobile;
|
|
491
332
|
try {
|
|
492
333
|
S.io = new IntersectionObserver(entries => {
|
|
493
334
|
for (const e of entries) {
|
|
@@ -496,7 +337,7 @@
|
|
|
496
337
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
497
338
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
498
339
|
}
|
|
499
|
-
}, { root: null, rootMargin:
|
|
340
|
+
}, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
500
341
|
} catch (_) { S.io = null; }
|
|
501
342
|
return S.io;
|
|
502
343
|
}
|
|
@@ -516,6 +357,18 @@
|
|
|
516
357
|
startShow(id);
|
|
517
358
|
}
|
|
518
359
|
|
|
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
|
+
|
|
519
372
|
function drainQueue() {
|
|
520
373
|
if (isBlocked()) return;
|
|
521
374
|
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
@@ -536,48 +389,39 @@
|
|
|
536
389
|
drainQueue();
|
|
537
390
|
};
|
|
538
391
|
const timer = setTimeout(release, 7000);
|
|
539
|
-
|
|
540
392
|
requestAnimationFrame(() => {
|
|
541
393
|
try {
|
|
542
394
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
543
395
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
544
396
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
-
|
|
546
397
|
const t = ts();
|
|
547
398
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
548
399
|
S.lastShow.set(id, t);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
400
|
+
// Marquer le wrap avec le timestamp de fill pour bloquer le recyclage
|
|
401
|
+
// Si la pub charge après is-empty, retirer le collapse
|
|
402
|
+
try { document.getElementById(`${PH_PREFIX}${id}`)?.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
|
|
552
403
|
window.ezstandalone = window.ezstandalone || {};
|
|
553
404
|
const ez = window.ezstandalone;
|
|
554
405
|
const doShow = () => {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
setTimeout(() => {
|
|
406
|
+
// define + displayMore remettent le slot en état actif si Ezoic l'a oublié
|
|
407
|
+
try { ez.define([id]); } catch (_) {}
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
try { ez.displayMore([id]); } catch (_) {}
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
try { ez.showAds(id); } catch (_) {}
|
|
412
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
413
|
+
}, 200);
|
|
414
|
+
}, 200);
|
|
558
415
|
};
|
|
559
416
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
560
417
|
} catch (_) { clearTimeout(timer); release(); }
|
|
561
418
|
});
|
|
562
419
|
}
|
|
563
420
|
|
|
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
421
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
577
422
|
//
|
|
578
|
-
// Intercepte ez.showAds() pour
|
|
579
|
-
//
|
|
580
|
-
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
423
|
+
// Intercepte ez.showAds() pour ignorer les appels pendant blockedUntil
|
|
424
|
+
// et filtrer les ids dont le placeholder n'est pas connecté au DOM.
|
|
581
425
|
|
|
582
426
|
function patchShowAds() {
|
|
583
427
|
const apply = () => {
|
|
@@ -613,37 +457,19 @@
|
|
|
613
457
|
async function runCore() {
|
|
614
458
|
if (isBlocked()) return 0;
|
|
615
459
|
patchShowAds();
|
|
616
|
-
|
|
617
460
|
const cfg = await fetchConfig();
|
|
618
461
|
if (!cfg || cfg.excluded) return 0;
|
|
619
462
|
initPools(cfg);
|
|
620
|
-
|
|
621
463
|
const kind = getKind();
|
|
622
464
|
if (kind === 'other') return 0;
|
|
623
|
-
|
|
624
465
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
625
466
|
if (!normBool(cfgEnable)) return 0;
|
|
626
467
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
627
468
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
628
469
|
};
|
|
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
|
-
);
|
|
470
|
+
if (kind === 'topic') return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
471
|
+
if (kind === 'categoryTopics') return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
472
|
+
return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
647
473
|
}
|
|
648
474
|
|
|
649
475
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -667,11 +493,9 @@
|
|
|
667
493
|
S.lastBurstTs = t;
|
|
668
494
|
S.pageKey = pageKey();
|
|
669
495
|
S.burstDeadline = t + 2000;
|
|
670
|
-
|
|
671
496
|
if (S.burstActive) return;
|
|
672
497
|
S.burstActive = true;
|
|
673
498
|
S.burstCount = 0;
|
|
674
|
-
|
|
675
499
|
const step = () => {
|
|
676
500
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
677
501
|
S.burstActive = false; return;
|
|
@@ -690,21 +514,26 @@
|
|
|
690
514
|
function cleanup() {
|
|
691
515
|
blockedUntil = ts() + 1500;
|
|
692
516
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
693
|
-
S.cfg
|
|
694
|
-
|
|
695
|
-
S.
|
|
696
|
-
S.
|
|
517
|
+
S.cfg = null;
|
|
518
|
+
_cfgErrorUntil = 0;
|
|
519
|
+
S.poolsReady = false;
|
|
520
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
521
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
697
522
|
S.mountedIds.clear();
|
|
698
523
|
S.lastShow.clear();
|
|
699
524
|
S.wrapByKey.clear();
|
|
700
|
-
S.inflight
|
|
701
|
-
S.pending
|
|
525
|
+
S.inflight = 0;
|
|
526
|
+
S.pending = [];
|
|
702
527
|
S.pendingSet.clear();
|
|
703
|
-
S.burstActive
|
|
704
|
-
S.runQueued
|
|
528
|
+
S.burstActive = false;
|
|
529
|
+
S.runQueued = false;
|
|
530
|
+
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
531
|
+
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
532
|
+
// exister en permanence — la déconnecter pendant la navigation cause
|
|
533
|
+
// des erreurs CMP postMessage et la disparition des pubs.
|
|
705
534
|
}
|
|
706
535
|
|
|
707
|
-
// ── MutationObserver
|
|
536
|
+
// ── MutationObserver DOM ───────────────────────────────────────────────────
|
|
708
537
|
|
|
709
538
|
function ensureDomObserver() {
|
|
710
539
|
if (S.domObs) return;
|
|
@@ -714,9 +543,8 @@
|
|
|
714
543
|
for (const m of muts) {
|
|
715
544
|
for (const n of m.addedNodes) {
|
|
716
545
|
if (n.nodeType !== 1) continue;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
546
|
+
if (allSel.some(s => { try { return n.matches(s); } catch (_) { return false; } }) ||
|
|
547
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch (_) { return false; } })) {
|
|
720
548
|
requestBurst(); return;
|
|
721
549
|
}
|
|
722
550
|
}
|
|
@@ -736,6 +564,7 @@
|
|
|
736
564
|
'cannot call refresh on the same page',
|
|
737
565
|
'no placeholders are currently defined in Refresh',
|
|
738
566
|
'Debugger iframe already exists',
|
|
567
|
+
'[CMP] Error in custom getTCData',
|
|
739
568
|
`with id ${PH_PREFIX}`,
|
|
740
569
|
];
|
|
741
570
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -758,9 +587,9 @@
|
|
|
758
587
|
(document.body || document.documentElement).appendChild(f);
|
|
759
588
|
};
|
|
760
589
|
inject();
|
|
761
|
-
if (!
|
|
762
|
-
|
|
763
|
-
|
|
590
|
+
if (!S.tcfObs) {
|
|
591
|
+
S.tcfObs = new MutationObserver(inject);
|
|
592
|
+
S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
764
593
|
}
|
|
765
594
|
} catch (_) {}
|
|
766
595
|
}
|
|
@@ -792,22 +621,19 @@
|
|
|
792
621
|
function bindNodeBB() {
|
|
793
622
|
const $ = window.jQuery;
|
|
794
623
|
if (!$) return;
|
|
795
|
-
|
|
796
624
|
$(window).off('.nbbEzoic');
|
|
797
625
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
798
626
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
799
627
|
S.pageKey = pageKey();
|
|
800
628
|
blockedUntil = 0;
|
|
801
|
-
muteConsole(); ensureTcfLocator();
|
|
629
|
+
muteConsole(); ensureTcfLocator();
|
|
802
630
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
803
631
|
});
|
|
804
|
-
|
|
805
632
|
const burstEvts = [
|
|
806
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
807
|
-
'action:categories.loaded',
|
|
633
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
634
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
808
635
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
809
636
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
-
|
|
811
637
|
try {
|
|
812
638
|
require(['hooks'], hooks => {
|
|
813
639
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -826,6 +652,11 @@
|
|
|
826
652
|
ticking = true;
|
|
827
653
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
828
654
|
}, { passive: true });
|
|
655
|
+
let resizeTimer = 0;
|
|
656
|
+
window.addEventListener('resize', () => {
|
|
657
|
+
clearTimeout(resizeTimer);
|
|
658
|
+
resizeTimer = setTimeout(getIO, 500);
|
|
659
|
+
}, { passive: true });
|
|
829
660
|
}
|
|
830
661
|
|
|
831
662
|
// ── 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;
|