nodebb-plugin-ezoic-infinite 1.8.16 → 1.8.18
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 +116 -74
- package/package.json +2 -2
- package/plugin.json +10 -26
- package/public/client.js +354 -302
package/library.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const meta
|
|
3
|
+
const meta = require.main.require('./src/meta');
|
|
4
4
|
const groups = require.main.require('./src/groups');
|
|
5
|
-
const db
|
|
5
|
+
const db = require.main.require('./src/database');
|
|
6
6
|
|
|
7
7
|
const SETTINGS_KEY = 'ezoic-infinite';
|
|
8
8
|
const plugin = {};
|
|
9
9
|
|
|
10
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
10
12
|
function normalizeExcludedGroups(value) {
|
|
11
13
|
if (!value) return [];
|
|
12
14
|
if (Array.isArray(value)) return value;
|
|
13
|
-
|
|
15
|
+
// NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
|
|
16
|
+
const s = String(value).trim();
|
|
17
|
+
if (s.startsWith('[')) {
|
|
18
|
+
try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
|
|
19
|
+
}
|
|
20
|
+
// Fallback : séparation par virgule
|
|
21
|
+
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
function parseBool(v, def = false) {
|
|
@@ -21,33 +29,22 @@ function parseBool(v, def = false) {
|
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
async function getAllGroups() {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
26
|
-
|
|
27
32
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
28
33
|
if (!names || !names.length) {
|
|
29
34
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
30
35
|
}
|
|
31
36
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
32
37
|
const data = await groups.getGroupsData(filtered);
|
|
33
|
-
const valid =
|
|
38
|
+
const valid = data.filter(g => g && g.name);
|
|
34
39
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
35
|
-
|
|
36
|
-
_groupsCacheAt = now;
|
|
37
|
-
_groupsCache = valid;
|
|
38
40
|
return valid;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
let _settingsCacheAt = 0;
|
|
43
|
-
const SETTINGS_TTL = 30000; // 30s
|
|
44
|
-
|
|
45
|
-
let _groupsCache = null;
|
|
46
|
-
let _groupsCacheAt = 0;
|
|
47
|
-
const GROUPS_TTL = 5 * 60 * 1000; // 5min
|
|
43
|
+
// ── Settings cache (30s TTL) ────────────────────────────────────────────────
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
let _settingsCache = null;
|
|
46
|
+
let _settingsCacheAt = 0;
|
|
47
|
+
const SETTINGS_TTL = 30_000;
|
|
51
48
|
|
|
52
49
|
async function getSettings() {
|
|
53
50
|
const now = Date.now();
|
|
@@ -55,101 +52,146 @@ async function getSettings() {
|
|
|
55
52
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
56
53
|
_settingsCacheAt = Date.now();
|
|
57
54
|
_settingsCache = {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Home/categories list ads (between categories on / or /categories)
|
|
65
|
-
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
66
|
-
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
55
|
+
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
56
|
+
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
57
|
+
placeholderIds: (s.placeholderIds || '').trim(),
|
|
58
|
+
intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
|
|
59
|
+
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
60
|
+
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
67
61
|
categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
|
|
68
|
-
intervalCategories:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
75
|
-
|
|
76
|
-
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
62
|
+
intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
|
|
63
|
+
enableMessageAds: parseBool(s.enableMessageAds, false),
|
|
64
|
+
showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
|
|
65
|
+
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
66
|
+
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
67
|
+
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
77
68
|
};
|
|
78
69
|
return _settingsCache;
|
|
79
70
|
}
|
|
80
71
|
|
|
81
72
|
async function isUserExcluded(uid, excludedGroups) {
|
|
82
73
|
if (!uid || !excludedGroups.length) return false;
|
|
74
|
+
const userGroups = await groups.getUserGroups([uid]);
|
|
75
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// ── Groups cache (5 min TTL) ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
let _groupsCache = null;
|
|
83
|
+
let _groupsCacheAt = 0;
|
|
84
|
+
const GROUPS_TTL = 5 * 60_000;
|
|
83
85
|
|
|
86
|
+
async function getAllGroupsCached() {
|
|
84
87
|
const now = Date.now();
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
89
|
+
const g = await getAllGroups();
|
|
90
|
+
_groupsCache = g;
|
|
91
|
+
_groupsCacheAt = Date.now();
|
|
92
|
+
return _groupsCache;
|
|
93
|
+
}
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
const excluded = (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
95
|
+
// ── Exclusion cache (per uid, 30s TTL) ─────────────────────────────────────
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
const _excludedCache = new Map(); // uid -> { v:boolean, t:number, sig:string }
|
|
98
|
+
const EXCLUDED_TTL = 30_000;
|
|
99
|
+
const EXCLUDED_MAX = 10_000;
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
function excludedSig(excludedGroups) {
|
|
102
|
+
// signature stable to invalidate when groups list changes
|
|
103
|
+
return excludedGroups.join('\u0001');
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
async function isUserExcludedCached(uid, excludedGroups) {
|
|
107
|
+
if (!uid || !excludedGroups.length) return false;
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const sig = excludedSig(excludedGroups);
|
|
110
|
+
const hit = _excludedCache.get(uid);
|
|
111
|
+
if (hit && hit.sig === sig && (now - hit.t) < EXCLUDED_TTL) return hit.v;
|
|
112
|
+
const v = await isUserExcluded(uid, excludedGroups);
|
|
113
|
+
if (_excludedCache.size > EXCLUDED_MAX) _excludedCache.clear();
|
|
114
|
+
_excludedCache.set(uid, { v, t: now, sig });
|
|
115
|
+
return v;
|
|
116
|
+
}
|
|
117
|
+
// ── Scripts Ezoic ──────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const EZOIC_SCRIPTS = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
|
|
120
|
+
<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
|
|
121
|
+
<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
|
|
122
|
+
<script>
|
|
123
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
124
|
+
ezstandalone.cmd = ezstandalone.cmd || [];
|
|
125
|
+
</script>`;
|
|
126
|
+
|
|
127
|
+
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
98
128
|
|
|
99
129
|
plugin.onSettingsSet = function (data) {
|
|
100
|
-
|
|
101
|
-
if (data && data.hash === SETTINGS_KEY) {
|
|
102
|
-
_settingsCache = null;
|
|
103
|
-
_excludedCache.clear();
|
|
104
|
-
}
|
|
130
|
+
if (data && data.hash === SETTINGS_KEY) { _settingsCache = null; _groupsCache = null; _excludedCache.clear(); }
|
|
105
131
|
};
|
|
106
132
|
|
|
107
|
-
|
|
108
133
|
plugin.addAdminNavigation = async (header) => {
|
|
109
134
|
header.plugins = header.plugins || [];
|
|
110
|
-
header.plugins.push({
|
|
111
|
-
route: '/plugins/ezoic-infinite',
|
|
112
|
-
icon: 'fa-ad',
|
|
113
|
-
name: 'Ezoic Infinite Ads'
|
|
114
|
-
});
|
|
135
|
+
header.plugins.push({ route: '/plugins/ezoic-infinite', icon: 'fa-ad', name: 'Ezoic Infinite Ads' });
|
|
115
136
|
return header;
|
|
116
137
|
};
|
|
117
138
|
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
141
|
+
*
|
|
142
|
+
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
143
|
+
* (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
|
|
144
|
+
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
145
|
+
* et est rendu via req.app.renderAsync('header', hookReturn.templateData).
|
|
146
|
+
* On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
|
|
147
|
+
* tout en préservant ce dernier.
|
|
148
|
+
*/
|
|
149
|
+
plugin.injectEzoicHead = async (data) => {
|
|
150
|
+
try {
|
|
120
151
|
const settings = await getSettings();
|
|
121
|
-
const
|
|
152
|
+
const uid = data.req?.uid ?? 0;
|
|
153
|
+
const excluded = await isUserExcludedCached(uid, settings.excludedGroups);
|
|
154
|
+
if (!excluded) {
|
|
155
|
+
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
156
|
+
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
157
|
+
}
|
|
158
|
+
} catch (_) {}
|
|
159
|
+
return data;
|
|
160
|
+
};
|
|
122
161
|
|
|
162
|
+
plugin.init = async ({ router, middleware }) => {
|
|
163
|
+
async function render(req, res) {
|
|
164
|
+
const settings = await getSettings();
|
|
165
|
+
const allGroups = await getAllGroupsCached();
|
|
123
166
|
res.render('admin/plugins/ezoic-infinite', {
|
|
124
167
|
title: 'Ezoic Infinite Ads',
|
|
125
168
|
...settings,
|
|
126
169
|
enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
|
|
127
|
-
enableMessageAds_checked:
|
|
170
|
+
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
128
171
|
allGroups,
|
|
129
172
|
});
|
|
130
173
|
}
|
|
131
174
|
|
|
132
|
-
router.get('/admin/plugins/ezoic-infinite',
|
|
175
|
+
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
133
176
|
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
134
177
|
|
|
135
178
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
136
179
|
const settings = await getSettings();
|
|
137
|
-
const excluded = await
|
|
138
|
-
|
|
180
|
+
const excluded = await isUserExcludedCached(req.uid, settings.excludedGroups);
|
|
139
181
|
res.json({
|
|
140
182
|
excluded,
|
|
141
|
-
enableBetweenAds:
|
|
142
|
-
showFirstTopicAd:
|
|
143
|
-
placeholderIds:
|
|
144
|
-
intervalPosts:
|
|
145
|
-
enableCategoryAds:
|
|
146
|
-
showFirstCategoryAd:
|
|
183
|
+
enableBetweenAds: settings.enableBetweenAds,
|
|
184
|
+
showFirstTopicAd: settings.showFirstTopicAd,
|
|
185
|
+
placeholderIds: settings.placeholderIds,
|
|
186
|
+
intervalPosts: settings.intervalPosts,
|
|
187
|
+
enableCategoryAds: settings.enableCategoryAds,
|
|
188
|
+
showFirstCategoryAd: settings.showFirstCategoryAd,
|
|
147
189
|
categoryPlaceholderIds: settings.categoryPlaceholderIds,
|
|
148
|
-
intervalCategories:
|
|
149
|
-
enableMessageAds:
|
|
150
|
-
showFirstMessageAd:
|
|
151
|
-
messagePlaceholderIds:
|
|
152
|
-
messageIntervalPosts:
|
|
190
|
+
intervalCategories: settings.intervalCategories,
|
|
191
|
+
enableMessageAds: settings.enableMessageAds,
|
|
192
|
+
showFirstMessageAd: settings.showFirstMessageAd,
|
|
193
|
+
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
194
|
+
messageIntervalPosts: settings.messageIntervalPosts,
|
|
153
195
|
});
|
|
154
196
|
});
|
|
155
197
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.18",
|
|
4
4
|
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"compatibility": "^4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"private": false
|
|
21
|
-
}
|
|
21
|
+
}
|
package/plugin.json
CHANGED
|
@@ -4,30 +4,14 @@
|
|
|
4
4
|
"description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
|
|
5
5
|
"library": "./library.js",
|
|
6
6
|
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
{
|
|
12
|
-
"hook": "filter:admin.header.build",
|
|
13
|
-
"method": "addAdminNavigation"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"hook": "action:settings.set",
|
|
17
|
-
"method": "onSettingsSet"
|
|
18
|
-
}
|
|
7
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
8
|
+
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
9
|
+
{ "hook": "action:settings.set", "method": "onSettingsSet" },
|
|
10
|
+
{ "hook": "filter:middleware.renderHeader","method": "injectEzoicHead" }
|
|
19
11
|
],
|
|
20
|
-
"staticDirs": {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"scripts": [
|
|
27
|
-
"public/client.js"
|
|
28
|
-
],
|
|
29
|
-
"templates": "public/templates",
|
|
30
|
-
"css": [
|
|
31
|
-
"public/style.css"
|
|
32
|
-
]
|
|
33
|
-
}
|
|
12
|
+
"staticDirs": { "public": "public" },
|
|
13
|
+
"acpScripts": [ "public/admin.js" ],
|
|
14
|
+
"scripts": [ "public/client.js" ],
|
|
15
|
+
"templates": "public/templates",
|
|
16
|
+
"css": [ "public/style.css" ]
|
|
17
|
+
}
|
package/public/client.js
CHANGED
|
@@ -1,55 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v36
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 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).
|
|
4
|
+
* Historique des corrections majeures
|
|
5
|
+
* ────────────────────────────────────
|
|
6
|
+
* v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
|
|
12
7
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
|
|
9
|
+
* la position dans le batch courant.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
* Fix : marge large fixe par device, observer créé une seule fois.
|
|
16
|
+
* v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
|
|
21
17
|
*
|
|
22
|
-
*
|
|
23
|
-
* Fix : 200ms.
|
|
18
|
+
* v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
|
|
24
19
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
31
67
|
*/
|
|
32
|
-
(function () {
|
|
68
|
+
(function nbbEzoicInfinite() {
|
|
33
69
|
'use strict';
|
|
34
70
|
|
|
35
71
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
36
72
|
|
|
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
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Marges IO larges et fixes (pas de reconstruction d'observer)
|
|
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
|
+
|
|
87
|
+
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
53
88
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
54
89
|
const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
|
|
55
90
|
|
|
@@ -60,42 +95,38 @@
|
|
|
60
95
|
};
|
|
61
96
|
|
|
62
97
|
/**
|
|
63
|
-
* Table
|
|
98
|
+
* Table KIND — source de vérité par kindClass.
|
|
64
99
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
100
|
+
* sel sélecteur CSS complet des éléments cibles
|
|
101
|
+
* baseTag préfixe tag pour querySelector d'ancre
|
|
102
|
+
* (vide pour posts : le sélecteur commence par '[')
|
|
103
|
+
* anchorAttr attribut DOM stable → clé unique du wrap
|
|
104
|
+
* ordinalAttr attribut 0-based pour le calcul de l'intervalle
|
|
105
|
+
* null → fallback positionnel (catégories)
|
|
71
106
|
*/
|
|
72
107
|
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' },
|
|
108
|
+
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
109
|
+
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
110
|
+
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
76
111
|
};
|
|
77
112
|
|
|
78
|
-
// ── État
|
|
113
|
+
// ── État global ────────────────────────────────────────────────────────────
|
|
79
114
|
|
|
80
115
|
const S = {
|
|
81
|
-
pageKey:
|
|
82
|
-
cfg:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
inflight: 0,
|
|
96
|
-
pending: [],
|
|
97
|
-
pendingSet: new Set(),
|
|
98
|
-
|
|
116
|
+
pageKey: null,
|
|
117
|
+
cfg: null,
|
|
118
|
+
poolsReady: false,
|
|
119
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
120
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
121
|
+
mountedIds: new Set(),
|
|
122
|
+
lastShow: new Map(),
|
|
123
|
+
io: null,
|
|
124
|
+
domObs: null,
|
|
125
|
+
mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
|
|
126
|
+
inflight: 0, // showAds() en cours
|
|
127
|
+
pending: [], // ids en attente de slot inflight
|
|
128
|
+
pendingSet: new Set(),
|
|
129
|
+
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
99
130
|
runQueued: false,
|
|
100
131
|
burstActive: false,
|
|
101
132
|
burstDeadline: 0,
|
|
@@ -104,8 +135,12 @@
|
|
|
104
135
|
};
|
|
105
136
|
|
|
106
137
|
let blockedUntil = 0;
|
|
107
|
-
|
|
108
|
-
const ts
|
|
138
|
+
|
|
139
|
+
const ts = () => Date.now();
|
|
140
|
+
const isBlocked = () => ts() < blockedUntil;
|
|
141
|
+
const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
|
|
142
|
+
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
143
|
+
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
109
144
|
|
|
110
145
|
function mutate(fn) {
|
|
111
146
|
S.mutGuard++;
|
|
@@ -123,44 +158,22 @@
|
|
|
123
158
|
return S.cfg;
|
|
124
159
|
}
|
|
125
160
|
|
|
126
|
-
function initPools(cfg) {
|
|
127
|
-
// (Perf) Ne reparse les pools que si les strings ont changé.
|
|
128
|
-
const sig = `${cfg.placeholderIds || ''}§${cfg.messagePlaceholderIds || ''}§${cfg.categoryPlaceholderIds || ''}`;
|
|
129
|
-
if (S.poolSig === sig) return;
|
|
130
|
-
S.poolSig = sig;
|
|
131
|
-
|
|
132
|
-
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
133
|
-
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
134
|
-
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
135
|
-
|
|
136
|
-
// Réinitialise les curseurs si une pool change
|
|
137
|
-
S.cursors.topics = 0;
|
|
138
|
-
S.cursors.posts = 0;
|
|
139
|
-
S.cursors.categories = 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
161
|
function parseIds(raw) {
|
|
143
|
-
// Accepte : un ID par ligne, ou séparés par virgules/espaces (ACP le mentionne).
|
|
144
162
|
const out = [], seen = new Set();
|
|
145
|
-
const
|
|
146
|
-
.replace(/\r/g, '\n')
|
|
147
|
-
.split(/[\n\s,]+/)
|
|
148
|
-
.map(s => s.trim())
|
|
149
|
-
.filter(Boolean);
|
|
150
|
-
|
|
151
|
-
for (const v of parts) {
|
|
163
|
+
for (const v of String(raw || '').split(/[\s,]+/).map(s => s.trim()).filter(Boolean)) {
|
|
152
164
|
const n = parseInt(v, 10);
|
|
153
165
|
if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
|
|
154
166
|
}
|
|
155
167
|
return out;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
function initPools(cfg) {
|
|
171
|
+
if (S.poolsReady) return;
|
|
172
|
+
S.pools.topics = parseIds(cfg.placeholderIds);
|
|
173
|
+
S.pools.posts = parseIds(cfg.messagePlaceholderIds);
|
|
174
|
+
S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
|
|
175
|
+
S.poolsReady = true;
|
|
176
|
+
}
|
|
164
177
|
|
|
165
178
|
// ── Page identity ──────────────────────────────────────────────────────────
|
|
166
179
|
|
|
@@ -184,13 +197,13 @@
|
|
|
184
197
|
return 'other';
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
// ── DOM
|
|
200
|
+
// ── Items DOM ──────────────────────────────────────────────────────────────
|
|
188
201
|
|
|
189
202
|
function getPosts() {
|
|
190
203
|
return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
|
|
191
204
|
if (!el.isConnected) return false;
|
|
192
205
|
if (!el.querySelector('[component="post/content"]')) return false;
|
|
193
|
-
const p = el.parentElement?.closest(
|
|
206
|
+
const p = el.parentElement?.closest(SEL.post);
|
|
194
207
|
if (p && p !== el) return false;
|
|
195
208
|
return el.getAttribute('component') !== 'post/parent';
|
|
196
209
|
});
|
|
@@ -199,53 +212,87 @@
|
|
|
199
212
|
const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
|
|
200
213
|
const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
|
|
201
214
|
|
|
215
|
+
// ── Wraps — détection ──────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Vérifie qu'un wrap a encore son ancre dans le DOM.
|
|
219
|
+
* Utilisé par adjacentWrap pour ignorer les wraps orphelins.
|
|
220
|
+
*/
|
|
221
|
+
function wrapIsLive(wrap) {
|
|
222
|
+
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
223
|
+
const key = wrap.getAttribute(A_ANCHOR);
|
|
224
|
+
if (!key) return false;
|
|
225
|
+
// Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
|
|
226
|
+
// et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
|
|
227
|
+
if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
228
|
+
// Fallback : registre pas encore à jour ou wrap non enregistré.
|
|
229
|
+
const colonIdx = key.indexOf(':');
|
|
230
|
+
const klass = key.slice(0, colonIdx);
|
|
231
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
232
|
+
const cfg = KIND[klass];
|
|
233
|
+
if (!cfg) return false;
|
|
234
|
+
// Optimisation : si l'ancre est un frère direct du wrap, pas besoin
|
|
235
|
+
// de querySelector global — on cherche parmi les voisins immédiats.
|
|
236
|
+
const parent = wrap.parentElement;
|
|
237
|
+
if (parent) {
|
|
238
|
+
for (const sib of parent.children) {
|
|
239
|
+
if (sib === wrap) continue;
|
|
240
|
+
try {
|
|
241
|
+
if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
|
|
242
|
+
return sib.isConnected;
|
|
243
|
+
}
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Dernier recours : querySelector global
|
|
248
|
+
try {
|
|
249
|
+
const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
|
|
250
|
+
return !!(found?.isConnected);
|
|
251
|
+
} catch (_) { return false; }
|
|
252
|
+
}
|
|
253
|
+
|
|
202
254
|
function adjacentWrap(el) {
|
|
203
|
-
return
|
|
204
|
-
el.nextElementSibling?.classList?.contains(WRAP_CLASS) ||
|
|
205
|
-
el.previousElementSibling?.classList?.contains(WRAP_CLASS)
|
|
206
|
-
);
|
|
255
|
+
return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
|
|
207
256
|
}
|
|
208
257
|
|
|
209
|
-
// ── Ancres stables
|
|
258
|
+
// ── Ancres stables ─────────────────────────────────────────────────────────
|
|
210
259
|
|
|
211
260
|
/**
|
|
212
|
-
* Retourne
|
|
213
|
-
*
|
|
214
|
-
* Fallback positionnel si l'attribut est absent.
|
|
261
|
+
* Retourne la valeur de l'attribut stable pour cet élément,
|
|
262
|
+
* ou un fallback positionnel si l'attribut est absent.
|
|
215
263
|
*/
|
|
216
|
-
function stableId(
|
|
217
|
-
const attr = KIND[
|
|
264
|
+
function stableId(klass, el) {
|
|
265
|
+
const attr = KIND[klass]?.anchorAttr;
|
|
218
266
|
if (attr) {
|
|
219
267
|
const v = el.getAttribute(attr);
|
|
220
268
|
if (v !== null && v !== '') return v;
|
|
221
269
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
i++;
|
|
228
|
-
}
|
|
229
|
-
} catch (_) {}
|
|
270
|
+
let i = 0;
|
|
271
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
272
|
+
if (s === el) return `i${i}`;
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
230
275
|
return 'i0';
|
|
231
276
|
}
|
|
232
277
|
|
|
233
|
-
const
|
|
278
|
+
const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
|
|
234
279
|
|
|
235
|
-
function findWrap(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
`.${WRAP_CLASS}[${A_ANCHOR}="${anchorKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
|
|
239
|
-
);
|
|
240
|
-
} catch (_) { return null; }
|
|
280
|
+
function findWrap(key) {
|
|
281
|
+
const w = S.wrapByKey.get(key);
|
|
282
|
+
return (w?.isConnected) ? w : null;
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
// ── Pool ───────────────────────────────────────────────────────────────────
|
|
244
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Retourne le prochain id disponible dans le pool (round-robin),
|
|
289
|
+
* ou null si tous les ids sont montés.
|
|
290
|
+
*/
|
|
245
291
|
function pickId(poolKey) {
|
|
246
292
|
const pool = S.pools[poolKey];
|
|
293
|
+
if (!pool.length) return null;
|
|
247
294
|
for (let t = 0; t < pool.length; t++) {
|
|
248
|
-
const i
|
|
295
|
+
const i = S.cursors[poolKey] % pool.length;
|
|
249
296
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
250
297
|
const id = pool[i];
|
|
251
298
|
if (!S.mountedIds.has(id)) return id;
|
|
@@ -253,7 +300,69 @@
|
|
|
253
300
|
return null;
|
|
254
301
|
}
|
|
255
302
|
|
|
256
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
305
|
+
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
306
|
+
* destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
|
|
307
|
+
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
308
|
+
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
309
|
+
*/
|
|
310
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
311
|
+
const ez = window.ezstandalone;
|
|
312
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
313
|
+
typeof ez?.define !== 'function' ||
|
|
314
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
315
|
+
|
|
316
|
+
const vh = window.innerHeight || 800;
|
|
317
|
+
// Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
|
|
318
|
+
// après pour neutraliser l'IO — plus de showAds parasite possible.
|
|
319
|
+
const threshold = -vh;
|
|
320
|
+
let bestEmpty = null, bestEmptyBottom = Infinity;
|
|
321
|
+
let bestFilled = null, bestFilledBottom = Infinity;
|
|
322
|
+
|
|
323
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
|
|
324
|
+
try {
|
|
325
|
+
const rect = wrap.getBoundingClientRect();
|
|
326
|
+
if (rect.bottom > threshold) return;
|
|
327
|
+
if (!isFilled(wrap)) {
|
|
328
|
+
if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
|
|
329
|
+
} else {
|
|
330
|
+
if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
|
|
331
|
+
}
|
|
332
|
+
} catch (_) {}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const best = bestEmpty ?? bestFilled;
|
|
336
|
+
if (!best) return null;
|
|
337
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
338
|
+
if (!Number.isFinite(id)) return null;
|
|
339
|
+
|
|
340
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
341
|
+
// Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
|
|
342
|
+
// parasite si le nœud était encore dans la zone IO_MARGIN.
|
|
343
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
344
|
+
mutate(() => {
|
|
345
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
346
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
347
|
+
best.setAttribute(A_SHOWN, '0');
|
|
348
|
+
best.classList.remove('is-empty');
|
|
349
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
350
|
+
if (ph) ph.innerHTML = '';
|
|
351
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
352
|
+
});
|
|
353
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
354
|
+
S.wrapByKey.set(newKey, best);
|
|
355
|
+
|
|
356
|
+
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
357
|
+
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
358
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
359
|
+
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
360
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
361
|
+
|
|
362
|
+
return { id, wrap: best };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
257
366
|
|
|
258
367
|
function makeWrap(id, klass, key) {
|
|
259
368
|
const w = document.createElement('div');
|
|
@@ -261,6 +370,7 @@
|
|
|
261
370
|
w.setAttribute(A_ANCHOR, key);
|
|
262
371
|
w.setAttribute(A_WRAPID, String(id));
|
|
263
372
|
w.setAttribute(A_CREATED, String(ts()));
|
|
373
|
+
w.setAttribute(A_SHOWN, '0');
|
|
264
374
|
w.style.cssText = 'width:100%;display:block;';
|
|
265
375
|
const ph = document.createElement('div');
|
|
266
376
|
ph.id = `${PH_PREFIX}${id}`;
|
|
@@ -270,115 +380,85 @@
|
|
|
270
380
|
}
|
|
271
381
|
|
|
272
382
|
function insertAfter(el, id, klass, key) {
|
|
273
|
-
if (!el?.insertAdjacentElement)
|
|
274
|
-
if (findWrap(key))
|
|
275
|
-
if (S.mountedIds.has(id))
|
|
383
|
+
if (!el?.insertAdjacentElement) return null;
|
|
384
|
+
if (findWrap(key)) return null;
|
|
385
|
+
if (S.mountedIds.has(id)) return null;
|
|
276
386
|
if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
|
|
277
387
|
const w = makeWrap(id, klass, key);
|
|
278
388
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
279
389
|
S.mountedIds.add(id);
|
|
390
|
+
S.wrapByKey.set(key, w);
|
|
280
391
|
return w;
|
|
281
392
|
}
|
|
282
393
|
|
|
283
394
|
function dropWrap(w) {
|
|
284
395
|
try {
|
|
396
|
+
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
397
|
+
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
285
398
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
286
399
|
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// "parameter 1 is not of type Element" sur le prochain observe).
|
|
290
|
-
try {
|
|
291
|
-
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
292
|
-
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
293
|
-
} catch (_) {}
|
|
400
|
+
const key = w.getAttribute(A_ANCHOR);
|
|
401
|
+
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
294
402
|
w.remove();
|
|
295
403
|
} catch (_) {}
|
|
296
404
|
}
|
|
297
405
|
|
|
298
|
-
// ── Prune
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
406
|
+
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
407
|
+
//
|
|
408
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
409
|
+
//
|
|
410
|
+
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
411
|
+
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
412
|
+
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
413
|
+
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
414
|
+
// liste après un long scroll et bloquent les nouvelles injections.
|
|
415
|
+
//
|
|
416
|
+
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
417
|
+
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
418
|
+
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
419
|
+
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
420
|
+
|
|
421
|
+
function pruneOrphansBetween() {
|
|
422
|
+
const klass = 'ezoic-ad-between';
|
|
423
|
+
const cfg = KIND[klass];
|
|
424
|
+
|
|
425
|
+
// Build a fast lookup of existing anchors once (avoid querySelector per wrap)
|
|
426
|
+
const anchors = new Set();
|
|
427
|
+
document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`).forEach(el => {
|
|
428
|
+
const v = el.getAttribute(cfg.anchorAttr);
|
|
429
|
+
if (v) anchors.add(String(v));
|
|
430
|
+
});
|
|
323
431
|
|
|
324
432
|
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
325
|
-
|
|
433
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
434
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
326
435
|
|
|
327
436
|
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
328
|
-
const sid = key.slice(klass.length + 1); // après "
|
|
437
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
329
438
|
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
330
439
|
|
|
331
|
-
if (!
|
|
440
|
+
if (!anchors.has(String(sid))) mutate(() => dropWrap(w));
|
|
332
441
|
});
|
|
333
442
|
}
|
|
334
|
-
// ── Decluster ──────────────────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
|
|
338
|
-
* Priorité : filled > en grâce (fill en cours) > vide.
|
|
339
|
-
* Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
|
|
340
|
-
*/
|
|
341
|
-
function decluster(klass) {
|
|
342
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
|
|
343
|
-
// Grace sur le wrap courant : on le saute entièrement
|
|
344
|
-
const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
|
|
345
|
-
if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
|
|
346
|
-
|
|
347
|
-
let prev = w.previousElementSibling, steps = 0;
|
|
348
|
-
while (prev && steps++ < 3) {
|
|
349
|
-
if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
|
|
350
|
-
|
|
351
|
-
const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
|
|
352
|
-
if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
|
|
353
|
-
|
|
354
|
-
if (!isFilled(w)) mutate(() => dropWrap(w));
|
|
355
|
-
else if (!isFilled(prev)) mutate(() => dropWrap(prev));
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
443
|
|
|
361
444
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
362
445
|
|
|
363
446
|
/**
|
|
364
|
-
* Ordinal 0-based pour le calcul de l'intervalle.
|
|
365
|
-
*
|
|
366
|
-
* Pour catégories : position dans le parent (page statique, pas d'infinite scroll).
|
|
447
|
+
* Ordinal 0-based pour le calcul de l'intervalle d'injection.
|
|
448
|
+
* Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
|
|
367
449
|
*/
|
|
368
450
|
function ordinal(klass, el) {
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
} catch (_) {}
|
|
451
|
+
const attr = KIND[klass]?.ordinalAttr;
|
|
452
|
+
if (attr) {
|
|
453
|
+
const v = el.getAttribute(attr);
|
|
454
|
+
if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
455
|
+
}
|
|
456
|
+
const fullSel = KIND[klass]?.sel ?? '';
|
|
457
|
+
let i = 0;
|
|
458
|
+
for (const s of el.parentElement?.children ?? []) {
|
|
459
|
+
if (s === el) return i;
|
|
460
|
+
if (!fullSel || s.matches?.(fullSel)) i++;
|
|
461
|
+
}
|
|
382
462
|
return 0;
|
|
383
463
|
}
|
|
384
464
|
|
|
@@ -387,23 +467,25 @@
|
|
|
387
467
|
let inserted = 0;
|
|
388
468
|
|
|
389
469
|
for (const el of items) {
|
|
390
|
-
if (inserted >=
|
|
470
|
+
if (inserted >= MAX_INSERTS_RUN) break;
|
|
391
471
|
if (!el?.isConnected) continue;
|
|
392
472
|
|
|
393
|
-
const ord
|
|
394
|
-
|
|
395
|
-
if (!isTarget) continue;
|
|
396
|
-
|
|
473
|
+
const ord = ordinal(klass, el);
|
|
474
|
+
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
397
475
|
if (adjacentWrap(el)) continue;
|
|
398
476
|
|
|
399
|
-
const key =
|
|
400
|
-
if (findWrap(key)) continue;
|
|
477
|
+
const key = anchorKey(klass, el);
|
|
478
|
+
if (findWrap(key)) continue;
|
|
401
479
|
|
|
402
480
|
const id = pickId(poolKey);
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
481
|
+
if (id) {
|
|
482
|
+
const w = insertAfter(el, id, klass, key);
|
|
483
|
+
if (w) { observePh(id); inserted++; }
|
|
484
|
+
} else {
|
|
485
|
+
const recycled = recycleAndMove(klass, el, key);
|
|
486
|
+
if (!recycled) break;
|
|
487
|
+
inserted++;
|
|
488
|
+
}
|
|
407
489
|
}
|
|
408
490
|
return inserted;
|
|
409
491
|
}
|
|
@@ -411,14 +493,7 @@
|
|
|
411
493
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
412
494
|
|
|
413
495
|
function getIO() {
|
|
414
|
-
|
|
415
|
-
if (S.io && S.ioMargin === margin) return S.io;
|
|
416
|
-
// Si la marge doit changer (resize/orientation), on recrée l'observer.
|
|
417
|
-
if (S.io && S.ioMargin !== margin) {
|
|
418
|
-
try { S.io.disconnect(); } catch (_) {}
|
|
419
|
-
S.io = null;
|
|
420
|
-
}
|
|
421
|
-
S.ioMargin = margin;
|
|
496
|
+
if (S.io) return S.io;
|
|
422
497
|
try {
|
|
423
498
|
S.io = new IntersectionObserver(entries => {
|
|
424
499
|
for (const e of entries) {
|
|
@@ -427,7 +502,7 @@
|
|
|
427
502
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
428
503
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
429
504
|
}
|
|
430
|
-
}, { root: null, rootMargin:
|
|
505
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
431
506
|
} catch (_) { S.io = null; }
|
|
432
507
|
return S.io;
|
|
433
508
|
}
|
|
@@ -478,7 +553,6 @@
|
|
|
478
553
|
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
479
554
|
S.lastShow.set(id, t);
|
|
480
555
|
|
|
481
|
-
// Horodater le show sur le wrap pour grace period + emptyCheck
|
|
482
556
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
483
557
|
|
|
484
558
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -499,7 +573,6 @@
|
|
|
499
573
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
500
574
|
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
501
575
|
if (!wrap || !ph?.isConnected) return;
|
|
502
|
-
// Un show plus récent → ne pas toucher
|
|
503
576
|
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
504
577
|
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
505
578
|
} catch (_) {}
|
|
@@ -507,6 +580,10 @@
|
|
|
507
580
|
}
|
|
508
581
|
|
|
509
582
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
583
|
+
//
|
|
584
|
+
// Intercepte ez.showAds() pour :
|
|
585
|
+
// – ignorer les appels pendant blockedUntil
|
|
586
|
+
// – filtrer les ids dont le placeholder n'est pas en DOM
|
|
510
587
|
|
|
511
588
|
function patchShowAds() {
|
|
512
589
|
const apply = () => {
|
|
@@ -518,7 +595,7 @@
|
|
|
518
595
|
const orig = ez.showAds.bind(ez);
|
|
519
596
|
ez.showAds = function (...args) {
|
|
520
597
|
if (isBlocked()) return;
|
|
521
|
-
const ids
|
|
598
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
522
599
|
const seen = new Set();
|
|
523
600
|
for (const v of ids) {
|
|
524
601
|
const id = parseInt(v, 10);
|
|
@@ -537,7 +614,7 @@
|
|
|
537
614
|
}
|
|
538
615
|
}
|
|
539
616
|
|
|
540
|
-
// ── Core
|
|
617
|
+
// ── Core ───────────────────────────────────────────────────────────────────
|
|
541
618
|
|
|
542
619
|
async function runCore() {
|
|
543
620
|
if (isBlocked()) return 0;
|
|
@@ -552,30 +629,30 @@
|
|
|
552
629
|
|
|
553
630
|
const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
|
|
554
631
|
if (!normBool(cfgEnable)) return 0;
|
|
555
|
-
const items = getItems();
|
|
556
632
|
const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
|
|
557
|
-
|
|
558
|
-
const n = injectBetween(klass, items, interval, normBool(cfgShowFirst), poolKey);
|
|
559
|
-
if (n) decluster(klass);
|
|
560
|
-
return n;
|
|
633
|
+
return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
|
|
561
634
|
};
|
|
562
635
|
|
|
563
636
|
if (kind === 'topic') return exec(
|
|
564
637
|
'ezoic-ad-message', getPosts,
|
|
565
638
|
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
566
639
|
);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
640
|
+
|
|
641
|
+
if (kind === 'categoryTopics') {
|
|
642
|
+
pruneOrphansBetween();
|
|
643
|
+
return exec(
|
|
644
|
+
'ezoic-ad-between', getTopics,
|
|
645
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return exec(
|
|
572
650
|
'ezoic-ad-categories', getCategories,
|
|
573
651
|
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
574
652
|
);
|
|
575
|
-
return 0;
|
|
576
653
|
}
|
|
577
654
|
|
|
578
|
-
// ── Scheduler
|
|
655
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
579
656
|
|
|
580
657
|
function scheduleRun(cb) {
|
|
581
658
|
if (S.runQueued) return;
|
|
@@ -593,10 +670,8 @@
|
|
|
593
670
|
if (isBlocked()) return;
|
|
594
671
|
const t = ts();
|
|
595
672
|
if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
|
|
596
|
-
S.lastBurstTs
|
|
597
|
-
|
|
598
|
-
const pk = pageKey();
|
|
599
|
-
S.pageKey = pk;
|
|
673
|
+
S.lastBurstTs = t;
|
|
674
|
+
S.pageKey = pageKey();
|
|
600
675
|
S.burstDeadline = t + 2000;
|
|
601
676
|
|
|
602
677
|
if (S.burstActive) return;
|
|
@@ -604,7 +679,7 @@
|
|
|
604
679
|
S.burstCount = 0;
|
|
605
680
|
|
|
606
681
|
const step = () => {
|
|
607
|
-
if (pageKey() !==
|
|
682
|
+
if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
|
|
608
683
|
S.burstActive = false; return;
|
|
609
684
|
}
|
|
610
685
|
S.burstCount++;
|
|
@@ -616,17 +691,18 @@
|
|
|
616
691
|
step();
|
|
617
692
|
}
|
|
618
693
|
|
|
619
|
-
// ── Cleanup
|
|
694
|
+
// ── Cleanup navigation ─────────────────────────────────────────────────────
|
|
620
695
|
|
|
621
696
|
function cleanup() {
|
|
622
697
|
blockedUntil = ts() + 1500;
|
|
623
698
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
624
699
|
S.cfg = null;
|
|
625
|
-
S.
|
|
700
|
+
S.poolsReady = false;
|
|
626
701
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
627
702
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
628
703
|
S.mountedIds.clear();
|
|
629
704
|
S.lastShow.clear();
|
|
705
|
+
S.wrapByKey.clear();
|
|
630
706
|
S.inflight = 0;
|
|
631
707
|
S.pending = [];
|
|
632
708
|
S.pendingSet.clear();
|
|
@@ -634,19 +710,19 @@
|
|
|
634
710
|
S.runQueued = false;
|
|
635
711
|
}
|
|
636
712
|
|
|
637
|
-
// ──
|
|
713
|
+
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
638
714
|
|
|
639
715
|
function ensureDomObserver() {
|
|
640
716
|
if (S.domObs) return;
|
|
717
|
+
const allSel = [SEL.post, SEL.topic, SEL.category];
|
|
641
718
|
S.domObs = new MutationObserver(muts => {
|
|
642
719
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
643
720
|
for (const m of muts) {
|
|
644
|
-
if (!m.addedNodes?.length) continue;
|
|
645
721
|
for (const n of m.addedNodes) {
|
|
646
722
|
if (n.nodeType !== 1) continue;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
723
|
+
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
724
|
+
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
725
|
+
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
650
726
|
requestBurst(); return;
|
|
651
727
|
}
|
|
652
728
|
}
|
|
@@ -660,7 +736,14 @@
|
|
|
660
736
|
function muteConsole() {
|
|
661
737
|
if (window.__nbbEzMuted) return;
|
|
662
738
|
window.__nbbEzMuted = true;
|
|
663
|
-
const MUTED = [
|
|
739
|
+
const MUTED = [
|
|
740
|
+
'[EzoicAds JS]: Placeholder Id',
|
|
741
|
+
'No valid placeholders for loadMore',
|
|
742
|
+
'cannot call refresh on the same page',
|
|
743
|
+
'no placeholders are currently defined in Refresh',
|
|
744
|
+
'Debugger iframe already exists',
|
|
745
|
+
`with id ${PH_PREFIX}`,
|
|
746
|
+
];
|
|
664
747
|
for (const m of ['log', 'info', 'warn', 'error']) {
|
|
665
748
|
const orig = console[m];
|
|
666
749
|
if (typeof orig !== 'function') continue;
|
|
@@ -672,29 +755,18 @@
|
|
|
672
755
|
}
|
|
673
756
|
|
|
674
757
|
function ensureTcfLocator() {
|
|
675
|
-
// Le CMP utilise une iframe nommée __tcfapiLocator pour router les
|
|
676
|
-
// postMessage TCF. En navigation ajaxify, NodeBB peut retirer cette
|
|
677
|
-
// iframe du DOM (vidage partiel du body), ce qui provoque :
|
|
678
|
-
// "Cannot read properties of null (reading 'postMessage')"
|
|
679
|
-
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
680
|
-
// Solution : la recrée immédiatement si elle disparaît, via un observer.
|
|
681
758
|
try {
|
|
682
759
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
683
|
-
|
|
684
760
|
const inject = () => {
|
|
685
761
|
if (document.getElementById('__tcfapiLocator')) return;
|
|
686
762
|
const f = document.createElement('iframe');
|
|
687
763
|
f.style.display = 'none'; f.id = f.name = '__tcfapiLocator';
|
|
688
764
|
(document.body || document.documentElement).appendChild(f);
|
|
689
765
|
};
|
|
690
|
-
|
|
691
766
|
inject();
|
|
692
|
-
|
|
693
|
-
// Observer dédié — si quelqu'un retire l'iframe, on la remet.
|
|
694
767
|
if (!window.__nbbTcfObs) {
|
|
695
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
696
|
-
window.__nbbTcfObs.observe(document.documentElement,
|
|
697
|
-
{ childList: true, subtree: true });
|
|
768
|
+
window.__nbbTcfObs = new MutationObserver(inject);
|
|
769
|
+
window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
698
770
|
}
|
|
699
771
|
} catch (_) {}
|
|
700
772
|
}
|
|
@@ -704,10 +776,10 @@
|
|
|
704
776
|
const head = document.head;
|
|
705
777
|
if (!head) return;
|
|
706
778
|
for (const [rel, href, cors] of [
|
|
707
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
708
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
709
|
-
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
710
|
-
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
779
|
+
['preconnect', 'https://g.ezoic.net', true ],
|
|
780
|
+
['preconnect', 'https://go.ezoic.net', true ],
|
|
781
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
782
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
711
783
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
712
784
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
713
785
|
]) {
|
|
@@ -721,7 +793,7 @@
|
|
|
721
793
|
}
|
|
722
794
|
}
|
|
723
795
|
|
|
724
|
-
// ── Bindings
|
|
796
|
+
// ── Bindings ───────────────────────────────────────────────────────────────
|
|
725
797
|
|
|
726
798
|
function bindNodeBB() {
|
|
727
799
|
const $ = window.jQuery;
|
|
@@ -732,19 +804,16 @@
|
|
|
732
804
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
733
805
|
S.pageKey = pageKey();
|
|
734
806
|
blockedUntil = 0;
|
|
735
|
-
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
736
|
-
getIO(); ensureDomObserver(); requestBurst();
|
|
807
|
+
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
808
|
+
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
737
809
|
});
|
|
738
810
|
|
|
739
|
-
const
|
|
740
|
-
'action:ajaxify.contentLoaded',
|
|
741
|
-
'action:posts.loaded', 'action:topics.loaded',
|
|
811
|
+
const burstEvts = [
|
|
812
|
+
'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
|
|
742
813
|
'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
|
|
743
814
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
815
|
+
$(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
|
|
744
816
|
|
|
745
|
-
$(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
|
|
746
|
-
|
|
747
|
-
// Hooks AMD (NodeBB 4.x — redondant avec jQuery events mais sans risque)
|
|
748
817
|
try {
|
|
749
818
|
require(['hooks'], hooks => {
|
|
750
819
|
if (typeof hooks?.on !== 'function') return;
|
|
@@ -756,22 +825,6 @@
|
|
|
756
825
|
} catch (_) {}
|
|
757
826
|
}
|
|
758
827
|
|
|
759
|
-
|
|
760
|
-
function bindResize() {
|
|
761
|
-
let t = null;
|
|
762
|
-
window.addEventListener('resize', () => {
|
|
763
|
-
clearTimeout(t);
|
|
764
|
-
t = setTimeout(() => {
|
|
765
|
-
try { getIO(); } catch (_) {}
|
|
766
|
-
// Ré-observer les placeholders existants (si IO recréé)
|
|
767
|
-
try {
|
|
768
|
-
document.querySelectorAll(`.${WRAP_CLASS} [id^="${PH_PREFIX}"]`).forEach(ph => {
|
|
769
|
-
if (ph instanceof Element) { try { S.io?.observe(ph); } catch (_) {} }
|
|
770
|
-
});
|
|
771
|
-
} catch (_) {}
|
|
772
|
-
}, 200);
|
|
773
|
-
}, { passive: true });
|
|
774
|
-
}
|
|
775
828
|
function bindScroll() {
|
|
776
829
|
let ticking = false;
|
|
777
830
|
window.addEventListener('scroll', () => {
|
|
@@ -791,7 +844,6 @@
|
|
|
791
844
|
getIO();
|
|
792
845
|
ensureDomObserver();
|
|
793
846
|
bindNodeBB();
|
|
794
|
-
bindResize();
|
|
795
847
|
bindScroll();
|
|
796
848
|
blockedUntil = 0;
|
|
797
849
|
requestBurst();
|