nodebb-plugin-ezoic-infinite 1.7.81 → 1.7.83
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 +374 -191
- package/public/style.css +19 -14
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,49 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
3
|
*
|
|
4
|
-
* Historique
|
|
5
|
-
*
|
|
6
|
-
* v18 Ancrage stable par data-pid / data-index.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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.
|
|
26
67
|
*/
|
|
27
68
|
(function nbbEzoicInfinite() {
|
|
28
69
|
'use strict';
|
|
29
70
|
|
|
30
71
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
31
72
|
|
|
32
|
-
const WRAP_CLASS
|
|
33
|
-
const PH_PREFIX
|
|
34
|
-
const A_ANCHOR
|
|
35
|
-
const A_WRAPID
|
|
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
|
|
36
79
|
|
|
37
|
-
const EMPTY_CHECK_MS =
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
|
42
86
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const IO_MARGIN_MOBILE = '0px 0px 1500px 0px';
|
|
87
|
+
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
88
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
89
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
47
90
|
|
|
48
91
|
const SEL = {
|
|
49
92
|
post: '[component="post"][data-pid]',
|
|
@@ -53,10 +96,13 @@
|
|
|
53
96
|
|
|
54
97
|
/**
|
|
55
98
|
* Table KIND — source de vérité par kindClass.
|
|
56
|
-
*
|
|
99
|
+
*
|
|
100
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
57
101
|
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
58
103
|
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
59
|
-
* 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)
|
|
60
106
|
*/
|
|
61
107
|
const KIND = {
|
|
62
108
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
@@ -67,43 +113,73 @@
|
|
|
67
113
|
// ── État global ────────────────────────────────────────────────────────────
|
|
68
114
|
|
|
69
115
|
const S = {
|
|
70
|
-
pageKey:
|
|
71
|
-
cfg:
|
|
72
|
-
poolsReady:
|
|
73
|
-
pools:
|
|
74
|
-
cursors:
|
|
75
|
-
mountedIds:
|
|
76
|
-
lastShow:
|
|
77
|
-
io:
|
|
78
|
-
domObs:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
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,
|
|
87
132
|
burstDeadline: 0,
|
|
88
|
-
burstCount:
|
|
89
|
-
lastBurstTs:
|
|
133
|
+
burstCount: 0,
|
|
134
|
+
lastBurstTs: 0,
|
|
135
|
+
emptyChecks: new Map(), // id -> timeout ids[]
|
|
90
136
|
};
|
|
91
137
|
|
|
92
|
-
let blockedUntil
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
let blockedUntil = 0;
|
|
139
|
+
|
|
140
|
+
const ts = () => Date.now();
|
|
141
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
142
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
143
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
144
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
145
|
+
|
|
146
|
+
function clearEmptyChecks(id) {
|
|
147
|
+
const timers = S.emptyChecks.get(id);
|
|
148
|
+
if (!timers) return;
|
|
149
|
+
for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
|
|
150
|
+
S.emptyChecks.delete(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function queueEmptyCheck(id, timerId) {
|
|
154
|
+
const arr = S.emptyChecks.get(id) || [];
|
|
155
|
+
arr.push(timerId);
|
|
156
|
+
S.emptyChecks.set(id, arr);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function uncollapseIfFilled(ph) {
|
|
160
|
+
try {
|
|
161
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
162
|
+
if (!wrap) return false;
|
|
163
|
+
if (!isFilled(ph)) return false;
|
|
164
|
+
wrap.classList.remove('is-empty');
|
|
165
|
+
return true;
|
|
166
|
+
} catch (_) { return false; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function watchPlaceholderFill(ph) {
|
|
170
|
+
if (!ph || ph.__nbbFillObs) return;
|
|
171
|
+
try {
|
|
172
|
+
const obs = new MutationObserver(() => { if (uncollapseIfFilled(ph)) return; });
|
|
173
|
+
obs.observe(ph, { childList: true, subtree: true, attributes: true });
|
|
174
|
+
ph.__nbbFillObs = obs;
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
uncollapseIfFilled(ph);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function unwatchPlaceholderFill(ph) {
|
|
180
|
+
try { ph?.__nbbFillObs?.disconnect?.(); } catch (_) {}
|
|
181
|
+
try { if (ph) delete ph.__nbbFillObs; } catch (_) {}
|
|
182
|
+
}
|
|
107
183
|
|
|
108
184
|
function mutate(fn) {
|
|
109
185
|
S.mutGuard++;
|
|
@@ -114,12 +190,10 @@
|
|
|
114
190
|
|
|
115
191
|
async function fetchConfig() {
|
|
116
192
|
if (S.cfg) return S.cfg;
|
|
117
|
-
if (Date.now() < _cfgErrorUntil) return null;
|
|
118
193
|
try {
|
|
119
194
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
120
|
-
if (r.ok)
|
|
121
|
-
|
|
122
|
-
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
195
|
+
if (r.ok) S.cfg = await r.json();
|
|
196
|
+
} catch (_) {}
|
|
123
197
|
return S.cfg;
|
|
124
198
|
}
|
|
125
199
|
|
|
@@ -138,26 +212,8 @@
|
|
|
138
212
|
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
139
213
|
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
140
214
|
S.poolsReady = true;
|
|
141
|
-
// Déclarer tous les ids en une seule fois — requis pour que showAds()
|
|
142
|
-
// fonctionne sur des slots insérés dynamiquement (infinite scroll).
|
|
143
|
-
const allIds = [...S.pools.topics, ...S.pools.posts, ...S.pools.categories];
|
|
144
|
-
if (allIds.length) ezCmd(ez => ez.define(allIds));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Helpers Ezoic ──────────────────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
function ezCmd(fn) {
|
|
150
|
-
try {
|
|
151
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
152
|
-
const ez = window.ezstandalone;
|
|
153
|
-
const exec = () => { try { fn(ez); } catch (_) {} };
|
|
154
|
-
typeof ez.cmd?.push === 'function' ? ez.cmd.push(exec) : exec();
|
|
155
|
-
} catch (_) {}
|
|
156
215
|
}
|
|
157
216
|
|
|
158
|
-
function notifyEzoicSpa() { ezCmd(ez => ez.setIsSinglePageApplication?.(true)); }
|
|
159
|
-
function notifyEzoicNewPage() { ezCmd(ez => ez.newPage?.()); }
|
|
160
|
-
|
|
161
217
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
162
218
|
|
|
163
219
|
function pageKey() {
|
|
@@ -197,37 +253,53 @@
|
|
|
197
253
|
|
|
198
254
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
199
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
258
|
+
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
259
|
+
*/
|
|
200
260
|
function wrapIsLive(wrap) {
|
|
201
261
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
202
262
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
203
263
|
if (!key) return false;
|
|
264
|
+
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
265
|
+
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
204
266
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
267
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
205
268
|
const colonIdx = key.indexOf(':');
|
|
206
269
|
const klass = key.slice(0, colonIdx);
|
|
207
270
|
const anchorId = key.slice(colonIdx + 1);
|
|
208
271
|
const cfg = KIND[klass];
|
|
209
272
|
if (!cfg) return false;
|
|
273
|
+
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
274
|
+
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
210
275
|
const parent = wrap.parentElement;
|
|
211
276
|
if (parent) {
|
|
212
277
|
for (const sib of parent.children) {
|
|
213
278
|
if (sib === wrap) continue;
|
|
214
279
|
try {
|
|
215
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
280
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
216
281
|
return sib.isConnected;
|
|
282
|
+
}
|
|
217
283
|
} catch (_) {}
|
|
218
284
|
}
|
|
219
285
|
}
|
|
286
|
+
// Dernier recours : querySelector global
|
|
220
287
|
try {
|
|
221
288
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
222
289
|
return !!(found?.isConnected);
|
|
223
290
|
} catch (_) { return false; }
|
|
224
291
|
}
|
|
225
292
|
|
|
226
|
-
|
|
227
|
-
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
293
|
+
function adjacentWrap(el) {
|
|
294
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
295
|
+
}
|
|
228
296
|
|
|
229
297
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
230
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
301
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
302
|
+
*/
|
|
231
303
|
function stableId(klass, el) {
|
|
232
304
|
const attr = KIND[klass]?.anchorAttr;
|
|
233
305
|
if (attr) {
|
|
@@ -246,15 +318,18 @@
|
|
|
246
318
|
|
|
247
319
|
function findWrap(key) {
|
|
248
320
|
const w = S.wrapByKey.get(key);
|
|
249
|
-
return w?.isConnected ? w : null;
|
|
321
|
+
return (w?.isConnected) ? w : null;
|
|
250
322
|
}
|
|
251
323
|
|
|
252
324
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
253
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
328
|
+
* ou null si tous les ids sont montés.
|
|
329
|
+
*/
|
|
254
330
|
function pickId(poolKey) {
|
|
255
331
|
const pool = S.pools[poolKey];
|
|
256
332
|
if (!pool.length) return null;
|
|
257
|
-
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
258
333
|
for (let t = 0; t < pool.length; t++) {
|
|
259
334
|
const i = S.cursors[poolKey] % pool.length;
|
|
260
335
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -264,13 +339,78 @@
|
|
|
264
339
|
return null;
|
|
265
340
|
}
|
|
266
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
344
|
+
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
345
|
+
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
346
|
+
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
347
|
+
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
348
|
+
*/
|
|
349
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
350
|
+
const ez = window.ezstandalone;
|
|
351
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
352
|
+
typeof ez?.define !== 'function' ||
|
|
353
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
354
|
+
|
|
355
|
+
const vh = window.innerHeight || 800;
|
|
356
|
+
// Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
|
|
357
|
+
// après pour neutraliser l'IO — plus de showAds parasite possible.
|
|
358
|
+
const threshold = -vh;
|
|
359
|
+
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
360
|
+
let bestFilled = null, bestFilledBottom = Infinity;
|
|
361
|
+
|
|
362
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
363
|
+
try {
|
|
364
|
+
const rect = wrap.getBoundingClientRect();
|
|
365
|
+
if (rect.bottom > threshold) return;
|
|
366
|
+
if (!isFilled(wrap)) {
|
|
367
|
+
if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
|
|
368
|
+
} else {
|
|
369
|
+
if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
|
|
370
|
+
}
|
|
371
|
+
} catch (_) {}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const best = bestEmpty ?? bestFilled;
|
|
375
|
+
if (!best) return null;
|
|
376
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
377
|
+
if (!Number.isFinite(id)) return null;
|
|
378
|
+
|
|
379
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
380
|
+
// Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
|
|
381
|
+
// parasite si le nœud était encore dans la zone IO_MARGIN.
|
|
382
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
383
|
+
clearEmptyChecks(id);
|
|
384
|
+
mutate(() => {
|
|
385
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
386
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
387
|
+
best.setAttribute(A_SHOWN, '0');
|
|
388
|
+
best.classList.remove('is-empty');
|
|
389
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
390
|
+
if (ph) { ph.innerHTML = ''; watchPlaceholderFill(ph); }
|
|
391
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
392
|
+
});
|
|
393
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
394
|
+
S.wrapByKey.set(newKey, best);
|
|
395
|
+
|
|
396
|
+
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
397
|
+
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
398
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
399
|
+
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
400
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
401
|
+
|
|
402
|
+
return { id, wrap: best };
|
|
403
|
+
}
|
|
404
|
+
|
|
267
405
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
268
406
|
|
|
269
407
|
function makeWrap(id, klass, key) {
|
|
270
408
|
const w = document.createElement('div');
|
|
271
409
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
272
|
-
w.setAttribute(A_ANCHOR,
|
|
273
|
-
w.setAttribute(A_WRAPID,
|
|
410
|
+
w.setAttribute(A_ANCHOR, key);
|
|
411
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
412
|
+
w.setAttribute(A_CREATED, String(ts()));
|
|
413
|
+
w.setAttribute(A_SHOWN, '0');
|
|
274
414
|
w.style.cssText = 'width:100%;display:block;';
|
|
275
415
|
const ph = document.createElement('div');
|
|
276
416
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -279,30 +419,7 @@
|
|
|
279
419
|
return w;
|
|
280
420
|
}
|
|
281
421
|
|
|
282
|
-
function
|
|
283
|
-
// Phase 1 : attend qu'une vraie pub apparaisse (iframe/ins).
|
|
284
|
-
// Phase 2 : si la pub disparaît (AMP lazy unload) → réobserve le placeholder
|
|
285
|
-
// pour relancer showAds quand il repasse dans le viewport.
|
|
286
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
287
|
-
if (!ph) return;
|
|
288
|
-
let filled = false;
|
|
289
|
-
const obs = new MutationObserver(() => {
|
|
290
|
-
const hasPub = !!(ph.querySelector('iframe, ins'));
|
|
291
|
-
if (!filled && hasPub) {
|
|
292
|
-
filled = true;
|
|
293
|
-
wrap.setAttribute('data-ez-filled', '1');
|
|
294
|
-
wrap.classList.remove('is-empty');
|
|
295
|
-
} else if (filled && !hasPub) {
|
|
296
|
-
// Pub disparue → retirer data-ez-filled et réobserver pour reload
|
|
297
|
-
wrap.removeAttribute('data-ez-filled');
|
|
298
|
-
filled = false;
|
|
299
|
-
try { getIO()?.observe(ph); } catch (_) {}
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
obs.observe(ph, { childList: true, subtree: true });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function insertAfter(el, id, klass, key) {
|
|
422
|
+
function insertAfter(el, id, klass, key) {
|
|
306
423
|
if (!el?.insertAdjacentElement) return null;
|
|
307
424
|
if (findWrap(key)) return null;
|
|
308
425
|
if (S.mountedIds.has(id)) return null;
|
|
@@ -311,24 +428,60 @@
|
|
|
311
428
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
312
429
|
S.mountedIds.add(id);
|
|
313
430
|
S.wrapByKey.set(key, w);
|
|
314
|
-
|
|
431
|
+
try { watchPlaceholderFill(w.querySelector(`#${PH_PREFIX}${id}`)); } catch (_) {}
|
|
315
432
|
return w;
|
|
316
433
|
}
|
|
317
434
|
|
|
318
435
|
function dropWrap(w) {
|
|
319
436
|
try {
|
|
320
437
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
321
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
438
|
+
if (ph instanceof Element) { S.io?.unobserve(ph); unwatchPlaceholderFill(ph); }
|
|
322
439
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
323
|
-
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
440
|
+
if (Number.isFinite(id)) { S.mountedIds.delete(id); clearEmptyChecks(id); }
|
|
324
441
|
const key = w.getAttribute(A_ANCHOR);
|
|
325
442
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
326
443
|
w.remove();
|
|
327
444
|
} catch (_) {}
|
|
328
445
|
}
|
|
329
446
|
|
|
447
|
+
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
448
|
+
//
|
|
449
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
450
|
+
//
|
|
451
|
+
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
452
|
+
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
453
|
+
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
454
|
+
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
455
|
+
// liste après un long scroll et bloquent les nouvelles injections.
|
|
456
|
+
//
|
|
457
|
+
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
458
|
+
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
459
|
+
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
460
|
+
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
461
|
+
|
|
462
|
+
function pruneOrphansBetween() {
|
|
463
|
+
const klass = 'ezoic-ad-between';
|
|
464
|
+
const cfg = KIND[klass];
|
|
465
|
+
|
|
466
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
467
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
468
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
469
|
+
|
|
470
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
471
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
472
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
473
|
+
|
|
474
|
+
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
475
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
330
479
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
331
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
483
|
+
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
484
|
+
*/
|
|
332
485
|
function ordinal(klass, el) {
|
|
333
486
|
const attr = KIND[klass]?.ordinalAttr;
|
|
334
487
|
if (attr) {
|
|
@@ -347,18 +500,27 @@
|
|
|
347
500
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
348
501
|
if (!items.length) return 0;
|
|
349
502
|
let inserted = 0;
|
|
503
|
+
|
|
350
504
|
for (const el of items) {
|
|
351
505
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
352
506
|
if (!el?.isConnected) continue;
|
|
507
|
+
|
|
353
508
|
const ord = ordinal(klass, el);
|
|
354
509
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
355
510
|
if (adjacentWrap(el)) continue;
|
|
511
|
+
|
|
356
512
|
const key = anchorKey(klass, el);
|
|
357
513
|
if (findWrap(key)) continue;
|
|
514
|
+
|
|
358
515
|
const id = pickId(poolKey);
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
516
|
+
if (id) {
|
|
517
|
+
const w = insertAfter(el, id, klass, key);
|
|
518
|
+
if (w) { observePh(id); inserted++; }
|
|
519
|
+
} else {
|
|
520
|
+
const recycled = recycleAndMove(klass, el, key);
|
|
521
|
+
if (!recycled) break;
|
|
522
|
+
inserted++;
|
|
523
|
+
}
|
|
362
524
|
}
|
|
363
525
|
return inserted;
|
|
364
526
|
}
|
|
@@ -366,10 +528,7 @@
|
|
|
366
528
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
367
529
|
|
|
368
530
|
function getIO() {
|
|
369
|
-
|
|
370
|
-
if (S.io && _ioMobile === mobile) return S.io;
|
|
371
|
-
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
372
|
-
_ioMobile = mobile;
|
|
531
|
+
if (S.io) return S.io;
|
|
373
532
|
try {
|
|
374
533
|
S.io = new IntersectionObserver(entries => {
|
|
375
534
|
for (const e of entries) {
|
|
@@ -378,30 +537,16 @@
|
|
|
378
537
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
379
538
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
380
539
|
}
|
|
381
|
-
}, { root: null, rootMargin:
|
|
540
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
382
541
|
} catch (_) { S.io = null; }
|
|
383
542
|
return S.io;
|
|
384
543
|
}
|
|
385
544
|
|
|
386
545
|
function observePh(id) {
|
|
387
546
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
388
|
-
if (ph?.isConnected)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
function scheduleEmptyCheck(id) {
|
|
392
|
-
setTimeout(() => {
|
|
393
|
-
try {
|
|
394
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
395
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
396
|
-
if (!wrap || !ph?.isConnected) return;
|
|
397
|
-
// Ne pas colapser si watchFill a détecté une vraie pub (iframe/ins)
|
|
398
|
-
if (wrap.getAttribute('data-ez-filled')) return;
|
|
399
|
-
// Ne pas colapser si une iframe est présente (pub AMP cross-origin)
|
|
400
|
-
if (wrap.querySelector('iframe, ins')) return;
|
|
401
|
-
// Pas de pub détectée → collapse
|
|
402
|
-
wrap.classList.add('is-empty');
|
|
403
|
-
} catch (_) {}
|
|
404
|
-
}, EMPTY_CHECK_MS);
|
|
547
|
+
if (!ph?.isConnected) return;
|
|
548
|
+
watchPlaceholderFill(ph);
|
|
549
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
405
550
|
}
|
|
406
551
|
|
|
407
552
|
function enqueueShow(id) {
|
|
@@ -434,30 +579,54 @@
|
|
|
434
579
|
drainQueue();
|
|
435
580
|
};
|
|
436
581
|
const timer = setTimeout(release, 7000);
|
|
582
|
+
|
|
437
583
|
requestAnimationFrame(() => {
|
|
438
584
|
try {
|
|
439
585
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
440
586
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
441
|
-
if (!ph?.isConnected) { clearTimeout(timer); return release(); }
|
|
442
|
-
|
|
443
|
-
|
|
587
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
588
|
+
clearEmptyChecks(id);
|
|
589
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
|
|
590
|
+
|
|
444
591
|
const t = ts();
|
|
445
592
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
446
593
|
S.lastShow.set(id, t);
|
|
447
|
-
|
|
448
|
-
|
|
594
|
+
|
|
595
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
596
|
+
|
|
597
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
598
|
+
const ez = window.ezstandalone;
|
|
599
|
+
const doShow = () => {
|
|
449
600
|
try { ez.showAds(id); } catch (_) {}
|
|
450
|
-
scheduleEmptyCheck(id);
|
|
601
|
+
scheduleEmptyCheck(id, t);
|
|
451
602
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
452
|
-
}
|
|
603
|
+
};
|
|
604
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
453
605
|
} catch (_) { clearTimeout(timer); release(); }
|
|
454
606
|
});
|
|
455
607
|
}
|
|
456
608
|
|
|
609
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
610
|
+
clearEmptyChecks(id);
|
|
611
|
+
const delays = [EMPTY_CHECK_MS, EMPTY_CHECK_MS + 5000, EMPTY_CHECK_MS + 15000];
|
|
612
|
+
const runCheck = () => {
|
|
613
|
+
try {
|
|
614
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
615
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
616
|
+
if (!wrap || !ph?.isConnected) return;
|
|
617
|
+
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
618
|
+
if (uncollapseIfFilled(ph)) return;
|
|
619
|
+
wrap.classList.add('is-empty');
|
|
620
|
+
} catch (_) {}
|
|
621
|
+
};
|
|
622
|
+
for (const d of delays) queueEmptyCheck(id, setTimeout(runCheck, d));
|
|
623
|
+
}
|
|
624
|
+
|
|
457
625
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
458
626
|
//
|
|
459
|
-
// Intercepte ez.showAds() pour
|
|
460
|
-
//
|
|
627
|
+
// Intercepte ez.showAds() pour :
|
|
628
|
+
// – ignorer les appels pendant blockedUntil
|
|
629
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
461
630
|
|
|
462
631
|
function patchShowAds() {
|
|
463
632
|
const apply = () => {
|
|
@@ -493,19 +662,37 @@
|
|
|
493
662
|
async function runCore() {
|
|
494
663
|
if (isBlocked()) return 0;
|
|
495
664
|
patchShowAds();
|
|
665
|
+
|
|
496
666
|
const cfg = await fetchConfig();
|
|
497
667
|
if (!cfg || cfg.excluded) return 0;
|
|
498
668
|
initPools(cfg);
|
|
669
|
+
|
|
499
670
|
const kind = getKind();
|
|
500
671
|
if (kind === 'other') return 0;
|
|
672
|
+
|
|
501
673
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
502
674
|
if (!normBool(cfgEnable)) return 0;
|
|
503
675
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
504
676
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
505
677
|
};
|
|
506
|
-
|
|
507
|
-
if (kind === '
|
|
508
|
-
|
|
678
|
+
|
|
679
|
+
if (kind === 'topic') return exec(
|
|
680
|
+
'ezoic-ad-message', getPosts,
|
|
681
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
if (kind === 'categoryTopics') {
|
|
685
|
+
pruneOrphansBetween();
|
|
686
|
+
return exec(
|
|
687
|
+
'ezoic-ad-between', getTopics,
|
|
688
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return exec(
|
|
693
|
+
'ezoic-ad-categories', getCategories,
|
|
694
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
695
|
+
);
|
|
509
696
|
}
|
|
510
697
|
|
|
511
698
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -529,9 +716,11 @@
|
|
|
529
716
|
S.lastBurstTs = t;
|
|
530
717
|
S.pageKey = pageKey();
|
|
531
718
|
S.burstDeadline = t + 2000;
|
|
719
|
+
|
|
532
720
|
if (S.burstActive) return;
|
|
533
721
|
S.burstActive = true;
|
|
534
722
|
S.burstCount = 0;
|
|
723
|
+
|
|
535
724
|
const step = () => {
|
|
536
725
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
537
726
|
S.burstActive = false; return;
|
|
@@ -551,25 +740,22 @@
|
|
|
551
740
|
blockedUntil = ts() + 1500;
|
|
552
741
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
553
742
|
S.cfg = null;
|
|
554
|
-
_cfgErrorUntil = 0;
|
|
555
743
|
S.poolsReady = false;
|
|
556
744
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
557
|
-
S.cursors = { topics: 0,
|
|
745
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
558
746
|
S.mountedIds.clear();
|
|
559
747
|
S.lastShow.clear();
|
|
560
748
|
S.wrapByKey.clear();
|
|
561
|
-
S.inflight
|
|
562
|
-
S.pending
|
|
749
|
+
S.inflight = 0;
|
|
750
|
+
S.pending = [];
|
|
563
751
|
S.pendingSet.clear();
|
|
564
|
-
S.
|
|
565
|
-
S.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// rester en vie pendant toute la session — la déconnecter entre deux
|
|
569
|
-
// navigations cause des erreurs CMP postMessage.
|
|
752
|
+
for (const timers of S.emptyChecks.values()) for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
|
|
753
|
+
S.emptyChecks.clear();
|
|
754
|
+
S.burstActive = false;
|
|
755
|
+
S.runQueued = false;
|
|
570
756
|
}
|
|
571
757
|
|
|
572
|
-
// ── MutationObserver
|
|
758
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
573
759
|
|
|
574
760
|
function ensureDomObserver() {
|
|
575
761
|
if (S.domObs) return;
|
|
@@ -579,8 +765,9 @@
|
|
|
579
765
|
for (const m of muts) {
|
|
580
766
|
for (const n of m.addedNodes) {
|
|
581
767
|
if (n.nodeType !== 1) continue;
|
|
582
|
-
|
|
583
|
-
|
|
768
|
+
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
769
|
+
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
770
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
584
771
|
requestBurst(); return;
|
|
585
772
|
}
|
|
586
773
|
}
|
|
@@ -600,7 +787,6 @@
|
|
|
600
787
|
'cannot call refresh on the same page',
|
|
601
788
|
'no placeholders are currently defined in Refresh',
|
|
602
789
|
'Debugger iframe already exists',
|
|
603
|
-
'[CMP] Error in custom getTCData',
|
|
604
790
|
`with id ${PH_PREFIX}`,
|
|
605
791
|
];
|
|
606
792
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -623,9 +809,9 @@
|
|
|
623
809
|
(document.body || document.documentElement).appendChild(f);
|
|
624
810
|
};
|
|
625
811
|
inject();
|
|
626
|
-
if (!
|
|
627
|
-
|
|
628
|
-
|
|
812
|
+
if (!window.__nbbTcfObs) {
|
|
813
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
814
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
629
815
|
}
|
|
630
816
|
} catch (_) {}
|
|
631
817
|
}
|
|
@@ -657,20 +843,22 @@
|
|
|
657
843
|
function bindNodeBB() {
|
|
658
844
|
const $ = window.jQuery;
|
|
659
845
|
if (!$) return;
|
|
846
|
+
|
|
660
847
|
$(window).off('.nbbEzoic');
|
|
661
848
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
662
849
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
663
850
|
S.pageKey = pageKey();
|
|
664
851
|
blockedUntil = 0;
|
|
665
|
-
muteConsole();
|
|
666
|
-
ensureTcfLocator();
|
|
852
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
667
853
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
668
854
|
});
|
|
855
|
+
|
|
669
856
|
const burstEvts = [
|
|
670
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
671
|
-
'action:categories.loaded',
|
|
857
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
858
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
672
859
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
673
860
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
861
|
+
|
|
674
862
|
try {
|
|
675
863
|
require(['hooks'], hooks => {
|
|
676
864
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -689,11 +877,6 @@
|
|
|
689
877
|
ticking = true;
|
|
690
878
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
691
879
|
}, { passive: true });
|
|
692
|
-
let resizeTimer = 0;
|
|
693
|
-
window.addEventListener('resize', () => {
|
|
694
|
-
clearTimeout(resizeTimer);
|
|
695
|
-
resizeTimer = setTimeout(getIO, 500);
|
|
696
|
-
}, { passive: true });
|
|
697
880
|
}
|
|
698
881
|
|
|
699
882
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
package/public/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — style.css (
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — style.css (v20)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* ── Wrapper ──────────────────────────────────────────────────────────────── */
|
|
@@ -56,23 +56,19 @@
|
|
|
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
|
-
display:
|
|
65
|
+
display: block !important;
|
|
66
|
+
height: 1px !important;
|
|
67
|
+
min-height: 1px !important;
|
|
68
|
+
max-height: 1px !important;
|
|
69
|
+
margin: 0 !important;
|
|
70
|
+
padding: 0 !important;
|
|
71
|
+
overflow: hidden !important;
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
|
|
@@ -80,3 +76,12 @@
|
|
|
80
76
|
margin: 0 !important;
|
|
81
77
|
padding: 0 !important;
|
|
82
78
|
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/* Filet de sécurité : si Ezoic a rempli le wrap, annuler le collapse même si .is-empty est resté */
|
|
82
|
+
.nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
|
|
83
|
+
height: auto !important;
|
|
84
|
+
min-height: 1px !important;
|
|
85
|
+
max-height: none !important;
|
|
86
|
+
overflow: visible !important;
|
|
87
|
+
}
|