nodebb-plugin-ezoic-infinite 1.7.15 → 1.7.17
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 +68 -12
- package/package.json +13 -4
- package/public/client.js +231 -166
- package/public/style.css +5 -5
- package/public/templates/admin/plugins/ezoic-infinite.tpl +18 -8
package/library.js
CHANGED
|
@@ -9,8 +9,10 @@ const plugin = {};
|
|
|
9
9
|
|
|
10
10
|
function normalizeExcludedGroups(value) {
|
|
11
11
|
if (!value) return [];
|
|
12
|
-
|
|
13
|
-
return
|
|
12
|
+
const arr = Array.isArray(value) ? value : String(value).split(',');
|
|
13
|
+
return arr
|
|
14
|
+
.map(s => String(s).trim())
|
|
15
|
+
.filter(Boolean);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function parseBool(v, def = false) {
|
|
@@ -20,18 +22,30 @@ function parseBool(v, def = false) {
|
|
|
20
22
|
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
let _groupsCache = null;
|
|
26
|
+
let _groupsCacheAt = 0;
|
|
27
|
+
const GROUPS_TTL = 60000; // 60s
|
|
28
|
+
|
|
23
29
|
async function getAllGroups() {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
32
|
+
|
|
24
33
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
25
34
|
if (!names || !names.length) {
|
|
26
35
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
27
36
|
}
|
|
28
37
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
38
|
const data = await groups.getGroupsData(filtered);
|
|
39
|
+
|
|
30
40
|
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
31
|
-
const valid = data.filter(g => g && g.name);
|
|
41
|
+
const valid = (data || []).filter(g => g && g.name);
|
|
32
42
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
33
|
-
|
|
43
|
+
|
|
44
|
+
_groupsCache = valid;
|
|
45
|
+
_groupsCacheAt = now;
|
|
46
|
+
return _groupsCache;
|
|
34
47
|
}
|
|
48
|
+
|
|
35
49
|
let _settingsCache = null;
|
|
36
50
|
let _settingsCacheAt = 0;
|
|
37
51
|
const SETTINGS_TTL = 30000; // 30s
|
|
@@ -39,8 +53,13 @@ const SETTINGS_TTL = 30000; // 30s
|
|
|
39
53
|
async function getSettings() {
|
|
40
54
|
const now = Date.now();
|
|
41
55
|
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
let s = {};
|
|
57
|
+
try {
|
|
58
|
+
s = await meta.settings.get(SETTINGS_KEY);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
s = {};
|
|
61
|
+
}
|
|
62
|
+
_settingsCacheAt = now;
|
|
44
63
|
_settingsCache = {
|
|
45
64
|
// Between-post ads (simple blocks) in category topic list
|
|
46
65
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
@@ -60,6 +79,9 @@ async function getSettings() {
|
|
|
60
79
|
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
61
80
|
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
62
81
|
|
|
82
|
+
// Avoid globally muting console unless explicitly enabled
|
|
83
|
+
muteEzoicConsole: parseBool(s.muteEzoicConsole, false),
|
|
84
|
+
|
|
63
85
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
64
86
|
};
|
|
65
87
|
return _settingsCache;
|
|
@@ -68,7 +90,8 @@ async function getSettings() {
|
|
|
68
90
|
async function isUserExcluded(uid, excludedGroups) {
|
|
69
91
|
if (!uid || !excludedGroups.length) return false;
|
|
70
92
|
const userGroups = await groups.getUserGroups([uid]);
|
|
71
|
-
|
|
93
|
+
const excluded = new Set(excludedGroups.map(n => String(n).toLowerCase().trim()).filter(Boolean));
|
|
94
|
+
return (userGroups[0] || []).some(g => excluded.has(String(g.name).toLowerCase().trim()));
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
plugin.onSettingsSet = function (data) {
|
|
@@ -93,21 +116,53 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
93
116
|
const settings = await getSettings();
|
|
94
117
|
const allGroups = await getAllGroups();
|
|
95
118
|
|
|
119
|
+
const excludedSet = new Set((settings.excludedGroups || []).map(n => String(n).toLowerCase().trim()).filter(Boolean));
|
|
120
|
+
const allGroupsWithSelected = (allGroups || []).map(g => ({
|
|
121
|
+
...g,
|
|
122
|
+
selected: excludedSet.has(String(g.name).toLowerCase().trim()) ? 'selected' : '',
|
|
123
|
+
}));
|
|
124
|
+
|
|
96
125
|
res.render('admin/plugins/ezoic-infinite', {
|
|
97
126
|
title: 'Ezoic Infinite Ads',
|
|
98
127
|
...settings,
|
|
128
|
+
|
|
129
|
+
// SSR-friendly checkbox states
|
|
99
130
|
enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
|
|
131
|
+
showFirstTopicAd_checked: settings.showFirstTopicAd ? 'checked' : '',
|
|
132
|
+
|
|
133
|
+
enableCategoryAds_checked: settings.enableCategoryAds ? 'checked' : '',
|
|
134
|
+
showFirstCategoryAd_checked: settings.showFirstCategoryAd ? 'checked' : '',
|
|
135
|
+
|
|
100
136
|
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
101
|
-
|
|
137
|
+
showFirstMessageAd_checked: settings.showFirstMessageAd ? 'checked' : '',
|
|
138
|
+
|
|
139
|
+
muteEzoicConsole_checked: settings.muteEzoicConsole ? 'checked' : '',
|
|
140
|
+
|
|
141
|
+
allGroups: allGroupsWithSelected,
|
|
102
142
|
});
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
106
|
-
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
145
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.ensureLoggedIn, middleware.admin.checkPrivileges, middleware.admin.buildHeader, render);
|
|
146
|
+
router.get('/api/admin/plugins/ezoic-infinite', middleware.ensureLoggedIn, middleware.admin.checkPrivileges, render);
|
|
107
147
|
|
|
108
148
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
|
-
|
|
110
|
-
|
|
149
|
+
let settings;
|
|
150
|
+
try {
|
|
151
|
+
settings = await getSettings();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
settings = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!settings) {
|
|
157
|
+
return res.json({ excluded: false });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let excluded = false;
|
|
161
|
+
try {
|
|
162
|
+
excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
excluded = false;
|
|
165
|
+
}
|
|
111
166
|
|
|
112
167
|
res.json({
|
|
113
168
|
excluded,
|
|
@@ -123,6 +178,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
123
178
|
showFirstMessageAd: settings.showFirstMessageAd,
|
|
124
179
|
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
125
180
|
messageIntervalPosts: settings.messageIntervalPosts,
|
|
181
|
+
muteEzoicConsole: settings.muteEzoicConsole,
|
|
126
182
|
});
|
|
127
183
|
});
|
|
128
184
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.17",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
"infinite-scroll"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"nodebb": ">=4.0.0"
|
|
15
|
+
"nodebb": ">=4.0.0",
|
|
16
|
+
"node": ">=18"
|
|
16
17
|
},
|
|
17
18
|
"nbbpm": {
|
|
18
19
|
"compatibility": "^4.0.0"
|
|
19
20
|
},
|
|
20
|
-
"private": false
|
|
21
|
-
|
|
21
|
+
"private": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"library.js",
|
|
24
|
+
"plugin.json",
|
|
25
|
+
"public/"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"lint": "node -c library.js && node -c public/admin.js && node -c public/client.js"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/public/client.js
CHANGED
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js (v20)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Correctifs critiques vs v19
|
|
5
|
+
* ───────────────────────────
|
|
6
|
+
* [BUG FATAL] Pubs catégories disparaissent au premier scroll sur mobile
|
|
7
|
+
* pruneOrphans cherchait l'ancre via `[data-index]`, mais les éléments
|
|
8
|
+
* `li[component="categories/category"]` ont `data-cid`, pas `data-index`.
|
|
9
|
+
* → anchorEl toujours null → suppression à chaque runCore() → disparition.
|
|
10
|
+
* Fix : table KIND_META qui mappe chaque kindClass vers son attribut d'ancre
|
|
11
|
+
* stable (data-pid pour posts, data-index pour topics, data-cid pour catégories).
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* [BUG] decluster : break trop large stoppait tout quand UN wrap était en grâce
|
|
14
|
+
* Fix : on skip uniquement le wrap courant, pas toute la boucle.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Fix posts : baseTag vide → ordinal fallback cassé → pubs jamais injectées.
|
|
15
|
-
* IO fixe (une instance, jamais recréée). Burst cooldown 200 ms.
|
|
16
|
-
* Fix unobserve(null) → corruption IO → pubads error au scroll retour.
|
|
17
|
-
* Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
|
|
16
|
+
* [BUG] injectBetween : `break` sur pool épuisé empêchait d'observer les wraps
|
|
17
|
+
* existants sur les items suivants. Fix : `continue` au lieu de `break`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* [PERF] IntersectionObserver recréé à chaque scroll boost → très coûteux mobile.
|
|
20
|
+
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
21
|
+
*
|
|
22
|
+
* [PERF] Burst cooldown 100ms trop court sur mobile → rafales en cascade.
|
|
23
|
+
* Fix : 200ms.
|
|
24
|
+
*
|
|
25
|
+
* Nettoyage
|
|
26
|
+
* ─────────
|
|
27
|
+
* - Suppression du scroll boost (complexité sans gain mesurable côté SPA statique)
|
|
28
|
+
* - MAX_INFLIGHT unique desktop/mobile (inutile de différencier)
|
|
29
|
+
* - getAnchorKey/getGlobalOrdinal fusionnés en helpers cohérents
|
|
30
|
+
* - Commentaires internes allégés (code auto-documenté)
|
|
25
31
|
*/
|
|
26
32
|
(function () {
|
|
27
33
|
'use strict';
|
|
28
34
|
|
|
29
35
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
30
36
|
|
|
31
|
-
const WRAP_CLASS
|
|
32
|
-
const PH_PREFIX
|
|
33
|
-
const A_ANCHOR
|
|
34
|
-
const A_WRAPID
|
|
35
|
-
const A_CREATED
|
|
36
|
-
const A_SHOWN
|
|
37
|
-
|
|
38
|
-
const MIN_PRUNE_AGE_MS
|
|
39
|
-
const FILL_GRACE_MS
|
|
40
|
-
const EMPTY_CHECK_MS
|
|
41
|
-
const
|
|
42
|
-
const MAX_INFLIGHT
|
|
43
|
-
const SHOW_THROTTLE_MS
|
|
44
|
-
const BURST_COOLDOWN_MS
|
|
45
|
-
|
|
46
|
-
// IO
|
|
37
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
38
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
39
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
40
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic (number string)
|
|
41
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création (ms)
|
|
42
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds (ms)
|
|
43
|
+
|
|
44
|
+
const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
|
|
45
|
+
const FILL_GRACE_MS = 25_000; // fenêtre post-showAds où l'on ne decluster pas
|
|
46
|
+
const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
|
|
47
|
+
const MAX_INSERTS_PER_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
47
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
48
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
49
55
|
|
|
@@ -54,40 +60,41 @@
|
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
|
-
* Table
|
|
63
|
+
* Table centrale : kindClass → { selector DOM, attribut d'ancre stable }
|
|
58
64
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* data-cid catégories (id catégorie, immuable)
|
|
66
|
-
* ordinalAttr: attribut 0-based pour le calcul de l'intervalle
|
|
67
|
-
* data-index posts + topics (fourni par NodeBB)
|
|
68
|
-
* null catégories (page statique → fallback positionnel)
|
|
65
|
+
* L'attribut d'ancre est l'identifiant que NodeBB pose TOUJOURS sur l'élément,
|
|
66
|
+
* quelle que soit la page ou la virtualisation :
|
|
67
|
+
* posts → data-pid (id du message, unique et permanent)
|
|
68
|
+
* topics → data-index (position 0-based dans la liste, fourni par NodeBB)
|
|
69
|
+
* catégories → data-cid (id de la catégorie, unique et permanent)
|
|
70
|
+
* ← C'était le bug v19 : on cherchait data-index ici
|
|
69
71
|
*/
|
|
70
72
|
const KIND = {
|
|
71
|
-
'ezoic-ad-message': { sel: SEL.post,
|
|
72
|
-
'ezoic-ad-between': { sel: SEL.topic,
|
|
73
|
-
'ezoic-ad-categories': { sel: SEL.category,
|
|
73
|
+
'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid' },
|
|
74
|
+
'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index' },
|
|
75
|
+
'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid' },
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
78
80
|
const S = {
|
|
79
|
-
pageKey:
|
|
80
|
-
cfg:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
pageKey: null,
|
|
82
|
+
cfg: null,
|
|
83
|
+
|
|
84
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
85
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
86
|
+
mountedIds: new Set(), // IDs Ezoic actuellement dans le DOM
|
|
87
|
+
reuseSeq: new Map(), // id → compteur pour générer des IDs DOM uniques
|
|
88
|
+
lastShow: new Map(), // id → timestamp dernier show
|
|
89
|
+
|
|
90
|
+
io: null,
|
|
91
|
+
domObs: null,
|
|
92
|
+
mutGuard: 0, // compteur internalMutation
|
|
93
|
+
|
|
94
|
+
inflight: 0,
|
|
95
|
+
pending: [],
|
|
96
|
+
pendingSet: new Set(),
|
|
97
|
+
|
|
91
98
|
runQueued: false,
|
|
92
99
|
burstActive: false,
|
|
93
100
|
burstDeadline: 0,
|
|
@@ -96,11 +103,8 @@
|
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
let blockedUntil = 0;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
102
|
-
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
103
|
-
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
106
|
+
const isBlocked = () => Date.now() < blockedUntil;
|
|
107
|
+
const ts = () => Date.now();
|
|
104
108
|
|
|
105
109
|
function mutate(fn) {
|
|
106
110
|
S.mutGuard++;
|
|
@@ -118,20 +122,30 @@
|
|
|
118
122
|
return S.cfg;
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
function initPools(cfg) {
|
|
126
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
127
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
128
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
function parseIds(raw) {
|
|
122
132
|
const out = [], seen = new Set();
|
|
123
|
-
for (const v of String(raw || '').split(
|
|
133
|
+
for (const v of String(raw || '').split(/[\s,]+/).map(s => s.trim()).filter(Boolean)) {
|
|
124
134
|
const n = parseInt(v, 10);
|
|
125
135
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
126
136
|
}
|
|
127
137
|
return out;
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
// La réutilisation des IDs du pool est toujours autorisée pour éviter les
|
|
141
|
+
// situations "pool épuisé". Les IDs HTML des placeholders sont rendus uniques
|
|
142
|
+
// (suffixés) afin de ne pas dupliquer d'ID dans le DOM.
|
|
143
|
+
const allowReuse = () => true;
|
|
144
|
+
|
|
145
|
+
const isFilled = (n) =>
|
|
146
|
+
!!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
147
|
+
|
|
148
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
135
149
|
|
|
136
150
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
137
151
|
|
|
@@ -155,13 +169,13 @@
|
|
|
155
169
|
return 'other';
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
// ──
|
|
172
|
+
// ── DOM helpers ────────────────────────────────────────────────────────────
|
|
159
173
|
|
|
160
174
|
function getPosts() {
|
|
161
175
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
162
176
|
if (!el.isConnected) return false;
|
|
163
177
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
164
|
-
const p = el.parentElement?.closest(
|
|
178
|
+
const p = el.parentElement?.closest('[component="post"][data-pid]');
|
|
165
179
|
if (p && p !== el) return false;
|
|
166
180
|
return el.getAttribute('component') !== 'post/parent';
|
|
167
181
|
});
|
|
@@ -177,28 +191,36 @@
|
|
|
177
191
|
);
|
|
178
192
|
}
|
|
179
193
|
|
|
180
|
-
// ── Ancres stables
|
|
194
|
+
// ── Ancres stables ────────────────────────────────────────────────────────
|
|
181
195
|
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Retourne l'identifiant stable de l'élément selon son kindClass.
|
|
198
|
+
* Utilise l'attribut défini dans KIND (data-pid, data-index, data-cid).
|
|
199
|
+
* Fallback positionnel si l'attribut est absent.
|
|
200
|
+
*/
|
|
201
|
+
function stableId(kindClass, el) {
|
|
202
|
+
const attr = KIND[kindClass]?.anchorAttr;
|
|
184
203
|
if (attr) {
|
|
185
204
|
const v = el.getAttribute(attr);
|
|
186
205
|
if (v !== null && v !== '') return v;
|
|
187
206
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
// Fallback : position dans le parent
|
|
208
|
+
try {
|
|
209
|
+
let i = 0;
|
|
210
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
211
|
+
if (s === el) return `i${i}`;
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
} catch (_) {}
|
|
193
215
|
return 'i0';
|
|
194
216
|
}
|
|
195
217
|
|
|
196
|
-
const
|
|
218
|
+
const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
197
219
|
|
|
198
|
-
function findWrap(
|
|
220
|
+
function findWrap(anchorKey) {
|
|
199
221
|
try {
|
|
200
222
|
return document.querySelector(
|
|
201
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
223
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
202
224
|
);
|
|
203
225
|
} catch (_) { return null; }
|
|
204
226
|
}
|
|
@@ -208,35 +230,39 @@
|
|
|
208
230
|
function pickId(poolKey) {
|
|
209
231
|
const pool = S.pools[poolKey];
|
|
210
232
|
for (let t = 0; t < pool.length; t++) {
|
|
211
|
-
const i
|
|
233
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
212
234
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
213
235
|
const id = pool[i];
|
|
214
|
-
|
|
236
|
+
return id;
|
|
215
237
|
}
|
|
216
238
|
return null;
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
220
242
|
|
|
243
|
+
function nextDomPlaceholderId(id) {
|
|
244
|
+
const cur = (S.reuseSeq.get(id) || 0) + 1;
|
|
245
|
+
S.reuseSeq.set(id, cur);
|
|
246
|
+
return `${PH_PREFIX}${id}-${cur}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
function makeWrap(id, klass, key) {
|
|
222
|
-
const w
|
|
250
|
+
const w = document.createElement('div');
|
|
223
251
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
224
252
|
w.setAttribute(A_ANCHOR, key);
|
|
225
253
|
w.setAttribute(A_WRAPID, String(id));
|
|
226
254
|
w.setAttribute(A_CREATED, String(ts()));
|
|
227
255
|
w.style.cssText = 'width:100%;display:block;';
|
|
228
256
|
const ph = document.createElement('div');
|
|
229
|
-
ph.id =
|
|
257
|
+
ph.id = nextDomPlaceholderId(id);
|
|
230
258
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
231
259
|
w.appendChild(ph);
|
|
232
260
|
return w;
|
|
233
261
|
}
|
|
234
262
|
|
|
235
263
|
function insertAfter(el, id, klass, key) {
|
|
236
|
-
if (!el?.insertAdjacentElement)
|
|
237
|
-
if (findWrap(key))
|
|
238
|
-
if (S.mountedIds.has(id)) return null;
|
|
239
|
-
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
264
|
+
if (!el?.insertAdjacentElement) return null;
|
|
265
|
+
if (findWrap(key)) return null; // ancre déjà présente
|
|
240
266
|
const w = makeWrap(id, klass, key);
|
|
241
267
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
242
268
|
S.mountedIds.add(id);
|
|
@@ -245,12 +271,15 @@
|
|
|
245
271
|
|
|
246
272
|
function dropWrap(w) {
|
|
247
273
|
try {
|
|
248
|
-
// Unobserve avant remove — guard instanceof évite unobserve(null)
|
|
249
|
-
// qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
|
|
250
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
251
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
252
274
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
253
275
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
276
|
+
// IMPORTANT : ne passer unobserve que si c'est un vrai Element.
|
|
277
|
+
// unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
|
|
278
|
+
// "parameter 1 is not of type Element" sur le prochain observe).
|
|
279
|
+
try {
|
|
280
|
+
const ph = w.querySelector('[data-ezoic-id]') || w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
281
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
282
|
+
} catch (_) {}
|
|
254
283
|
w.remove();
|
|
255
284
|
} catch (_) {}
|
|
256
285
|
}
|
|
@@ -258,42 +287,46 @@
|
|
|
258
287
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
259
288
|
|
|
260
289
|
/**
|
|
261
|
-
* Supprime les wraps
|
|
290
|
+
* Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
|
|
291
|
+
*
|
|
292
|
+
* L'ancre est retrouvée via l'attribut stable défini dans KIND[kindClass].anchorAttr.
|
|
293
|
+
* Exemples :
|
|
294
|
+
* ezoic-ad-message → cherche [data-pid="123"]
|
|
295
|
+
* ezoic-ad-between → cherche [data-index="5"]
|
|
296
|
+
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
262
297
|
*
|
|
263
|
-
* On ne
|
|
264
|
-
* - Les wraps remplis peuvent être temporairement orphelins lors d'une
|
|
265
|
-
* virtualisation NodeBB — l'ancre reviendra.
|
|
266
|
-
* - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
|
|
267
|
-
* retirer le nœud sous ses pieds génère des erreurs non critiques mais
|
|
268
|
-
* inutiles. Le cleanup de navigation gère la suppression définitive.
|
|
298
|
+
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
269
299
|
*/
|
|
270
300
|
function pruneOrphans(klass) {
|
|
271
301
|
const meta = KIND[klass];
|
|
272
302
|
if (!meta) return;
|
|
273
303
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
|
|
305
|
+
|
|
306
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
307
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
277
308
|
|
|
278
309
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
279
|
-
const sid = key.slice(klass.length + 1);
|
|
280
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
310
|
+
const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
|
|
311
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
281
312
|
|
|
282
|
-
const
|
|
283
|
-
|
|
313
|
+
const anchorEl = document.querySelector(
|
|
314
|
+
`${baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`
|
|
315
|
+
);
|
|
284
316
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
285
|
-
}
|
|
317
|
+
});
|
|
286
318
|
}
|
|
287
319
|
|
|
288
320
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
289
321
|
|
|
290
322
|
/**
|
|
291
|
-
* Deux wraps adjacents → supprimer le moins prioritaire.
|
|
292
|
-
* Priorité : filled > en grâce
|
|
293
|
-
*
|
|
323
|
+
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
324
|
+
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
325
|
+
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
294
326
|
*/
|
|
295
327
|
function decluster(klass) {
|
|
296
328
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
329
|
+
// Grace sur le wrap courant : on le saute entièrement
|
|
297
330
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
298
331
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
299
332
|
|
|
@@ -302,11 +335,10 @@
|
|
|
302
335
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
303
336
|
|
|
304
337
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
305
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
338
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
306
339
|
|
|
307
340
|
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
308
341
|
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
309
|
-
// les deux remplis → on ne touche pas
|
|
310
342
|
break;
|
|
311
343
|
}
|
|
312
344
|
}
|
|
@@ -316,22 +348,23 @@
|
|
|
316
348
|
|
|
317
349
|
/**
|
|
318
350
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
319
|
-
*
|
|
320
|
-
*
|
|
351
|
+
* Pour posts/topics : data-index (NodeBB 3+/4+, toujours présent).
|
|
352
|
+
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
321
353
|
*/
|
|
322
354
|
function ordinal(klass, el) {
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
355
|
+
const di = el.getAttribute('data-index');
|
|
356
|
+
if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
|
|
357
|
+
// Fallback positionnel
|
|
358
|
+
try {
|
|
359
|
+
const tag = KIND[klass]?.sel?.split('[')?.[0] ?? '';
|
|
360
|
+
if (tag) {
|
|
361
|
+
let i = 0;
|
|
362
|
+
for (const n of el.parentElement?.querySelectorAll(`:scope > ${tag}`) ?? []) {
|
|
363
|
+
if (n === el) return i;
|
|
364
|
+
i++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} catch (_) {}
|
|
335
368
|
return 0;
|
|
336
369
|
}
|
|
337
370
|
|
|
@@ -340,18 +373,20 @@
|
|
|
340
373
|
let inserted = 0;
|
|
341
374
|
|
|
342
375
|
for (const el of items) {
|
|
343
|
-
if (inserted >=
|
|
376
|
+
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
344
377
|
if (!el?.isConnected) continue;
|
|
345
378
|
|
|
346
|
-
const ord
|
|
347
|
-
|
|
379
|
+
const ord = ordinal(klass, el);
|
|
380
|
+
const isTarget = (showFirst && ord === 0) || ((ord + 1) % interval === 0);
|
|
381
|
+
if (!isTarget) continue;
|
|
382
|
+
|
|
348
383
|
if (adjacentWrap(el)) continue;
|
|
349
384
|
|
|
350
|
-
const key =
|
|
351
|
-
if (findWrap(key)) continue;
|
|
385
|
+
const key = makeAnchorKey(klass, el);
|
|
386
|
+
if (findWrap(key)) continue; // déjà là → pas de pickId inutile
|
|
352
387
|
|
|
353
388
|
const id = pickId(poolKey);
|
|
354
|
-
if (!id) continue;
|
|
389
|
+
if (!id) continue; // pool momentanément épuisé, on continue (items suivants ≠ besoin d'id)
|
|
355
390
|
|
|
356
391
|
const w = insertAfter(el, id, klass, key);
|
|
357
392
|
if (w) { observePh(id); inserted++; }
|
|
@@ -363,6 +398,7 @@
|
|
|
363
398
|
|
|
364
399
|
function getIO() {
|
|
365
400
|
if (S.io) return S.io;
|
|
401
|
+
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
366
402
|
try {
|
|
367
403
|
S.io = new IntersectionObserver(entries => {
|
|
368
404
|
for (const e of entries) {
|
|
@@ -371,7 +407,7 @@
|
|
|
371
407
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
372
408
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
373
409
|
}
|
|
374
|
-
}, { root: null, rootMargin:
|
|
410
|
+
}, { root: null, rootMargin: margin, threshold: 0 });
|
|
375
411
|
} catch (_) { S.io = null; }
|
|
376
412
|
return S.io;
|
|
377
413
|
}
|
|
@@ -422,6 +458,7 @@
|
|
|
422
458
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
423
459
|
S.lastShow.set(id, t);
|
|
424
460
|
|
|
461
|
+
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
425
462
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
426
463
|
|
|
427
464
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -442,6 +479,7 @@
|
|
|
442
479
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
443
480
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
444
481
|
if (!wrap || !ph?.isConnected) return;
|
|
482
|
+
// Un show plus récent → ne pas toucher
|
|
445
483
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
446
484
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
447
485
|
} catch (_) {}
|
|
@@ -460,7 +498,7 @@
|
|
|
460
498
|
const orig = ez.showAds.bind(ez);
|
|
461
499
|
ez.showAds = function (...args) {
|
|
462
500
|
if (isBlocked()) return;
|
|
463
|
-
const ids
|
|
501
|
+
const ids = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
|
|
464
502
|
const seen = new Set();
|
|
465
503
|
for (const v of ids) {
|
|
466
504
|
const id = parseInt(v, 10);
|
|
@@ -479,7 +517,7 @@
|
|
|
479
517
|
}
|
|
480
518
|
}
|
|
481
519
|
|
|
482
|
-
// ── Core
|
|
520
|
+
// ── Core run ───────────────────────────────────────────────────────────────
|
|
483
521
|
|
|
484
522
|
async function runCore() {
|
|
485
523
|
if (isBlocked()) return 0;
|
|
@@ -494,9 +532,10 @@
|
|
|
494
532
|
|
|
495
533
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
496
534
|
if (!normBool(cfgEnable)) return 0;
|
|
535
|
+
const items = getItems();
|
|
497
536
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
498
537
|
pruneOrphans(klass);
|
|
499
|
-
const n = injectBetween(klass,
|
|
538
|
+
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
500
539
|
if (n) decluster(klass);
|
|
501
540
|
return n;
|
|
502
541
|
};
|
|
@@ -509,13 +548,14 @@
|
|
|
509
548
|
'ezoic-ad-between', getTopics,
|
|
510
549
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
511
550
|
);
|
|
512
|
-
return exec(
|
|
551
|
+
if (kind === 'categories') return exec(
|
|
513
552
|
'ezoic-ad-categories', getCategories,
|
|
514
553
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
515
554
|
);
|
|
555
|
+
return 0;
|
|
516
556
|
}
|
|
517
557
|
|
|
518
|
-
// ── Scheduler
|
|
558
|
+
// ── Scheduler / Burst ──────────────────────────────────────────────────────
|
|
519
559
|
|
|
520
560
|
function scheduleRun(cb) {
|
|
521
561
|
if (S.runQueued) return;
|
|
@@ -525,7 +565,7 @@
|
|
|
525
565
|
if (S.pageKey && pageKey() !== S.pageKey) return;
|
|
526
566
|
let n = 0;
|
|
527
567
|
try { n = await runCore(); } catch (_) {}
|
|
528
|
-
cb?.(n);
|
|
568
|
+
try { cb?.(n); } catch (_) {}
|
|
529
569
|
});
|
|
530
570
|
}
|
|
531
571
|
|
|
@@ -533,8 +573,10 @@
|
|
|
533
573
|
if (isBlocked()) return;
|
|
534
574
|
const t = ts();
|
|
535
575
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
536
|
-
S.lastBurstTs
|
|
537
|
-
|
|
576
|
+
S.lastBurstTs = t;
|
|
577
|
+
|
|
578
|
+
const pk = pageKey();
|
|
579
|
+
S.pageKey = pk;
|
|
538
580
|
S.burstDeadline = t + 2000;
|
|
539
581
|
|
|
540
582
|
if (S.burstActive) return;
|
|
@@ -542,7 +584,7 @@
|
|
|
542
584
|
S.burstCount = 0;
|
|
543
585
|
|
|
544
586
|
const step = () => {
|
|
545
|
-
if (pageKey() !==
|
|
587
|
+
if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
546
588
|
S.burstActive = false; return;
|
|
547
589
|
}
|
|
548
590
|
S.burstCount++;
|
|
@@ -554,10 +596,14 @@
|
|
|
554
596
|
step();
|
|
555
597
|
}
|
|
556
598
|
|
|
557
|
-
// ── Cleanup navigation
|
|
599
|
+
// ── Cleanup (navigation ajaxify) ───────────────────────────────────────────
|
|
558
600
|
|
|
559
601
|
function cleanup() {
|
|
560
602
|
blockedUntil = ts() + 1500;
|
|
603
|
+
|
|
604
|
+
// Disconnect observers to avoid doing work while ajaxify swaps content
|
|
605
|
+
try { if (S.domObs) { S.domObs.disconnect(); S.domObs = null; } } catch (_) {}
|
|
606
|
+
try { if (window.__nbbTcfObs) { window.__nbbTcfObs.disconnect(); window.__nbbTcfObs = null; } } catch (_) {}
|
|
561
607
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
562
608
|
S.cfg = null;
|
|
563
609
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
@@ -571,17 +617,19 @@
|
|
|
571
617
|
S.runQueued = false;
|
|
572
618
|
}
|
|
573
619
|
|
|
574
|
-
// ──
|
|
620
|
+
// ── DOM Observer ───────────────────────────────────────────────────────────
|
|
575
621
|
|
|
576
622
|
function ensureDomObserver() {
|
|
577
623
|
if (S.domObs) return;
|
|
578
|
-
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
579
624
|
S.domObs = new MutationObserver(muts => {
|
|
580
625
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
581
626
|
for (const m of muts) {
|
|
627
|
+
if (!m.addedNodes?.length) continue;
|
|
582
628
|
for (const n of m.addedNodes) {
|
|
583
629
|
if (n.nodeType !== 1) continue;
|
|
584
|
-
if (
|
|
630
|
+
if (n.matches?.(SEL.post) || n.querySelector?.(SEL.post) ||
|
|
631
|
+
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
632
|
+
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
585
633
|
requestBurst(); return;
|
|
586
634
|
}
|
|
587
635
|
}
|
|
@@ -593,6 +641,7 @@
|
|
|
593
641
|
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
594
642
|
|
|
595
643
|
function muteConsole() {
|
|
644
|
+
if (!S.cfg || !S.cfg.muteEzoicConsole) return;
|
|
596
645
|
if (window.__nbbEzMuted) return;
|
|
597
646
|
window.__nbbEzMuted = true;
|
|
598
647
|
const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
|
|
@@ -607,21 +656,29 @@
|
|
|
607
656
|
}
|
|
608
657
|
|
|
609
658
|
function ensureTcfLocator() {
|
|
610
|
-
//
|
|
611
|
-
// En navigation ajaxify, NodeBB peut
|
|
612
|
-
//
|
|
659
|
+
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
660
|
+
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
661
|
+
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
662
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
663
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
664
|
+
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
613
665
|
try {
|
|
614
666
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
667
|
+
|
|
615
668
|
const inject = () => {
|
|
616
669
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
617
670
|
const f = document.createElement('iframe');
|
|
618
671
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
619
672
|
(document.body || document.documentElement).appendChild(f);
|
|
620
673
|
};
|
|
674
|
+
|
|
621
675
|
inject();
|
|
676
|
+
|
|
677
|
+
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
622
678
|
if (!window.__nbbTcfObs) {
|
|
623
|
-
window.__nbbTcfObs = new MutationObserver(inject);
|
|
624
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
679
|
+
window.__nbbTcfObs = new MutationObserver(() => inject());
|
|
680
|
+
window.__nbbTcfObs.observe(document.documentElement,
|
|
681
|
+
{ childList: true, subtree: true });
|
|
625
682
|
}
|
|
626
683
|
} catch (_) {}
|
|
627
684
|
}
|
|
@@ -631,10 +688,10 @@
|
|
|
631
688
|
const head = document.head;
|
|
632
689
|
if (!head) return;
|
|
633
690
|
for (const [rel, href, cors] of [
|
|
634
|
-
['preconnect', 'https://g.ezoic.net', true
|
|
635
|
-
['preconnect', 'https://go.ezoic.net', true
|
|
636
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true
|
|
637
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true
|
|
691
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
692
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
693
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
694
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
638
695
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
639
696
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
640
697
|
]) {
|
|
@@ -648,7 +705,7 @@
|
|
|
648
705
|
}
|
|
649
706
|
}
|
|
650
707
|
|
|
651
|
-
// ── Bindings
|
|
708
|
+
// ── Bindings NodeBB ────────────────────────────────────────────────────────
|
|
652
709
|
|
|
653
710
|
function bindNodeBB() {
|
|
654
711
|
const $ = window.jQuery;
|
|
@@ -659,16 +716,19 @@
|
|
|
659
716
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
660
717
|
S.pageKey = pageKey();
|
|
661
718
|
blockedUntil = 0;
|
|
662
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
663
|
-
|
|
719
|
+
muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
|
|
720
|
+
getIO(); ensureDomObserver(); requestBurst();
|
|
664
721
|
});
|
|
665
722
|
|
|
666
|
-
const
|
|
667
|
-
'action:ajaxify.contentLoaded',
|
|
723
|
+
const BURST_EVENTS = [
|
|
724
|
+
'action:ajaxify.contentLoaded',
|
|
725
|
+
'action:posts.loaded', 'action:topics.loaded',
|
|
668
726
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
669
727
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
670
|
-
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
671
728
|
|
|
729
|
+
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
730
|
+
|
|
731
|
+
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
672
732
|
try {
|
|
673
733
|
require(['hooks'], hooks => {
|
|
674
734
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -684,6 +744,11 @@
|
|
|
684
744
|
let ticking = false;
|
|
685
745
|
window.addEventListener('scroll', () => {
|
|
686
746
|
if (ticking) return;
|
|
747
|
+
// Scroll bursts only matter on pages where content streams in
|
|
748
|
+
const kind = getKind();
|
|
749
|
+
if (kind !== 'topic' && kind !== 'categoryTopics' && kind !== 'categories') return;
|
|
750
|
+
if (isBlocked()) return;
|
|
751
|
+
|
|
687
752
|
ticking = true;
|
|
688
753
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
689
754
|
}, { passive: true });
|
package/public/style.css
CHANGED
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
overflow: hidden !important;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/* ──
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
/* ── Note ───────────────────────────────────────────────────────────── */
|
|
75
|
+
/*
|
|
76
|
+
On évite volontairement de cibler .ezoic-ad globalement,
|
|
77
|
+
pour ne pas impacter d'autres emplacements/optimisations Ezoic hors plugin.
|
|
78
|
+
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
|
|
9
9
|
<label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
|
|
10
10
|
<div class="form-check mt-2">
|
|
11
|
-
<input class="form-check-input" type="checkbox" name="showFirstTopicAd" />
|
|
11
|
+
<input class="form-check-input" type="checkbox" name="showFirstTopicAd" {showFirstTopicAd_checked} />
|
|
12
12
|
<label class="form-check-label">Afficher une pub après le 1er sujet</label>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<div class="mb-3">
|
|
17
17
|
<label class="form-label" for="placeholderIds">Pool d’IDs Ezoic (entre posts)</label>
|
|
18
18
|
<textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
|
|
19
|
-
<p class="form-text">Un ID par ligne (ou séparé par virgules/espaces)
|
|
19
|
+
<p class="form-text">Un ID par ligne (ou séparé par virgules/espaces).</p>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div class="mb-3">
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
<p class="form-text">Insère des pubs entre les catégories sur la page d’accueil (liste des catégories).</p>
|
|
34
34
|
|
|
35
35
|
<div class="form-check mb-3">
|
|
36
|
-
<input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
|
|
36
|
+
<input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds" {enableCategoryAds_checked}>
|
|
37
37
|
<label class="form-check-label" for="enableCategoryAds">Activer les pubs entre les catégories</label>
|
|
38
38
|
<div class="form-check mt-2">
|
|
39
|
-
<input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
|
|
39
|
+
<input class="form-check-input" type="checkbox" name="showFirstCategoryAd" {showFirstCategoryAd_checked} />
|
|
40
40
|
<label class="form-check-label">Afficher une pub après la 1ère catégorie</label>
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<div class="mb-3">
|
|
45
45
|
<label class="form-label" for="categoryPlaceholderIds">Pool d’IDs Ezoic (catégories)</label>
|
|
46
46
|
<textarea id="categoryPlaceholderIds" name="categoryPlaceholderIds" class="form-control" rows="4">{categoryPlaceholderIds}</textarea>
|
|
47
|
-
<p class="form-text">IDs numériques, un par ligne. Utilise un pool dédié (différent des pools topics/messages).</p>
|
|
47
|
+
<p class="form-text">IDs numériques, un par ligne (ou séparés par virgules/espaces). Utilise un pool dédié (différent des pools topics/messages).</p>
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div class="mb-3">
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
<input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
|
|
60
60
|
<label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
|
|
61
61
|
<div class="form-check mt-2">
|
|
62
|
-
<input class="form-check-input" type="checkbox" name="showFirstMessageAd" />
|
|
62
|
+
<input class="form-check-input" type="checkbox" name="showFirstMessageAd" {showFirstMessageAd_checked} />
|
|
63
63
|
<label class="form-check-label">Afficher une pub après le 1er message</label>
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
<div class="mb-3">
|
|
68
68
|
<label class="form-label" for="messagePlaceholderIds">Pool d’IDs Ezoic (message)</label>
|
|
69
69
|
<textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
|
|
70
|
-
<p class="form-text">Pool séparé recommandé pour
|
|
70
|
+
<p class="form-text">Pool séparé recommandé pour limiter la réutilisation d’IDs entre emplacements.</p>
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
<div class="mb-3">
|
|
@@ -82,12 +82,22 @@
|
|
|
82
82
|
<label class="form-label" for="excludedGroups">Groupes exclus</label>
|
|
83
83
|
<select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
|
|
84
84
|
<!-- BEGIN allGroups -->
|
|
85
|
-
<option value="{allGroups.name}">{allGroups.name}</option>
|
|
85
|
+
<option value="{allGroups.name}" {allGroups.selected}>{allGroups.name}</option>
|
|
86
86
|
<!-- END allGroups -->
|
|
87
87
|
</select>
|
|
88
88
|
<p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
|
|
89
89
|
</div>
|
|
90
90
|
|
|
91
|
+
|
|
92
|
+
<hr/>
|
|
93
|
+
|
|
94
|
+
<h4 class="mt-3">Options avancées</h4>
|
|
95
|
+
|
|
96
|
+
<div class="form-check mb-3">
|
|
97
|
+
<input class="form-check-input" type="checkbox" id="muteEzoicConsole" name="muteEzoicConsole" {muteEzoicConsole_checked}>
|
|
98
|
+
<label class="form-check-label" for="muteEzoicConsole">Filtrer certains logs Ezoic dans la console (évite le bruit)</label>
|
|
99
|
+
<p class="form-text">Désactivé par défaut pour ne pas impacter les logs d’autres plugins.</p>
|
|
100
|
+
</div>
|
|
91
101
|
<button id="save" class="btn btn-primary">Enregistrer</button>
|
|
92
102
|
</form>
|
|
93
103
|
</div>
|