nodebb-plugin-ezoic-infinite 1.7.67 → 1.7.68
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 +149 -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,92 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v69
|
|
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
|
-
* 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.
|
|
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
|
+
* v36 S.wrapByKey Map O(1). MutationObserver optimisé.
|
|
11
|
+
* v38 Pool épuisé → break propre (ez.refresh supprimé).
|
|
12
|
+
* v49 Fix normalizeExcludedGroups (JSON.parse tableau NodeBB).
|
|
13
|
+
* v51 fetchConfig backoff 10s. IO recrée au resize.
|
|
14
|
+
* v52 pruneOrphansBetween supprimé (NodeBB virtualise les topics).
|
|
15
|
+
* v56 scheduleEmptyCheck supprimé (collapse prématuré).
|
|
16
|
+
* v58 tcfObs survit aux navigations (iframe CMP permanente).
|
|
17
|
+
* v62 is-empty réintroduit, déclenché à l'insertion du wrap.
|
|
18
|
+
* v64 recycleAndMove supprimé (slots restaient en 'unused' après destroy).
|
|
19
|
+
* v67 define(allIds) au boot — Ezoic enregistre tous les slots en interne.
|
|
20
|
+
* v68 setIsSinglePageApplication(true) + newPage() à chaque navigation.
|
|
21
|
+
* v69 scheduleEmptyCheck déplacé dans startShow (après showAds, comme v50).
|
|
22
|
+
* IO_MARGIN réduit (800px/1200px) : évite que AMP charge une pub trop
|
|
23
|
+
* tôt et la retire immédiatement car déjà hors viewport au chargement.
|
|
24
|
+
* Nettoyage prod final : S.recycling orphelin supprimé, helpers Ezoic
|
|
25
|
+
* SPA extraits en fonctions dédiées, commentaires legacy retirés.
|
|
67
26
|
*/
|
|
68
27
|
(function nbbEzoicInfinite() {
|
|
69
28
|
'use strict';
|
|
70
29
|
|
|
71
30
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
72
31
|
|
|
73
|
-
const WRAP_CLASS
|
|
74
|
-
const PH_PREFIX
|
|
75
|
-
const A_ANCHOR
|
|
76
|
-
const A_WRAPID
|
|
77
|
-
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
|
-
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
32
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
33
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
34
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
35
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
79
36
|
|
|
80
|
-
const EMPTY_CHECK_MS =
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
|
|
37
|
+
const EMPTY_CHECK_MS = 5_000; // collapse wrap vide 5s après insertion
|
|
38
|
+
const MAX_INSERTS_RUN = 6; // insertions max par appel runCore
|
|
39
|
+
const MAX_INFLIGHT = 4; // showAds() simultanés max
|
|
40
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
41
|
+
const BURST_COOLDOWN_MS = 200; // délai min entre deux bursts
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
43
|
+
const IO_MARGIN_DESKTOP = '800px 0px 800px 0px';
|
|
44
|
+
const IO_MARGIN_MOBILE = '1200px 0px 1200px 0px';
|
|
90
45
|
|
|
91
46
|
const SEL = {
|
|
92
47
|
post: '[component="post"][data-pid]',
|
|
@@ -96,13 +51,10 @@
|
|
|
96
51
|
|
|
97
52
|
/**
|
|
98
53
|
* Table KIND — source de vérité par kindClass.
|
|
99
|
-
*
|
|
100
|
-
* sel sélecteur CSS complet des éléments cibles
|
|
54
|
+
* sel sélecteur CSS des éléments cibles
|
|
101
55
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
-
* (vide pour posts : le sélecteur commence par '[')
|
|
103
56
|
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
104
|
-
* ordinalAttr attribut 0-based pour
|
|
105
|
-
* null → fallback positionnel (catégories)
|
|
57
|
+
* ordinalAttr attribut 0-based pour calcul de l'intervalle (null = fallback positionnel)
|
|
106
58
|
*/
|
|
107
59
|
const KIND = {
|
|
108
60
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -113,34 +65,38 @@
|
|
|
113
65
|
// ── État global ────────────────────────────────────────────────────────────
|
|
114
66
|
|
|
115
67
|
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
|
-
|
|
68
|
+
pageKey: null,
|
|
69
|
+
cfg: null,
|
|
70
|
+
poolsReady: false,
|
|
71
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
72
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
73
|
+
mountedIds: new Set(),
|
|
74
|
+
lastShow: new Map(),
|
|
75
|
+
io: null,
|
|
76
|
+
domObs: null,
|
|
77
|
+
tcfObs: null, // survit aux navigations — ne jamais déconnecter
|
|
78
|
+
mutGuard: 0,
|
|
79
|
+
inflight: 0,
|
|
80
|
+
pending: [],
|
|
81
|
+
pendingSet: new Set(),
|
|
82
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
83
|
+
runQueued: false,
|
|
84
|
+
burstActive: false,
|
|
132
85
|
burstDeadline: 0,
|
|
133
|
-
burstCount:
|
|
134
|
-
lastBurstTs:
|
|
86
|
+
burstCount: 0,
|
|
87
|
+
lastBurstTs: 0,
|
|
135
88
|
};
|
|
136
89
|
|
|
137
|
-
let blockedUntil
|
|
90
|
+
let blockedUntil = 0;
|
|
91
|
+
let _cfgErrorUntil = 0;
|
|
92
|
+
let _ioMobile = null;
|
|
138
93
|
|
|
139
|
-
const ts
|
|
140
|
-
const isBlocked
|
|
141
|
-
const isMobile
|
|
142
|
-
const
|
|
143
|
-
const
|
|
94
|
+
const ts = () => Date.now();
|
|
95
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
96
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
97
|
+
const _BOOL_TRUE = new Set([true, 'true', 1, '1', 'on']);
|
|
98
|
+
const normBool = v => _BOOL_TRUE.has(v);
|
|
99
|
+
const isFilled = n => !!(n?.querySelector('iframe, ins, img, video, [data-google-container-id]'));
|
|
144
100
|
|
|
145
101
|
function mutate(fn) {
|
|
146
102
|
S.mutGuard++;
|
|
@@ -151,10 +107,12 @@
|
|
|
151
107
|
|
|
152
108
|
async function fetchConfig() {
|
|
153
109
|
if (S.cfg) return S.cfg;
|
|
110
|
+
if (Date.now() < _cfgErrorUntil) return null;
|
|
154
111
|
try {
|
|
155
112
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
156
|
-
if (r.ok) S.cfg = await r.json();
|
|
157
|
-
|
|
113
|
+
if (r.ok) { S.cfg = await r.json(); }
|
|
114
|
+
else { _cfgErrorUntil = Date.now() + 10_000; }
|
|
115
|
+
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
158
116
|
return S.cfg;
|
|
159
117
|
}
|
|
160
118
|
|
|
@@ -173,8 +131,26 @@
|
|
|
173
131
|
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
174
132
|
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
175
133
|
S.poolsReady = true;
|
|
134
|
+
// Déclarer tous les ids en une seule fois — requis pour que showAds()
|
|
135
|
+
// fonctionne sur des slots insérés dynamiquement (infinite scroll).
|
|
136
|
+
const allIds = [...S.pools.topics, ...S.pools.posts, ...S.pools.categories];
|
|
137
|
+
if (allIds.length) ezCmd(ez => ez.define(allIds));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Helpers Ezoic ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function ezCmd(fn) {
|
|
143
|
+
try {
|
|
144
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
145
|
+
const ez = window.ezstandalone;
|
|
146
|
+
const exec = () => { try { fn(ez); } catch (_) {} };
|
|
147
|
+
typeof ez.cmd?.push === 'function' ? ez.cmd.push(exec) : exec();
|
|
148
|
+
} catch (_) {}
|
|
176
149
|
}
|
|
177
150
|
|
|
151
|
+
function notifyEzoicSpa() { ezCmd(ez => ez.setIsSinglePageApplication?.(true)); }
|
|
152
|
+
function notifyEzoicNewPage() { ezCmd(ez => ez.newPage?.()); }
|
|
153
|
+
|
|
178
154
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
179
155
|
|
|
180
156
|
function pageKey() {
|
|
@@ -214,53 +190,37 @@
|
|
|
214
190
|
|
|
215
191
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
216
192
|
|
|
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
193
|
function wrapIsLive(wrap) {
|
|
222
194
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
223
195
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
224
196
|
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
197
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
-
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
229
198
|
const colonIdx = key.indexOf(':');
|
|
230
199
|
const klass = key.slice(0, colonIdx);
|
|
231
200
|
const anchorId = key.slice(colonIdx + 1);
|
|
232
201
|
const cfg = KIND[klass];
|
|
233
202
|
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
203
|
const parent = wrap.parentElement;
|
|
237
204
|
if (parent) {
|
|
238
205
|
for (const sib of parent.children) {
|
|
239
206
|
if (sib === wrap) continue;
|
|
240
207
|
try {
|
|
241
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
208
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
242
209
|
return sib.isConnected;
|
|
243
|
-
}
|
|
244
210
|
} catch (_) {}
|
|
245
211
|
}
|
|
246
212
|
}
|
|
247
|
-
// Dernier recours : querySelector global
|
|
248
213
|
try {
|
|
249
214
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
250
215
|
return !!(found?.isConnected);
|
|
251
216
|
} catch (_) { return false; }
|
|
252
217
|
}
|
|
253
218
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
219
|
+
const adjacentWrap = el =>
|
|
220
|
+
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
257
221
|
|
|
258
222
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
259
223
|
|
|
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
224
|
function stableId(klass, el) {
|
|
265
225
|
const attr = KIND[klass]?.anchorAttr;
|
|
266
226
|
if (attr) {
|
|
@@ -279,18 +239,15 @@
|
|
|
279
239
|
|
|
280
240
|
function findWrap(key) {
|
|
281
241
|
const w = S.wrapByKey.get(key);
|
|
282
|
-
return
|
|
242
|
+
return w?.isConnected ? w : null;
|
|
283
243
|
}
|
|
284
244
|
|
|
285
245
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
286
246
|
|
|
287
|
-
/**
|
|
288
|
-
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
-
* ou null si tous les ids sont montés.
|
|
290
|
-
*/
|
|
291
247
|
function pickId(poolKey) {
|
|
292
248
|
const pool = S.pools[poolKey];
|
|
293
249
|
if (!pool.length) return null;
|
|
250
|
+
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
294
251
|
for (let t = 0; t < pool.length; t++) {
|
|
295
252
|
const i = S.cursors[poolKey] % pool.length;
|
|
296
253
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -300,77 +257,13 @@
|
|
|
300
257
|
return null;
|
|
301
258
|
}
|
|
302
259
|
|
|
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
260
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
366
261
|
|
|
367
262
|
function makeWrap(id, klass, key) {
|
|
368
263
|
const w = document.createElement('div');
|
|
369
264
|
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');
|
|
265
|
+
w.setAttribute(A_ANCHOR, key);
|
|
266
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
374
267
|
w.style.cssText = 'width:100%;display:block;';
|
|
375
268
|
const ph = document.createElement('div');
|
|
376
269
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -403,44 +296,8 @@
|
|
|
403
296
|
} catch (_) {}
|
|
404
297
|
}
|
|
405
298
|
|
|
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
299
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
439
300
|
|
|
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
301
|
function ordinal(klass, el) {
|
|
445
302
|
const attr = KIND[klass]?.ordinalAttr;
|
|
446
303
|
if (attr) {
|
|
@@ -459,27 +316,18 @@
|
|
|
459
316
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
460
317
|
if (!items.length) return 0;
|
|
461
318
|
let inserted = 0;
|
|
462
|
-
|
|
463
319
|
for (const el of items) {
|
|
464
320
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
465
321
|
if (!el?.isConnected) continue;
|
|
466
|
-
|
|
467
322
|
const ord = ordinal(klass, el);
|
|
468
323
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
469
324
|
if (adjacentWrap(el)) continue;
|
|
470
|
-
|
|
471
325
|
const key = anchorKey(klass, el);
|
|
472
326
|
if (findWrap(key)) continue;
|
|
473
|
-
|
|
474
327
|
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
|
-
}
|
|
328
|
+
if (!id) break;
|
|
329
|
+
const w = insertAfter(el, id, klass, key);
|
|
330
|
+
if (w) { observePh(id); inserted++; }
|
|
483
331
|
}
|
|
484
332
|
return inserted;
|
|
485
333
|
}
|
|
@@ -487,7 +335,10 @@
|
|
|
487
335
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
488
336
|
|
|
489
337
|
function getIO() {
|
|
490
|
-
|
|
338
|
+
const mobile = isMobile();
|
|
339
|
+
if (S.io && _ioMobile === mobile) return S.io;
|
|
340
|
+
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
341
|
+
_ioMobile = mobile;
|
|
491
342
|
try {
|
|
492
343
|
S.io = new IntersectionObserver(entries => {
|
|
493
344
|
for (const e of entries) {
|
|
@@ -496,7 +347,7 @@
|
|
|
496
347
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
497
348
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
498
349
|
}
|
|
499
|
-
}, { root: null, rootMargin:
|
|
350
|
+
}, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
500
351
|
} catch (_) { S.io = null; }
|
|
501
352
|
return S.io;
|
|
502
353
|
}
|
|
@@ -506,6 +357,17 @@
|
|
|
506
357
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
507
358
|
}
|
|
508
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
|
+
if (!isFilled(wrap)) wrap.classList.add('is-empty');
|
|
367
|
+
} catch (_) {}
|
|
368
|
+
}, EMPTY_CHECK_MS);
|
|
369
|
+
}
|
|
370
|
+
|
|
509
371
|
function enqueueShow(id) {
|
|
510
372
|
if (!id || isBlocked()) return;
|
|
511
373
|
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
@@ -536,48 +398,28 @@
|
|
|
536
398
|
drainQueue();
|
|
537
399
|
};
|
|
538
400
|
const timer = setTimeout(release, 7000);
|
|
539
|
-
|
|
540
401
|
requestAnimationFrame(() => {
|
|
541
402
|
try {
|
|
542
403
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
543
404
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
544
405
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
-
|
|
546
406
|
const t = ts();
|
|
547
407
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
548
408
|
S.lastShow.set(id, t);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
553
|
-
const ez = window.ezstandalone;
|
|
554
|
-
const doShow = () => {
|
|
409
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
|
|
410
|
+
ezCmd(ez => {
|
|
555
411
|
try { ez.showAds(id); } catch (_) {}
|
|
556
|
-
scheduleEmptyCheck(id
|
|
412
|
+
scheduleEmptyCheck(id);
|
|
557
413
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
558
|
-
};
|
|
559
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
414
|
+
});
|
|
560
415
|
} catch (_) { clearTimeout(timer); release(); }
|
|
561
416
|
});
|
|
562
417
|
}
|
|
563
418
|
|
|
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
419
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
577
420
|
//
|
|
578
|
-
// Intercepte ez.showAds() pour
|
|
579
|
-
//
|
|
580
|
-
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
421
|
+
// Intercepte ez.showAds() pour ignorer les appels pendant blockedUntil
|
|
422
|
+
// et filtrer les ids dont le placeholder n'est pas connecté au DOM.
|
|
581
423
|
|
|
582
424
|
function patchShowAds() {
|
|
583
425
|
const apply = () => {
|
|
@@ -613,37 +455,19 @@
|
|
|
613
455
|
async function runCore() {
|
|
614
456
|
if (isBlocked()) return 0;
|
|
615
457
|
patchShowAds();
|
|
616
|
-
|
|
617
458
|
const cfg = await fetchConfig();
|
|
618
459
|
if (!cfg || cfg.excluded) return 0;
|
|
619
460
|
initPools(cfg);
|
|
620
|
-
|
|
621
461
|
const kind = getKind();
|
|
622
462
|
if (kind === 'other') return 0;
|
|
623
|
-
|
|
624
463
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
625
464
|
if (!normBool(cfgEnable)) return 0;
|
|
626
465
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
627
466
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
628
467
|
};
|
|
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
|
-
);
|
|
468
|
+
if (kind === 'topic') return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
|
|
469
|
+
if (kind === 'categoryTopics') return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
|
|
470
|
+
return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
|
|
647
471
|
}
|
|
648
472
|
|
|
649
473
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -667,11 +491,9 @@
|
|
|
667
491
|
S.lastBurstTs = t;
|
|
668
492
|
S.pageKey = pageKey();
|
|
669
493
|
S.burstDeadline = t + 2000;
|
|
670
|
-
|
|
671
494
|
if (S.burstActive) return;
|
|
672
495
|
S.burstActive = true;
|
|
673
496
|
S.burstCount = 0;
|
|
674
|
-
|
|
675
497
|
const step = () => {
|
|
676
498
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
677
499
|
S.burstActive = false; return;
|
|
@@ -691,20 +513,25 @@
|
|
|
691
513
|
blockedUntil = ts() + 1500;
|
|
692
514
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
693
515
|
S.cfg = null;
|
|
516
|
+
_cfgErrorUntil = 0;
|
|
694
517
|
S.poolsReady = false;
|
|
695
518
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
696
|
-
S.cursors = { topics: 0,
|
|
519
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
697
520
|
S.mountedIds.clear();
|
|
698
521
|
S.lastShow.clear();
|
|
699
522
|
S.wrapByKey.clear();
|
|
700
|
-
S.inflight
|
|
701
|
-
S.pending
|
|
523
|
+
S.inflight = 0;
|
|
524
|
+
S.pending = [];
|
|
702
525
|
S.pendingSet.clear();
|
|
703
|
-
S.burstActive
|
|
704
|
-
S.runQueued
|
|
526
|
+
S.burstActive = false;
|
|
527
|
+
S.runQueued = false;
|
|
528
|
+
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
529
|
+
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
530
|
+
// rester en vie pendant toute la session — la déconnecter entre deux
|
|
531
|
+
// navigations cause des erreurs CMP postMessage.
|
|
705
532
|
}
|
|
706
533
|
|
|
707
|
-
// ── MutationObserver
|
|
534
|
+
// ── MutationObserver DOM ───────────────────────────────────────────────────
|
|
708
535
|
|
|
709
536
|
function ensureDomObserver() {
|
|
710
537
|
if (S.domObs) return;
|
|
@@ -714,9 +541,8 @@
|
|
|
714
541
|
for (const m of muts) {
|
|
715
542
|
for (const n of m.addedNodes) {
|
|
716
543
|
if (n.nodeType !== 1) continue;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
544
|
+
if (allSel.some(s => { try { return n.matches(s); } catch (_) { return false; } }) ||
|
|
545
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch (_) { return false; } })) {
|
|
720
546
|
requestBurst(); return;
|
|
721
547
|
}
|
|
722
548
|
}
|
|
@@ -736,6 +562,7 @@
|
|
|
736
562
|
'cannot call refresh on the same page',
|
|
737
563
|
'no placeholders are currently defined in Refresh',
|
|
738
564
|
'Debugger iframe already exists',
|
|
565
|
+
'[CMP] Error in custom getTCData',
|
|
739
566
|
`with id ${PH_PREFIX}`,
|
|
740
567
|
];
|
|
741
568
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -758,9 +585,9 @@
|
|
|
758
585
|
(document.body || document.documentElement).appendChild(f);
|
|
759
586
|
};
|
|
760
587
|
inject();
|
|
761
|
-
if (!
|
|
762
|
-
|
|
763
|
-
|
|
588
|
+
if (!S.tcfObs) {
|
|
589
|
+
S.tcfObs = new MutationObserver(inject);
|
|
590
|
+
S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
764
591
|
}
|
|
765
592
|
} catch (_) {}
|
|
766
593
|
}
|
|
@@ -792,22 +619,21 @@
|
|
|
792
619
|
function bindNodeBB() {
|
|
793
620
|
const $ = window.jQuery;
|
|
794
621
|
if (!$) return;
|
|
795
|
-
|
|
796
622
|
$(window).off('.nbbEzoic');
|
|
797
623
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
798
624
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
799
625
|
S.pageKey = pageKey();
|
|
800
626
|
blockedUntil = 0;
|
|
801
|
-
muteConsole();
|
|
627
|
+
muteConsole();
|
|
628
|
+
ensureTcfLocator();
|
|
629
|
+
notifyEzoicNewPage();
|
|
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 ───────────────────────────────────────────────────────────────────
|
|
@@ -834,6 +665,7 @@
|
|
|
834
665
|
muteConsole();
|
|
835
666
|
ensureTcfLocator();
|
|
836
667
|
warmNetwork();
|
|
668
|
+
notifyEzoicSpa(); // NodeBB est une SPA — Ezoic ajuste son cycle interne
|
|
837
669
|
patchShowAds();
|
|
838
670
|
getIO();
|
|
839
671
|
ensureDomObserver();
|
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;
|