@blocklet/editor 2.1.127 → 2.1.128
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/BookmarkPlugin/Bookmark.js +3 -2
- package/lib/ext/InlineTranslationPlugin/EditorTranslator.d.ts +2 -0
- package/lib/ext/InlineTranslationPlugin/EditorTranslator.js +15 -8
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.d.ts +1 -0
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.js +103 -44
- package/package.json +2 -2
|
@@ -85,9 +85,9 @@ const Root = styled.a `
|
|
|
85
85
|
.be-bookmark-image {
|
|
86
86
|
flex: 0 0 auto;
|
|
87
87
|
margin-left: auto;
|
|
88
|
+
// The recommended size for an Open Graph (OG) image is 1200 x 630 pixels with an aspect ratio of 1.91:1
|
|
88
89
|
width: 300px;
|
|
89
|
-
|
|
90
|
-
max-height: 144px;
|
|
90
|
+
height: 158px;
|
|
91
91
|
background-size: cover;
|
|
92
92
|
background-position: center center;
|
|
93
93
|
}
|
|
@@ -95,6 +95,7 @@ const Root = styled.a `
|
|
|
95
95
|
@media (max-width: 768px) {
|
|
96
96
|
.be-bookmark-image {
|
|
97
97
|
width: 200px;
|
|
98
|
+
height: 105px;
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -20,6 +20,7 @@ export declare class EditorTranslator {
|
|
|
20
20
|
private nodeKeyToSidMap;
|
|
21
21
|
private sidToNodeKeyMap;
|
|
22
22
|
private translations;
|
|
23
|
+
private sourceItems;
|
|
23
24
|
constructor(editor: LexicalEditor, options: EditorTranslatorOptions);
|
|
24
25
|
get isTranslated(): boolean;
|
|
25
26
|
setEditor(editor: LexicalEditor): void;
|
|
@@ -34,5 +35,6 @@ export declare class EditorTranslator {
|
|
|
34
35
|
applyTranslations(): void;
|
|
35
36
|
restore(sid?: string): void;
|
|
36
37
|
preprocessNodes(): Promise<void>;
|
|
38
|
+
getOriginalText(sid: string): string | undefined;
|
|
37
39
|
}
|
|
38
40
|
export declare function useEditorTranslator(options: EditorTranslatorOptions): EditorTranslator;
|
|
@@ -15,6 +15,7 @@ export class EditorTranslator {
|
|
|
15
15
|
nodeKeyToSidMap = new Map();
|
|
16
16
|
sidToNodeKeyMap = new Map();
|
|
17
17
|
translations = [];
|
|
18
|
+
sourceItems = [];
|
|
18
19
|
constructor(editor, options) {
|
|
19
20
|
this.editor = editor;
|
|
20
21
|
this.options = options;
|
|
@@ -62,11 +63,12 @@ export class EditorTranslator {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
try {
|
|
66
|
+
this.sourceItems = nodes.map(({ node, html }) => ({
|
|
67
|
+
sid: this.nodeKeyToSidMap.get(node.getKey()),
|
|
68
|
+
text: html,
|
|
69
|
+
}));
|
|
65
70
|
this.translations = await translateService({
|
|
66
|
-
sourceItems:
|
|
67
|
-
sid: this.nodeKeyToSidMap.get(node.getKey()),
|
|
68
|
-
text: html,
|
|
69
|
-
})),
|
|
71
|
+
sourceItems: this.sourceItems,
|
|
70
72
|
targetLanguage,
|
|
71
73
|
useCache,
|
|
72
74
|
});
|
|
@@ -102,10 +104,12 @@ export class EditorTranslator {
|
|
|
102
104
|
node.append(translationNode);
|
|
103
105
|
}
|
|
104
106
|
else {
|
|
105
|
-
node.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
node.getChildren().forEach((child) => child.remove());
|
|
108
|
+
node.append(translationNode
|
|
109
|
+
// $createTranslationNode({
|
|
110
|
+
// translatedNodes: node.getChildren(),
|
|
111
|
+
// data: { sid, displayMode: this.options.displayMode },
|
|
112
|
+
// }) // 利用一个隐藏的 TranslationNode 来存储 original nodes, 用于单个结点恢复
|
|
109
113
|
);
|
|
110
114
|
}
|
|
111
115
|
}
|
|
@@ -168,6 +172,9 @@ export class EditorTranslator {
|
|
|
168
172
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
169
173
|
this.preprocessEditorState = this.editor.getEditorState().clone();
|
|
170
174
|
}
|
|
175
|
+
getOriginalText(sid) {
|
|
176
|
+
return this.sourceItems.find((item) => item.sid === sid)?.text;
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
export function useEditorTranslator(options) {
|
|
173
180
|
const [editor] = useLexicalComposerContext();
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { jsxs as _jsxs,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
-
import { useEffect,
|
|
4
|
-
import { Box } from '@mui/material';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Box, Stack } from '@mui/material';
|
|
5
5
|
import { useInViewport, usePrevious } from 'ahooks';
|
|
6
6
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
7
|
+
import { usePopper } from 'react-popper';
|
|
7
8
|
import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
|
|
8
9
|
import { useEditorTranslator } from './EditorTranslator';
|
|
9
10
|
import { NODE_TYPE } from './TranslationNode';
|
|
10
11
|
const translations = {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
translatedContent: {
|
|
13
|
+
en: 'AI-Translated Content',
|
|
14
|
+
zh: 'AI 翻译内容',
|
|
15
|
+
},
|
|
16
|
+
original: {
|
|
17
|
+
en: 'Original',
|
|
18
|
+
zh: '原文',
|
|
19
|
+
},
|
|
13
20
|
};
|
|
14
|
-
function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore }) {
|
|
21
|
+
function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore, showTranslationBadge = true, }) {
|
|
15
22
|
const [editor] = useLexicalComposerContext();
|
|
16
23
|
const [inViewport] = useInViewport(editor.getRootElement());
|
|
17
24
|
const setStatus = useInlineTranslationStore((s) => s.setStatus);
|
|
@@ -22,7 +29,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
22
29
|
const status = useStatus(editor);
|
|
23
30
|
const previousStatus = usePrevious(status);
|
|
24
31
|
const editorTranslator = useEditorTranslator({ displayMode });
|
|
25
|
-
const hoveringTranslationElementRef = useHoveringTranslationElement();
|
|
26
32
|
const { locale } = useLocaleContext();
|
|
27
33
|
const handleTranslate = async () => {
|
|
28
34
|
try {
|
|
@@ -61,17 +67,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
61
67
|
useEffect(() => {
|
|
62
68
|
editorTranslator.updateOptions({ displayMode });
|
|
63
69
|
}, [editorTranslator, displayMode]);
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const handler = (event) => {
|
|
66
|
-
if (event.ctrlKey && hoveringTranslationElementRef.current?.dataset?.sid) {
|
|
67
|
-
editorTranslator.restore(hoveringTranslationElementRef.current.dataset.sid);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
document.addEventListener('keydown', handler);
|
|
71
|
-
return () => {
|
|
72
|
-
document.removeEventListener('keydown', handler);
|
|
73
|
-
};
|
|
74
|
-
}, [editor, editorTranslator]);
|
|
75
70
|
// 对于翻译后的内容,禁止 checkbox 点击交互
|
|
76
71
|
useEffect(() => {
|
|
77
72
|
const onClick = (event) => {
|
|
@@ -93,17 +88,18 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
93
88
|
if (editor.isEditable()) {
|
|
94
89
|
return null;
|
|
95
90
|
}
|
|
96
|
-
return status === 'completed' && editorTranslator.isTranslated ? (_jsx(Box, { sx: { mb: 0.5 }, children: _jsxs(Box, { className: "inline-translation-badge", sx: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
return (_jsxs(_Fragment, { children: [_jsx(TranslationElementPopper, { editorTranslator: editorTranslator }), showTranslationBadge && status === 'completed' && editorTranslator.isTranslated ? (_jsx(Box, { sx: { mb: 0.5 }, children: _jsxs(Box, { className: "inline-translation-badge", sx: {
|
|
92
|
+
display: 'inline-block',
|
|
93
|
+
px: 1,
|
|
94
|
+
fontSize: 12,
|
|
95
|
+
fontWeight: 'medium',
|
|
96
|
+
color: 'text.secondary',
|
|
97
|
+
bgcolor: 'grey.50',
|
|
98
|
+
border: 1,
|
|
99
|
+
borderColor: 'divider',
|
|
100
|
+
borderRadius: 0.5,
|
|
101
|
+
}, children: ["\uD83E\uDD16", ' ', translations.translatedContent[locale] ||
|
|
102
|
+
translations.translatedContent.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }))] }));
|
|
107
103
|
}
|
|
108
104
|
export function InlineTranslationPlugin(props) {
|
|
109
105
|
const [editor] = useLexicalComposerContext();
|
|
@@ -112,23 +108,86 @@ export function InlineTranslationPlugin(props) {
|
|
|
112
108
|
}
|
|
113
109
|
return _jsx(InternalInlineTranslationPlugin, { ...props });
|
|
114
110
|
}
|
|
115
|
-
function
|
|
111
|
+
function TranslationElementPopper({ editorTranslator }) {
|
|
116
112
|
const [editor] = useLexicalComposerContext();
|
|
117
|
-
const
|
|
113
|
+
const [popperElement, setPopperElement] = useState(null);
|
|
114
|
+
const [hoveringTranslationElement, setHoveringTranslationElement] = useState(null);
|
|
115
|
+
const [originalText, setOriginalText] = useState(null);
|
|
116
|
+
const { locale } = useLocaleContext();
|
|
117
|
+
const reset = () => {
|
|
118
|
+
setHoveringTranslationElement(null);
|
|
119
|
+
setOriginalText(null);
|
|
120
|
+
};
|
|
121
|
+
const isInsidePopper = (element) => {
|
|
122
|
+
return popperElement && (popperElement === element || popperElement.contains(element));
|
|
123
|
+
};
|
|
124
|
+
const { styles, attributes } = usePopper(hoveringTranslationElement, popperElement, {
|
|
125
|
+
strategy: 'fixed',
|
|
126
|
+
placement: 'bottom-start',
|
|
127
|
+
});
|
|
118
128
|
useEffect(() => {
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
return editor.registerRootListener((rootElement, prevRootElement) => {
|
|
130
|
+
const handleMouseOver = (event) => {
|
|
131
|
+
const target = event.target;
|
|
132
|
+
if (isInsidePopper(target)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const translationElement = target.closest(`.${NODE_TYPE}`);
|
|
136
|
+
if (translationElement) {
|
|
137
|
+
setHoveringTranslationElement(translationElement);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
reset();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
let timer = null;
|
|
144
|
+
const handleMouseOut = (event) => {
|
|
145
|
+
const relatedTarget = event.relatedTarget;
|
|
146
|
+
if (timer) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
}
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
const editorContainer = rootElement?.closest('.be-shell');
|
|
151
|
+
// - popper 在 editorContainer 与 editor root 之间, 所以使用 `editorContainer.contains(relatedTarget)` 来检测
|
|
152
|
+
// - `editorContainer === relatedTarget` 也表示从 editor 移出
|
|
153
|
+
if (rootElement && (editorContainer === relatedTarget || !editorContainer?.contains(relatedTarget))) {
|
|
154
|
+
reset();
|
|
155
|
+
}
|
|
156
|
+
}, 100);
|
|
157
|
+
};
|
|
158
|
+
if (prevRootElement) {
|
|
159
|
+
prevRootElement.removeEventListener('mouseover', handleMouseOver);
|
|
160
|
+
prevRootElement.removeEventListener('mouseout', handleMouseOut);
|
|
123
161
|
}
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
if (rootElement) {
|
|
163
|
+
rootElement.addEventListener('mouseover', handleMouseOver);
|
|
164
|
+
rootElement.addEventListener('mouseout', handleMouseOut);
|
|
126
165
|
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
|
|
166
|
+
});
|
|
167
|
+
}, [editor]);
|
|
168
|
+
if (!hoveringTranslationElement) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return (_jsx(Box, { ref: setPopperElement, style: styles.popper, ...attributes.popper, sx: { zIndex: 'modal', mt: '-4px' }, onMouseLeave: reset, children: originalText ? (_jsxs(Box, { sx: {
|
|
172
|
+
position: 'relative',
|
|
173
|
+
zIndex: 'modal',
|
|
174
|
+
maxWidth: 600,
|
|
175
|
+
maxHeight: 400,
|
|
176
|
+
p: 2,
|
|
177
|
+
border: 1,
|
|
178
|
+
borderColor: 'divider',
|
|
179
|
+
borderRadius: 1,
|
|
180
|
+
fontSize: 13,
|
|
181
|
+
bgcolor: '#fff',
|
|
182
|
+
boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
183
|
+
'h1,h2,h3,h4,h5,h6': {
|
|
184
|
+
margin: 0,
|
|
185
|
+
},
|
|
186
|
+
}, children: [_jsx(Box, { sx: { color: 'text.secondary' }, children: translations.original[locale] || translations.original.en }), _jsx(Box, { dangerouslySetInnerHTML: { __html: originalText } })] })) : (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 0.5, sx: {
|
|
187
|
+
height: 28,
|
|
188
|
+
pr: 2,
|
|
189
|
+
color: 'text.secondary',
|
|
190
|
+
fontSize: 12,
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
}, onClick: () => setOriginalText(originalText ? null : editorTranslator.getOriginalText(hoveringTranslationElement.dataset.sid) || ''), children: [_jsx("i", { className: "iconify", "data-icon": "tabler:tooltip", "data-height": 16 }), translations.original[locale] || translations.original.en] })) }));
|
|
134
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.128",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"ufo": "^1.5.4",
|
|
67
67
|
"url-join": "^4.0.1",
|
|
68
68
|
"zustand": "^4.5.5",
|
|
69
|
-
"@blocklet/pdf": "^2.1.
|
|
69
|
+
"@blocklet/pdf": "^2.1.128"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@babel/core": "^7.25.2",
|