nodebb-plugin-ezoic-infinite 1.7.80 → 1.7.82
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 +321 -194
- package/public/style.css +10 -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,34 @@
|
|
|
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,
|
|
90
135
|
};
|
|
91
136
|
|
|
92
|
-
let blockedUntil
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const _BOOL_TRUE = new Set([true, 'true', 1, '1', 'on']);
|
|
100
|
-
const normBool = v => _BOOL_TRUE.has(v);
|
|
101
|
-
// isFilled : exclut l'img reportline Ezoic (ezoicbwa.png) — faux positif
|
|
102
|
-
// data-ez-filled : marqué par watchFill() dès qu'Ezoic touche au placeholder
|
|
103
|
-
// (nécessaire pour les pubs AMP dont l'iframe est cross-origin et invisible)
|
|
104
|
-
const isFilled = n => !!(n?.closest?.('.nodebb-ezoic-wrap')?.getAttribute('data-ez-filled'))
|
|
105
|
-
|| !!(n?.querySelector('iframe, ins, video, [data-google-container-id]'))
|
|
106
|
-
|| !!(n?.querySelector('img:not([src*="ezoicbwa"]):not([src*="ezodn.com"])'));
|
|
137
|
+
let blockedUntil = 0;
|
|
138
|
+
|
|
139
|
+
const ts = () => Date.now();
|
|
140
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
141
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
142
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
143
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
107
144
|
|
|
108
145
|
function mutate(fn) {
|
|
109
146
|
S.mutGuard++;
|
|
@@ -114,12 +151,10 @@
|
|
|
114
151
|
|
|
115
152
|
async function fetchConfig() {
|
|
116
153
|
if (S.cfg) return S.cfg;
|
|
117
|
-
if (Date.now() < _cfgErrorUntil) return null;
|
|
118
154
|
try {
|
|
119
155
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
120
|
-
if (r.ok)
|
|
121
|
-
|
|
122
|
-
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
156
|
+
if (r.ok) S.cfg = await r.json();
|
|
157
|
+
} catch (_) {}
|
|
123
158
|
return S.cfg;
|
|
124
159
|
}
|
|
125
160
|
|
|
@@ -138,26 +173,8 @@
|
|
|
138
173
|
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
139
174
|
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
140
175
|
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
176
|
}
|
|
146
177
|
|
|
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
|
-
}
|
|
157
|
-
|
|
158
|
-
function notifyEzoicSpa() { ezCmd(ez => ez.setIsSinglePageApplication?.(true)); }
|
|
159
|
-
function notifyEzoicNewPage() { ezCmd(ez => ez.newPage?.()); }
|
|
160
|
-
|
|
161
178
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
162
179
|
|
|
163
180
|
function pageKey() {
|
|
@@ -197,37 +214,53 @@
|
|
|
197
214
|
|
|
198
215
|
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
199
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
|
+
*/
|
|
200
221
|
function wrapIsLive(wrap) {
|
|
201
222
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
202
223
|
const key = wrap.getAttribute(A_ANCHOR);
|
|
203
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).
|
|
204
227
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
205
229
|
const colonIdx = key.indexOf(':');
|
|
206
230
|
const klass = key.slice(0, colonIdx);
|
|
207
231
|
const anchorId = key.slice(colonIdx + 1);
|
|
208
232
|
const cfg = KIND[klass];
|
|
209
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.
|
|
210
236
|
const parent = wrap.parentElement;
|
|
211
237
|
if (parent) {
|
|
212
238
|
for (const sib of parent.children) {
|
|
213
239
|
if (sib === wrap) continue;
|
|
214
240
|
try {
|
|
215
|
-
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
|
|
241
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
216
242
|
return sib.isConnected;
|
|
243
|
+
}
|
|
217
244
|
} catch (_) {}
|
|
218
245
|
}
|
|
219
246
|
}
|
|
247
|
+
// Dernier recours : querySelector global
|
|
220
248
|
try {
|
|
221
249
|
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
222
250
|
return !!(found?.isConnected);
|
|
223
251
|
} catch (_) { return false; }
|
|
224
252
|
}
|
|
225
253
|
|
|
226
|
-
|
|
227
|
-
wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
254
|
+
function adjacentWrap(el) {
|
|
255
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
256
|
+
}
|
|
228
257
|
|
|
229
258
|
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
230
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
|
+
*/
|
|
231
264
|
function stableId(klass, el) {
|
|
232
265
|
const attr = KIND[klass]?.anchorAttr;
|
|
233
266
|
if (attr) {
|
|
@@ -246,15 +279,18 @@
|
|
|
246
279
|
|
|
247
280
|
function findWrap(key) {
|
|
248
281
|
const w = S.wrapByKey.get(key);
|
|
249
|
-
return w?.isConnected ? w : null;
|
|
282
|
+
return (w?.isConnected) ? w : null;
|
|
250
283
|
}
|
|
251
284
|
|
|
252
285
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
253
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
+
* ou null si tous les ids sont montés.
|
|
290
|
+
*/
|
|
254
291
|
function pickId(poolKey) {
|
|
255
292
|
const pool = S.pools[poolKey];
|
|
256
293
|
if (!pool.length) return null;
|
|
257
|
-
if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
|
|
258
294
|
for (let t = 0; t < pool.length; t++) {
|
|
259
295
|
const i = S.cursors[poolKey] % pool.length;
|
|
260
296
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -264,13 +300,77 @@
|
|
|
264
300
|
return null;
|
|
265
301
|
}
|
|
266
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
|
+
|
|
267
365
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
268
366
|
|
|
269
367
|
function makeWrap(id, klass, key) {
|
|
270
368
|
const w = document.createElement('div');
|
|
271
369
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
272
|
-
w.setAttribute(A_ANCHOR,
|
|
273
|
-
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');
|
|
274
374
|
w.style.cssText = 'width:100%;display:block;';
|
|
275
375
|
const ph = document.createElement('div');
|
|
276
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -279,30 +379,7 @@
|
|
|
279
379
|
return w;
|
|
280
380
|
}
|
|
281
381
|
|
|
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) {
|
|
382
|
+
function insertAfter(el, id, klass, key) {
|
|
306
383
|
if (!el?.insertAdjacentElement) return null;
|
|
307
384
|
if (findWrap(key)) return null;
|
|
308
385
|
if (S.mountedIds.has(id)) return null;
|
|
@@ -311,7 +388,6 @@
|
|
|
311
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
312
389
|
S.mountedIds.add(id);
|
|
313
390
|
S.wrapByKey.set(key, w);
|
|
314
|
-
watchFill(id, w);
|
|
315
391
|
return w;
|
|
316
392
|
}
|
|
317
393
|
|
|
@@ -327,8 +403,44 @@
|
|
|
327
403
|
} catch (_) {}
|
|
328
404
|
}
|
|
329
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
|
+
|
|
330
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
331
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
|
+
*/
|
|
332
444
|
function ordinal(klass, el) {
|
|
333
445
|
const attr = KIND[klass]?.ordinalAttr;
|
|
334
446
|
if (attr) {
|
|
@@ -347,18 +459,27 @@
|
|
|
347
459
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
348
460
|
if (!items.length) return 0;
|
|
349
461
|
let inserted = 0;
|
|
462
|
+
|
|
350
463
|
for (const el of items) {
|
|
351
464
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
352
465
|
if (!el?.isConnected) continue;
|
|
466
|
+
|
|
353
467
|
const ord = ordinal(klass, el);
|
|
354
468
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
355
469
|
if (adjacentWrap(el)) continue;
|
|
470
|
+
|
|
356
471
|
const key = anchorKey(klass, el);
|
|
357
472
|
if (findWrap(key)) continue;
|
|
473
|
+
|
|
358
474
|
const id = pickId(poolKey);
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
483
|
}
|
|
363
484
|
return inserted;
|
|
364
485
|
}
|
|
@@ -366,10 +487,7 @@
|
|
|
366
487
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
367
488
|
|
|
368
489
|
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;
|
|
490
|
+
if (S.io) return S.io;
|
|
373
491
|
try {
|
|
374
492
|
S.io = new IntersectionObserver(entries => {
|
|
375
493
|
for (const e of entries) {
|
|
@@ -378,7 +496,7 @@
|
|
|
378
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
379
497
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
380
498
|
}
|
|
381
|
-
}, { root: null, rootMargin:
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
382
500
|
} catch (_) { S.io = null; }
|
|
383
501
|
return S.io;
|
|
384
502
|
}
|
|
@@ -388,22 +506,6 @@
|
|
|
388
506
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
389
507
|
}
|
|
390
508
|
|
|
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);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
509
|
function enqueueShow(id) {
|
|
408
510
|
if (!id || isBlocked()) return;
|
|
409
511
|
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
@@ -434,35 +536,48 @@
|
|
|
434
536
|
drainQueue();
|
|
435
537
|
};
|
|
436
538
|
const timer = setTimeout(release, 7000);
|
|
539
|
+
|
|
437
540
|
requestAnimationFrame(() => {
|
|
438
541
|
try {
|
|
439
542
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
440
543
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
441
|
-
if (!ph?.isConnected) { clearTimeout(timer); return release(); }
|
|
442
|
-
|
|
443
|
-
if (isFilled(ph) || wrap?.getAttribute('data-ez-filled')) { clearTimeout(timer); return release(); }
|
|
544
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
545
|
+
|
|
444
546
|
const t = ts();
|
|
445
547
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
446
548
|
S.lastShow.set(id, t);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
scheduleEmptyCheck(id);
|
|
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 = () => {
|
|
555
|
+
try { ez.showAds(id); } catch (_) {}
|
|
556
|
+
scheduleEmptyCheck(id, t);
|
|
456
557
|
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
457
|
-
}
|
|
558
|
+
};
|
|
559
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
458
560
|
} catch (_) { clearTimeout(timer); release(); }
|
|
459
561
|
});
|
|
460
562
|
}
|
|
461
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
|
+
|
|
462
576
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
463
577
|
//
|
|
464
|
-
// Intercepte ez.showAds() pour
|
|
465
|
-
//
|
|
578
|
+
// Intercepte ez.showAds() pour :
|
|
579
|
+
// – ignorer les appels pendant blockedUntil
|
|
580
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
466
581
|
|
|
467
582
|
function patchShowAds() {
|
|
468
583
|
const apply = () => {
|
|
@@ -498,19 +613,37 @@
|
|
|
498
613
|
async function runCore() {
|
|
499
614
|
if (isBlocked()) return 0;
|
|
500
615
|
patchShowAds();
|
|
616
|
+
|
|
501
617
|
const cfg = await fetchConfig();
|
|
502
618
|
if (!cfg || cfg.excluded) return 0;
|
|
503
619
|
initPools(cfg);
|
|
620
|
+
|
|
504
621
|
const kind = getKind();
|
|
505
622
|
if (kind === 'other') return 0;
|
|
623
|
+
|
|
506
624
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
507
625
|
if (!normBool(cfgEnable)) return 0;
|
|
508
626
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
509
627
|
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
510
628
|
};
|
|
511
|
-
|
|
512
|
-
if (kind === '
|
|
513
|
-
|
|
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
|
+
);
|
|
514
647
|
}
|
|
515
648
|
|
|
516
649
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -534,9 +667,11 @@
|
|
|
534
667
|
S.lastBurstTs = t;
|
|
535
668
|
S.pageKey = pageKey();
|
|
536
669
|
S.burstDeadline = t + 2000;
|
|
670
|
+
|
|
537
671
|
if (S.burstActive) return;
|
|
538
672
|
S.burstActive = true;
|
|
539
673
|
S.burstCount = 0;
|
|
674
|
+
|
|
540
675
|
const step = () => {
|
|
541
676
|
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
542
677
|
S.burstActive = false; return;
|
|
@@ -556,25 +691,20 @@
|
|
|
556
691
|
blockedUntil = ts() + 1500;
|
|
557
692
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
558
693
|
S.cfg = null;
|
|
559
|
-
_cfgErrorUntil = 0;
|
|
560
694
|
S.poolsReady = false;
|
|
561
695
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
562
|
-
S.cursors = { topics: 0,
|
|
696
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
563
697
|
S.mountedIds.clear();
|
|
564
698
|
S.lastShow.clear();
|
|
565
699
|
S.wrapByKey.clear();
|
|
566
|
-
S.inflight
|
|
567
|
-
S.pending
|
|
700
|
+
S.inflight = 0;
|
|
701
|
+
S.pending = [];
|
|
568
702
|
S.pendingSet.clear();
|
|
569
|
-
S.burstActive
|
|
570
|
-
S.runQueued
|
|
571
|
-
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
572
|
-
// tcfObs intentionnellement préservé : l'iframe __tcfapiLocator doit
|
|
573
|
-
// rester en vie pendant toute la session — la déconnecter entre deux
|
|
574
|
-
// navigations cause des erreurs CMP postMessage.
|
|
703
|
+
S.burstActive = false;
|
|
704
|
+
S.runQueued = false;
|
|
575
705
|
}
|
|
576
706
|
|
|
577
|
-
// ── MutationObserver
|
|
707
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
578
708
|
|
|
579
709
|
function ensureDomObserver() {
|
|
580
710
|
if (S.domObs) return;
|
|
@@ -584,8 +714,9 @@
|
|
|
584
714
|
for (const m of muts) {
|
|
585
715
|
for (const n of m.addedNodes) {
|
|
586
716
|
if (n.nodeType !== 1) continue;
|
|
587
|
-
|
|
588
|
-
|
|
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;} })) {
|
|
589
720
|
requestBurst(); return;
|
|
590
721
|
}
|
|
591
722
|
}
|
|
@@ -605,7 +736,6 @@
|
|
|
605
736
|
'cannot call refresh on the same page',
|
|
606
737
|
'no placeholders are currently defined in Refresh',
|
|
607
738
|
'Debugger iframe already exists',
|
|
608
|
-
'[CMP] Error in custom getTCData',
|
|
609
739
|
`with id ${PH_PREFIX}`,
|
|
610
740
|
];
|
|
611
741
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
@@ -628,9 +758,9 @@
|
|
|
628
758
|
(document.body || document.documentElement).appendChild(f);
|
|
629
759
|
};
|
|
630
760
|
inject();
|
|
631
|
-
if (!
|
|
632
|
-
|
|
633
|
-
|
|
761
|
+
if (!window.__nbbTcfObs) {
|
|
762
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
763
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
634
764
|
}
|
|
635
765
|
} catch (_) {}
|
|
636
766
|
}
|
|
@@ -662,20 +792,22 @@
|
|
|
662
792
|
function bindNodeBB() {
|
|
663
793
|
const $ = window.jQuery;
|
|
664
794
|
if (!$) return;
|
|
795
|
+
|
|
665
796
|
$(window).off('.nbbEzoic');
|
|
666
797
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
667
798
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
668
799
|
S.pageKey = pageKey();
|
|
669
800
|
blockedUntil = 0;
|
|
670
|
-
muteConsole();
|
|
671
|
-
ensureTcfLocator();
|
|
801
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
672
802
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
673
803
|
});
|
|
804
|
+
|
|
674
805
|
const burstEvts = [
|
|
675
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
676
|
-
'action:categories.loaded',
|
|
806
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
807
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
677
808
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
678
809
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
+
|
|
679
811
|
try {
|
|
680
812
|
require(['hooks'], hooks => {
|
|
681
813
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -694,11 +826,6 @@
|
|
|
694
826
|
ticking = true;
|
|
695
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
696
828
|
}, { passive: true });
|
|
697
|
-
let resizeTimer = 0;
|
|
698
|
-
window.addEventListener('resize', () => {
|
|
699
|
-
clearTimeout(resizeTimer);
|
|
700
|
-
resizeTimer = setTimeout(getIO, 500);
|
|
701
|
-
}, { passive: true });
|
|
702
829
|
}
|
|
703
830
|
|
|
704
831
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
package/public/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — style.css (
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — style.css (v20)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* ── Wrapper ──────────────────────────────────────────────────────────────── */
|
|
@@ -56,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) ────────────────────────────────────── */
|