nodebb-plugin-ezoic-infinite 1.7.17 → 1.7.19
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 +12 -68
- package/package.json +4 -13
- package/public/client.js +237 -234
- package/public/style.css +5 -5
- package/public/templates/admin/plugins/ezoic-infinite.tpl +8 -18
package/library.js
CHANGED
|
@@ -9,10 +9,8 @@ const plugin = {};
|
|
|
9
9
|
|
|
10
10
|
function normalizeExcludedGroups(value) {
|
|
11
11
|
if (!value) return [];
|
|
12
|
-
|
|
13
|
-
return
|
|
14
|
-
.map(s => String(s).trim())
|
|
15
|
-
.filter(Boolean);
|
|
12
|
+
if (Array.isArray(value)) return value;
|
|
13
|
+
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
function parseBool(v, def = false) {
|
|
@@ -22,30 +20,18 @@ function parseBool(v, def = false) {
|
|
|
22
20
|
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
let _groupsCache = null;
|
|
26
|
-
let _groupsCacheAt = 0;
|
|
27
|
-
const GROUPS_TTL = 60000; // 60s
|
|
28
|
-
|
|
29
23
|
async function getAllGroups() {
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
32
|
-
|
|
33
24
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
34
25
|
if (!names || !names.length) {
|
|
35
26
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
36
27
|
}
|
|
37
28
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
38
29
|
const data = await groups.getGroupsData(filtered);
|
|
39
|
-
|
|
40
30
|
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
41
|
-
const valid =
|
|
31
|
+
const valid = data.filter(g => g && g.name);
|
|
42
32
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
43
|
-
|
|
44
|
-
_groupsCache = valid;
|
|
45
|
-
_groupsCacheAt = now;
|
|
46
|
-
return _groupsCache;
|
|
33
|
+
return valid;
|
|
47
34
|
}
|
|
48
|
-
|
|
49
35
|
let _settingsCache = null;
|
|
50
36
|
let _settingsCacheAt = 0;
|
|
51
37
|
const SETTINGS_TTL = 30000; // 30s
|
|
@@ -53,13 +39,8 @@ const SETTINGS_TTL = 30000; // 30s
|
|
|
53
39
|
async function getSettings() {
|
|
54
40
|
const now = Date.now();
|
|
55
41
|
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
s = await meta.settings.get(SETTINGS_KEY);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
s = {};
|
|
61
|
-
}
|
|
62
|
-
_settingsCacheAt = now;
|
|
42
|
+
const s = await meta.settings.get(SETTINGS_KEY);
|
|
43
|
+
_settingsCacheAt = Date.now();
|
|
63
44
|
_settingsCache = {
|
|
64
45
|
// Between-post ads (simple blocks) in category topic list
|
|
65
46
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
@@ -79,9 +60,6 @@ async function getSettings() {
|
|
|
79
60
|
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
80
61
|
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
81
62
|
|
|
82
|
-
// Avoid globally muting console unless explicitly enabled
|
|
83
|
-
muteEzoicConsole: parseBool(s.muteEzoicConsole, false),
|
|
84
|
-
|
|
85
63
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
86
64
|
};
|
|
87
65
|
return _settingsCache;
|
|
@@ -90,8 +68,7 @@ async function getSettings() {
|
|
|
90
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
91
69
|
if (!uid || !excludedGroups.length) return false;
|
|
92
70
|
const userGroups = await groups.getUserGroups([uid]);
|
|
93
|
-
|
|
94
|
-
return (userGroups[0] || []).some(g => excluded.has(String(g.name).toLowerCase().trim()));
|
|
71
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
95
72
|
}
|
|
96
73
|
|
|
97
74
|
plugin.onSettingsSet = function (data) {
|
|
@@ -116,53 +93,21 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
116
93
|
const settings = await getSettings();
|
|
117
94
|
const allGroups = await getAllGroups();
|
|
118
95
|
|
|
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
|
-
|
|
125
96
|
res.render('admin/plugins/ezoic-infinite', {
|
|
126
97
|
title: 'Ezoic Infinite Ads',
|
|
127
98
|
...settings,
|
|
128
|
-
|
|
129
|
-
// SSR-friendly checkbox states
|
|
130
99
|
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
|
-
|
|
136
100
|
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
muteEzoicConsole_checked: settings.muteEzoicConsole ? 'checked' : '',
|
|
140
|
-
|
|
141
|
-
allGroups: allGroupsWithSelected,
|
|
101
|
+
allGroups,
|
|
142
102
|
});
|
|
143
103
|
}
|
|
144
104
|
|
|
145
|
-
router.get('/admin/plugins/ezoic-infinite', middleware.
|
|
146
|
-
router.get('/api/admin/plugins/ezoic-infinite',
|
|
105
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
106
|
+
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
147
107
|
|
|
148
108
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
109
|
+
const settings = await getSettings();
|
|
110
|
+
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
166
111
|
|
|
167
112
|
res.json({
|
|
168
113
|
excluded,
|
|
@@ -178,7 +123,6 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
178
123
|
showFirstMessageAd: settings.showFirstMessageAd,
|
|
179
124
|
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
180
125
|
messageIntervalPosts: settings.messageIntervalPosts,
|
|
181
|
-
muteEzoicConsole: settings.muteEzoicConsole,
|
|
182
126
|
});
|
|
183
127
|
});
|
|
184
128
|
};
|
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.19",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,19 +12,10 @@
|
|
|
12
12
|
"infinite-scroll"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"nodebb": ">=4.0.0"
|
|
16
|
-
"node": ">=18"
|
|
15
|
+
"nodebb": ">=4.0.0"
|
|
17
16
|
},
|
|
18
17
|
"nbbpm": {
|
|
19
18
|
"compatibility": "^4.0.0"
|
|
20
19
|
},
|
|
21
|
-
"private": false
|
|
22
|
-
|
|
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
|
-
}
|
|
20
|
+
"private": false
|
|
21
|
+
}
|
package/public/client.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v25
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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).
|
|
4
|
+
* Historique des corrections majeures
|
|
5
|
+
* ────────────────────────────────────
|
|
6
|
+
* v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
|
|
7
|
+
* Suppression du recyclage de wraps (moveWrapAfter). Cleanup complet navigation.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
|
|
10
|
+
* la position dans le batch courant.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* v20 Table KIND : anchorAttr/ordinalAttr/baseTag explicites par kindClass.
|
|
13
|
+
* Fix fatal catégories : data-cid au lieu de data-index inexistant.
|
|
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.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - Commentaires internes allégés (code auto-documenté)
|
|
19
|
+
* v25 Base v20.1 avec :
|
|
20
|
+
* • Fix scroll-up / virtualisation NodeBB :
|
|
21
|
+
* – pruneOrphans : PRUNE_STABLE_MS = 45 s, isFilled guard en premier.
|
|
22
|
+
* – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
|
|
23
|
+
* • Recyclage d'id (pool épuisé en infinite scroll) :
|
|
24
|
+
* – pickRecyclableWrap() : sélectionne le wrap vide le plus loin au-dessus
|
|
25
|
+
* du viewport (seuil -6 × vh), jamais pour ezoic-ad-message.
|
|
26
|
+
* – moveWrapAfter() : déplace le wrap vers sa nouvelle ancre.
|
|
27
|
+
* – scrollDir tracking pour n'autoriser le recyclage qu'en scroll down.
|
|
28
|
+
* • Table KIND unifiée avec baseTag + ordinalAttr + recyclable flag.
|
|
29
|
+
* • ordinal() : utilise KIND[klass].ordinalAttr, fallback positionnel propre.
|
|
31
30
|
*/
|
|
32
31
|
(function () {
|
|
33
32
|
'use strict';
|
|
34
33
|
|
|
35
34
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
35
|
|
|
37
|
-
const WRAP_CLASS
|
|
38
|
-
const PH_PREFIX
|
|
39
|
-
const A_ANCHOR
|
|
40
|
-
const A_WRAPID
|
|
41
|
-
const A_CREATED
|
|
42
|
-
const A_SHOWN
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const FILL_GRACE_MS
|
|
46
|
-
const EMPTY_CHECK_MS
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
37
|
+
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
38
|
+
const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
|
|
39
|
+
const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
|
|
40
|
+
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
41
|
+
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
42
|
+
|
|
43
|
+
const PRUNE_STABLE_MS = 45_000; // délai avant pruning (évite faux-orphelins scroll-up)
|
|
44
|
+
const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
|
|
45
|
+
const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
|
|
46
|
+
const RECYCLE_THRESHOLD = 6; // nb de viewports au-dessus du seuil de recyclage
|
|
47
|
+
const MAX_INSERTS_RUN = 6;
|
|
48
|
+
const MAX_INFLIGHT = 4;
|
|
49
|
+
const SHOW_THROTTLE_MS = 900;
|
|
50
|
+
const BURST_COOLDOWN_MS = 200;
|
|
51
|
+
|
|
52
|
+
// IO : marges larges fixes — une seule instance, jamais recréée
|
|
53
53
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
54
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
55
|
|
|
@@ -60,51 +60,54 @@
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Table
|
|
63
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
65
|
+
* sel : sélecteur CSS complet
|
|
66
|
+
* baseTag : préfixe tag pour querySelector d'ancre
|
|
67
|
+
* (vide pour posts car sélecteur commence par '[')
|
|
68
|
+
* anchorAttr : attribut DOM stable → clé unique du wrap
|
|
69
|
+
* data-pid posts / data-index topics / data-cid catégories
|
|
70
|
+
* ordinalAttr: attribut 0-based pour calcul de l'intervalle
|
|
71
|
+
* null → fallback positionnel (catégories)
|
|
72
|
+
* recyclable : autoriser le recyclage d'id quand le pool est épuisé
|
|
73
|
+
* false pour ezoic-ad-message (sauts visuels indésirables)
|
|
71
74
|
*/
|
|
72
75
|
const KIND = {
|
|
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' },
|
|
76
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index', recyclable: false },
|
|
77
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index', recyclable: true },
|
|
78
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null, recyclable: true },
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
// ── État ───────────────────────────────────────────────────────────────────
|
|
79
82
|
|
|
80
83
|
const S = {
|
|
81
|
-
pageKey:
|
|
82
|
-
cfg:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
inflight: 0,
|
|
95
|
-
pending: [],
|
|
96
|
-
pendingSet: new Set(),
|
|
97
|
-
|
|
84
|
+
pageKey: null,
|
|
85
|
+
cfg: null,
|
|
86
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
87
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
88
|
+
mountedIds: new Set(),
|
|
89
|
+
lastShow: new Map(),
|
|
90
|
+
io: null,
|
|
91
|
+
domObs: null,
|
|
92
|
+
mutGuard: 0,
|
|
93
|
+
inflight: 0,
|
|
94
|
+
pending: [],
|
|
95
|
+
pendingSet: new Set(),
|
|
98
96
|
runQueued: false,
|
|
99
97
|
burstActive: false,
|
|
100
98
|
burstDeadline: 0,
|
|
101
99
|
burstCount: 0,
|
|
102
100
|
lastBurstTs: 0,
|
|
101
|
+
scrollDir: 1, // 1 = down, -1 = up
|
|
102
|
+
lastScrollY: 0,
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
let blockedUntil = 0;
|
|
106
|
-
const
|
|
107
|
-
const
|
|
106
|
+
const ts = () => Date.now();
|
|
107
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
108
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
109
|
+
const normBool = (v) => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
110
|
+
const isFilled = (n) => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
108
111
|
|
|
109
112
|
function mutate(fn) {
|
|
110
113
|
S.mutGuard++;
|
|
@@ -122,30 +125,20 @@
|
|
|
122
125
|
return S.cfg;
|
|
123
126
|
}
|
|
124
127
|
|
|
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
|
-
|
|
131
128
|
function parseIds(raw) {
|
|
132
129
|
const out = [], seen = new Set();
|
|
133
|
-
for (const v of String(raw || '').split(/
|
|
130
|
+
for (const v of String(raw || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
134
131
|
const n = parseInt(v, 10);
|
|
135
132
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
136
133
|
}
|
|
137
134
|
return out;
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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; } };
|
|
137
|
+
function initPools(cfg) {
|
|
138
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
139
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
140
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
141
|
+
}
|
|
149
142
|
|
|
150
143
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
151
144
|
|
|
@@ -169,13 +162,13 @@
|
|
|
169
162
|
return 'other';
|
|
170
163
|
}
|
|
171
164
|
|
|
172
|
-
// ── DOM
|
|
165
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
173
166
|
|
|
174
167
|
function getPosts() {
|
|
175
168
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
176
169
|
if (!el.isConnected) return false;
|
|
177
170
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
178
|
-
const p = el.parentElement?.closest(
|
|
171
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
179
172
|
if (p && p !== el) return false;
|
|
180
173
|
return el.getAttribute('component') !== 'post/parent';
|
|
181
174
|
});
|
|
@@ -191,36 +184,28 @@
|
|
|
191
184
|
);
|
|
192
185
|
}
|
|
193
186
|
|
|
194
|
-
// ── Ancres stables
|
|
187
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
195
188
|
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
189
|
+
function stableId(klass, el) {
|
|
190
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
203
191
|
if (attr) {
|
|
204
192
|
const v = el.getAttribute(attr);
|
|
205
193
|
if (v !== null && v !== '') return v;
|
|
206
194
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
i++;
|
|
213
|
-
}
|
|
214
|
-
} catch (_) {}
|
|
195
|
+
let i = 0;
|
|
196
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
197
|
+
if (s === el) return `i${i}`;
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
215
200
|
return 'i0';
|
|
216
201
|
}
|
|
217
202
|
|
|
218
|
-
const
|
|
203
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
219
204
|
|
|
220
|
-
function findWrap(
|
|
205
|
+
function findWrap(key) {
|
|
221
206
|
try {
|
|
222
207
|
return document.querySelector(
|
|
223
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${
|
|
208
|
+
`.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
224
209
|
);
|
|
225
210
|
} catch (_) { return null; }
|
|
226
211
|
}
|
|
@@ -230,39 +215,78 @@
|
|
|
230
215
|
function pickId(poolKey) {
|
|
231
216
|
const pool = S.pools[poolKey];
|
|
232
217
|
for (let t = 0; t < pool.length; t++) {
|
|
233
|
-
const i
|
|
218
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
234
219
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
235
220
|
const id = pool[i];
|
|
236
|
-
return id;
|
|
221
|
+
if (!S.mountedIds.has(id)) return id;
|
|
237
222
|
}
|
|
238
223
|
return null;
|
|
239
224
|
}
|
|
240
225
|
|
|
241
|
-
// ──
|
|
226
|
+
// ── Recyclage d'id ─────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Sélectionne le wrap vide le plus éloigné au-dessus du viewport.
|
|
230
|
+
* Conditions : kindClass.recyclable = true, scroll vers le bas,
|
|
231
|
+
* wrap vide (non filled), rect.bottom < -(RECYCLE_THRESHOLD × vh).
|
|
232
|
+
*/
|
|
233
|
+
function pickRecyclableWrap(klass) {
|
|
234
|
+
if (!KIND[klass]?.recyclable) return null;
|
|
235
|
+
if (S.scrollDir < 0) return null;
|
|
236
|
+
|
|
237
|
+
const vh = Math.max(300, window.innerHeight || 800);
|
|
238
|
+
const threshold = -(vh * RECYCLE_THRESHOLD);
|
|
239
|
+
let best = null, bestBottom = Infinity;
|
|
240
|
+
|
|
241
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
242
|
+
if (!w.isConnected || isFilled(w)) continue;
|
|
243
|
+
try {
|
|
244
|
+
const rect = w.getBoundingClientRect();
|
|
245
|
+
if (rect.bottom < threshold && rect.bottom < bestBottom) {
|
|
246
|
+
bestBottom = rect.bottom;
|
|
247
|
+
best = w;
|
|
248
|
+
}
|
|
249
|
+
} catch (_) {}
|
|
250
|
+
}
|
|
251
|
+
return best;
|
|
252
|
+
}
|
|
242
253
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Déplace un wrap recyclé vers sa nouvelle ancre el.
|
|
256
|
+
* Réinitialise A_ANCHOR, A_CREATED, supprime A_SHOWN.
|
|
257
|
+
*/
|
|
258
|
+
function moveWrapAfter(el, wrap, newKey) {
|
|
259
|
+
try {
|
|
260
|
+
if (!el || !wrap?.isConnected) return null;
|
|
261
|
+
wrap.setAttribute(A_ANCHOR, newKey);
|
|
262
|
+
wrap.setAttribute(A_CREATED, String(ts()));
|
|
263
|
+
wrap.removeAttribute(A_SHOWN);
|
|
264
|
+
mutate(() => el.insertAdjacentElement('afterend', wrap));
|
|
265
|
+
return wrap;
|
|
266
|
+
} catch (_) { return null; }
|
|
247
267
|
}
|
|
248
268
|
|
|
269
|
+
// ── Wraps DOM ──────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
249
271
|
function makeWrap(id, klass, key) {
|
|
250
|
-
const w
|
|
272
|
+
const w = document.createElement('div');
|
|
251
273
|
w.className = `${WRAP_CLASS} ${klass}`;
|
|
252
274
|
w.setAttribute(A_ANCHOR, key);
|
|
253
275
|
w.setAttribute(A_WRAPID, String(id));
|
|
254
276
|
w.setAttribute(A_CREATED, String(ts()));
|
|
255
277
|
w.style.cssText = 'width:100%;display:block;';
|
|
256
278
|
const ph = document.createElement('div');
|
|
257
|
-
ph.id =
|
|
279
|
+
ph.id = `${PH_PREFIX}${id}`;
|
|
258
280
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
259
281
|
w.appendChild(ph);
|
|
260
282
|
return w;
|
|
261
283
|
}
|
|
262
284
|
|
|
263
285
|
function insertAfter(el, id, klass, key) {
|
|
264
|
-
if (!el?.insertAdjacentElement)
|
|
265
|
-
if (findWrap(key))
|
|
286
|
+
if (!el?.insertAdjacentElement) return null;
|
|
287
|
+
if (findWrap(key)) return null;
|
|
288
|
+
if (S.mountedIds.has(id)) return null;
|
|
289
|
+
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
266
290
|
const w = makeWrap(id, klass, key);
|
|
267
291
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
268
292
|
S.mountedIds.add(id);
|
|
@@ -271,15 +295,10 @@
|
|
|
271
295
|
|
|
272
296
|
function dropWrap(w) {
|
|
273
297
|
try {
|
|
298
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
299
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
274
300
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
275
301
|
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 (_) {}
|
|
283
302
|
w.remove();
|
|
284
303
|
} catch (_) {}
|
|
285
304
|
}
|
|
@@ -287,58 +306,59 @@
|
|
|
287
306
|
// ── Prune ──────────────────────────────────────────────────────────────────
|
|
288
307
|
|
|
289
308
|
/**
|
|
290
|
-
* Supprime les wraps dont l'
|
|
309
|
+
* Supprime les wraps VIDES dont l'ancre a disparu du DOM.
|
|
291
310
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* ezoic-ad-between → cherche [data-index="5"]
|
|
296
|
-
* ezoic-ad-categories → cherche [data-cid="7"] ← fix v20
|
|
297
|
-
*
|
|
298
|
-
* On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
|
|
311
|
+
* isFilled en premier : un wrap rempli n'est JAMAIS supprimé.
|
|
312
|
+
* PRUNE_STABLE_MS (45 s) : pendant cette fenêtre une ancre absente est
|
|
313
|
+
* considérée comme virtualisée par NodeBB (scroll up), pas comme orphelin réel.
|
|
299
314
|
*/
|
|
300
315
|
function pruneOrphans(klass) {
|
|
301
316
|
const meta = KIND[klass];
|
|
302
317
|
if (!meta) return;
|
|
303
318
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
|
|
319
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
320
|
+
if (isFilled(w)) continue;
|
|
321
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
|
|
308
322
|
|
|
309
323
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
310
|
-
const sid = key.slice(klass.length + 1);
|
|
311
|
-
if (!sid) { mutate(() => dropWrap(w));
|
|
324
|
+
const sid = key.slice(klass.length + 1);
|
|
325
|
+
if (!sid) { mutate(() => dropWrap(w)); continue; }
|
|
312
326
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
);
|
|
327
|
+
const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
|
|
328
|
+
const anchorEl = document.querySelector(sel);
|
|
316
329
|
if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
|
|
317
|
-
}
|
|
330
|
+
}
|
|
318
331
|
}
|
|
319
332
|
|
|
320
333
|
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
321
334
|
|
|
322
335
|
/**
|
|
323
|
-
* Deux wraps adjacents
|
|
324
|
-
*
|
|
325
|
-
*
|
|
336
|
+
* Deux wraps adjacents → supprimer le courant s'il est vide et hors grâce.
|
|
337
|
+
* Guards dans l'ordre :
|
|
338
|
+
* 1. isFilled(w) → jamais toucher un wrap rempli
|
|
339
|
+
* 2. A_CREATED < FILL_GRACE → wrap trop récent (pas encore showAds'd)
|
|
340
|
+
* 3. A_SHOWN grace → fill en cours
|
|
341
|
+
* 4. isFilled(prev) → voisin rempli, intouchable → break
|
|
342
|
+
* 5. A_CREATED prev grace → voisin trop récent → break
|
|
343
|
+
* 6. A_SHOWN prev grace → break
|
|
344
|
+
* → les deux vides et hors grâce : supprimer le courant
|
|
326
345
|
*/
|
|
327
346
|
function decluster(klass) {
|
|
328
347
|
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
329
|
-
|
|
348
|
+
if (isFilled(w)) continue;
|
|
349
|
+
if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
|
|
330
350
|
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
331
351
|
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
332
352
|
|
|
333
353
|
let prev = w.previousElementSibling, steps = 0;
|
|
334
354
|
while (prev && steps++ < 3) {
|
|
335
355
|
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
336
|
-
|
|
356
|
+
if (isFilled(prev)) break;
|
|
357
|
+
if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
|
|
337
358
|
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
338
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
359
|
+
if (pShown && ts() - pShown < FILL_GRACE_MS) break;
|
|
339
360
|
|
|
340
|
-
|
|
341
|
-
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
361
|
+
mutate(() => dropWrap(w));
|
|
342
362
|
break;
|
|
343
363
|
}
|
|
344
364
|
}
|
|
@@ -348,23 +368,21 @@
|
|
|
348
368
|
|
|
349
369
|
/**
|
|
350
370
|
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
351
|
-
*
|
|
352
|
-
*
|
|
371
|
+
* Utilise KIND[klass].ordinalAttr (data-index pour posts/topics).
|
|
372
|
+
* Catégories : ordinalAttr = null → fallback positionnel.
|
|
353
373
|
*/
|
|
354
374
|
function ordinal(klass, el) {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
} catch (_) {}
|
|
375
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
376
|
+
if (attr) {
|
|
377
|
+
const v = el.getAttribute(attr);
|
|
378
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
379
|
+
}
|
|
380
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
381
|
+
let i = 0;
|
|
382
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
383
|
+
if (s === el) return i;
|
|
384
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
385
|
+
}
|
|
368
386
|
return 0;
|
|
369
387
|
}
|
|
370
388
|
|
|
@@ -373,23 +391,33 @@
|
|
|
373
391
|
let inserted = 0;
|
|
374
392
|
|
|
375
393
|
for (const el of items) {
|
|
376
|
-
if (inserted >=
|
|
394
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
377
395
|
if (!el?.isConnected) continue;
|
|
378
396
|
|
|
379
|
-
const ord
|
|
380
|
-
|
|
381
|
-
if (!isTarget) continue;
|
|
382
|
-
|
|
397
|
+
const ord = ordinal(klass, el);
|
|
398
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
383
399
|
if (adjacentWrap(el)) continue;
|
|
384
400
|
|
|
385
|
-
const key =
|
|
386
|
-
if (findWrap(key)) continue;
|
|
401
|
+
const key = anchorKey(klass, el);
|
|
402
|
+
if (findWrap(key)) continue;
|
|
387
403
|
|
|
404
|
+
// 1. Tentative pool normal
|
|
388
405
|
const id = pickId(poolKey);
|
|
389
|
-
if (
|
|
406
|
+
if (id) {
|
|
407
|
+
const w = insertAfter(el, id, klass, key);
|
|
408
|
+
if (w) { observePh(id); inserted++; }
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
390
411
|
|
|
391
|
-
|
|
392
|
-
|
|
412
|
+
// 2. Pool épuisé → tentative de recyclage
|
|
413
|
+
const recyclable = pickRecyclableWrap(klass);
|
|
414
|
+
if (recyclable) {
|
|
415
|
+
const rid = parseInt(recyclable.getAttribute(A_WRAPID), 10);
|
|
416
|
+
const w = moveWrapAfter(el, recyclable, key);
|
|
417
|
+
if (w && Number.isFinite(rid)) { observePh(rid); inserted++; }
|
|
418
|
+
}
|
|
419
|
+
// Pool épuisé et pas de recyclage : on continue (items suivants peuvent
|
|
420
|
+
// avoir un wrap existant via findWrap, on ne break pas)
|
|
393
421
|
}
|
|
394
422
|
return inserted;
|
|
395
423
|
}
|
|
@@ -398,7 +426,6 @@
|
|
|
398
426
|
|
|
399
427
|
function getIO() {
|
|
400
428
|
if (S.io) return S.io;
|
|
401
|
-
const margin = isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP;
|
|
402
429
|
try {
|
|
403
430
|
S.io = new IntersectionObserver(entries => {
|
|
404
431
|
for (const e of entries) {
|
|
@@ -407,7 +434,7 @@
|
|
|
407
434
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
408
435
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
409
436
|
}
|
|
410
|
-
}, { root: null, rootMargin:
|
|
437
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
411
438
|
} catch (_) { S.io = null; }
|
|
412
439
|
return S.io;
|
|
413
440
|
}
|
|
@@ -458,7 +485,6 @@
|
|
|
458
485
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
459
486
|
S.lastShow.set(id, t);
|
|
460
487
|
|
|
461
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
462
488
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
463
489
|
|
|
464
490
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -479,7 +505,6 @@
|
|
|
479
505
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
480
506
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
481
507
|
if (!wrap || !ph?.isConnected) return;
|
|
482
|
-
// Un show plus récent → ne pas toucher
|
|
483
508
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
484
509
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
485
510
|
} catch (_) {}
|
|
@@ -498,7 +523,7 @@
|
|
|
498
523
|
const orig = ez.showAds.bind(ez);
|
|
499
524
|
ez.showAds = function (...args) {
|
|
500
525
|
if (isBlocked()) return;
|
|
501
|
-
const ids
|
|
526
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
502
527
|
const seen = new Set();
|
|
503
528
|
for (const v of ids) {
|
|
504
529
|
const id = parseInt(v, 10);
|
|
@@ -517,7 +542,7 @@
|
|
|
517
542
|
}
|
|
518
543
|
}
|
|
519
544
|
|
|
520
|
-
// ── Core
|
|
545
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
521
546
|
|
|
522
547
|
async function runCore() {
|
|
523
548
|
if (isBlocked()) return 0;
|
|
@@ -532,10 +557,9 @@
|
|
|
532
557
|
|
|
533
558
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
534
559
|
if (!normBool(cfgEnable)) return 0;
|
|
535
|
-
const items = getItems();
|
|
536
560
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
537
561
|
pruneOrphans(klass);
|
|
538
|
-
const n = injectBetween(klass,
|
|
562
|
+
const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
539
563
|
if (n) decluster(klass);
|
|
540
564
|
return n;
|
|
541
565
|
};
|
|
@@ -548,14 +572,13 @@
|
|
|
548
572
|
'ezoic-ad-between', getTopics,
|
|
549
573
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
550
574
|
);
|
|
551
|
-
|
|
575
|
+
return exec(
|
|
552
576
|
'ezoic-ad-categories', getCategories,
|
|
553
577
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
554
578
|
);
|
|
555
|
-
return 0;
|
|
556
579
|
}
|
|
557
580
|
|
|
558
|
-
// ── Scheduler
|
|
581
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
559
582
|
|
|
560
583
|
function scheduleRun(cb) {
|
|
561
584
|
if (S.runQueued) return;
|
|
@@ -573,10 +596,8 @@
|
|
|
573
596
|
if (isBlocked()) return;
|
|
574
597
|
const t = ts();
|
|
575
598
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
576
|
-
S.lastBurstTs
|
|
577
|
-
|
|
578
|
-
const pk = pageKey();
|
|
579
|
-
S.pageKey = pk;
|
|
599
|
+
S.lastBurstTs = t;
|
|
600
|
+
S.pageKey = pageKey();
|
|
580
601
|
S.burstDeadline = t + 2000;
|
|
581
602
|
|
|
582
603
|
if (S.burstActive) return;
|
|
@@ -584,7 +605,7 @@
|
|
|
584
605
|
S.burstCount = 0;
|
|
585
606
|
|
|
586
607
|
const step = () => {
|
|
587
|
-
if (pageKey() !==
|
|
608
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
588
609
|
S.burstActive = false; return;
|
|
589
610
|
}
|
|
590
611
|
S.burstCount++;
|
|
@@ -596,14 +617,10 @@
|
|
|
596
617
|
step();
|
|
597
618
|
}
|
|
598
619
|
|
|
599
|
-
// ── Cleanup
|
|
620
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
600
621
|
|
|
601
622
|
function cleanup() {
|
|
602
623
|
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 (_) {}
|
|
607
624
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
608
625
|
S.cfg = null;
|
|
609
626
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
@@ -617,19 +634,17 @@
|
|
|
617
634
|
S.runQueued = false;
|
|
618
635
|
}
|
|
619
636
|
|
|
620
|
-
// ──
|
|
637
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
621
638
|
|
|
622
639
|
function ensureDomObserver() {
|
|
623
640
|
if (S.domObs) return;
|
|
641
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
624
642
|
S.domObs = new MutationObserver(muts => {
|
|
625
643
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
626
644
|
for (const m of muts) {
|
|
627
|
-
if (!m.addedNodes?.length) continue;
|
|
628
645
|
for (const n of m.addedNodes) {
|
|
629
646
|
if (n.nodeType !== 1) continue;
|
|
630
|
-
if (n.matches?.(
|
|
631
|
-
n.matches?.(SEL.topic) || n.querySelector?.(SEL.topic) ||
|
|
632
|
-
n.matches?.(SEL.category) || n.querySelector?.(SEL.category)) {
|
|
647
|
+
if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
|
|
633
648
|
requestBurst(); return;
|
|
634
649
|
}
|
|
635
650
|
}
|
|
@@ -641,7 +656,6 @@
|
|
|
641
656
|
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
642
657
|
|
|
643
658
|
function muteConsole() {
|
|
644
|
-
if (!S.cfg || !S.cfg.muteEzoicConsole) return;
|
|
645
659
|
if (window.__nbbEzMuted) return;
|
|
646
660
|
window.__nbbEzMuted = true;
|
|
647
661
|
const MUTED = ['[EzoicAds JS]: Placeholder Id', 'Debugger iframe already exists', `with id ${PH_PREFIX}`];
|
|
@@ -656,29 +670,18 @@
|
|
|
656
670
|
}
|
|
657
671
|
|
|
658
672
|
function ensureTcfLocator() {
|
|
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.
|
|
665
673
|
try {
|
|
666
674
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
667
|
-
|
|
668
675
|
const inject = () => {
|
|
669
676
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
670
677
|
const f = document.createElement('iframe');
|
|
671
678
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
672
679
|
(document.body || document.documentElement).appendChild(f);
|
|
673
680
|
};
|
|
674
|
-
|
|
675
681
|
inject();
|
|
676
|
-
|
|
677
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
678
682
|
if (!window.__nbbTcfObs) {
|
|
679
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
680
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
681
|
-
{ childList: true, subtree: true });
|
|
683
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
684
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
682
685
|
}
|
|
683
686
|
} catch (_) {}
|
|
684
687
|
}
|
|
@@ -688,10 +691,10 @@
|
|
|
688
691
|
const head = document.head;
|
|
689
692
|
if (!head) return;
|
|
690
693
|
for (const [rel, href, cors] of [
|
|
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],
|
|
694
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
695
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
696
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
697
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
695
698
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
696
699
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
697
700
|
]) {
|
|
@@ -705,7 +708,7 @@
|
|
|
705
708
|
}
|
|
706
709
|
}
|
|
707
710
|
|
|
708
|
-
// ── Bindings
|
|
711
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
709
712
|
|
|
710
713
|
function bindNodeBB() {
|
|
711
714
|
const $ = window.jQuery;
|
|
@@ -716,19 +719,16 @@
|
|
|
716
719
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
717
720
|
S.pageKey = pageKey();
|
|
718
721
|
blockedUntil = 0;
|
|
719
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
720
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
722
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
723
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
721
724
|
});
|
|
722
725
|
|
|
723
|
-
const
|
|
724
|
-
'action:ajaxify.contentLoaded',
|
|
725
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
726
|
+
const burstEvts = [
|
|
727
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
726
728
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
727
729
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
730
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
728
731
|
|
|
729
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
730
|
-
|
|
731
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
732
732
|
try {
|
|
733
733
|
require(['hooks'], hooks => {
|
|
734
734
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -743,12 +743,14 @@
|
|
|
743
743
|
function bindScroll() {
|
|
744
744
|
let ticking = false;
|
|
745
745
|
window.addEventListener('scroll', () => {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
746
|
+
// Suivi direction du scroll (nécessaire pour le recyclage conditionnel)
|
|
747
|
+
try {
|
|
748
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
749
|
+
const d = y - S.lastScrollY;
|
|
750
|
+
if (Math.abs(d) > 4) { S.scrollDir = d > 0 ? 1 : -1; S.lastScrollY = y; }
|
|
751
|
+
} catch (_) {}
|
|
751
752
|
|
|
753
|
+
if (ticking) return;
|
|
752
754
|
ticking = true;
|
|
753
755
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
754
756
|
}, { passive: true });
|
|
@@ -757,6 +759,7 @@
|
|
|
757
759
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
758
760
|
|
|
759
761
|
S.pageKey = pageKey();
|
|
762
|
+
try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
|
|
760
763
|
muteConsole();
|
|
761
764
|
ensureTcfLocator();
|
|
762
765
|
warmNetwork();
|
package/public/style.css
CHANGED
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
overflow: hidden !important;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/* ──
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
/* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
|
|
75
|
+
.ezoic-ad {
|
|
76
|
+
margin: 0 !important;
|
|
77
|
+
padding: 0 !important;
|
|
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" />
|
|
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).</p>
|
|
19
|
+
<p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</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">
|
|
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" />
|
|
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
|
|
47
|
+
<p class="form-text">IDs numériques, un par ligne. 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" />
|
|
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 éviter la réutilisation d’IDs. IMPORTANT : ne réutilise pas les mêmes IDs dans les deux pools.</p>
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
<div class="mb-3">
|
|
@@ -82,22 +82,12 @@
|
|
|
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}"
|
|
85
|
+
<option value="{allGroups.name}">{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>
|
|
101
91
|
<button id="save" class="btn btn-primary">Enregistrer</button>
|
|
102
92
|
</form>
|
|
103
93
|
</div>
|