@blocklet/editor 2.4.75 → 2.4.77
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/lib/ext/CustomComponent/CustomComponent.d.ts +3 -1
- package/lib/ext/CustomComponent/CustomComponent.js +2 -2
- package/lib/ext/CustomComponent/CustomComponentNode.js +2 -1
- package/lib/ext/CustomComponent/components/Code.d.ts +11 -0
- package/lib/ext/CustomComponent/components/Code.js +171 -0
- package/lib/ext/CustomComponent/components/index.d.ts +2 -0
- package/lib/ext/CustomComponent/components/index.js +2 -0
- package/lib/main/themes/customTheme.js +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type EditorThemeClasses } from 'lexical';
|
|
1
2
|
interface CustomComponentProps {
|
|
2
3
|
component: string;
|
|
3
4
|
properties?: any;
|
|
5
|
+
editorTheme?: EditorThemeClasses;
|
|
4
6
|
}
|
|
5
|
-
export declare function CustomComponent({ component, properties }: CustomComponentProps): import("react/jsx-runtime").JSX.Element | null;
|
|
7
|
+
export declare function CustomComponent({ component, properties, editorTheme }: CustomComponentProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
8
|
export {};
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box } from '@mui/material';
|
|
3
3
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
4
4
|
import components from './components';
|
|
5
|
-
export function CustomComponent({ component, properties }) {
|
|
5
|
+
export function CustomComponent({ component, properties, editorTheme }) {
|
|
6
6
|
const Component = components[component];
|
|
7
7
|
if (!Component) {
|
|
8
8
|
return null;
|
|
@@ -11,5 +11,5 @@ export function CustomComponent({ component, properties }) {
|
|
|
11
11
|
console.error('CustomComponent error', component, properties, error);
|
|
12
12
|
return null;
|
|
13
13
|
};
|
|
14
|
-
return (_jsx(Box, { sx: { py: 1 }, children: _jsx(ErrorBoundary, { fallbackRender: fallbackRender, children: _jsx(Component, { ...properties }) }) }));
|
|
14
|
+
return (_jsx(Box, { sx: { py: 1 }, children: _jsx(ErrorBoundary, { fallbackRender: fallbackRender, children: _jsx(Component, { ...properties, editorTheme: editorTheme }) }) }));
|
|
15
15
|
}
|
|
@@ -65,6 +65,7 @@ export class CustomComponentNode extends DecoratorBlockNode {
|
|
|
65
65
|
}
|
|
66
66
|
static importDOM() {
|
|
67
67
|
return {
|
|
68
|
+
'x-code': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
|
|
68
69
|
'x-card': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
|
|
69
70
|
'x-cards': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
|
|
70
71
|
'x-code-group': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
|
|
@@ -99,7 +100,7 @@ export class CustomComponentNode extends DecoratorBlockNode {
|
|
|
99
100
|
base: customComponentTheme.base || '',
|
|
100
101
|
focus: customComponentTheme.focus || '',
|
|
101
102
|
};
|
|
102
|
-
return (_jsx(BlockWithAlignableContents, { className: className, format: this.__format, nodeKey: this.__key, children: _jsx(CustomComponent, { component: this.__data.component, properties: this.__data.properties }) }));
|
|
103
|
+
return (_jsx(BlockWithAlignableContents, { className: className, format: this.__format, nodeKey: this.__key, children: _jsx(CustomComponent, { component: this.__data.component, properties: this.__data.properties, editorTheme: config.theme }) }));
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
export function $createCustomComponentNode(data) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type EditorThemeClasses } from 'lexical';
|
|
2
|
+
export interface CodeProps {
|
|
3
|
+
editorTheme: EditorThemeClasses;
|
|
4
|
+
language: string;
|
|
5
|
+
title: string;
|
|
6
|
+
body?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
foldable?: string;
|
|
9
|
+
foldthreshold?: string;
|
|
10
|
+
}
|
|
11
|
+
export default function Code({ editorTheme, language, title, body, icon, foldable: _foldable, foldthreshold: _foldThreshold, }: CodeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { codeToHtml } from 'shiki';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Box, IconButton, styled, useTheme, Typography, Stack } from '@mui/material';
|
|
5
|
+
import { Icon } from '@iconify/react';
|
|
6
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
7
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
8
|
+
import { useMemoizedFn } from 'ahooks';
|
|
9
|
+
const translations = {
|
|
10
|
+
en: {
|
|
11
|
+
seeAllLines: 'See all {lines} lines',
|
|
12
|
+
collapseCode: 'Collapse code',
|
|
13
|
+
},
|
|
14
|
+
zh: {
|
|
15
|
+
seeAllLines: '查看所有 {lines} 行',
|
|
16
|
+
collapseCode: '折叠代码',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const XCode = styled('div')(({ theme }) => ({
|
|
20
|
+
position: 'relative',
|
|
21
|
+
borderRadius: theme.shape.borderRadius,
|
|
22
|
+
border: '1px solid',
|
|
23
|
+
borderColor: theme.palette.divider,
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
pre: {
|
|
26
|
+
margin: 0,
|
|
27
|
+
borderRadius: 0,
|
|
28
|
+
code: {
|
|
29
|
+
margin: 0,
|
|
30
|
+
border: 0,
|
|
31
|
+
borderRadius: 0,
|
|
32
|
+
background: 'transparent',
|
|
33
|
+
color: 'inherit',
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
/** Header 组件 */
|
|
39
|
+
function CodeHeader({ title, icon, actions }) {
|
|
40
|
+
if (!title && !icon)
|
|
41
|
+
return null;
|
|
42
|
+
return (_jsxs(Box, { sx: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'space-between',
|
|
46
|
+
p: 1.5,
|
|
47
|
+
borderBottom: 1,
|
|
48
|
+
borderColor: 'divider',
|
|
49
|
+
bgcolor: 'background.paper',
|
|
50
|
+
borderRadius: '8px 8px 0 0',
|
|
51
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1, flex: 1 }, children: [icon && _jsx(Box, { component: Icon, icon: icon, sx: { fontSize: '1.2em', color: 'text.secondary' } }), title && (_jsx(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500 }, children: title }))] }), actions && _jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: actions })] }));
|
|
52
|
+
}
|
|
53
|
+
/** 通用操作按钮 */
|
|
54
|
+
function ActionButton({ className, icon, onClick, sx = {}, }) {
|
|
55
|
+
return (_jsx(IconButton, { className: className, size: "small", sx: {
|
|
56
|
+
color: 'text.secondary',
|
|
57
|
+
bgcolor: 'transparent',
|
|
58
|
+
'&:hover': {
|
|
59
|
+
color: 'text.primary',
|
|
60
|
+
bgcolor: 'action.hover',
|
|
61
|
+
},
|
|
62
|
+
...sx,
|
|
63
|
+
}, onClick: onClick, children: _jsx(Box, { component: Icon, icon: icon, sx: { fontSize: '1em' } }) }));
|
|
64
|
+
}
|
|
65
|
+
/** 复制按钮 */
|
|
66
|
+
function CopyButton({ content, sx = {} }) {
|
|
67
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
68
|
+
const handleCopy = async () => {
|
|
69
|
+
try {
|
|
70
|
+
await navigator.clipboard.writeText(content);
|
|
71
|
+
setIsCopied(true);
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
setIsCopied(false);
|
|
74
|
+
}, 1500);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error('Failed to copy: ', err);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return (_jsx(ActionButton, { className: "copy-button", icon: isCopied ? 'tabler:check' : 'tabler:copy', onClick: handleCopy, sx: {
|
|
81
|
+
...sx,
|
|
82
|
+
...(isCopied && { color: 'success.main', '&:hover': { color: 'success.main' } }),
|
|
83
|
+
} }));
|
|
84
|
+
}
|
|
85
|
+
/** 折叠按钮 */
|
|
86
|
+
function FoldButton({ isCollapsed, onToggle, sx = {} }) {
|
|
87
|
+
return (_jsx(ActionButton, { className: "fold-button", icon: isCollapsed ? 'tabler:chevron-down' : 'tabler:chevron-up', onClick: onToggle, sx: sx }));
|
|
88
|
+
}
|
|
89
|
+
/** 折叠 Footer */
|
|
90
|
+
function FoldFooter({ remainingLines, isCollapsed, onToggle, }) {
|
|
91
|
+
const { locale = 'en' } = useLocaleContext();
|
|
92
|
+
const t = useMemoizedFn((key, data = {}) => translate(translations, key, locale, 'en', data));
|
|
93
|
+
return (_jsx(Box, { sx: {
|
|
94
|
+
display: 'flex',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'space-between',
|
|
97
|
+
p: 1.5,
|
|
98
|
+
borderTop: 1,
|
|
99
|
+
borderColor: 'divider',
|
|
100
|
+
bgcolor: 'background.paper',
|
|
101
|
+
borderRadius: '0 0 8px 8px',
|
|
102
|
+
cursor: 'pointer',
|
|
103
|
+
'&:hover': {
|
|
104
|
+
bgcolor: 'action.hover',
|
|
105
|
+
},
|
|
106
|
+
}, onClick: onToggle, children: _jsxs(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500, display: 'flex', alignItems: 'center' }, children: [_jsx(Box, { component: Icon, icon: "tabler:dots", sx: { fontSize: '1em', mr: 1 } }), isCollapsed ? t('seeAllLines', { lines: remainingLines }) : t('collapseCode', { lines: remainingLines })] }) }));
|
|
107
|
+
}
|
|
108
|
+
export default function Code({ editorTheme, language, title, body = '', icon = '', foldable: _foldable = 'true', foldthreshold: _foldThreshold = '25', // ps: dataset 会强制将 key 小写
|
|
109
|
+
}) {
|
|
110
|
+
const [highlightedCode, setHighlightedCode] = useState('');
|
|
111
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
112
|
+
const muiTheme = useTheme();
|
|
113
|
+
// 参数类型转换(html dataset 只支持 string value)
|
|
114
|
+
const foldable = _foldable === 'true';
|
|
115
|
+
let foldThreshold = parseInt(_foldThreshold, 10);
|
|
116
|
+
if (Number.isNaN(foldThreshold)) {
|
|
117
|
+
foldThreshold = 25;
|
|
118
|
+
}
|
|
119
|
+
const lineCount = body ? body.split('\n').length : 0;
|
|
120
|
+
const showFoldButton = foldable && lineCount > foldThreshold;
|
|
121
|
+
// 初始折叠状态:启用折叠且超过阈值就折叠
|
|
122
|
+
const [isCollapsed, setIsCollapsed] = useState(foldable && lineCount > foldThreshold);
|
|
123
|
+
// 获取最终需要显示的内容
|
|
124
|
+
const getDisplayContent = () => {
|
|
125
|
+
if (!isCollapsed || lineCount <= foldThreshold) {
|
|
126
|
+
return body;
|
|
127
|
+
}
|
|
128
|
+
const lines = body.split('\n');
|
|
129
|
+
const previewLines = lines.slice(0, foldThreshold);
|
|
130
|
+
return previewLines.join('\n');
|
|
131
|
+
};
|
|
132
|
+
const displayLineCount = isCollapsed && lineCount > foldThreshold ? foldThreshold : lineCount;
|
|
133
|
+
const gutter = displayLineCount > 0 ? Array.from({ length: displayLineCount }, (_, i) => i + 1).join('\n') : '';
|
|
134
|
+
const remainingLines = isCollapsed && lineCount > foldThreshold ? lineCount - foldThreshold : 0;
|
|
135
|
+
// 选择高亮主题
|
|
136
|
+
const shikiTheme = muiTheme.palette.mode === 'dark' ? 'github-dark' : 'github-light';
|
|
137
|
+
// 使用 shiki 进行代码高亮
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const highlightCode = async () => {
|
|
140
|
+
const contentToHighlight = getDisplayContent();
|
|
141
|
+
if (!contentToHighlight) {
|
|
142
|
+
setHighlightedCode('');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
setIsLoading(true);
|
|
146
|
+
try {
|
|
147
|
+
const html = await codeToHtml(contentToHighlight, {
|
|
148
|
+
lang: language,
|
|
149
|
+
theme: shikiTheme,
|
|
150
|
+
});
|
|
151
|
+
setHighlightedCode(html.replace(/<code([^>]*)>/g, `<code$1 class="${editorTheme.code}" data-gutter="${gutter}" data-language="${language}">`));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('Code highlighting failed:', error);
|
|
155
|
+
setHighlightedCode(contentToHighlight);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
setIsLoading(false);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
highlightCode();
|
|
162
|
+
}, [body, language, shikiTheme, isCollapsed, foldThreshold, gutter]);
|
|
163
|
+
const shouldShowHeader = title || icon;
|
|
164
|
+
const actions = (_jsxs(_Fragment, { children: [showFoldButton && _jsx(FoldButton, { isCollapsed: isCollapsed, onToggle: () => setIsCollapsed(!isCollapsed) }), _jsx(CopyButton, { content: body })] }));
|
|
165
|
+
return (_jsxs(XCode, { className: "x-code", children: [shouldShowHeader && (_jsx(CodeHeader, { title: title, icon: icon, actions: _jsx(Stack, { alignItems: "center", direction: "row", spacing: 0.5, children: actions }) })), !isLoading && highlightedCode ? (_jsxs(_Fragment, { children: [_jsx("div", { dangerouslySetInnerHTML: { __html: highlightedCode } }), !shouldShowHeader && (_jsx(Stack, { alignItems: "center", direction: "row", spacing: 0.5, sx: { position: 'absolute', right: '12px', top: '12px' }, children: actions }))] })) : (_jsx("pre", { children: _jsx("code", { className: editorTheme.code, "data-gutter": gutter, "data-language": language, style: {
|
|
166
|
+
opacity: 0.15,
|
|
167
|
+
filter: 'blur(0.5px)',
|
|
168
|
+
pointerEvents: 'none',
|
|
169
|
+
userSelect: 'none',
|
|
170
|
+
}, children: getDisplayContent() }) })), showFoldButton && (_jsx(FoldFooter, { remainingLines: remainingLines, isCollapsed: isCollapsed, onToggle: () => setIsCollapsed(!isCollapsed) }))] }));
|
|
171
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.77",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -66,11 +66,12 @@
|
|
|
66
66
|
"react-error-boundary": "^3.1.4",
|
|
67
67
|
"react-player": "^2.16.0",
|
|
68
68
|
"react-popper": "^2.3.0",
|
|
69
|
+
"shiki": "1.14.1",
|
|
69
70
|
"split-pane-react": "^0.1.3",
|
|
70
71
|
"ufo": "^1.5.4",
|
|
71
72
|
"url-join": "^4.0.1",
|
|
72
73
|
"zustand": "^4.5.5",
|
|
73
|
-
"@blocklet/pdf": "2.4.
|
|
74
|
+
"@blocklet/pdf": "2.4.77"
|
|
74
75
|
},
|
|
75
76
|
"devDependencies": {
|
|
76
77
|
"@babel/core": "^7.25.2",
|