nodebb-plugin-ezoic-infinite 1.8.68 → 1.8.70
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 +57 -124
- package/package.json +2 -2
- package/public/client.js +374 -433
- package/public/style.css +19 -95
package/public/client.js
CHANGED
|
@@ -1,45 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
67
|
*/
|
|
10
68
|
(function nbbEzoicInfinite() {
|
|
11
69
|
'use strict';
|
|
12
70
|
|
|
13
|
-
// ──
|
|
14
|
-
|
|
15
|
-
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
16
|
-
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
71
|
+
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
17
72
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const TIMING = {
|
|
26
|
-
EMPTY_CHECK_MS_1: 30_000,
|
|
27
|
-
EMPTY_CHECK_MS_2: 60_000,
|
|
28
|
-
MIN_PRUNE_AGE_MS: 8_000,
|
|
29
|
-
RECYCLE_MIN_AGE_MS: 5_000,
|
|
30
|
-
SHOW_THROTTLE_MS: 900,
|
|
31
|
-
BURST_COOLDOWN_MS: 200,
|
|
32
|
-
BLOCK_DURATION_MS: 1_500,
|
|
33
|
-
SHOW_TIMEOUT_MS: 7_000,
|
|
34
|
-
SHOW_RELEASE_MS: 700,
|
|
35
|
-
RECYCLE_DELAY_MS: 450,
|
|
36
|
-
};
|
|
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
|
|
37
79
|
|
|
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
|
|
|
87
|
+
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
43
88
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
44
89
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
45
90
|
|
|
@@ -49,50 +94,39 @@
|
|
|
49
94
|
category: 'li[component="categories/category"]',
|
|
50
95
|
};
|
|
51
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Table KIND — source de vérité par kindClass.
|
|
99
|
+
*
|
|
100
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
101
|
+
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
103
|
+
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
104
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
105
|
+
* null → fallback positionnel (catégories)
|
|
106
|
+
*/
|
|
52
107
|
const KIND = {
|
|
53
108
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
54
109
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
55
110
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
56
111
|
};
|
|
57
112
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// ── Utility ────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
const now = () => Date.now();
|
|
63
|
-
const isMobile = () => window.innerWidth < 768;
|
|
64
|
-
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
65
|
-
const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
|
|
66
|
-
|
|
67
|
-
function parseIds(raw) {
|
|
68
|
-
const out = [], seen = new Set();
|
|
69
|
-
for (const v of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
70
|
-
const n = parseInt(v, 10);
|
|
71
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── State ──────────────────────────────────────────────────────────────────
|
|
113
|
+
// ── État global ────────────────────────────────────────────────────────────
|
|
77
114
|
|
|
78
115
|
const S = {
|
|
79
116
|
pageKey: null,
|
|
80
|
-
kind: null,
|
|
81
117
|
cfg: null,
|
|
82
118
|
poolsReady: false,
|
|
83
119
|
pools: { topics: [], posts: [], categories: [] },
|
|
84
120
|
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
85
121
|
mountedIds: new Set(),
|
|
86
122
|
lastShow: new Map(),
|
|
87
|
-
wrapByKey: new Map(),
|
|
88
|
-
wrapsByClass: new Map(),
|
|
89
123
|
io: null,
|
|
90
124
|
domObs: null,
|
|
91
|
-
mutGuard: 0,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
pending: [],
|
|
125
|
+
mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
|
|
126
|
+
inflight: 0, // showAds() en cours
|
|
127
|
+
pending: [], // ids en attente de slot inflight
|
|
95
128
|
pendingSet: new Set(),
|
|
129
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
96
130
|
runQueued: false,
|
|
97
131
|
burstActive: false,
|
|
98
132
|
burstDeadline: 0,
|
|
@@ -100,7 +134,13 @@
|
|
|
100
134
|
lastBurstTs: 0,
|
|
101
135
|
};
|
|
102
136
|
|
|
103
|
-
|
|
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]'));
|
|
104
144
|
|
|
105
145
|
function mutate(fn) {
|
|
106
146
|
S.mutGuard++;
|
|
@@ -111,10 +151,6 @@
|
|
|
111
151
|
|
|
112
152
|
async function fetchConfig() {
|
|
113
153
|
if (S.cfg) return S.cfg;
|
|
114
|
-
try {
|
|
115
|
-
const inline = window.__nbbEzoicCfg;
|
|
116
|
-
if (inline && typeof inline === 'object') { S.cfg = inline; return S.cfg; }
|
|
117
|
-
} catch (_) {}
|
|
118
154
|
try {
|
|
119
155
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
120
156
|
if (r.ok) S.cfg = await r.json();
|
|
@@ -122,6 +158,15 @@
|
|
|
122
158
|
return S.cfg;
|
|
123
159
|
}
|
|
124
160
|
|
|
161
|
+
function parseIds(raw) {
|
|
162
|
+
const out = [], seen = new Set();
|
|
163
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
164
|
+
const n = parseInt(v, 10);
|
|
165
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
125
170
|
function initPools(cfg) {
|
|
126
171
|
if (S.poolsReady) return;
|
|
127
172
|
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
@@ -141,7 +186,7 @@
|
|
|
141
186
|
return location.pathname;
|
|
142
187
|
}
|
|
143
188
|
|
|
144
|
-
function
|
|
189
|
+
function getKind() {
|
|
145
190
|
const p = location.pathname;
|
|
146
191
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
147
192
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
@@ -152,213 +197,181 @@
|
|
|
152
197
|
return 'other';
|
|
153
198
|
}
|
|
154
199
|
|
|
155
|
-
|
|
156
|
-
return S.kind || (S.kind = detectKind());
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
200
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
160
201
|
|
|
161
202
|
function getPosts() {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (parent && parent !== el) continue;
|
|
170
|
-
if (el.getAttribute('component') === 'post/parent') continue;
|
|
171
|
-
out.push(el);
|
|
172
|
-
}
|
|
173
|
-
return out;
|
|
203
|
+
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
204
|
+
if (!el.isConnected) return false;
|
|
205
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
206
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
207
|
+
if (p && p !== el) return false;
|
|
208
|
+
return el.getAttribute('component') !== 'post/parent';
|
|
209
|
+
});
|
|
174
210
|
}
|
|
175
211
|
|
|
176
212
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
177
213
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
178
214
|
|
|
179
|
-
// ──
|
|
180
|
-
|
|
181
|
-
function stableId(klass, el) {
|
|
182
|
-
const attr = KIND[klass]?.anchorAttr;
|
|
183
|
-
if (attr) {
|
|
184
|
-
const v = el.getAttribute(attr);
|
|
185
|
-
if (v != null && v !== '') return v;
|
|
186
|
-
}
|
|
187
|
-
const children = el.parentElement?.children;
|
|
188
|
-
if (!children) return 'i0';
|
|
189
|
-
for (let i = 0; i < children.length; i++) {
|
|
190
|
-
if (children[i] === el) return `i${i}`;
|
|
191
|
-
}
|
|
192
|
-
return 'i0';
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
196
|
-
|
|
197
|
-
function findWrap(key) {
|
|
198
|
-
const w = S.wrapByKey.get(key);
|
|
199
|
-
return w?.isConnected ? w : null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function getWrapSet(klass) {
|
|
203
|
-
let set = S.wrapsByClass.get(klass);
|
|
204
|
-
if (!set) { set = new Set(); S.wrapsByClass.set(klass, set); }
|
|
205
|
-
return set;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── GC disconnected wraps ──────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
function gcDisconnectedWraps() {
|
|
211
|
-
for (const [key, w] of Array.from(S.wrapByKey)) {
|
|
212
|
-
if (!w?.isConnected) S.wrapByKey.delete(key);
|
|
213
|
-
}
|
|
214
|
-
for (const [klass, set] of Array.from(S.wrapsByClass)) {
|
|
215
|
-
for (const w of Array.from(set)) {
|
|
216
|
-
if (w?.isConnected) continue;
|
|
217
|
-
set.delete(w);
|
|
218
|
-
const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
|
|
219
|
-
if (Number.isFinite(id)) {
|
|
220
|
-
S.mountedIds.delete(id);
|
|
221
|
-
S.lastShow.delete(id);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (!set.size) S.wrapsByClass.delete(klass);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ── Wrap lifecycle ─────────────────────────────────────────────────────────
|
|
215
|
+
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
229
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
|
+
*/
|
|
230
221
|
function wrapIsLive(wrap) {
|
|
231
222
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
232
|
-
const key = wrap.getAttribute(
|
|
223
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
233
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).
|
|
234
227
|
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
235
229
|
const colonIdx = key.indexOf(':');
|
|
236
230
|
const klass = key.slice(0, colonIdx);
|
|
237
231
|
const anchorId = key.slice(colonIdx + 1);
|
|
238
232
|
const cfg = KIND[klass];
|
|
239
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.
|
|
240
236
|
const parent = wrap.parentElement;
|
|
241
237
|
if (parent) {
|
|
242
|
-
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
243
238
|
for (const sib of parent.children) {
|
|
244
|
-
if (sib
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
if (sib === wrap) continue;
|
|
240
|
+
try {
|
|
241
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
242
|
+
return sib.isConnected;
|
|
243
|
+
}
|
|
244
|
+
} catch (_) {}
|
|
247
245
|
}
|
|
248
246
|
}
|
|
247
|
+
// Dernier recours : querySelector global
|
|
249
248
|
try {
|
|
250
|
-
|
|
249
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
250
|
+
return !!(found?.isConnected);
|
|
251
251
|
} catch (_) { return false; }
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
function adjacentWrap(el) {
|
|
255
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
256
|
+
}
|
|
255
257
|
|
|
256
|
-
// ──
|
|
258
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
257
259
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
262
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
263
|
+
*/
|
|
264
|
+
function stableId(klass, el) {
|
|
265
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
266
|
+
if (attr) {
|
|
267
|
+
const v = el.getAttribute(attr);
|
|
268
|
+
if (v !== null && v !== '') return v;
|
|
269
|
+
}
|
|
270
|
+
let i = 0;
|
|
271
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
272
|
+
if (s === el) return `i${i}`;
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
return 'i0';
|
|
264
276
|
}
|
|
265
277
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
279
|
+
|
|
280
|
+
function findWrap(key) {
|
|
281
|
+
const w = S.wrapByKey.get(key);
|
|
282
|
+
return (w?.isConnected) ? w : null;
|
|
271
283
|
}
|
|
272
284
|
|
|
273
|
-
// ── Pool
|
|
285
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
274
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
+
* ou null si tous les ids sont montés.
|
|
290
|
+
*/
|
|
275
291
|
function pickId(poolKey) {
|
|
276
292
|
const pool = S.pools[poolKey];
|
|
277
293
|
if (!pool.length) return null;
|
|
278
294
|
for (let t = 0; t < pool.length; t++) {
|
|
279
|
-
const
|
|
295
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
280
296
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
281
|
-
const id = pool[
|
|
297
|
+
const id = pool[i];
|
|
282
298
|
if (!S.mountedIds.has(id)) return id;
|
|
283
299
|
}
|
|
284
300
|
return null;
|
|
285
301
|
}
|
|
286
302
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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) {
|
|
290
311
|
const ez = window.ezstandalone;
|
|
291
312
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
292
|
-
typeof ez?.
|
|
293
|
-
|
|
294
|
-
const threshold = -(3 * (window.innerHeight || 800));
|
|
295
|
-
const t = now();
|
|
296
|
-
let bestEmpty = null, bestEmptyY = Infinity;
|
|
297
|
-
let bestFull = null, bestFullY = Infinity;
|
|
313
|
+
typeof ez?.define !== 'function' ||
|
|
314
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
298
315
|
|
|
299
|
-
const
|
|
300
|
-
|
|
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;
|
|
301
322
|
|
|
302
|
-
|
|
323
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
303
324
|
try {
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
const bottom = wrap.getBoundingClientRect().bottom;
|
|
307
|
-
if (bottom > threshold) continue;
|
|
325
|
+
const rect = wrap.getBoundingClientRect();
|
|
326
|
+
if (rect.bottom > threshold) return;
|
|
308
327
|
if (!isFilled(wrap)) {
|
|
309
|
-
if (bottom <
|
|
328
|
+
if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
|
|
310
329
|
} else {
|
|
311
|
-
if (bottom <
|
|
330
|
+
if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
|
|
312
331
|
}
|
|
313
332
|
} catch (_) {}
|
|
314
|
-
}
|
|
333
|
+
});
|
|
315
334
|
|
|
316
|
-
const best = bestEmpty ??
|
|
335
|
+
const best = bestEmpty ?? bestFilled;
|
|
317
336
|
if (!best) return null;
|
|
318
|
-
const id = parseInt(best.getAttribute(
|
|
337
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
319
338
|
if (!Number.isFinite(id)) return null;
|
|
320
|
-
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
321
339
|
|
|
322
|
-
|
|
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');
|
|
323
349
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
324
|
-
if (ph)
|
|
325
|
-
|
|
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);
|
|
326
355
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
best.setAttribute(ATTR.CREATED, String(now()));
|
|
333
|
-
best.setAttribute(ATTR.SHOWN, '0');
|
|
334
|
-
best.classList.remove('is-empty');
|
|
335
|
-
best.replaceChildren();
|
|
336
|
-
const fresh = document.createElement('div');
|
|
337
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
338
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
339
|
-
best.appendChild(fresh);
|
|
340
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
341
|
-
});
|
|
342
|
-
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
343
|
-
S.wrapByKey.set(newKey, best);
|
|
344
|
-
setTimeout(() => { observePh(id); enqueueShow(id); }, TIMING.RECYCLE_DELAY_MS);
|
|
345
|
-
}, TIMING.RECYCLE_DELAY_MS);
|
|
346
|
-
};
|
|
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 (_) {}
|
|
347
361
|
|
|
348
|
-
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle(); } catch (_) {}
|
|
349
362
|
return { id, wrap: best };
|
|
350
363
|
}
|
|
351
364
|
|
|
352
|
-
// ──
|
|
365
|
+
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
353
366
|
|
|
354
367
|
function makeWrap(id, klass, key) {
|
|
355
368
|
const w = document.createElement('div');
|
|
356
369
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
357
|
-
w.setAttribute(
|
|
358
|
-
w.setAttribute(
|
|
359
|
-
w.setAttribute(
|
|
360
|
-
w.setAttribute(
|
|
361
|
-
w.style.cssText = 'width:100%;display:block';
|
|
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');
|
|
374
|
+
w.style.cssText = 'width:100%;display:block;';
|
|
362
375
|
const ph = document.createElement('div');
|
|
363
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
364
377
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -367,15 +380,14 @@
|
|
|
367
380
|
}
|
|
368
381
|
|
|
369
382
|
function insertAfter(el, id, klass, key) {
|
|
370
|
-
if (!el?.insertAdjacentElement)
|
|
371
|
-
if (findWrap(key))
|
|
372
|
-
if (S.mountedIds.has(id))
|
|
383
|
+
if (!el?.insertAdjacentElement) return null;
|
|
384
|
+
if (findWrap(key)) return null;
|
|
385
|
+
if (S.mountedIds.has(id)) return null;
|
|
373
386
|
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
374
387
|
const w = makeWrap(id, klass, key);
|
|
375
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
376
389
|
S.mountedIds.add(id);
|
|
377
390
|
S.wrapByKey.set(key, w);
|
|
378
|
-
getWrapSet(klass).add(w);
|
|
379
391
|
return w;
|
|
380
392
|
}
|
|
381
393
|
|
|
@@ -383,58 +395,63 @@
|
|
|
383
395
|
try {
|
|
384
396
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
385
397
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
386
|
-
const id = parseInt(w.getAttribute(
|
|
387
|
-
if (Number.isFinite(id))
|
|
388
|
-
|
|
389
|
-
S.lastShow.delete(id);
|
|
390
|
-
}
|
|
391
|
-
const key = w.getAttribute(ATTR.ANCHOR);
|
|
398
|
+
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
399
|
+
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
400
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
392
401
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
393
|
-
for (const cls of w.classList) {
|
|
394
|
-
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
395
|
-
S.wrapsByClass.get(cls)?.delete(w);
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
402
|
w.remove();
|
|
400
403
|
} catch (_) {}
|
|
401
404
|
}
|
|
402
405
|
|
|
403
|
-
// ── Prune (
|
|
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.
|
|
404
420
|
|
|
405
421
|
function pruneOrphansBetween() {
|
|
406
422
|
const klass = 'ezoic-ad-between';
|
|
407
423
|
const cfg = KIND[klass];
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
const sid = key.slice(klass.length + 1);
|
|
421
|
-
if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
422
|
-
}
|
|
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
|
+
});
|
|
423
436
|
}
|
|
424
437
|
|
|
425
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
426
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
|
+
*/
|
|
427
444
|
function ordinal(klass, el) {
|
|
428
445
|
const attr = KIND[klass]?.ordinalAttr;
|
|
429
446
|
if (attr) {
|
|
430
447
|
const v = el.getAttribute(attr);
|
|
431
|
-
if (v
|
|
448
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
432
449
|
}
|
|
433
450
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
434
451
|
let i = 0;
|
|
435
452
|
for (const s of el.parentElement?.children ?? []) {
|
|
436
453
|
if (s === el) return i;
|
|
437
|
-
|
|
454
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
438
455
|
}
|
|
439
456
|
return 0;
|
|
440
457
|
}
|
|
@@ -442,27 +459,32 @@
|
|
|
442
459
|
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
443
460
|
if (!items.length) return 0;
|
|
444
461
|
let inserted = 0;
|
|
462
|
+
|
|
445
463
|
for (const el of items) {
|
|
446
464
|
if (inserted >= MAX_INSERTS_RUN) break;
|
|
447
465
|
if (!el?.isConnected) continue;
|
|
466
|
+
|
|
448
467
|
const ord = ordinal(klass, el);
|
|
449
468
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
450
469
|
if (adjacentWrap(el)) continue;
|
|
470
|
+
|
|
451
471
|
const key = anchorKey(klass, el);
|
|
452
472
|
if (findWrap(key)) continue;
|
|
473
|
+
|
|
453
474
|
const id = pickId(poolKey);
|
|
454
475
|
if (id) {
|
|
455
476
|
const w = insertAfter(el, id, klass, key);
|
|
456
477
|
if (w) { observePh(id); inserted++; }
|
|
457
478
|
} else {
|
|
458
|
-
|
|
479
|
+
const recycled = recycleAndMove(klass, el, key);
|
|
480
|
+
if (!recycled) break;
|
|
459
481
|
inserted++;
|
|
460
482
|
}
|
|
461
483
|
}
|
|
462
484
|
return inserted;
|
|
463
485
|
}
|
|
464
486
|
|
|
465
|
-
// ── IntersectionObserver
|
|
487
|
+
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
466
488
|
|
|
467
489
|
function getIO() {
|
|
468
490
|
if (S.io) return S.io;
|
|
@@ -472,13 +494,9 @@
|
|
|
472
494
|
if (!e.isIntersecting) continue;
|
|
473
495
|
if (e.target instanceof Element) S.io?.unobserve(e.target);
|
|
474
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
475
|
-
if (id > 0) enqueueShow(id);
|
|
497
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
476
498
|
}
|
|
477
|
-
}, {
|
|
478
|
-
root: null,
|
|
479
|
-
rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP,
|
|
480
|
-
threshold: 0,
|
|
481
|
-
});
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
482
500
|
} catch (_) { S.io = null; }
|
|
483
501
|
return S.io;
|
|
484
502
|
}
|
|
@@ -488,11 +506,9 @@
|
|
|
488
506
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
489
507
|
}
|
|
490
508
|
|
|
491
|
-
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
492
|
-
|
|
493
509
|
function enqueueShow(id) {
|
|
494
510
|
if (!id || isBlocked()) return;
|
|
495
|
-
if (
|
|
511
|
+
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
496
512
|
if (S.inflight >= MAX_INFLIGHT) {
|
|
497
513
|
if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
|
|
498
514
|
return;
|
|
@@ -519,26 +535,26 @@
|
|
|
519
535
|
S.inflight = Math.max(0, S.inflight - 1);
|
|
520
536
|
drainQueue();
|
|
521
537
|
};
|
|
522
|
-
const timer = setTimeout(release,
|
|
538
|
+
const timer = setTimeout(release, 7000);
|
|
523
539
|
|
|
524
540
|
requestAnimationFrame(() => {
|
|
525
541
|
try {
|
|
526
542
|
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
527
543
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
528
544
|
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
529
|
-
|
|
530
|
-
|
|
545
|
+
|
|
546
|
+
const t = ts();
|
|
547
|
+
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
531
548
|
S.lastShow.set(id, t);
|
|
532
|
-
|
|
533
|
-
try {
|
|
549
|
+
|
|
550
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
534
551
|
|
|
535
552
|
window.ezstandalone = window.ezstandalone || {};
|
|
536
553
|
const ez = window.ezstandalone;
|
|
537
554
|
const doShow = () => {
|
|
538
555
|
try { ez.showAds(id); } catch (_) {}
|
|
539
|
-
if (wrap) scheduleUncollapseChecks(wrap);
|
|
540
556
|
scheduleEmptyCheck(id, t);
|
|
541
|
-
setTimeout(() => { clearTimeout(timer); release(); },
|
|
557
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
542
558
|
};
|
|
543
559
|
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
544
560
|
} catch (_) { clearTimeout(timer); release(); }
|
|
@@ -546,23 +562,22 @@
|
|
|
546
562
|
}
|
|
547
563
|
|
|
548
564
|
function scheduleEmptyCheck(id, showTs) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (ph.offsetHeight > 10) return;
|
|
559
|
-
wrap.classList.add('is-empty');
|
|
560
|
-
} catch (_) {}
|
|
561
|
-
}, delay);
|
|
562
|
-
}
|
|
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);
|
|
563
574
|
}
|
|
564
575
|
|
|
565
576
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
577
|
+
//
|
|
578
|
+
// Intercepte ez.showAds() pour :
|
|
579
|
+
// – ignorer les appels pendant blockedUntil
|
|
580
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
566
581
|
|
|
567
582
|
function patchShowAds() {
|
|
568
583
|
const apply = () => {
|
|
@@ -573,9 +588,8 @@
|
|
|
573
588
|
window.__nbbEzPatched = true;
|
|
574
589
|
const orig = ez.showAds.bind(ez);
|
|
575
590
|
ez.showAds = function (...args) {
|
|
576
|
-
if (!args.length) return orig();
|
|
577
591
|
if (isBlocked()) return;
|
|
578
|
-
const ids
|
|
592
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
579
593
|
const seen = new Set();
|
|
580
594
|
for (const v of ids) {
|
|
581
595
|
const id = parseInt(v, 10);
|
|
@@ -599,7 +613,6 @@
|
|
|
599
613
|
async function runCore() {
|
|
600
614
|
if (isBlocked()) return 0;
|
|
601
615
|
patchShowAds();
|
|
602
|
-
try { gcDisconnectedWraps(); } catch (_) {}
|
|
603
616
|
|
|
604
617
|
const cfg = await fetchConfig();
|
|
605
618
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -610,16 +623,27 @@
|
|
|
610
623
|
|
|
611
624
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
612
625
|
if (!normBool(cfgEnable)) return 0;
|
|
613
|
-
|
|
626
|
+
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
627
|
+
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
614
628
|
};
|
|
615
629
|
|
|
616
|
-
if (kind === 'topic')
|
|
617
|
-
|
|
630
|
+
if (kind === 'topic') return exec(
|
|
631
|
+
'ezoic-ad-message', getPosts,
|
|
632
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
633
|
+
);
|
|
634
|
+
|
|
618
635
|
if (kind === 'categoryTopics') {
|
|
619
636
|
pruneOrphansBetween();
|
|
620
|
-
return exec(
|
|
637
|
+
return exec(
|
|
638
|
+
'ezoic-ad-between', getTopics,
|
|
639
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
640
|
+
);
|
|
621
641
|
}
|
|
622
|
-
|
|
642
|
+
|
|
643
|
+
return exec(
|
|
644
|
+
'ezoic-ad-categories', getCategories,
|
|
645
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
646
|
+
);
|
|
623
647
|
}
|
|
624
648
|
|
|
625
649
|
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
@@ -638,16 +662,18 @@
|
|
|
638
662
|
|
|
639
663
|
function requestBurst() {
|
|
640
664
|
if (isBlocked()) return;
|
|
641
|
-
const t =
|
|
642
|
-
if (t - S.lastBurstTs <
|
|
665
|
+
const t = ts();
|
|
666
|
+
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
643
667
|
S.lastBurstTs = t;
|
|
644
668
|
S.pageKey = pageKey();
|
|
645
|
-
S.burstDeadline = t +
|
|
669
|
+
S.burstDeadline = t + 2000;
|
|
670
|
+
|
|
646
671
|
if (S.burstActive) return;
|
|
647
672
|
S.burstActive = true;
|
|
648
673
|
S.burstCount = 0;
|
|
674
|
+
|
|
649
675
|
const step = () => {
|
|
650
|
-
if (pageKey() !== S.pageKey || isBlocked() ||
|
|
676
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
651
677
|
S.burstActive = false; return;
|
|
652
678
|
}
|
|
653
679
|
S.burstCount++;
|
|
@@ -659,163 +685,88 @@
|
|
|
659
685
|
step();
|
|
660
686
|
}
|
|
661
687
|
|
|
662
|
-
// ── Cleanup
|
|
688
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
663
689
|
|
|
664
690
|
function cleanup() {
|
|
665
|
-
|
|
666
|
-
mutate(() => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
S.
|
|
670
|
-
S.
|
|
671
|
-
S.pools = { topics: [], posts: [], categories: [] };
|
|
672
|
-
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
691
|
+
blockedUntil = ts() + 1500;
|
|
692
|
+
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
693
|
+
S.cfg = null;
|
|
694
|
+
S.poolsReady = false;
|
|
695
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
696
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
673
697
|
S.mountedIds.clear();
|
|
674
698
|
S.lastShow.clear();
|
|
675
699
|
S.wrapByKey.clear();
|
|
676
|
-
S.
|
|
677
|
-
S.
|
|
678
|
-
S.inflight = 0;
|
|
679
|
-
S.pending = [];
|
|
700
|
+
S.inflight = 0;
|
|
701
|
+
S.pending = [];
|
|
680
702
|
S.pendingSet.clear();
|
|
681
|
-
S.burstActive
|
|
682
|
-
S.runQueued
|
|
703
|
+
S.burstActive = false;
|
|
704
|
+
S.runQueued = false;
|
|
683
705
|
}
|
|
684
706
|
|
|
685
707
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
686
708
|
|
|
687
709
|
function ensureDomObserver() {
|
|
688
710
|
if (S.domObs) return;
|
|
711
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
689
712
|
S.domObs = new MutationObserver(muts => {
|
|
690
713
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
691
|
-
let needsBurst = false;
|
|
692
|
-
const kind = getKind();
|
|
693
|
-
const relevantSels =
|
|
694
|
-
kind === 'topic' ? [SEL.post] :
|
|
695
|
-
kind === 'categoryTopics' ? [SEL.topic] :
|
|
696
|
-
kind === 'categories' ? [SEL.category] :
|
|
697
|
-
[SEL.post, SEL.topic, SEL.category];
|
|
698
|
-
outer:
|
|
699
714
|
for (const m of muts) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
} catch (_) {}
|
|
707
|
-
}
|
|
708
|
-
for (const node of m.addedNodes) {
|
|
709
|
-
if (!(node instanceof Element)) continue;
|
|
710
|
-
try {
|
|
711
|
-
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
712
|
-
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) || m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
|
|
713
|
-
if (wrap) clearEmptyIfFilled(wrap);
|
|
714
|
-
}
|
|
715
|
-
} catch (_) {}
|
|
716
|
-
try {
|
|
717
|
-
const reinserted = node.classList?.contains(WRAP_CLASS) ? [node] : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
718
|
-
for (const wrap of reinserted) {
|
|
719
|
-
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
720
|
-
if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
|
|
721
|
-
}
|
|
722
|
-
} catch (_) {}
|
|
723
|
-
if (!needsBurst) {
|
|
724
|
-
for (const sel of relevantSels) {
|
|
725
|
-
try { if (node.matches(sel) || node.querySelector(sel)) { needsBurst = true; break outer; } } catch (_) {}
|
|
726
|
-
}
|
|
715
|
+
for (const n of m.addedNodes) {
|
|
716
|
+
if (n.nodeType !== 1) continue;
|
|
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;} })) {
|
|
720
|
+
requestBurst(); return;
|
|
727
721
|
}
|
|
728
722
|
}
|
|
729
723
|
}
|
|
730
|
-
if (needsBurst) requestBurst();
|
|
731
724
|
});
|
|
732
725
|
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
733
726
|
}
|
|
734
727
|
|
|
735
|
-
// ──
|
|
736
|
-
|
|
737
|
-
function ensureTcfLocator() {
|
|
738
|
-
if (!window.__tcfapi && !window.__cmp) return;
|
|
739
|
-
const LOCATOR_ID = '__tcfapiLocator';
|
|
740
|
-
const ensureInHead = () => {
|
|
741
|
-
let el = document.getElementById(LOCATOR_ID);
|
|
742
|
-
if (el) { if (el.parentElement !== document.head) try { document.head.appendChild(el); } catch (_) {} return; }
|
|
743
|
-
const f = document.createElement('iframe');
|
|
744
|
-
f.style.display = 'none'; f.id = f.name = LOCATOR_ID;
|
|
745
|
-
try { document.head.appendChild(f); } catch (_) { (document.body || document.documentElement).appendChild(f); }
|
|
746
|
-
};
|
|
747
|
-
ensureInHead();
|
|
748
|
-
if (!window.__nbbCmpGuarded) {
|
|
749
|
-
window.__nbbCmpGuarded = true;
|
|
750
|
-
if (typeof window.__tcfapi === 'function') {
|
|
751
|
-
const orig = window.__tcfapi;
|
|
752
|
-
window.__tcfapi = function (cmd, ver, cb, param) {
|
|
753
|
-
try { return orig.call(this, cmd, ver, function (...a) { try { cb?.(...a); } catch (_) {} }, param); }
|
|
754
|
-
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.call(this, cmd, ver, cb, param); } catch (_) {} } }
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
if (typeof window.__cmp === 'function') {
|
|
758
|
-
const orig = window.__cmp;
|
|
759
|
-
window.__cmp = function (...a) {
|
|
760
|
-
try { return orig.apply(this, a); }
|
|
761
|
-
catch (e) { if (e?.message?.includes('null')) { ensureInHead(); try { return orig.apply(this, a); } catch (_) {} } }
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
if (!window.__nbbTcfObs) {
|
|
766
|
-
window.__nbbTcfObs = new MutationObserver(() => { if (!document.getElementById(LOCATOR_ID)) ensureInHead(); });
|
|
767
|
-
try { window.__nbbTcfObs.observe(document.body || document.documentElement, { childList: true, subtree: false }); } catch (_) {}
|
|
768
|
-
try { if (document.head) window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false }); } catch (_) {}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
773
|
-
|
|
774
|
-
function protectAriaHidden() {
|
|
775
|
-
if (window.__nbbAriaObs) return;
|
|
776
|
-
const fix = () => { try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {} };
|
|
777
|
-
fix();
|
|
778
|
-
window.__nbbAriaObs = new MutationObserver(fix);
|
|
779
|
-
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// ── Console muting ─────────────────────────────────────────────────────────
|
|
728
|
+
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
783
729
|
|
|
784
730
|
function muteConsole() {
|
|
785
731
|
if (window.__nbbEzMuted) return;
|
|
786
732
|
window.__nbbEzMuted = true;
|
|
787
733
|
const MUTED = [
|
|
734
|
+
'[EzoicAds JS]: Placeholder Id',
|
|
735
|
+
'No valid placeholders for loadMore',
|
|
736
|
+
'cannot call refresh on the same page',
|
|
737
|
+
'no placeholders are currently defined in Refresh',
|
|
738
|
+
'Debugger iframe already exists',
|
|
788
739
|
`with id ${PH_PREFIX}`,
|
|
789
|
-
'adsbygoogle.push() error',
|
|
790
|
-
'already been defined',
|
|
791
|
-
'bad response. Status',
|
|
792
|
-
'slotDestroyed',
|
|
793
|
-
'identity bridging',
|
|
794
|
-
'[EzoicAds JS]: Placeholder',
|
|
795
|
-
'No valid placeholders',
|
|
796
|
-
'cannot call refresh',
|
|
797
|
-
'no placeholders are currently defined',
|
|
798
|
-
'Debugger iframe already',
|
|
799
|
-
'Error in custom getTCData',
|
|
800
|
-
'no interstitial API',
|
|
801
|
-
'JS-Enable should only',
|
|
802
740
|
];
|
|
803
|
-
for (const
|
|
804
|
-
const orig = console[
|
|
741
|
+
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
742
|
+
const orig = console[m];
|
|
805
743
|
if (typeof orig !== 'function') continue;
|
|
806
|
-
console[
|
|
807
|
-
if (typeof
|
|
808
|
-
|
|
744
|
+
console[m] = function (...a) {
|
|
745
|
+
if (typeof a[0] === 'string' && MUTED.some(p => a[0].includes(p))) return;
|
|
746
|
+
orig.apply(console, a);
|
|
809
747
|
};
|
|
810
748
|
}
|
|
811
749
|
}
|
|
812
750
|
|
|
813
|
-
|
|
751
|
+
function ensureTcfLocator() {
|
|
752
|
+
try {
|
|
753
|
+
if (!window.__tcfapi && !window.__cmp) return;
|
|
754
|
+
const inject = () => {
|
|
755
|
+
if (document.getElementById('__tcfapiLocator')) return;
|
|
756
|
+
const f = document.createElement('iframe');
|
|
757
|
+
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
758
|
+
(document.body || document.documentElement).appendChild(f);
|
|
759
|
+
};
|
|
760
|
+
inject();
|
|
761
|
+
if (!window.__nbbTcfObs) {
|
|
762
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
763
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
764
|
+
}
|
|
765
|
+
} catch (_) {}
|
|
766
|
+
}
|
|
814
767
|
|
|
815
|
-
|
|
768
|
+
const _warmed = new Set();
|
|
816
769
|
function warmNetwork() {
|
|
817
|
-
if (_warmed) return;
|
|
818
|
-
_warmed = true;
|
|
819
770
|
const head = document.head;
|
|
820
771
|
if (!head) return;
|
|
821
772
|
for (const [rel, href, cors] of [
|
|
@@ -826,7 +777,9 @@
|
|
|
826
777
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
827
778
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
828
779
|
]) {
|
|
829
|
-
|
|
780
|
+
const k = `${rel}|${href}`;
|
|
781
|
+
if (_warmed.has(k)) continue;
|
|
782
|
+
_warmed.add(k);
|
|
830
783
|
const l = document.createElement('link');
|
|
831
784
|
l.rel = rel; l.href = href;
|
|
832
785
|
if (cors) l.crossOrigin = 'anonymous';
|
|
@@ -839,25 +792,29 @@
|
|
|
839
792
|
function bindNodeBB() {
|
|
840
793
|
const $ = window.jQuery;
|
|
841
794
|
if (!$) return;
|
|
795
|
+
|
|
842
796
|
$(window).off('.nbbEzoic');
|
|
843
797
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
844
798
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
845
|
-
S.pageKey
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
requestBurst();
|
|
799
|
+
S.pageKey = pageKey();
|
|
800
|
+
blockedUntil = 0;
|
|
801
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
802
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
849
803
|
});
|
|
804
|
+
|
|
850
805
|
const burstEvts = [
|
|
851
|
-
'action:ajaxify.contentLoaded', 'action:posts.loaded',
|
|
852
|
-
'action:
|
|
853
|
-
'action:category.loaded', 'action:topic.loaded',
|
|
806
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
807
|
+
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
854
808
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
855
809
|
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
810
|
+
|
|
856
811
|
try {
|
|
857
812
|
require(['hooks'], hooks => {
|
|
858
813
|
if (typeof hooks?.on !== 'function') return;
|
|
859
|
-
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
814
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
815
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
860
816
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
817
|
+
}
|
|
861
818
|
});
|
|
862
819
|
} catch (_) {}
|
|
863
820
|
}
|
|
@@ -865,7 +822,8 @@
|
|
|
865
822
|
function bindScroll() {
|
|
866
823
|
let ticking = false;
|
|
867
824
|
window.addEventListener('scroll', () => {
|
|
868
|
-
if (ticking) return;
|
|
825
|
+
if (ticking) return;
|
|
826
|
+
ticking = true;
|
|
869
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
870
828
|
}, { passive: true });
|
|
871
829
|
}
|
|
@@ -875,30 +833,13 @@
|
|
|
875
833
|
S.pageKey = pageKey();
|
|
876
834
|
muteConsole();
|
|
877
835
|
ensureTcfLocator();
|
|
878
|
-
protectAriaHidden();
|
|
879
836
|
warmNetwork();
|
|
880
837
|
patchShowAds();
|
|
881
838
|
getIO();
|
|
882
839
|
ensureDomObserver();
|
|
883
840
|
bindNodeBB();
|
|
884
841
|
bindScroll();
|
|
885
|
-
|
|
842
|
+
blockedUntil = 0;
|
|
886
843
|
requestBurst();
|
|
887
844
|
|
|
888
|
-
// Retry boot: sa.min.js async + Cloudflare Rocket Loader + NodeBB SPA
|
|
889
|
-
// can cause client.js to boot before DOM/Ezoic are ready.
|
|
890
|
-
// Retries stop once ads are mounted or after ~10s.
|
|
891
|
-
let _retries = 0;
|
|
892
|
-
function retryBoot() {
|
|
893
|
-
if (_retries >= 12 || S.mountedIds.size > 0) return;
|
|
894
|
-
_retries++;
|
|
895
|
-
patchShowAds();
|
|
896
|
-
if (!isBlocked() && !S.burstActive) {
|
|
897
|
-
S.lastBurstTs = 0;
|
|
898
|
-
requestBurst();
|
|
899
|
-
}
|
|
900
|
-
setTimeout(retryBoot, _retries <= 4 ? 300 : 1000);
|
|
901
|
-
}
|
|
902
|
-
setTimeout(retryBoot, 250);
|
|
903
|
-
|
|
904
845
|
})();
|