@blocklet/editor 2.1.127 → 2.1.129
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 +3 -0
- package/lib/ext/InlineTranslationPlugin/EditorTranslator.js +18 -8
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.d.ts +1 -0
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.js +138 -44
- package/lib/ext/InlineTranslationPlugin/TranslationNode.js +1 -0
- 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,7 @@ 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;
|
|
39
|
+
getTranslationText(sid: string): string | undefined;
|
|
37
40
|
}
|
|
38
41
|
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,12 @@ 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
|
+
}
|
|
178
|
+
getTranslationText(sid) {
|
|
179
|
+
return this.translations.find((item) => item.sid === sid)?.text;
|
|
180
|
+
}
|
|
171
181
|
}
|
|
172
182
|
export function useEditorTranslator(options) {
|
|
173
183
|
const [editor] = useLexicalComposerContext();
|
|
@@ -1,17 +1,31 @@
|
|
|
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, useRef } from 'react';
|
|
4
|
-
import { Box } from '@mui/material';
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Box, ClickAwayListener, 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';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
|
+
import { css, Global } from '@emotion/react';
|
|
10
|
+
import blue from '@mui/material/colors/blue';
|
|
7
11
|
import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
|
|
8
12
|
import { useEditorTranslator } from './EditorTranslator';
|
|
9
13
|
import { NODE_TYPE } from './TranslationNode';
|
|
10
14
|
const translations = {
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
translatedContent: {
|
|
16
|
+
en: 'AI-Translated Content',
|
|
17
|
+
zh: 'AI 翻译内容',
|
|
18
|
+
},
|
|
19
|
+
original: {
|
|
20
|
+
en: 'Original',
|
|
21
|
+
zh: '原文',
|
|
22
|
+
},
|
|
23
|
+
translation: {
|
|
24
|
+
en: 'Translation',
|
|
25
|
+
zh: '翻译',
|
|
26
|
+
},
|
|
13
27
|
};
|
|
14
|
-
function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore }) {
|
|
28
|
+
function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore, showTranslationBadge = true, }) {
|
|
15
29
|
const [editor] = useLexicalComposerContext();
|
|
16
30
|
const [inViewport] = useInViewport(editor.getRootElement());
|
|
17
31
|
const setStatus = useInlineTranslationStore((s) => s.setStatus);
|
|
@@ -22,7 +36,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
22
36
|
const status = useStatus(editor);
|
|
23
37
|
const previousStatus = usePrevious(status);
|
|
24
38
|
const editorTranslator = useEditorTranslator({ displayMode });
|
|
25
|
-
const hoveringTranslationElementRef = useHoveringTranslationElement();
|
|
26
39
|
const { locale } = useLocaleContext();
|
|
27
40
|
const handleTranslate = async () => {
|
|
28
41
|
try {
|
|
@@ -61,17 +74,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
61
74
|
useEffect(() => {
|
|
62
75
|
editorTranslator.updateOptions({ displayMode });
|
|
63
76
|
}, [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
77
|
// 对于翻译后的内容,禁止 checkbox 点击交互
|
|
76
78
|
useEffect(() => {
|
|
77
79
|
const onClick = (event) => {
|
|
@@ -93,17 +95,18 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
93
95
|
if (editor.isEditable()) {
|
|
94
96
|
return null;
|
|
95
97
|
}
|
|
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
|
-
|
|
98
|
+
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: {
|
|
99
|
+
display: 'inline-block',
|
|
100
|
+
px: 1,
|
|
101
|
+
fontSize: 12,
|
|
102
|
+
fontWeight: 'medium',
|
|
103
|
+
color: 'text.secondary',
|
|
104
|
+
bgcolor: 'grey.50',
|
|
105
|
+
border: 1,
|
|
106
|
+
borderColor: 'divider',
|
|
107
|
+
borderRadius: 0.5,
|
|
108
|
+
}, children: ["\uD83E\uDD16", ' ', translations.translatedContent[locale] ||
|
|
109
|
+
translations.translatedContent.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }))] }));
|
|
107
110
|
}
|
|
108
111
|
export function InlineTranslationPlugin(props) {
|
|
109
112
|
const [editor] = useLexicalComposerContext();
|
|
@@ -112,23 +115,114 @@ export function InlineTranslationPlugin(props) {
|
|
|
112
115
|
}
|
|
113
116
|
return _jsx(InternalInlineTranslationPlugin, { ...props });
|
|
114
117
|
}
|
|
115
|
-
function
|
|
118
|
+
function TranslationElementPopper({ editorTranslator }) {
|
|
116
119
|
const [editor] = useLexicalComposerContext();
|
|
117
|
-
const
|
|
120
|
+
const [popperElement, setPopperElement] = useState(null);
|
|
121
|
+
const popperElementRef = useRef(null);
|
|
122
|
+
const [hoveringTranslationElement, setHoveringTranslationElement] = useState(null);
|
|
123
|
+
const [originalText, setOriginalText] = useState(null);
|
|
124
|
+
const { locale } = useLocaleContext();
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
popperElementRef.current = popperElement;
|
|
127
|
+
}, [popperElement]);
|
|
128
|
+
const reset = () => {
|
|
129
|
+
setHoveringTranslationElement(null);
|
|
130
|
+
setOriginalText(null);
|
|
131
|
+
};
|
|
132
|
+
const isInsidePopper = (element) => {
|
|
133
|
+
return (popperElementRef.current && (popperElementRef.current === element || popperElementRef.current.contains(element)));
|
|
134
|
+
};
|
|
135
|
+
const hoveringTranslationElementPopper = usePopper(hoveringTranslationElement, popperElement, {
|
|
136
|
+
strategy: 'fixed',
|
|
137
|
+
placement: 'left-start',
|
|
138
|
+
modifiers: [
|
|
139
|
+
{
|
|
140
|
+
name: 'flip',
|
|
141
|
+
options: {
|
|
142
|
+
fallbackPlacements: ['bottom-start'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
const translationPreviewPopper = usePopper(hoveringTranslationElement, popperElement, {
|
|
148
|
+
strategy: 'fixed',
|
|
149
|
+
placement: 'bottom-start',
|
|
150
|
+
modifiers: [
|
|
151
|
+
{
|
|
152
|
+
name: 'flip',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
118
156
|
useEffect(() => {
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
return editor.registerRootListener((rootElement, prevRootElement) => {
|
|
158
|
+
const handleMouseOver = (event) => {
|
|
159
|
+
const target = event.target;
|
|
160
|
+
if (isInsidePopper(target)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const translationElement = target.closest(`.${NODE_TYPE}`);
|
|
164
|
+
if (translationElement) {
|
|
165
|
+
setHoveringTranslationElement(translationElement);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
reset();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
let timer = null;
|
|
172
|
+
const handleMouseOut = (event) => {
|
|
173
|
+
const relatedTarget = event.relatedTarget;
|
|
174
|
+
if (timer) {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
}
|
|
177
|
+
timer = setTimeout(() => {
|
|
178
|
+
if (rootElement && !rootElement?.contains(relatedTarget) && !isInsidePopper(relatedTarget)) {
|
|
179
|
+
reset();
|
|
180
|
+
}
|
|
181
|
+
}, 100);
|
|
182
|
+
};
|
|
183
|
+
if (prevRootElement) {
|
|
184
|
+
prevRootElement.removeEventListener('mouseover', handleMouseOver);
|
|
185
|
+
prevRootElement.removeEventListener('mouseout', handleMouseOut);
|
|
123
186
|
}
|
|
124
|
-
|
|
125
|
-
|
|
187
|
+
if (rootElement) {
|
|
188
|
+
rootElement.addEventListener('mouseover', handleMouseOver);
|
|
189
|
+
rootElement.addEventListener('mouseout', handleMouseOut);
|
|
126
190
|
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
191
|
+
});
|
|
192
|
+
}, [editor]);
|
|
193
|
+
if (!hoveringTranslationElement) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const globalStyles = css `
|
|
197
|
+
.inline-translation-node[data-eid='${editor.getKey()}'][data-sid='${hoveringTranslationElement.dataset.sid}'] > * {
|
|
198
|
+
background-color: ${blue[50]};
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
return createPortal(_jsxs(_Fragment, { children: [!!originalText && (_jsxs(_Fragment, { children: [_jsx(Global, { styles: globalStyles }), _jsx(ClickAwayListener, { onClickAway: reset, children: _jsxs(Box, { style: translationPreviewPopper.styles.popper, ...translationPreviewPopper.attributes.popper, sx: {
|
|
203
|
+
position: 'relative',
|
|
204
|
+
zIndex: 'modal',
|
|
205
|
+
maxWidth: 600,
|
|
206
|
+
maxHeight: 400,
|
|
207
|
+
p: 2,
|
|
208
|
+
border: 1,
|
|
209
|
+
borderColor: 'divider',
|
|
210
|
+
borderRadius: 1,
|
|
211
|
+
fontSize: 13,
|
|
212
|
+
bgcolor: '#fff',
|
|
213
|
+
boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
214
|
+
'h1,h2,h3,h4,h5,h6': {
|
|
215
|
+
margin: 0,
|
|
216
|
+
},
|
|
217
|
+
}, children: [_jsxs(Box, { children: [_jsx(Box, { sx: { color: 'text.secondary', fontWeight: 'medium' }, children: translations.original[locale] || translations.original.en }), _jsx(Box, { dangerouslySetInnerHTML: { __html: originalText } })] }), _jsxs(Box, { sx: { mt: 1.5 }, children: [_jsx(Box, { sx: { color: 'text.secondary', fontWeight: 'medium' }, children: translations.translation[locale] ||
|
|
218
|
+
translations.translation.en }), _jsx(Box, { dangerouslySetInnerHTML: {
|
|
219
|
+
__html: editorTranslator.getTranslationText(hoveringTranslationElement.dataset.sid) || '',
|
|
220
|
+
} })] })] }) })] })), _jsx(Box, { ref: setPopperElement, style: hoveringTranslationElementPopper.styles.popper, ...hoveringTranslationElementPopper.attributes.popper, sx: { zIndex: 'modal' }, onMouseLeave: reset, children: !originalText && (_jsx(Stack, { direction: "row", alignItems: hoveringTranslationElement.clientHeight > 100 ? 'center' : 'flex-start', sx: {
|
|
221
|
+
pr: 1,
|
|
222
|
+
pt: 0.5,
|
|
223
|
+
color: 'text.secondary',
|
|
224
|
+
fontSize: 12,
|
|
225
|
+
cursor: 'pointer',
|
|
226
|
+
height: hoveringTranslationElement.clientHeight,
|
|
227
|
+
}, onClick: () => setOriginalText(originalText ? null : editorTranslator.getOriginalText(hoveringTranslationElement.dataset.sid) || ''), children: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 0.25, children: [_jsx("i", { className: "iconify", "data-icon": "tabler:tooltip", "data-height": 16 }), translations.original[locale] || translations.original.en] }) })) })] }), document.body);
|
|
134
228
|
}
|
|
@@ -15,6 +15,7 @@ export class TranslationNode extends ElementNode {
|
|
|
15
15
|
createDOM(config, editor) {
|
|
16
16
|
const dom = document.createElement('div');
|
|
17
17
|
dom.classList.add(NODE_TYPE, `${NODE_TYPE}-${this.__data.displayMode}`);
|
|
18
|
+
dom.dataset.eid = editor.getKey();
|
|
18
19
|
dom.dataset.sid = this.__data.sid;
|
|
19
20
|
return dom;
|
|
20
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.129",
|
|
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.129"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@babel/core": "^7.25.2",
|