nodebb-plugin-ezoic-infinite 1.7.65 → 1.7.67
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 +317 -146
- 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,42 +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
|
-
*
|
|
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.
|
|
23
67
|
*/
|
|
24
68
|
(function nbbEzoicInfinite() {
|
|
25
69
|
'use strict';
|
|
26
70
|
|
|
27
71
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
28
72
|
|
|
29
|
-
const WRAP_CLASS
|
|
30
|
-
const PH_PREFIX
|
|
31
|
-
const A_ANCHOR
|
|
32
|
-
const A_WRAPID
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
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
|
|
40
88
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
41
89
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
42
90
|
|
|
@@ -48,10 +96,13 @@
|
|
|
48
96
|
|
|
49
97
|
/**
|
|
50
98
|
* Table KIND — source de vérité par kindClass.
|
|
51
|
-
*
|
|
99
|
+
*
|
|
100
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
52
101
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
53
103
|
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
54
|
-
* ordinalAttr attribut 0-based pour calcul de l'intervalle
|
|
104
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
105
|
+
* null → fallback positionnel (catégories)
|
|
55
106
|
*/
|
|
56
107
|
const KIND = {
|
|
57
108
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -62,38 +113,34 @@
|
|
|
62
113
|
// ── État global ────────────────────────────────────────────────────────────
|
|
63
114
|
|
|
64
115
|
const S = {
|
|
65
|
-
pageKey:
|
|
66
|
-
cfg:
|
|
67
|
-
poolsReady:
|
|
68
|
-
pools:
|
|
69
|
-
cursors:
|
|
70
|
-
mountedIds:
|
|
71
|
-
lastShow:
|
|
72
|
-
io:
|
|
73
|
-
domObs:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
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,
|
|
82
132
|
burstDeadline: 0,
|
|
83
|
-
burstCount:
|
|
84
|
-
lastBurstTs:
|
|
133
|
+
burstCount: 0,
|
|
134
|
+
lastBurstTs: 0,
|
|
85
135
|
};
|
|
86
136
|
|
|
87
|
-
let blockedUntil
|
|
88
|
-
let _cfgErrorUntil = 0;
|
|
89
|
-
let _ioMobile = null;
|
|
137
|
+
let blockedUntil = 0;
|
|
90
138
|
|
|
91
|
-
const ts
|
|
92
|
-
const isBlocked
|
|
93
|
-
const isMobile
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
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]'));
|
|
97
144
|
|
|
98
145
|
function mutate(fn) {
|
|
99
146
|
S.mutGuard++;
|
|
@@ -104,12 +151,10 @@
|
|
|
104
151
|
|
|
105
152
|
async function fetchConfig() {
|
|
106
153
|
if (S.cfg) return S.cfg;
|
|
107
|
-
if (Date.now() < _cfgErrorUntil) return null;
|
|
108
154
|
try {
|
|
109
155
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
110
|
-
if (r.ok)
|
|
111
|
-
|
|
112
|
-
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
156
|
+
if (r.ok) S.cfg = await r.json();
|
|
157
|
+
} catch (_) {}
|
|
113
158
|
return S.cfg;
|
|
114
159
|
}
|
|
115
160
|
|
|
@@ -128,26 +173,8 @@
|
|
|
128
173
|
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
129
174
|
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
130
175
|
S.poolsReady = true;
|
|
131
|
-
// Déclarer tous les ids en une seule fois — requis pour que showAds()
|
|
132
|
-
// fonctionne sur des slots insérés dynamiquement (infinite scroll).
|
|
133
|
-
const allIds = [...S.pools.topics, ...S.pools.posts, ...S.pools.categories];
|
|
134
|
-
if (allIds.length) ezCmd(ez => ez.define(allIds));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Helpers Ezoic ──────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
function ezCmd(fn) {
|
|
140
|
-
try {
|
|
141
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
142
|
-
const ez = window.ezstandalone;
|
|
143
|
-
const exec = () => { try { fn(ez); } catch (_) {} };
|
|
144
|
-
typeof ez.cmd?.push === 'function' ? ez.cmd.push(exec) : exec();
|
|
145
|
-
} catch (_) {}
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
function notifyEzoicSpa() { ezCmd(ez => ez.setIsSinglePageApplication?.(true)); }
|
|
149
|
-
function notifyEzoicNewPage() { ezCmd(ez => ez.newPage?.()); }
|
|
150
|
-
|
|
151
178
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
152
179
|
|
|
153
180
|
function pageKey() {
|
|
@@ -187,37 +214,53 @@
|
|
|
187
214
|
|
|
188
215
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
189
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
|
+
*/
|
|
190
221
|
function wrapIsLive(wrap) {
|
|
191
222
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
192
223
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
193
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).
|
|
194
227
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
195
229
|
const colonIdx = key.indexOf(':');
|
|
196
230
|
const klass = key.slice(0, colonIdx);
|
|
197
231
|
const anchorId = key.slice(colonIdx + 1);
|
|
198
232
|
const cfg = KIND[klass];
|
|
199
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.
|
|
200
236
|
const parent = wrap.parentElement;
|
|
201
237
|
if (parent) {
|
|
202
238
|
for (const sib of parent.children) {
|
|
203
239
|
if (sib === wrap) continue;
|
|
204
240
|
try {
|
|
205
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
241
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
206
242
|
return sib.isConnected;
|
|
243
|
+
}
|
|
207
244
|
} catch (_) {}
|
|
208
245
|
}
|
|
209
246
|
}
|
|
247
|
+
// Dernier recours : querySelector global
|
|
210
248
|
try {
|
|
211
249
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
212
250
|
return !!(found?.isConnected);
|
|
213
251
|
} catch (_) { return false; }
|
|
214
252
|
}
|
|
215
253
|
|
|
216
|
-
|
|
217
|
-
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
254
|
+
function adjacentWrap(el) {
|
|
255
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
256
|
+
}
|
|
218
257
|
|
|
219
258
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
220
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
|
+
*/
|
|
221
264
|
function stableId(klass, el) {
|
|
222
265
|
const attr = KIND[klass]?.anchorAttr;
|
|
223
266
|
if (attr) {
|
|
@@ -236,15 +279,18 @@
|
|
|
236
279
|
|
|
237
280
|
function findWrap(key) {
|
|
238
281
|
const w = S.wrapByKey.get(key);
|
|
239
|
-
return w?.isConnected ? w : null;
|
|
282
|
+
return (w?.isConnected) ? w : null;
|
|
240
283
|
}
|
|
241
284
|
|
|
242
285
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
243
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
+
* ou null si tous les ids sont montés.
|
|
290
|
+
*/
|
|
244
291
|
function pickId(poolKey) {
|
|
245
292
|
const pool = S.pools[poolKey];
|
|
246
293
|
if (!pool.length) return null;
|
|
247
|
-
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
248
294
|
for (let t = 0; t < pool.length; t++) {
|
|
249
295
|
const i = S.cursors[poolKey] % pool.length;
|
|
250
296
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -254,13 +300,77 @@
|
|
|
254
300
|
return null;
|
|
255
301
|
}
|
|
256
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
|
+
|
|
257
365
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
258
366
|
|
|
259
367
|
function makeWrap(id, klass, key) {
|
|
260
368
|
const w = document.createElement('div');
|
|
261
369
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
262
|
-
w.setAttribute(A_ANCHOR,
|
|
263
|
-
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');
|
|
264
374
|
w.style.cssText = 'width:100%;display:block;';
|
|
265
375
|
const ph = document.createElement('div');
|
|
266
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -278,7 +388,6 @@
|
|
|
278
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
279
389
|
S.mountedIds.add(id);
|
|
280
390
|
S.wrapByKey.set(key, w);
|
|
281
|
-
scheduleEmptyCheck(id);
|
|
282
391
|
return w;
|
|
283
392
|
}
|
|
284
393
|
|
|
@@ -294,8 +403,44 @@
|
|
|
294
403
|
} catch (_) {}
|
|
295
404
|
}
|
|
296
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
|
+
|
|
297
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
298
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
|
+
*/
|
|
299
444
|
function ordinal(klass, el) {
|
|
300
445
|
const attr = KIND[klass]?.ordinalAttr;
|
|
301
446
|
if (attr) {
|
|
@@ -314,18 +459,27 @@
|
|
|
314
459
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
315
460
|
if (!items.length) return 0;
|
|
316
461
|
let inserted = 0;
|
|
462
|
+
|
|
317
463
|
for (const el of items) {
|
|
318
464
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
319
465
|
if (!el?.isConnected) continue;
|
|
466
|
+
|
|
320
467
|
const ord = ordinal(klass, el);
|
|
321
468
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
322
469
|
if (adjacentWrap(el)) continue;
|
|
470
|
+
|
|
323
471
|
const key = anchorKey(klass, el);
|
|
324
472
|
if (findWrap(key)) continue;
|
|
473
|
+
|
|
325
474
|
const id = pickId(poolKey);
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
}
|
|
329
483
|
}
|
|
330
484
|
return inserted;
|
|
331
485
|
}
|
|
@@ -333,10 +487,7 @@
|
|
|
333
487
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
334
488
|
|
|
335
489
|
function getIO() {
|
|
336
|
-
|
|
337
|
-
if (S.io && _ioMobile === mobile) return S.io;
|
|
338
|
-
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
339
|
-
_ioMobile = mobile;
|
|
490
|
+
if (S.io) return S.io;
|
|
340
491
|
try {
|
|
341
492
|
S.io = new IntersectionObserver(entries => {
|
|
342
493
|
for (const e of entries) {
|
|
@@ -345,7 +496,7 @@
|
|
|
345
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
346
497
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
347
498
|
}
|
|
348
|
-
}, { root: null, rootMargin:
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
349
500
|
} catch (_) { S.io = null; }
|
|
350
501
|
return S.io;
|
|
351
502
|
}
|
|
@@ -355,17 +506,6 @@
|
|
|
355
506
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
356
507
|
}
|
|
357
508
|
|
|
358
|
-
function scheduleEmptyCheck(id) {
|
|
359
|
-
setTimeout(() => {
|
|
360
|
-
try {
|
|
361
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
362
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
363
|
-
if (!wrap || !ph?.isConnected) return;
|
|
364
|
-
if (!isFilled(wrap)) wrap.classList.add('is-empty');
|
|
365
|
-
} catch (_) {}
|
|
366
|
-
}, EMPTY_CHECK_MS);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
509
|
function enqueueShow(id) {
|
|
370
510
|
if (!id || isBlocked()) return;
|
|
371
511
|
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
@@ -396,27 +536,48 @@
|
|
|
396
536
|
drainQueue();
|
|
397
537
|
};
|
|
398
538
|
const timer = setTimeout(release, 7000);
|
|
539
|
+
|
|
399
540
|
requestAnimationFrame(() => {
|
|
400
541
|
try {
|
|
401
542
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
402
543
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
403
544
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
+
|
|
404
546
|
const t = ts();
|
|
405
547
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
406
548
|
S.lastShow.set(id, t);
|
|
407
|
-
|
|
408
|
-
|
|
549
|
+
|
|
550
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
551
|
+
|
|
552
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
553
|
+
const ez = window.ezstandalone;
|
|
554
|
+
const doShow = () => {
|
|
409
555
|
try { ez.showAds(id); } catch (_) {}
|
|
556
|
+
scheduleEmptyCheck(id, t);
|
|
410
557
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
411
|
-
}
|
|
558
|
+
};
|
|
559
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
412
560
|
} catch (_) { clearTimeout(timer); release(); }
|
|
413
561
|
});
|
|
414
562
|
}
|
|
415
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
|
+
|
|
416
576
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
417
577
|
//
|
|
418
|
-
// Intercepte ez.showAds() pour
|
|
419
|
-
//
|
|
578
|
+
// Intercepte ez.showAds() pour :
|
|
579
|
+
// – ignorer les appels pendant blockedUntil
|
|
580
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
420
581
|
|
|
421
582
|
function patchShowAds() {
|
|
422
583
|
const apply = () => {
|
|
@@ -452,19 +613,37 @@
|
|
|
452
613
|
async function runCore() {
|
|
453
614
|
if (isBlocked()) return 0;
|
|
454
615
|
patchShowAds();
|
|
616
|
+
|
|
455
617
|
const cfg = await fetchConfig();
|
|
456
618
|
if (!cfg || cfg.excluded) return 0;
|
|
457
619
|
initPools(cfg);
|
|
620
|
+
|
|
458
621
|
const kind = getKind();
|
|
459
622
|
if (kind === 'other') return 0;
|
|
623
|
+
|
|
460
624
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
461
625
|
if (!normBool(cfgEnable)) return 0;
|
|
462
626
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
463
627
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
464
628
|
};
|
|
465
|
-
|
|
466
|
-
if (kind === '
|
|
467
|
-
|
|
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
|
+
);
|
|
468
647
|
}
|
|
469
648
|
|
|
470
649
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -488,9 +667,11 @@
|
|
|
488
667
|
S.lastBurstTs = t;
|
|
489
668
|
S.pageKey = pageKey();
|
|
490
669
|
S.burstDeadline = t + 2000;
|
|
670
|
+
|
|
491
671
|
if (S.burstActive) return;
|
|
492
672
|
S.burstActive = true;
|
|
493
673
|
S.burstCount = 0;
|
|
674
|
+
|
|
494
675
|
const step = () => {
|
|
495
676
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
496
677
|
S.burstActive = false; return;
|
|
@@ -510,25 +691,20 @@
|
|
|
510
691
|
blockedUntil = ts() + 1500;
|
|
511
692
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
512
693
|
S.cfg = null;
|
|
513
|
-
_cfgErrorUntil = 0;
|
|
514
694
|
S.poolsReady = false;
|
|
515
695
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
516
|
-
S.cursors = { topics: 0,
|
|
696
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
517
697
|
S.mountedIds.clear();
|
|
518
698
|
S.lastShow.clear();
|
|
519
699
|
S.wrapByKey.clear();
|
|
520
|
-
S.inflight
|
|
521
|
-
S.pending
|
|
700
|
+
S.inflight = 0;
|
|
701
|
+
S.pending = [];
|
|
522
702
|
S.pendingSet.clear();
|
|
523
|
-
S.burstActive
|
|
524
|
-
S.runQueued
|
|
525
|
-
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
526
|
-
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
527
|
-
// rester en vie pendant toute la session — la déconnecter entre deux
|
|
528
|
-
// navigations cause des erreurs CMP postMessage.
|
|
703
|
+
S.burstActive = false;
|
|
704
|
+
S.runQueued = false;
|
|
529
705
|
}
|
|
530
706
|
|
|
531
|
-
// ── MutationObserver
|
|
707
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
532
708
|
|
|
533
709
|
function ensureDomObserver() {
|
|
534
710
|
if (S.domObs) return;
|
|
@@ -538,8 +714,9 @@
|
|
|
538
714
|
for (const m of muts) {
|
|
539
715
|
for (const n of m.addedNodes) {
|
|
540
716
|
if (n.nodeType !== 1) continue;
|
|
541
|
-
|
|
542
|
-
|
|
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;} })) {
|
|
543
720
|
requestBurst(); return;
|
|
544
721
|
}
|
|
545
722
|
}
|
|
@@ -559,7 +736,6 @@
|
|
|
559
736
|
'cannot call refresh on the same page',
|
|
560
737
|
'no placeholders are currently defined in Refresh',
|
|
561
738
|
'Debugger iframe already exists',
|
|
562
|
-
'[CMP] Error in custom getTCData',
|
|
563
739
|
`with id ${PH_PREFIX}`,
|
|
564
740
|
];
|
|
565
741
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -582,9 +758,9 @@
|
|
|
582
758
|
(document.body || document.documentElement).appendChild(f);
|
|
583
759
|
};
|
|
584
760
|
inject();
|
|
585
|
-
if (!
|
|
586
|
-
|
|
587
|
-
|
|
761
|
+
if (!window.__nbbTcfObs) {
|
|
762
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
763
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
588
764
|
}
|
|
589
765
|
} catch (_) {}
|
|
590
766
|
}
|
|
@@ -616,21 +792,22 @@
|
|
|
616
792
|
function bindNodeBB() {
|
|
617
793
|
const $ = window.jQuery;
|
|
618
794
|
if (!$) return;
|
|
795
|
+
|
|
619
796
|
$(window).off('.nbbEzoic');
|
|
620
797
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
621
798
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
622
799
|
S.pageKey = pageKey();
|
|
623
800
|
blockedUntil = 0;
|
|
624
|
-
muteConsole();
|
|
625
|
-
ensureTcfLocator();
|
|
626
|
-
notifyEzoicNewPage();
|
|
801
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
627
802
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
628
803
|
});
|
|
804
|
+
|
|
629
805
|
const burstEvts = [
|
|
630
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
631
|
-
'action:categories.loaded',
|
|
806
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
807
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
632
808
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
633
809
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
+
|
|
634
811
|
try {
|
|
635
812
|
require(['hooks'], hooks => {
|
|
636
813
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -649,11 +826,6 @@
|
|
|
649
826
|
ticking = true;
|
|
650
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
651
828
|
}, { passive: true });
|
|
652
|
-
let resizeTimer = 0;
|
|
653
|
-
window.addEventListener('resize', () => {
|
|
654
|
-
clearTimeout(resizeTimer);
|
|
655
|
-
resizeTimer = setTimeout(getIO, 500);
|
|
656
|
-
}, { passive: true });
|
|
657
829
|
}
|
|
658
830
|
|
|
659
831
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
@@ -662,7 +834,6 @@
|
|
|
662
834
|
muteConsole();
|
|
663
835
|
ensureTcfLocator();
|
|
664
836
|
warmNetwork();
|
|
665
|
-
notifyEzoicSpa(); // NodeBB est une SPA — Ezoic ajuste son cycle interne
|
|
666
837
|
patchShowAds();
|
|
667
838
|
getIO();
|
|
668
839
|
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 (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;
|