nodebb-plugin-ezoic-infinite 1.8.22 → 1.8.24
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 +135 -143
- package/package.json +1 -1
- package/public/admin.js +16 -32
- package/public/client.js +655 -598
- package/public/style.css +17 -11
package/public/client.js
CHANGED
|
@@ -1,83 +1,192 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
|
+
*
|
|
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.
|
|
67
|
+
*/
|
|
68
|
+
(function nbbEzoicInfinite() {
|
|
2
69
|
'use strict';
|
|
3
70
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
71
|
+
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
74
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
75
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
76
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
77
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
|
+
|
|
80
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
|
|
81
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
|
+
const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
|
|
83
|
+
const MAX_INFLIGHT = 4; // max showAds() simultanés
|
|
84
|
+
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
85
|
+
const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
|
|
86
|
+
const MIN_PLACEHOLDER_HEIGHT = 50; // réservation minimale perçue
|
|
87
|
+
|
|
88
|
+
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
89
|
+
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
90
|
+
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
11
91
|
|
|
12
92
|
const SEL = {
|
|
13
|
-
post:
|
|
14
|
-
topic:
|
|
93
|
+
post: '[component="post"][data-pid]',
|
|
94
|
+
topic: 'li[component="category/topic"]',
|
|
15
95
|
category: 'li[component="categories/category"]',
|
|
16
96
|
};
|
|
17
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Table KIND — source de vérité par kindClass.
|
|
100
|
+
*
|
|
101
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
102
|
+
* baseTag préfixe tag pour querySelector d'ancre
|
|
103
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
104
|
+
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
105
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
106
|
+
* null → fallback positionnel (catégories)
|
|
107
|
+
*/
|
|
18
108
|
const KIND = {
|
|
19
|
-
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index'
|
|
20
|
-
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index'
|
|
21
|
-
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null
|
|
109
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
110
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
111
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
22
112
|
};
|
|
23
113
|
|
|
24
|
-
|
|
25
|
-
maxInsertsPerRun: 8,
|
|
26
|
-
maxShowInflight: 1,
|
|
27
|
-
maxShowBatch: 1,
|
|
28
|
-
showThrottleMs: 1500,
|
|
29
|
-
showCooldownMs: 15000,
|
|
30
|
-
showReleaseMs: 350,
|
|
31
|
-
showFailsafeMs: 8000,
|
|
32
|
-
batchFlushMs: 80,
|
|
33
|
-
runDebounceMs: 80,
|
|
34
|
-
orphanGraceMs: 12000,
|
|
35
|
-
retireGraceMs: 25000,
|
|
36
|
-
recentActivityMs: 6000,
|
|
37
|
-
viewportBufferDesktop: 700,
|
|
38
|
-
viewportBufferMobile: 350,
|
|
39
|
-
ioMarginDesktop: '1800px 0px 2200px 0px',
|
|
40
|
-
ioMarginMobile: '2400px 0px 2800px 0px',
|
|
41
|
-
maintenanceEveryMs: 1200,
|
|
42
|
-
minHeightRememberMin: 40,
|
|
43
|
-
};
|
|
114
|
+
// ── État global ────────────────────────────────────────────────────────────
|
|
44
115
|
|
|
45
116
|
const S = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
117
|
+
pageKey: null,
|
|
118
|
+
cfg: null,
|
|
119
|
+
poolsReady: false,
|
|
120
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
121
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
122
|
+
mountedIds: new Set(),
|
|
123
|
+
lastShow: new Map(),
|
|
124
|
+
io: null,
|
|
125
|
+
domObs: null,
|
|
126
|
+
mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
|
|
127
|
+
inflight: 0, // showAds() en cours
|
|
128
|
+
pending: [], // ids en attente de slot inflight
|
|
129
|
+
pendingSet: new Set(),
|
|
130
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
131
|
+
runQueued: false,
|
|
132
|
+
burstActive: false,
|
|
133
|
+
burstDeadline: 0,
|
|
134
|
+
burstCount: 0,
|
|
135
|
+
lastBurstTs: 0,
|
|
136
|
+
firstShown: false,
|
|
137
|
+
wrapsByClass: new Map(),
|
|
138
|
+
kind: null,
|
|
66
139
|
};
|
|
67
140
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
const
|
|
74
|
-
const
|
|
141
|
+
let blockedUntil = 0;
|
|
142
|
+
|
|
143
|
+
const ts = () => Date.now();
|
|
144
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
145
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
146
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
147
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
75
148
|
|
|
76
149
|
function mutate(fn) {
|
|
77
|
-
S.
|
|
78
|
-
try {
|
|
150
|
+
S.mutGuard++;
|
|
151
|
+
try { fn(); } finally { S.mutGuard--; }
|
|
79
152
|
}
|
|
80
153
|
|
|
154
|
+
// ── Config ─────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function fetchConfig() {
|
|
157
|
+
if (S.cfg) return S.cfg;
|
|
158
|
+
try {
|
|
159
|
+
if (window.__nbbEzoicCfg && typeof window.__nbbEzoicCfg === 'object') {
|
|
160
|
+
S.cfg = window.__nbbEzoicCfg;
|
|
161
|
+
return S.cfg;
|
|
162
|
+
}
|
|
163
|
+
} catch (_) {}
|
|
164
|
+
try {
|
|
165
|
+
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
166
|
+
if (r.ok) S.cfg = await r.json();
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
return S.cfg;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseIds(raw) {
|
|
172
|
+
const out = [], seen = new Set();
|
|
173
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
174
|
+
const n = parseInt(v, 10);
|
|
175
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function initPools(cfg) {
|
|
181
|
+
if (S.poolsReady) return;
|
|
182
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
183
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
184
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
185
|
+
S.poolsReady = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Page identity ──────────────────────────────────────────────────────────
|
|
189
|
+
|
|
81
190
|
function pageKey() {
|
|
82
191
|
try {
|
|
83
192
|
const d = window.ajaxify?.data;
|
|
@@ -87,718 +196,668 @@
|
|
|
87
196
|
return location.pathname;
|
|
88
197
|
}
|
|
89
198
|
|
|
90
|
-
function
|
|
199
|
+
function detectKind() {
|
|
91
200
|
const p = location.pathname;
|
|
92
|
-
if (/^\/topic\//.test(p))
|
|
93
|
-
if (/^\/category\//.test(p))
|
|
201
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
202
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
94
203
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
95
|
-
if (document.querySelector(SEL.post)) return 'topic';
|
|
96
|
-
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
97
204
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
205
|
+
if (document.querySelector(SEL.post)) return 'topic';
|
|
206
|
+
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
98
207
|
return 'other';
|
|
99
208
|
}
|
|
100
209
|
|
|
210
|
+
function getKind() {
|
|
211
|
+
if (S.kind) return S.kind;
|
|
212
|
+
S.kind = detectKind();
|
|
213
|
+
return S.kind;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
101
218
|
function getPosts() {
|
|
102
219
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
103
220
|
if (!el.isConnected) return false;
|
|
104
221
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
105
|
-
const p = el.parentElement?.closest
|
|
222
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
106
223
|
if (p && p !== el) return false;
|
|
107
224
|
return el.getAttribute('component') !== 'post/parent';
|
|
108
225
|
});
|
|
109
226
|
}
|
|
110
|
-
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
111
|
-
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
112
|
-
|
|
113
|
-
async function fetchConfig() {
|
|
114
|
-
if (S.cfg) return S.cfg;
|
|
115
|
-
try {
|
|
116
|
-
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
117
|
-
if (r.ok) S.cfg = await r.json();
|
|
118
|
-
} catch (_) {}
|
|
119
|
-
return S.cfg;
|
|
120
|
-
}
|
|
121
227
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const seen = new Set();
|
|
125
|
-
for (const line of String(raw || '').split(/\r?\n/)) {
|
|
126
|
-
const n = parseInt(line.trim(), 10);
|
|
127
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
228
|
+
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
229
|
+
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
131
230
|
|
|
132
|
-
|
|
133
|
-
if (S.poolsReady) return;
|
|
134
|
-
S.pools = {
|
|
135
|
-
topics: parseIds(cfg.placeholderIds),
|
|
136
|
-
posts: parseIds(cfg.messagePlaceholderIds),
|
|
137
|
-
categories: parseIds(cfg.categoryPlaceholderIds),
|
|
138
|
-
};
|
|
139
|
-
S.poolsReady = true;
|
|
140
|
-
}
|
|
231
|
+
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
141
232
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
235
|
+
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
236
|
+
*/
|
|
237
|
+
function wrapIsLive(wrap) {
|
|
238
|
+
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
239
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
240
|
+
if (!key) return false;
|
|
241
|
+
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
242
|
+
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
243
|
+
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
244
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
245
|
+
const colonIdx = key.indexOf(':');
|
|
246
|
+
const klass = key.slice(0, colonIdx);
|
|
247
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
248
|
+
const cfg = KIND[klass];
|
|
249
|
+
if (!cfg) return false;
|
|
250
|
+
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
251
|
+
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
252
|
+
const parent = wrap.parentElement;
|
|
253
|
+
if (parent) {
|
|
254
|
+
for (const sib of parent.children) {
|
|
255
|
+
if (sib === wrap) continue;
|
|
256
|
+
try {
|
|
257
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
258
|
+
return sib.isConnected;
|
|
259
|
+
}
|
|
260
|
+
} catch (_) {}
|
|
261
|
+
}
|
|
147
262
|
}
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function isSpecialLike(root) {
|
|
152
|
-
if (!(root instanceof Element)) return false;
|
|
153
|
-
const test = el => /adhesion|interstitial|anchor|sticky|outofpage/.test(textSig(el));
|
|
154
|
-
if (test(root)) return true;
|
|
263
|
+
// Dernier recours : querySelector global
|
|
155
264
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
} catch (_) {}
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function hasFixedLike(root, max = 16) {
|
|
163
|
-
if (!(root instanceof Element)) return false;
|
|
164
|
-
const q = [root];
|
|
165
|
-
let seen = 0;
|
|
166
|
-
while (q.length && seen < max) {
|
|
167
|
-
const n = q.shift();
|
|
168
|
-
seen++;
|
|
169
|
-
try {
|
|
170
|
-
const cs = window.getComputedStyle(n);
|
|
171
|
-
if (cs.position === 'fixed' || cs.position === 'sticky') return true;
|
|
172
|
-
} catch (_) {}
|
|
173
|
-
for (const c of n.children || []) q.push(c);
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
265
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
266
|
+
return !!(found?.isConnected);
|
|
267
|
+
} catch (_) { return false; }
|
|
176
268
|
}
|
|
177
269
|
|
|
178
|
-
function
|
|
179
|
-
|
|
270
|
+
function adjacentWrap(el) {
|
|
271
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
180
272
|
}
|
|
181
273
|
|
|
182
|
-
|
|
274
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
183
275
|
|
|
184
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
278
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
279
|
+
*/
|
|
280
|
+
function stableId(klass, el) {
|
|
185
281
|
const attr = KIND[klass]?.anchorAttr;
|
|
186
282
|
if (attr) {
|
|
187
283
|
const v = el.getAttribute(attr);
|
|
188
284
|
if (v !== null && v !== '') return v;
|
|
189
285
|
}
|
|
190
286
|
let i = 0;
|
|
191
|
-
for (const s of el.parentElement?.children
|
|
287
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
288
|
+
if (s === el) return `i${i}`;
|
|
289
|
+
i++;
|
|
290
|
+
}
|
|
192
291
|
return 'i0';
|
|
193
292
|
}
|
|
194
|
-
const anchorKey = (klass, el) => `${klass}:${anchorStableId(klass, el)}`;
|
|
195
293
|
|
|
196
|
-
|
|
294
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
295
|
+
|
|
296
|
+
function findWrap(key) {
|
|
197
297
|
const w = S.wrapByKey.get(key);
|
|
198
298
|
return (w?.isConnected) ? w : null;
|
|
199
299
|
}
|
|
200
300
|
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!klass) return;
|
|
206
|
-
const h = Math.round(wrap.getBoundingClientRect().height || 0);
|
|
207
|
-
if (h >= CFG.minHeightRememberMin) S.lastWrapHeightByClass.set(klass, h);
|
|
208
|
-
} catch (_) {}
|
|
301
|
+
function wrapsSet(klass) {
|
|
302
|
+
let set = S.wrapsByClass.get(klass);
|
|
303
|
+
if (!set) { set = new Set(); S.wrapsByClass.set(klass, set); }
|
|
304
|
+
return set;
|
|
209
305
|
}
|
|
306
|
+
const registerWrap = (klass, w) => wrapsSet(klass).add(w);
|
|
307
|
+
const unregisterWrap = (klass, w) => S.wrapsByClass.get(klass)?.delete(w);
|
|
210
308
|
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
const r = wrap.getBoundingClientRect();
|
|
214
|
-
const b = viewportBuffer();
|
|
215
|
-
const vh = window.innerHeight || 800;
|
|
216
|
-
return r.bottom > -b && r.top < vh + b;
|
|
217
|
-
} catch (_) { return true; }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function ensureRegistry(id, wrap) {
|
|
221
|
-
let rec = S.regById.get(id);
|
|
222
|
-
if (!rec) {
|
|
223
|
-
rec = {
|
|
224
|
-
id,
|
|
225
|
-
wrap,
|
|
226
|
-
key: wrap?.getAttribute(A_ANCHOR) || '',
|
|
227
|
-
state: 'idle',
|
|
228
|
-
typeClass: [...(wrap?.classList || [])].find(c => c.startsWith('ezoic-ad-')) || '',
|
|
229
|
-
isSpecial: false,
|
|
230
|
-
isFixedLike: false,
|
|
231
|
-
createdAt: now(),
|
|
232
|
-
shownAt: 0,
|
|
233
|
-
lastSeenAt: 0,
|
|
234
|
-
lastMutationAt: now(),
|
|
235
|
-
cooldownUntil: 0,
|
|
236
|
-
};
|
|
237
|
-
S.regById.set(id, rec);
|
|
238
|
-
}
|
|
239
|
-
if (wrap && rec.wrap !== wrap) rec.wrap = wrap;
|
|
240
|
-
if (wrap) {
|
|
241
|
-
rec.key = wrap.getAttribute(A_ANCHOR) || rec.key;
|
|
242
|
-
rec.typeClass = [...(wrap.classList || [])].find(c => c.startsWith('ezoic-ad-')) || rec.typeClass;
|
|
243
|
-
}
|
|
244
|
-
return rec;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function updateWrapState(rec, state) {
|
|
248
|
-
rec.state = state;
|
|
249
|
-
try { rec.wrap?.setAttribute?.(A_STATE, state); } catch (_) {}
|
|
250
|
-
}
|
|
309
|
+
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
251
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
313
|
+
* ou null si tous les ids sont montés.
|
|
314
|
+
*/
|
|
252
315
|
function pickId(poolKey) {
|
|
253
|
-
const pool = S.pools[poolKey]
|
|
316
|
+
const pool = S.pools[poolKey];
|
|
254
317
|
if (!pool.length) return null;
|
|
255
318
|
for (let t = 0; t < pool.length; t++) {
|
|
256
|
-
const
|
|
319
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
257
320
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
258
|
-
const id = pool[
|
|
321
|
+
const id = pool[i];
|
|
259
322
|
if (!S.mountedIds.has(id)) return id;
|
|
260
323
|
}
|
|
261
324
|
return null;
|
|
262
325
|
}
|
|
263
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
329
|
+
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
330
|
+
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
331
|
+
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
332
|
+
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
333
|
+
*/
|
|
334
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
335
|
+
const ez = window.ezstandalone;
|
|
336
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
337
|
+
typeof ez?.define !== 'function' ||
|
|
338
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
339
|
+
|
|
340
|
+
const vh = window.innerHeight || 800;
|
|
341
|
+
// Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
|
|
342
|
+
// après pour neutraliser l'IO — plus de showAds parasite possible.
|
|
343
|
+
const threshold = -vh;
|
|
344
|
+
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
345
|
+
let bestFilled = null, bestFilledBottom = Infinity;
|
|
346
|
+
|
|
347
|
+
for (const wrap of (S.wrapsByClass.get(klass) || [])) {
|
|
348
|
+
try {
|
|
349
|
+
const rect = wrap.getBoundingClientRect();
|
|
350
|
+
if (rect.bottom > threshold) return;
|
|
351
|
+
if (!isFilled(wrap)) {
|
|
352
|
+
if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
|
|
353
|
+
} else {
|
|
354
|
+
if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
|
|
355
|
+
}
|
|
356
|
+
} catch (_) {}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const best = bestEmpty ?? bestFilled;
|
|
360
|
+
if (!best) return null;
|
|
361
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
362
|
+
if (!Number.isFinite(id)) return null;
|
|
363
|
+
|
|
364
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
365
|
+
// Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
|
|
366
|
+
// parasite si le nœud était encore dans la zone IO_MARGIN.
|
|
367
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
368
|
+
mutate(() => {
|
|
369
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
370
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
371
|
+
best.setAttribute(A_SHOWN, '0');
|
|
372
|
+
best.classList.remove('is-empty');
|
|
373
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
374
|
+
if (ph) ph.innerHTML = '';
|
|
375
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
376
|
+
});
|
|
377
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
378
|
+
S.wrapByKey.set(newKey, best);
|
|
379
|
+
|
|
380
|
+
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
381
|
+
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
382
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
383
|
+
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
384
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
385
|
+
|
|
386
|
+
return { id, wrap: best };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
390
|
+
|
|
264
391
|
function makeWrap(id, klass, key) {
|
|
265
392
|
const w = document.createElement('div');
|
|
266
393
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
267
|
-
w.setAttribute(A_ANCHOR,
|
|
268
|
-
w.setAttribute(A_WRAPID,
|
|
269
|
-
w.setAttribute(A_CREATED, String(
|
|
270
|
-
w.setAttribute(A_SHOWN,
|
|
271
|
-
w.
|
|
272
|
-
w.style.cssText = 'display:block;width:100%;';
|
|
273
|
-
const h = S.lastWrapHeightByClass.get(klass);
|
|
274
|
-
if (Number.isFinite(h) && h > 0) w.style.minHeight = `${h}px`;
|
|
394
|
+
w.setAttribute(A_ANCHOR, key);
|
|
395
|
+
w.setAttribute(A_WRAPID, String(id));
|
|
396
|
+
w.setAttribute(A_CREATED, String(ts()));
|
|
397
|
+
w.setAttribute(A_SHOWN, '0');
|
|
398
|
+
w.style.cssText = 'width:100%;display:block;';
|
|
275
399
|
const ph = document.createElement('div');
|
|
276
400
|
ph.id = `${PH_PREFIX}${id}`;
|
|
277
401
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
402
|
+
ph.style.minHeight = `${MIN_PLACEHOLDER_HEIGHT}px`;
|
|
278
403
|
w.appendChild(ph);
|
|
279
404
|
return w;
|
|
280
405
|
}
|
|
281
406
|
|
|
282
|
-
function
|
|
283
|
-
if (!
|
|
284
|
-
if (
|
|
285
|
-
if (S.mountedIds.has(id))
|
|
286
|
-
if (
|
|
407
|
+
function insertAfter(el, id, klass, key) {
|
|
408
|
+
if (!el?.insertAdjacentElement) return null;
|
|
409
|
+
if (findWrap(key)) return null;
|
|
410
|
+
if (S.mountedIds.has(id)) return null;
|
|
411
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
287
412
|
const w = makeWrap(id, klass, key);
|
|
288
|
-
mutate(() =>
|
|
413
|
+
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
289
414
|
S.mountedIds.add(id);
|
|
290
415
|
S.wrapByKey.set(key, w);
|
|
291
|
-
|
|
292
|
-
rec.key = key;
|
|
293
|
-
rec.createdAt = now();
|
|
294
|
-
rec.lastMutationAt = now();
|
|
295
|
-
rec.isSpecial = false;
|
|
296
|
-
rec.isFixedLike = false;
|
|
297
|
-
updateWrapState(rec, 'idle');
|
|
298
|
-
observePlaceholder(id);
|
|
416
|
+
registerWrap(klass, w);
|
|
299
417
|
return w;
|
|
300
418
|
}
|
|
301
419
|
|
|
302
|
-
function
|
|
420
|
+
function dropWrap(w) {
|
|
303
421
|
try {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (key && S.wrapByKey.get(key) ===
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
S.lastShowById.delete(id);
|
|
314
|
-
const rec = S.regById.get(id);
|
|
315
|
-
if (rec) {
|
|
316
|
-
rec.wrap = null;
|
|
317
|
-
rec.state = 'retired';
|
|
318
|
-
rec.cooldownUntil = Math.max(rec.cooldownUntil || 0, now() + 3000);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
wrap.remove();
|
|
422
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
423
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
424
|
+
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
425
|
+
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
426
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
427
|
+
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
428
|
+
const klass = Array.from(w.classList || []).find(c => c !== WRAP_CLASS && c.startsWith('ezoic-ad-'));
|
|
429
|
+
if (klass) unregisterWrap(klass, w);
|
|
430
|
+
w.remove();
|
|
322
431
|
} catch (_) {}
|
|
323
432
|
}
|
|
324
433
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (!rec.isFixedLike) rec.isFixedLike = hasFixedLike(w);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function maintenance() {
|
|
359
|
-
if (isBlocked()) return;
|
|
360
|
-
sweepDisconnected();
|
|
361
|
-
const t = now();
|
|
362
|
-
for (const [id, rec] of S.regById) {
|
|
363
|
-
const w = rec.wrap;
|
|
364
|
-
if (!w?.isConnected) continue;
|
|
365
|
-
classifyWrap(rec);
|
|
366
|
-
if (wrapNearViewport(w)) {
|
|
367
|
-
rec.lastSeenAt = t;
|
|
368
|
-
if (rec.state === 'retiring') updateWrapState(rec, 'live');
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10) || rec.createdAt || 0;
|
|
372
|
-
const shown = parseInt(w.getAttribute(A_SHOWN) || '0', 10) || rec.shownAt || 0;
|
|
373
|
-
const age = t - Math.max(created, shown || 0, rec.lastMutationAt || 0);
|
|
374
|
-
const activeRecently = (t - (rec.lastMutationAt || 0)) < CFG.recentActivityMs;
|
|
375
|
-
const filled = isFilled(w);
|
|
376
|
-
if (filled) { rec.lastMutationAt = t; rec.shownAt = Math.max(rec.shownAt || 0, shown || 0); }
|
|
377
|
-
if (rec.isSpecial || rec.isFixedLike) continue; // never retire plugin-side
|
|
378
|
-
if (activeRecently) continue;
|
|
379
|
-
if (filled) continue; // keep rendered slots stable; no aggressive destroy/recycle
|
|
380
|
-
if (age < CFG.retireGraceMs) continue;
|
|
381
|
-
if (rec.state !== 'retiring') {
|
|
382
|
-
updateWrapState(rec, 'retiring');
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
// second pass confirmation
|
|
386
|
-
if (wrapNearViewport(w) || isFilled(w)) { updateWrapState(rec, 'live'); continue; }
|
|
387
|
-
releaseWrap(w);
|
|
434
|
+
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
435
|
+
//
|
|
436
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
437
|
+
//
|
|
438
|
+
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
439
|
+
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
440
|
+
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
441
|
+
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
442
|
+
// liste après un long scroll et bloquent les nouvelles injections.
|
|
443
|
+
//
|
|
444
|
+
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
445
|
+
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
446
|
+
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
447
|
+
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
448
|
+
|
|
449
|
+
function pruneOrphansBetween() {
|
|
450
|
+
const klass = 'ezoic-ad-between';
|
|
451
|
+
const cfg = KIND[klass];
|
|
452
|
+
const liveAnchors = new Set(Array.from(document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`))
|
|
453
|
+
.map(el => el.getAttribute(cfg.anchorAttr)).filter(Boolean));
|
|
454
|
+
|
|
455
|
+
for (const w of (S.wrapsByClass.get(klass) || [])) {
|
|
456
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
457
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
458
|
+
|
|
459
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
460
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
461
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
462
|
+
|
|
463
|
+
if (!liveAnchors.has(sid)) mutate(() => dropWrap(w));
|
|
388
464
|
}
|
|
389
465
|
}
|
|
390
466
|
|
|
391
|
-
|
|
392
|
-
if (S.maintenanceTimer) return;
|
|
393
|
-
S.maintenanceTimer = setTimeout(() => {
|
|
394
|
-
S.maintenanceTimer = 0;
|
|
395
|
-
maintenance();
|
|
396
|
-
}, CFG.maintenanceEveryMs);
|
|
397
|
-
}
|
|
467
|
+
// ── Injection ──────────────────────────────────────────────────────────────
|
|
398
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
471
|
+
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
472
|
+
*/
|
|
399
473
|
function ordinal(klass, el) {
|
|
400
474
|
const attr = KIND[klass]?.ordinalAttr;
|
|
401
475
|
if (attr) {
|
|
402
476
|
const v = el.getAttribute(attr);
|
|
403
477
|
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
404
478
|
}
|
|
479
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
405
480
|
let i = 0;
|
|
406
|
-
const
|
|
407
|
-
for (const s of el.parentElement?.children || []) {
|
|
481
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
408
482
|
if (s === el) return i;
|
|
409
483
|
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
410
484
|
}
|
|
411
485
|
return 0;
|
|
412
486
|
}
|
|
413
487
|
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
return isManaged(el.nextElementSibling) || isManaged(el.previousElementSibling);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function injectFor(klass, items, interval, showFirst) {
|
|
488
|
+
function injectBetween(klass, items, interval, showFirst, poolKey) {
|
|
489
|
+
if (!items.length) return 0;
|
|
420
490
|
let inserted = 0;
|
|
421
|
-
|
|
491
|
+
|
|
422
492
|
for (const el of items) {
|
|
423
|
-
if (inserted >=
|
|
493
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
424
494
|
if (!el?.isConnected) continue;
|
|
495
|
+
|
|
425
496
|
const ord = ordinal(klass, el);
|
|
426
497
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
427
|
-
if (
|
|
498
|
+
if (adjacentWrap(el)) continue;
|
|
499
|
+
|
|
428
500
|
const key = anchorKey(klass, el);
|
|
429
|
-
if (
|
|
501
|
+
if (findWrap(key)) continue;
|
|
502
|
+
|
|
430
503
|
const id = pickId(poolKey);
|
|
431
|
-
if (
|
|
432
|
-
|
|
504
|
+
if (id) {
|
|
505
|
+
const w = insertAfter(el, id, klass, key);
|
|
506
|
+
if (w) {
|
|
507
|
+
observePh(id);
|
|
508
|
+
if (!S.firstShown) { S.firstShown = true; enqueueShow(id); }
|
|
509
|
+
inserted++;
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
const recycled = recycleAndMove(klass, el, key);
|
|
513
|
+
if (!recycled) break;
|
|
514
|
+
inserted++;
|
|
515
|
+
}
|
|
433
516
|
}
|
|
434
517
|
return inserted;
|
|
435
518
|
}
|
|
436
519
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
520
|
+
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
function getIO() {
|
|
523
|
+
if (S.io) return S.io;
|
|
524
|
+
try {
|
|
525
|
+
S.io = new IntersectionObserver(entries => {
|
|
526
|
+
for (const e of entries) {
|
|
527
|
+
if (!e.isIntersecting) continue;
|
|
528
|
+
if (e.target instanceof Element) S.io?.unobserve(e.target);
|
|
529
|
+
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
530
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
531
|
+
}
|
|
532
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
533
|
+
} catch (_) { S.io = null; }
|
|
534
|
+
return S.io;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function observePh(id) {
|
|
538
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
539
|
+
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
457
540
|
}
|
|
458
541
|
|
|
459
542
|
function enqueueShow(id) {
|
|
460
|
-
if (isBlocked()) return;
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
S.showTimer = setTimeout(() => {
|
|
466
|
-
S.showTimer = 0;
|
|
467
|
-
drainShowQueue();
|
|
468
|
-
}, CFG.batchFlushMs);
|
|
543
|
+
if (!id || isBlocked()) return;
|
|
544
|
+
if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
545
|
+
if (S.inflight >= MAX_INFLIGHT) {
|
|
546
|
+
if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
|
|
547
|
+
return;
|
|
469
548
|
}
|
|
549
|
+
startShow(id);
|
|
470
550
|
}
|
|
471
551
|
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const preload = isMobile() ? 1600 : 1200;
|
|
480
|
-
if (r.top <= vh + preload && r.bottom >= -preload) enqueueShow(id);
|
|
481
|
-
} catch (_) {}
|
|
552
|
+
function drainQueue() {
|
|
553
|
+
if (isBlocked()) return;
|
|
554
|
+
while (S.inflight < MAX_INFLIGHT && S.pending.length) {
|
|
555
|
+
const id = S.pending.shift();
|
|
556
|
+
S.pendingSet.delete(id);
|
|
557
|
+
startShow(id);
|
|
558
|
+
}
|
|
482
559
|
}
|
|
483
560
|
|
|
484
|
-
function startShow(
|
|
485
|
-
if (!
|
|
486
|
-
S.
|
|
487
|
-
let
|
|
561
|
+
function startShow(id) {
|
|
562
|
+
if (!id || isBlocked()) return;
|
|
563
|
+
S.inflight++;
|
|
564
|
+
let done = false;
|
|
488
565
|
const release = () => {
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
S.
|
|
492
|
-
|
|
566
|
+
if (done) return;
|
|
567
|
+
done = true;
|
|
568
|
+
S.inflight = Math.max(0, S.inflight - 1);
|
|
569
|
+
drainQueue();
|
|
493
570
|
};
|
|
494
|
-
const
|
|
571
|
+
const timer = setTimeout(release, 7000);
|
|
495
572
|
|
|
496
573
|
requestAnimationFrame(() => {
|
|
497
574
|
try {
|
|
498
|
-
if (isBlocked()) { clearTimeout(
|
|
575
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
576
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
577
|
+
if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
|
|
578
|
+
|
|
579
|
+
const t = ts();
|
|
580
|
+
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
581
|
+
S.lastShow.set(id, t);
|
|
582
|
+
|
|
583
|
+
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
584
|
+
|
|
499
585
|
window.ezstandalone = window.ezstandalone || {};
|
|
500
586
|
const ez = window.ezstandalone;
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const ph = phEl(id);
|
|
506
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
507
|
-
if (!wrap) continue;
|
|
508
|
-
wrap.setAttribute(A_SHOWN, String(t));
|
|
509
|
-
const rec = ensureRegistry(id, wrap);
|
|
510
|
-
rec.wrap = wrap;
|
|
511
|
-
rec.shownAt = t;
|
|
512
|
-
rec.lastMutationAt = t;
|
|
513
|
-
rec.cooldownUntil = t + CFG.showCooldownMs;
|
|
514
|
-
classifyWrap(rec);
|
|
515
|
-
updateWrapState(rec, 'showing');
|
|
516
|
-
S.lastShowById.set(id, t);
|
|
517
|
-
valid.push(id);
|
|
518
|
-
}
|
|
519
|
-
if (!valid.length) { clearTimeout(guard); return release(); }
|
|
520
|
-
const run = () => {
|
|
521
|
-
try { ez.showAds(...valid); } catch (_) {
|
|
522
|
-
try { ez.showAds(valid); } catch (_) {}
|
|
523
|
-
}
|
|
524
|
-
setTimeout(() => {
|
|
525
|
-
for (const id of valid) {
|
|
526
|
-
const rec = S.regById.get(id);
|
|
527
|
-
if (!rec) continue;
|
|
528
|
-
const w = rec.wrap;
|
|
529
|
-
if (w?.isConnected) {
|
|
530
|
-
if (isFilled(w)) rec.lastMutationAt = now();
|
|
531
|
-
updateWrapState(rec, 'live');
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
clearTimeout(guard);
|
|
535
|
-
release();
|
|
536
|
-
}, CFG.showReleaseMs);
|
|
587
|
+
const doShow = () => {
|
|
588
|
+
try { ez.showAds(id); } catch (_) {}
|
|
589
|
+
scheduleEmptyCheck(id, t);
|
|
590
|
+
setTimeout(() => { clearTimeout(timer); release(); }, 700);
|
|
537
591
|
};
|
|
538
|
-
Array.isArray(ez.cmd) ? ez.cmd.push(
|
|
539
|
-
} catch (_) {
|
|
540
|
-
clearTimeout(guard);
|
|
541
|
-
release();
|
|
542
|
-
}
|
|
592
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
593
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
543
594
|
});
|
|
544
595
|
}
|
|
545
596
|
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!phEl(id)?.isConnected) continue;
|
|
557
|
-
picked.push(id);
|
|
558
|
-
}
|
|
559
|
-
if (picked.length) startShow(picked);
|
|
560
|
-
if (S.pendingShow.length) scheduleMaintenance();
|
|
597
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
598
|
+
setTimeout(() => {
|
|
599
|
+
try {
|
|
600
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
601
|
+
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
602
|
+
if (!wrap || !ph?.isConnected) return;
|
|
603
|
+
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
604
|
+
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
605
|
+
} catch (_) {}
|
|
606
|
+
}, EMPTY_CHECK_MS);
|
|
561
607
|
}
|
|
562
608
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const t = e.target;
|
|
569
|
-
if (!(t instanceof Element)) continue;
|
|
570
|
-
const id = parseInt(t.getAttribute('data-ezoic-id'), 10);
|
|
571
|
-
if (!Number.isFinite(id) || id <= 0) continue;
|
|
572
|
-
const rec = S.regById.get(id);
|
|
573
|
-
if (rec) rec.lastSeenAt = now();
|
|
574
|
-
if (e.isIntersecting) enqueueShow(id);
|
|
575
|
-
}
|
|
576
|
-
}, {
|
|
577
|
-
root: null,
|
|
578
|
-
rootMargin: isMobile() ? CFG.ioMarginMobile : CFG.ioMarginDesktop,
|
|
579
|
-
threshold: 0,
|
|
580
|
-
});
|
|
581
|
-
} catch (_) { S.io = null; }
|
|
582
|
-
return S.io;
|
|
583
|
-
}
|
|
609
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
610
|
+
//
|
|
611
|
+
// Intercepte ez.showAds() pour :
|
|
612
|
+
// – ignorer les appels pendant blockedUntil
|
|
613
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
584
614
|
|
|
585
615
|
function patchShowAds() {
|
|
586
|
-
const
|
|
616
|
+
const apply = () => {
|
|
587
617
|
try {
|
|
588
618
|
window.ezstandalone = window.ezstandalone || {};
|
|
589
619
|
const ez = window.ezstandalone;
|
|
590
|
-
if (window.
|
|
591
|
-
window.
|
|
620
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
621
|
+
window.__nbbEzPatched = true;
|
|
592
622
|
const orig = ez.showAds.bind(ez);
|
|
593
623
|
ez.showAds = function (...args) {
|
|
594
624
|
if (isBlocked()) return;
|
|
595
|
-
const ids
|
|
596
|
-
const valid = [];
|
|
625
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
597
626
|
const seen = new Set();
|
|
598
627
|
for (const v of ids) {
|
|
599
628
|
const id = parseInt(v, 10);
|
|
600
629
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
601
|
-
|
|
602
|
-
if (!ph?.isConnected) continue;
|
|
603
|
-
const wrap = ph.closest?.(`.${WRAP_CLASS}`);
|
|
604
|
-
if (!wrap?.isConnected) continue;
|
|
605
|
-
const rec = ensureRegistry(id, wrap);
|
|
606
|
-
if (rec.cooldownUntil && now() < rec.cooldownUntil && (now() - (rec.shownAt || 0)) < 500) continue;
|
|
630
|
+
if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
|
|
607
631
|
seen.add(id);
|
|
608
|
-
|
|
632
|
+
try { orig(id); } catch (_) {}
|
|
609
633
|
}
|
|
610
|
-
if (!valid.length) return;
|
|
611
|
-
try { return orig(...valid); } catch (_) { try { return orig(valid); } catch (_) {} }
|
|
612
634
|
};
|
|
613
635
|
} catch (_) {}
|
|
614
636
|
};
|
|
615
|
-
|
|
616
|
-
if (!window.
|
|
637
|
+
apply();
|
|
638
|
+
if (!window.__nbbEzPatched) {
|
|
617
639
|
window.ezstandalone = window.ezstandalone || {};
|
|
618
|
-
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(
|
|
640
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
619
641
|
}
|
|
620
642
|
}
|
|
621
643
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
644
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
async function runCore() {
|
|
647
|
+
if (isBlocked()) return 0;
|
|
648
|
+
patchShowAds();
|
|
649
|
+
|
|
650
|
+
const cfg = await fetchConfig();
|
|
651
|
+
if (!cfg || cfg.excluded) return 0;
|
|
652
|
+
initPools(cfg);
|
|
653
|
+
|
|
654
|
+
const kind = getKind();
|
|
655
|
+
if (kind === 'other') return 0;
|
|
656
|
+
|
|
657
|
+
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
658
|
+
if (!normBool(cfgEnable)) return 0;
|
|
659
|
+
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
660
|
+
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
if (kind === 'topic') return exec(
|
|
664
|
+
'ezoic-ad-message', getPosts,
|
|
665
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
if (kind === 'categoryTopics') {
|
|
669
|
+
pruneOrphansBetween();
|
|
670
|
+
return exec(
|
|
671
|
+
'ezoic-ad-between', getTopics,
|
|
672
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return exec(
|
|
677
|
+
'ezoic-ad-categories', getCategories,
|
|
678
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
function scheduleRun(cb) {
|
|
685
|
+
if (S.runQueued) return;
|
|
686
|
+
S.runQueued = true;
|
|
687
|
+
requestAnimationFrame(async () => {
|
|
688
|
+
S.runQueued = false;
|
|
689
|
+
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
690
|
+
let n = 0;
|
|
691
|
+
try { n = await runCore(); } catch (_) {}
|
|
692
|
+
try { cb?.(n); } catch (_) {}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function requestBurst() {
|
|
697
|
+
if (isBlocked()) return;
|
|
698
|
+
const t = ts();
|
|
699
|
+
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
700
|
+
S.lastBurstTs = t;
|
|
701
|
+
S.pageKey = pageKey();
|
|
702
|
+
S.burstDeadline = t + 2000;
|
|
703
|
+
|
|
704
|
+
if (S.burstActive) return;
|
|
705
|
+
S.burstActive = true;
|
|
706
|
+
S.burstCount = 0;
|
|
707
|
+
|
|
708
|
+
const step = () => {
|
|
709
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
710
|
+
S.burstActive = false; return;
|
|
631
711
|
}
|
|
632
|
-
|
|
712
|
+
S.burstCount++;
|
|
713
|
+
scheduleRun(n => {
|
|
714
|
+
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
715
|
+
setTimeout(step, n > 0 ? 150 : 300);
|
|
716
|
+
});
|
|
717
|
+
};
|
|
718
|
+
step();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
722
|
+
|
|
723
|
+
function cleanup() {
|
|
724
|
+
blockedUntil = ts() + 1500;
|
|
725
|
+
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
726
|
+
S.cfg = null;
|
|
727
|
+
S.poolsReady = false;
|
|
728
|
+
S.pools = { topics: [], posts: [], categories: [] };
|
|
729
|
+
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
730
|
+
S.mountedIds.clear();
|
|
731
|
+
S.lastShow.clear();
|
|
732
|
+
S.wrapByKey.clear();
|
|
733
|
+
S.wrapsByClass.clear();
|
|
734
|
+
S.kind = null;
|
|
735
|
+
S.inflight = 0;
|
|
736
|
+
S.pending = [];
|
|
737
|
+
S.pendingSet.clear();
|
|
738
|
+
S.burstActive = false;
|
|
739
|
+
S.runQueued = false;
|
|
740
|
+
S.firstShown = false;
|
|
633
741
|
}
|
|
634
742
|
|
|
635
|
-
|
|
743
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
744
|
+
|
|
745
|
+
function ensureDomObserver() {
|
|
636
746
|
if (S.domObs) return;
|
|
637
747
|
S.domObs = new MutationObserver(muts => {
|
|
638
|
-
if (S.
|
|
639
|
-
|
|
748
|
+
if (S.mutGuard > 0 || isBlocked()) return;
|
|
749
|
+
const relevant = (() => {
|
|
750
|
+
const k = getKind();
|
|
751
|
+
if (k === 'topic') return [SEL.post];
|
|
752
|
+
if (k === 'categoryTopics') return [SEL.topic];
|
|
753
|
+
if (k === 'categories') return [SEL.category];
|
|
754
|
+
return [SEL.post, SEL.topic, SEL.category];
|
|
755
|
+
})();
|
|
640
756
|
for (const m of muts) {
|
|
641
757
|
for (const n of m.addedNodes) {
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (Number.isFinite(id)) {
|
|
648
|
-
const rec = ensureRegistry(id, wrap);
|
|
649
|
-
rec.lastMutationAt = now();
|
|
650
|
-
classifyWrap(rec);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
if (n.matches?.(SEL.post) || n.matches?.(SEL.topic) || n.matches?.(SEL.category) ||
|
|
654
|
-
n.querySelector?.(SEL.post) || n.querySelector?.(SEL.topic) || n.querySelector?.(SEL.category)) {
|
|
655
|
-
shouldRun = true;
|
|
758
|
+
if (n.nodeType !== 1) continue;
|
|
759
|
+
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
760
|
+
if (relevant.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
761
|
+
relevant.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
762
|
+
requestBurst(); return;
|
|
656
763
|
}
|
|
657
764
|
}
|
|
658
|
-
for (const n of m.removedNodes) {
|
|
659
|
-
if (!(n instanceof Element)) continue;
|
|
660
|
-
if (n.classList?.contains(WRAP_CLASS) || n.querySelector?.(`.${WRAP_CLASS}`)) {
|
|
661
|
-
shouldRun = true;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
if (shouldRun) {
|
|
666
|
-
scheduleRun();
|
|
667
|
-
scheduleMaintenance();
|
|
668
765
|
}
|
|
669
766
|
});
|
|
670
767
|
try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
|
|
671
768
|
}
|
|
672
769
|
|
|
770
|
+
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
771
|
+
|
|
772
|
+
function muteConsole() {
|
|
773
|
+
if (window.__nbbEzMuted) return;
|
|
774
|
+
window.__nbbEzMuted = true;
|
|
775
|
+
const MUTED = [
|
|
776
|
+
'[EzoicAds JS]: Placeholder Id',
|
|
777
|
+
'No valid placeholders for loadMore',
|
|
778
|
+
'cannot call refresh on the same page',
|
|
779
|
+
'no placeholders are currently defined in Refresh',
|
|
780
|
+
'Debugger iframe already exists',
|
|
781
|
+
`with id ${PH_PREFIX}`,
|
|
782
|
+
];
|
|
783
|
+
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
784
|
+
const orig = console[m];
|
|
785
|
+
if (typeof orig !== 'function') continue;
|
|
786
|
+
console[m] = function (...a) {
|
|
787
|
+
if (typeof a[0] === 'string' && MUTED.some(p => a[0].includes(p))) return;
|
|
788
|
+
orig.apply(console, a);
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
673
793
|
function ensureTcfLocator() {
|
|
674
794
|
try {
|
|
675
795
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
676
796
|
const inject = () => {
|
|
677
797
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
678
798
|
const f = document.createElement('iframe');
|
|
679
|
-
f.style.display = 'none';
|
|
680
|
-
f.id = f.name = '__tcfapiLocator';
|
|
799
|
+
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
681
800
|
(document.body || document.documentElement).appendChild(f);
|
|
682
801
|
};
|
|
683
802
|
inject();
|
|
684
|
-
if (!window.
|
|
685
|
-
window.
|
|
686
|
-
window.
|
|
803
|
+
if (!window.__nbbTcfObs) {
|
|
804
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
805
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
687
806
|
}
|
|
688
807
|
} catch (_) {}
|
|
689
808
|
}
|
|
690
809
|
|
|
691
|
-
const
|
|
810
|
+
const _warmed = new Set();
|
|
692
811
|
function warmNetwork() {
|
|
693
812
|
const head = document.head;
|
|
694
813
|
if (!head) return;
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
['preconnect',
|
|
698
|
-
['preconnect',
|
|
699
|
-
['preconnect',
|
|
700
|
-
['dns-prefetch', 'https://g.ezoic.net',
|
|
814
|
+
for (const [rel, href, cors] of [
|
|
815
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
816
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
817
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
818
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
819
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
701
820
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
702
|
-
]
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
821
|
+
]) {
|
|
822
|
+
const k = `${rel}|${href}`;
|
|
823
|
+
if (_warmed.has(k)) continue;
|
|
824
|
+
_warmed.add(k);
|
|
825
|
+
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
707
826
|
const l = document.createElement('link');
|
|
708
827
|
l.rel = rel; l.href = href;
|
|
709
828
|
if (cors) l.crossOrigin = 'anonymous';
|
|
710
|
-
|
|
829
|
+
head.appendChild(l);
|
|
711
830
|
}
|
|
712
|
-
head.appendChild(frag);
|
|
713
831
|
}
|
|
714
832
|
|
|
715
|
-
|
|
716
|
-
if (isBlocked()) return 0;
|
|
717
|
-
patchShowAds();
|
|
718
|
-
const cfg = await fetchConfig();
|
|
719
|
-
if (!cfg || cfg.excluded) return 0;
|
|
720
|
-
initPools(cfg);
|
|
721
|
-
|
|
722
|
-
const kind = getKind();
|
|
723
|
-
if (kind === 'other') return 0;
|
|
724
|
-
|
|
725
|
-
if (kind === 'topic') {
|
|
726
|
-
if (!normBool(cfg.enableMessageAds)) return 0;
|
|
727
|
-
const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
|
|
728
|
-
return injectFor('ezoic-ad-message', getPosts(), interval, normBool(cfg.showFirstMessageAd));
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (kind === 'categoryTopics') {
|
|
732
|
-
if (!normBool(cfg.enableBetweenAds)) return 0;
|
|
733
|
-
const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 3);
|
|
734
|
-
return injectFor('ezoic-ad-between', getTopics(), interval, normBool(cfg.showFirstTopicAd));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (!normBool(cfg.enableCategoryAds)) return 0;
|
|
738
|
-
const interval = Math.max(1, parseInt(cfg.intervalCategories, 10) || 3);
|
|
739
|
-
return injectFor('ezoic-ad-categories', getCategories(), interval, normBool(cfg.showFirstCategoryAd));
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function scheduleRun() {
|
|
743
|
-
if (isBlocked()) return;
|
|
744
|
-
if (S.runTimer) return;
|
|
745
|
-
S.runTimer = setTimeout(async () => {
|
|
746
|
-
S.runTimer = 0;
|
|
747
|
-
const pk = pageKey();
|
|
748
|
-
if (S.pageKey && S.pageKey !== pk) return;
|
|
749
|
-
try { await runCore(); } catch (_) {}
|
|
750
|
-
scheduleMaintenance();
|
|
751
|
-
}, CFG.runDebounceMs);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function cleanup() {
|
|
755
|
-
S.blockedUntil = now() + 1500;
|
|
756
|
-
if (S.runTimer) { clearTimeout(S.runTimer); S.runTimer = 0; }
|
|
757
|
-
if (S.showTimer) { clearTimeout(S.showTimer); S.showTimer = 0; }
|
|
758
|
-
if (S.maintenanceTimer) { clearTimeout(S.maintenanceTimer); S.maintenanceTimer = 0; }
|
|
759
|
-
mutate(() => {
|
|
760
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) releaseWrap(w);
|
|
761
|
-
});
|
|
762
|
-
S.cfg = null;
|
|
763
|
-
S.poolsReady = false;
|
|
764
|
-
S.pools = { topics: [], posts: [], categories: [] };
|
|
765
|
-
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
766
|
-
S.mountedIds.clear();
|
|
767
|
-
S.wrapByKey.clear();
|
|
768
|
-
S.regById.clear();
|
|
769
|
-
S.pendingShow = [];
|
|
770
|
-
S.pendingSet.clear();
|
|
771
|
-
S.lastShowById.clear();
|
|
772
|
-
S.inflightShow = 0;
|
|
773
|
-
}
|
|
833
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
774
834
|
|
|
775
835
|
function bindNodeBB() {
|
|
776
836
|
const $ = window.jQuery;
|
|
777
837
|
if (!$) return;
|
|
778
|
-
|
|
779
|
-
$(window).
|
|
780
|
-
$(window).on('action:ajaxify.
|
|
781
|
-
|
|
782
|
-
S.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
getIO();
|
|
787
|
-
ensureObserver();
|
|
788
|
-
scheduleRun();
|
|
838
|
+
|
|
839
|
+
$(window).off('.nbbEzoic');
|
|
840
|
+
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
841
|
+
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
842
|
+
S.pageKey = pageKey();
|
|
843
|
+
S.kind = null;
|
|
844
|
+
blockedUntil = 0;
|
|
845
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
846
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
789
847
|
});
|
|
790
|
-
|
|
848
|
+
|
|
849
|
+
const burstEvts = [
|
|
791
850
|
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
792
851
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
793
|
-
];
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
852
|
+
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
853
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
854
|
+
|
|
797
855
|
try {
|
|
798
856
|
require(['hooks'], hooks => {
|
|
799
857
|
if (typeof hooks?.on !== 'function') return;
|
|
800
|
-
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
801
|
-
|
|
858
|
+
for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
|
|
859
|
+
'action:categories.loaded', 'action:topic.loaded']) {
|
|
860
|
+
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
802
861
|
}
|
|
803
862
|
});
|
|
804
863
|
} catch (_) {}
|
|
@@ -809,24 +868,22 @@
|
|
|
809
868
|
window.addEventListener('scroll', () => {
|
|
810
869
|
if (ticking) return;
|
|
811
870
|
ticking = true;
|
|
812
|
-
requestAnimationFrame(() => {
|
|
813
|
-
ticking = false;
|
|
814
|
-
if (!isBlocked()) {
|
|
815
|
-
scheduleMaintenance();
|
|
816
|
-
scheduleRun();
|
|
817
|
-
}
|
|
818
|
-
});
|
|
871
|
+
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
819
872
|
}, { passive: true });
|
|
820
873
|
}
|
|
821
874
|
|
|
875
|
+
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
876
|
+
|
|
822
877
|
S.pageKey = pageKey();
|
|
878
|
+
muteConsole();
|
|
823
879
|
ensureTcfLocator();
|
|
824
880
|
warmNetwork();
|
|
825
881
|
patchShowAds();
|
|
826
882
|
getIO();
|
|
827
|
-
|
|
883
|
+
ensureDomObserver();
|
|
828
884
|
bindNodeBB();
|
|
829
885
|
bindScroll();
|
|
830
|
-
|
|
831
|
-
|
|
886
|
+
blockedUntil = 0;
|
|
887
|
+
requestBurst();
|
|
888
|
+
|
|
832
889
|
})();
|