@kupola/kupola 1.4.9 → 1.5.2
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/dist/kupola.cjs.js +21 -21
- package/dist/kupola.cjs.js.map +1 -1
- package/dist/kupola.esm.js +1103 -940
- package/dist/kupola.esm.js.map +1 -1
- package/dist/kupola.umd.js +21 -21
- package/dist/kupola.umd.js.map +1 -1
- package/js/depends.js +12 -1
- package/js/i18n.js +5 -2
- package/js/kupola-config.js +125 -0
- package/js/kupola-core.js +42 -5
- package/js/modal.js +7 -3
- package/js/security.js +61 -0
- package/js/theme.js +9 -8
- package/package.json +1 -1
package/js/depends.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref } from './data-bind.js';
|
|
2
|
+
import { getConfig } from './kupola-config.js';
|
|
2
3
|
|
|
3
4
|
// ============================================================
|
|
4
5
|
// Scheduler - 批量更新、去重、微任务调度
|
|
@@ -273,6 +274,11 @@ class FetchedSource extends DependsSource {
|
|
|
273
274
|
|
|
274
275
|
async fetch(params) {
|
|
275
276
|
let url = this.config.source;
|
|
277
|
+
const httpConfig = getConfig('http');
|
|
278
|
+
|
|
279
|
+
if (httpConfig?.baseURL && !url.startsWith('http://') && !url.startsWith('https://')) {
|
|
280
|
+
url = httpConfig.baseURL + url.replace(/^\//, '');
|
|
281
|
+
}
|
|
276
282
|
|
|
277
283
|
// Replace path params (:id -> value)
|
|
278
284
|
for (const key in params) {
|
|
@@ -293,11 +299,16 @@ class FetchedSource extends DependsSource {
|
|
|
293
299
|
url += (url.includes('?') ? '&' : '?') + queryParts.join('&');
|
|
294
300
|
}
|
|
295
301
|
|
|
302
|
+
const globalHeaders = httpConfig?.headers || {};
|
|
296
303
|
const fetchOptions = {
|
|
297
304
|
method: this.method.toUpperCase(),
|
|
298
|
-
headers: { 'Content-Type': 'application/json', ...this.headers }
|
|
305
|
+
headers: { 'Content-Type': 'application/json', ...globalHeaders, ...this.headers }
|
|
299
306
|
};
|
|
300
307
|
|
|
308
|
+
if (httpConfig?.withCredentials) {
|
|
309
|
+
fetchOptions.credentials = 'include';
|
|
310
|
+
}
|
|
311
|
+
|
|
301
312
|
if (['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
|
|
302
313
|
fetchOptions.body = JSON.stringify(params);
|
|
303
314
|
}
|
package/js/i18n.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { getConfig } from './kupola-config.js';
|
|
2
|
+
|
|
1
3
|
class KupolaI18n {
|
|
2
4
|
constructor(options = {}) {
|
|
5
|
+
const config = getConfig();
|
|
3
6
|
this.locales = options.locales || {};
|
|
4
|
-
this.currentLocale = options.defaultLocale || 'zh-CN';
|
|
5
|
-
this.fallbackLocale = options.fallbackLocale || '
|
|
7
|
+
this.currentLocale = options.defaultLocale || config.i18n?.locale || 'zh-CN';
|
|
8
|
+
this.fallbackLocale = options.fallbackLocale || config.i18n?.fallbackLocale || 'en-US';
|
|
6
9
|
this.delimiter = options.delimiter || '.';
|
|
7
10
|
this.missingHandler = options.missingHandler || ((key) => {
|
|
8
11
|
console.warn(`Missing translation: ${key}`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const config = {
|
|
2
|
+
paths: {
|
|
3
|
+
icons: '/icons/',
|
|
4
|
+
base: '/'
|
|
5
|
+
},
|
|
6
|
+
theme: {
|
|
7
|
+
default: 'dark',
|
|
8
|
+
brand: 'zengqing'
|
|
9
|
+
},
|
|
10
|
+
i18n: {
|
|
11
|
+
locale: 'zh-CN',
|
|
12
|
+
fallbackLocale: 'en-US'
|
|
13
|
+
},
|
|
14
|
+
http: {
|
|
15
|
+
baseURL: '',
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
headers: {},
|
|
18
|
+
withCredentials: false
|
|
19
|
+
},
|
|
20
|
+
ui: {
|
|
21
|
+
defaultSize: 'md',
|
|
22
|
+
defaultVariant: 'default'
|
|
23
|
+
},
|
|
24
|
+
performance: {
|
|
25
|
+
lazyLoad: true,
|
|
26
|
+
debounceDelay: 300,
|
|
27
|
+
throttleDelay: 100,
|
|
28
|
+
animationEnabled: true
|
|
29
|
+
},
|
|
30
|
+
security: {
|
|
31
|
+
xssProtection: true,
|
|
32
|
+
sanitizeHtml: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
allowedTags: ['b', 'i', 'u', 'em', 'strong', 'a', 'br', 'p', 'span', 'div', 'img'],
|
|
35
|
+
allowedAttributes: {
|
|
36
|
+
'a': ['href', 'target', 'rel'],
|
|
37
|
+
'img': ['src', 'alt', 'width', 'height'],
|
|
38
|
+
'span': ['class', 'style'],
|
|
39
|
+
'div': ['class', 'style']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
dev: {
|
|
44
|
+
debug: false,
|
|
45
|
+
logLevel: 'warn'
|
|
46
|
+
},
|
|
47
|
+
extend: {
|
|
48
|
+
themeVariables: {}
|
|
49
|
+
},
|
|
50
|
+
components: {
|
|
51
|
+
autoInit: true,
|
|
52
|
+
silentErrors: false
|
|
53
|
+
},
|
|
54
|
+
store: {
|
|
55
|
+
persist: true,
|
|
56
|
+
prefix: 'kupola-'
|
|
57
|
+
},
|
|
58
|
+
events: {
|
|
59
|
+
global: true
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function setConfig(options) {
|
|
64
|
+
mergeDeep(config, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getConfig(key) {
|
|
68
|
+
if (!key) return config;
|
|
69
|
+
return getNestedValue(config, key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getIconsPath() {
|
|
73
|
+
return config.paths.base + config.paths.icons.replace(/^\//, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getBasePath() {
|
|
77
|
+
return config.paths.base;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getDefaultTheme() {
|
|
81
|
+
return config.theme.default;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getDefaultBrand() {
|
|
85
|
+
return config.theme.brand;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getHttpConfig() {
|
|
89
|
+
return config.http;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getUiConfig() {
|
|
93
|
+
return config.ui;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getSecurityConfig() {
|
|
97
|
+
return config.security;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getDevConfig() {
|
|
101
|
+
return config.dev;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getPerformanceConfig() {
|
|
105
|
+
return config.performance;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getExtendConfig() {
|
|
109
|
+
return config.extend;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mergeDeep(target, source) {
|
|
113
|
+
for (const key in source) {
|
|
114
|
+
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
|
115
|
+
mergeDeep(target[key], source[key]);
|
|
116
|
+
} else {
|
|
117
|
+
target[key] = source[key];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return target;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getNestedValue(obj, key) {
|
|
124
|
+
return key.split('.').reduce((o, k) => (o && o[k]) !== undefined ? o[k] : undefined, obj);
|
|
125
|
+
}
|
package/js/kupola-core.js
CHANGED
|
@@ -9,6 +9,7 @@ import { KupolaLifecycle } from './kupola-lifecycle.js';
|
|
|
9
9
|
import { kupolaInitializer } from './initializer.js';
|
|
10
10
|
import { kupolaData } from './data-bind.js';
|
|
11
11
|
import { initTheme } from './theme.js';
|
|
12
|
+
import { getConfig, getSecurityConfig } from './kupola-config.js';
|
|
12
13
|
|
|
13
14
|
let kupolaRegistry = null;
|
|
14
15
|
|
|
@@ -16,19 +17,55 @@ if (typeof window !== 'undefined') {
|
|
|
16
17
|
kupolaRegistry = new KupolaComponentRegistry();
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function _applySecurityHeaders() {
|
|
21
|
+
const securityConfig = getSecurityConfig();
|
|
22
|
+
|
|
23
|
+
if (securityConfig.xssProtection && typeof document !== 'undefined') {
|
|
24
|
+
let metaTag = document.querySelector('meta[http-equiv="X-XSS-Protection"]');
|
|
25
|
+
if (!metaTag) {
|
|
26
|
+
metaTag = document.createElement('meta');
|
|
27
|
+
metaTag.setAttribute('http-equiv', 'X-XSS-Protection');
|
|
28
|
+
metaTag.setAttribute('content', '1; mode=block');
|
|
29
|
+
document.head.insertBefore(metaTag, document.head.firstChild);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
metaTag = document.querySelector('meta[http-equiv="X-Content-Type-Options"]');
|
|
33
|
+
if (!metaTag) {
|
|
34
|
+
metaTag = document.createElement('meta');
|
|
35
|
+
metaTag.setAttribute('http-equiv', 'X-Content-Type-Options');
|
|
36
|
+
metaTag.setAttribute('content', 'nosniff');
|
|
37
|
+
document.head.insertBefore(metaTag, document.head.firstChild);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
metaTag = document.querySelector('meta[http-equiv="X-Frame-Options"]');
|
|
41
|
+
if (!metaTag) {
|
|
42
|
+
metaTag = document.createElement('meta');
|
|
43
|
+
metaTag.setAttribute('http-equiv', 'X-Frame-Options');
|
|
44
|
+
metaTag.setAttribute('content', 'SAMEORIGIN');
|
|
45
|
+
document.head.insertBefore(metaTag, document.head.firstChild);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
/** Bootstrap the Kupola component system (data binding, theme, component discovery). */
|
|
20
51
|
async function kupolaBootstrap() {
|
|
21
52
|
if (typeof window !== 'undefined') {
|
|
53
|
+
_applySecurityHeaders();
|
|
54
|
+
|
|
55
|
+
const config = getConfig();
|
|
22
56
|
// Load persisted data and bind data-bind elements
|
|
23
57
|
kupolaData.loadPersisted();
|
|
24
58
|
kupolaData.bind();
|
|
25
59
|
// Initialize theme
|
|
26
60
|
initTheme();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
|
|
62
|
+
if (config.components?.autoInit !== false) {
|
|
63
|
+
// Initialize function-based components first
|
|
64
|
+
await kupolaInitializer.initializeAll();
|
|
65
|
+
// Then bootstrap class-based components
|
|
66
|
+
if (kupolaRegistry) {
|
|
67
|
+
await kupolaRegistry.bootstrap();
|
|
68
|
+
}
|
|
32
69
|
}
|
|
33
70
|
}
|
|
34
71
|
}
|
package/js/modal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { kupolaInitializer } from './initializer.js';
|
|
2
|
+
import { getUiConfig } from './kupola-config.js';
|
|
2
3
|
|
|
3
4
|
class Modal {
|
|
4
5
|
constructor(element, options = {}) {
|
|
@@ -182,9 +183,12 @@ function createModal(options = {}) {
|
|
|
182
183
|
onCancel,
|
|
183
184
|
onOpen,
|
|
184
185
|
onClose,
|
|
185
|
-
footer = null
|
|
186
|
+
footer = null,
|
|
187
|
+
size = getUiConfig().defaultSize
|
|
186
188
|
} = options;
|
|
187
189
|
|
|
190
|
+
const sizeClass = size === 'sm' ? 'ds-btn--sm' : size === 'lg' ? 'ds-btn--lg' : '';
|
|
191
|
+
|
|
188
192
|
const container = document.createElement('div');
|
|
189
193
|
container.className = 'ds-modal-container';
|
|
190
194
|
|
|
@@ -194,8 +198,8 @@ function createModal(options = {}) {
|
|
|
194
198
|
footerHTML = `<div class="ds-modal__footer">${footer}</div>`;
|
|
195
199
|
} else if (showConfirm || showCancel) {
|
|
196
200
|
footerHTML = `<div class="ds-modal__footer">
|
|
197
|
-
${showCancel ? `<button class="ds-btn ${cancelClass}" data-modal-cancel>${cancelText}</button>` : ''}
|
|
198
|
-
${showConfirm ? `<button class="ds-btn ${confirmClass}" data-modal-confirm>${confirmText}</button>` : ''}
|
|
201
|
+
${showCancel ? `<button class="ds-btn ${sizeClass} ${cancelClass}" data-modal-cancel>${cancelText}</button>` : ''}
|
|
202
|
+
${showConfirm ? `<button class="ds-btn ${sizeClass} ${confirmClass}" data-modal-confirm>${confirmText}</button>` : ''}
|
|
199
203
|
</div>`;
|
|
200
204
|
}
|
|
201
205
|
}
|
package/js/security.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getSecurityConfig } from './kupola-config.js';
|
|
2
|
+
|
|
3
|
+
export function sanitizeHtml(html, options = {}) {
|
|
4
|
+
const securityConfig = getSecurityConfig();
|
|
5
|
+
const sanitizeConfig = securityConfig?.sanitizeHtml || {};
|
|
6
|
+
|
|
7
|
+
if (!sanitizeConfig.enabled && !options.force) {
|
|
8
|
+
return html;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const allowedTags = options.allowedTags || sanitizeConfig.allowedTags || [];
|
|
12
|
+
const allowedAttributes = options.allowedAttributes || sanitizeConfig.allowedAttributes || {};
|
|
13
|
+
|
|
14
|
+
if (typeof html !== 'string') {
|
|
15
|
+
return html;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const domParser = new DOMParser();
|
|
19
|
+
const doc = domParser.parseFromString(html, 'text/html');
|
|
20
|
+
const elements = doc.body.querySelectorAll('*');
|
|
21
|
+
|
|
22
|
+
elements.forEach(element => {
|
|
23
|
+
const tagName = element.tagName.toLowerCase();
|
|
24
|
+
|
|
25
|
+
if (!allowedTags.includes(tagName)) {
|
|
26
|
+
element.remove();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Array.from(element.attributes).forEach(attr => {
|
|
31
|
+
const attrName = attr.name.toLowerCase();
|
|
32
|
+
const tagAllowedAttrs = allowedAttributes[tagName] || [];
|
|
33
|
+
|
|
34
|
+
if (!tagAllowedAttrs.includes(attrName)) {
|
|
35
|
+
element.removeAttribute(attr.name);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return doc.body.innerHTML;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function escapeHtml(text) {
|
|
44
|
+
if (typeof text !== 'string') {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const div = document.createElement('div');
|
|
49
|
+
div.textContent = text;
|
|
50
|
+
return div.innerHTML;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function stripHtml(html) {
|
|
54
|
+
if (typeof html !== 'string') {
|
|
55
|
+
return html;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const domParser = new DOMParser();
|
|
59
|
+
const doc = domParser.parseFromString(html, 'text/html');
|
|
60
|
+
return doc.body.textContent || '';
|
|
61
|
+
}
|
package/js/theme.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getIconsPath, getDefaultTheme, getDefaultBrand } from './kupola-config.js';
|
|
2
|
+
|
|
1
3
|
const THEME_KEY = 'kupola-theme';
|
|
2
4
|
const BRAND_KEY = 'kupola-brand';
|
|
3
5
|
|
|
@@ -16,7 +18,7 @@ const BRAND_OPTIONS = [
|
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
function getTheme() {
|
|
19
|
-
return localStorage.getItem(THEME_KEY) ||
|
|
21
|
+
return localStorage.getItem(THEME_KEY) || getDefaultTheme();
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function setTheme(theme) {
|
|
@@ -39,7 +41,7 @@ function setTheme(theme) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function getBrand() {
|
|
42
|
-
return localStorage.getItem(BRAND_KEY) ||
|
|
44
|
+
return localStorage.getItem(BRAND_KEY) || getDefaultBrand();
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function setBrand(brandId) {
|
|
@@ -76,7 +78,8 @@ function updateThemeIcon(toggleBtn) {
|
|
|
76
78
|
const iconEl = toggleBtn.querySelector('.theme-icon');
|
|
77
79
|
if (iconEl) {
|
|
78
80
|
const currentTheme = getTheme();
|
|
79
|
-
|
|
81
|
+
const iconsPath = getIconsPath();
|
|
82
|
+
iconEl.src = currentTheme === 'dark' ? iconsPath + 'sun.svg' : iconsPath + 'moon.svg';
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -203,12 +206,10 @@ function createThemeToggle() {
|
|
|
203
206
|
|
|
204
207
|
const icon = document.createElement('img');
|
|
205
208
|
icon.className = 'theme-icon';
|
|
206
|
-
const
|
|
207
|
-
const currentScript = scripts[scripts.length - 1];
|
|
208
|
-
const scriptPath = currentScript.src.substring(0, currentScript.src.lastIndexOf('/') + 1);
|
|
209
|
+
const iconsPath = getIconsPath();
|
|
209
210
|
icon.src = getTheme() === 'dark'
|
|
210
|
-
?
|
|
211
|
-
:
|
|
211
|
+
? iconsPath + 'sun.svg'
|
|
212
|
+
: iconsPath + 'moon.svg';
|
|
212
213
|
icon.width = 14;
|
|
213
214
|
icon.height = 14;
|
|
214
215
|
icon.alt = 'Toggle theme';
|
package/package.json
CHANGED