@blocklet/editor 2.1.90 → 2.1.91
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/InlineTranslationPlugin/InlineTranslationPlugin.js +29 -4
- package/lib/ext/InlineTranslationPlugin/TranslationNode.d.ts +6 -2
- package/lib/ext/InlineTranslationPlugin/TranslationNode.js +34 -12
- package/lib/ext/InlineTranslationPlugin/store.d.ts +8 -1
- package/lib/ext/InlineTranslationPlugin/store.js +16 -1
- package/lib/ext/InlineTranslationPlugin/types.d.ts +1 -0
- package/lib/ext/InlineTranslationPlugin/types.js +1 -0
- package/lib/ext/InlineTranslationPlugin/utils.d.ts +6 -5
- package/lib/ext/InlineTranslationPlugin/utils.js +22 -15
- package/package.json +2 -2
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
-
import { useEffect } from 'react';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { Box } from '@mui/material';
|
|
5
5
|
import { useInViewport } from 'ahooks';
|
|
6
|
-
import {
|
|
6
|
+
import { applyTranslations, translateEditorNodes } from './utils';
|
|
7
7
|
import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
|
|
8
|
+
// 原始 editor 状态, 用于恢复
|
|
9
|
+
const originalEditorStates = {};
|
|
8
10
|
function InternalInlineTranslationPlugin({ translateService, detectLanguage }) {
|
|
9
11
|
const [editor] = useLexicalComposerContext();
|
|
10
12
|
const [inViewport] = useInViewport(editor.getRootElement());
|
|
11
13
|
const setStatus = useInlineTranslationStore((s) => s.setStatus);
|
|
12
14
|
const registerEditor = useInlineTranslationStore((s) => s.registerEditor);
|
|
15
|
+
const displayMode = useInlineTranslationStore((s) => s.displayMode);
|
|
13
16
|
const targetLanguage = useTargetLanguage();
|
|
14
17
|
const status = useStatus(editor);
|
|
18
|
+
const translationsRef = useRef(null);
|
|
15
19
|
const handleTranslate = async () => {
|
|
16
20
|
try {
|
|
17
21
|
setStatus(editor, 'processing');
|
|
18
|
-
|
|
22
|
+
originalEditorStates[editor.getKey()] = editor.getEditorState();
|
|
23
|
+
const translations = await translateEditorNodes({
|
|
19
24
|
editor,
|
|
20
25
|
translateService,
|
|
21
26
|
targetLanguage,
|
|
22
27
|
detectLanguage,
|
|
28
|
+
displayMode,
|
|
23
29
|
});
|
|
30
|
+
translationsRef.current = translations ?? null;
|
|
24
31
|
setStatus(editor, 'completed');
|
|
25
32
|
}
|
|
26
33
|
catch (err) {
|
|
@@ -28,12 +35,21 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage }) {
|
|
|
28
35
|
}
|
|
29
36
|
};
|
|
30
37
|
const handleRestore = () => {
|
|
31
|
-
|
|
38
|
+
if (originalEditorStates[editor.getKey()]) {
|
|
39
|
+
editor.setEditorState(originalEditorStates[editor.getKey()]);
|
|
40
|
+
delete originalEditorStates[editor.getKey()];
|
|
41
|
+
}
|
|
32
42
|
setStatus(editor, 'idle');
|
|
33
43
|
};
|
|
34
44
|
useEffect(() => {
|
|
35
45
|
return registerEditor(editor);
|
|
36
46
|
}, [editor]);
|
|
47
|
+
// 清理备份的 editor 状态
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
return () => {
|
|
50
|
+
delete originalEditorStates[editor.getKey()];
|
|
51
|
+
};
|
|
52
|
+
}, [editor]);
|
|
37
53
|
useEffect(() => {
|
|
38
54
|
if (inViewport && status === 'pending') {
|
|
39
55
|
handleTranslate();
|
|
@@ -44,6 +60,15 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage }) {
|
|
|
44
60
|
handleRestore();
|
|
45
61
|
}
|
|
46
62
|
}, [editor, status]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Warning: flushSync was called from inside a lifecycle method
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
if (translationsRef.current && originalEditorStates[editor.getKey()]) {
|
|
67
|
+
editor.setEditorState(originalEditorStates[editor.getKey()]);
|
|
68
|
+
applyTranslations(editor, translationsRef.current, displayMode);
|
|
69
|
+
}
|
|
70
|
+
}, 0);
|
|
71
|
+
}, [displayMode]);
|
|
47
72
|
if (editor.isEditable()) {
|
|
48
73
|
return null;
|
|
49
74
|
}
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from 'lexical';
|
|
3
3
|
import { DecoratorNode } from 'lexical';
|
|
4
|
+
import { DisplayMode } from './types';
|
|
4
5
|
export interface TranslationPayload {
|
|
5
6
|
key?: NodeKey;
|
|
6
7
|
html: string;
|
|
8
|
+
displayMode: DisplayMode;
|
|
7
9
|
}
|
|
8
10
|
export type SerializedTranslationNode = Spread<{
|
|
9
11
|
html: string;
|
|
12
|
+
displayMode: DisplayMode;
|
|
10
13
|
}, SerializedLexicalNode>;
|
|
11
14
|
export declare class TranslationNode extends DecoratorNode<JSX.Element> {
|
|
12
15
|
__html: string;
|
|
16
|
+
__displayMode: DisplayMode;
|
|
13
17
|
static getType(): string;
|
|
14
18
|
static clone(node: TranslationNode): TranslationNode;
|
|
15
19
|
static importJSON(serializedNode: SerializedTranslationNode): TranslationNode;
|
|
16
20
|
exportDOM(): DOMExportOutput;
|
|
17
21
|
static importDOM(): DOMConversionMap | null;
|
|
18
|
-
constructor(html: string, key?: NodeKey);
|
|
22
|
+
constructor(html: string, key?: NodeKey, displayMode?: DisplayMode);
|
|
19
23
|
exportJSON(): SerializedTranslationNode;
|
|
20
24
|
createDOM(config: EditorConfig): HTMLElement;
|
|
21
25
|
updateDOM(): false;
|
|
22
26
|
decorate(): JSX.Element;
|
|
23
27
|
}
|
|
24
|
-
export declare function $createTranslationNode({ html, key }: TranslationPayload): TranslationNode;
|
|
28
|
+
export declare function $createTranslationNode({ html, key, displayMode }: TranslationPayload): TranslationNode;
|
|
25
29
|
export declare function $isTranslationNode(node: LexicalNode | null | undefined): node is TranslationNode;
|
|
@@ -4,42 +4,62 @@ import { Box } from '@mui/material';
|
|
|
4
4
|
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
|
|
5
5
|
function $convertTranslationElement(domNode) {
|
|
6
6
|
const element = domNode;
|
|
7
|
-
const
|
|
7
|
+
const translationDataValue = element.getAttribute('data-lexical-inline-translation');
|
|
8
|
+
let displayMode = 'translationOnly';
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(translationDataValue);
|
|
11
|
+
if (parsed?.displayMode) {
|
|
12
|
+
displayMode = parsed.displayMode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.warn(`Failed to parse: ${translationDataValue}`, e);
|
|
17
|
+
}
|
|
18
|
+
const node = $createTranslationNode({ html: element.innerHTML, displayMode });
|
|
8
19
|
return { node };
|
|
9
20
|
}
|
|
10
21
|
export class TranslationNode extends DecoratorNode {
|
|
11
22
|
__html;
|
|
23
|
+
__displayMode;
|
|
12
24
|
static getType() {
|
|
13
25
|
return 'translation';
|
|
14
26
|
}
|
|
15
27
|
static clone(node) {
|
|
16
|
-
return new TranslationNode(node.__html, node.__key);
|
|
28
|
+
return new TranslationNode(node.__html, node.__key, node.__displayMode);
|
|
17
29
|
}
|
|
18
30
|
static importJSON(serializedNode) {
|
|
19
|
-
const { html } = serializedNode;
|
|
20
|
-
const node = $createTranslationNode({ html });
|
|
31
|
+
const { html, displayMode } = serializedNode;
|
|
32
|
+
const node = $createTranslationNode({ html, displayMode });
|
|
21
33
|
return node;
|
|
22
34
|
}
|
|
23
35
|
exportDOM() {
|
|
24
36
|
const element = document.createElement('span');
|
|
25
37
|
element.innerHTML = this.__html;
|
|
38
|
+
element.setAttribute('data-lexical-inline-translation', JSON.stringify({ displayMode: this.__displayMode }));
|
|
26
39
|
return { element };
|
|
27
40
|
}
|
|
28
41
|
static importDOM() {
|
|
29
42
|
return {
|
|
30
|
-
span: (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
span: (domNode) => {
|
|
44
|
+
if (!domNode.hasAttribute('data-lexical-inline-translation')) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
conversion: $convertTranslationElement,
|
|
49
|
+
priority: 0,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
34
52
|
};
|
|
35
53
|
}
|
|
36
|
-
constructor(html, key) {
|
|
54
|
+
constructor(html, key, displayMode = 'translationOnly') {
|
|
37
55
|
super(key);
|
|
38
56
|
this.__html = html;
|
|
57
|
+
this.__displayMode = displayMode;
|
|
39
58
|
}
|
|
40
59
|
exportJSON() {
|
|
41
60
|
return {
|
|
42
61
|
html: this.__html,
|
|
62
|
+
displayMode: this.__displayMode,
|
|
43
63
|
type: 'translation',
|
|
44
64
|
version: 1,
|
|
45
65
|
};
|
|
@@ -53,11 +73,13 @@ export class TranslationNode extends DecoratorNode {
|
|
|
53
73
|
return false;
|
|
54
74
|
}
|
|
55
75
|
decorate() {
|
|
56
|
-
return (_jsxs(_Fragment, { children: [_jsx("br", {}), _jsx(Box, { component: "span", sx: {
|
|
76
|
+
return (_jsxs(_Fragment, { children: [this.__displayMode === 'inline' && _jsx("br", {}), _jsx(Box, { component: "span", sx: {
|
|
77
|
+
...(this.__displayMode === 'inline' && { borderBottom: '2px dashed', borderColor: 'info.light' }),
|
|
78
|
+
}, dangerouslySetInnerHTML: { __html: this.__html } })] }));
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
|
-
export function $createTranslationNode({ html, key }) {
|
|
60
|
-
return $applyNodeReplacement(new TranslationNode(html, key));
|
|
81
|
+
export function $createTranslationNode({ html, key, displayMode }) {
|
|
82
|
+
return $applyNodeReplacement(new TranslationNode(html, key, displayMode));
|
|
61
83
|
}
|
|
62
84
|
export function $isTranslationNode(node) {
|
|
63
85
|
return node instanceof TranslationNode;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { LexicalEditor } from 'lexical';
|
|
2
|
+
import { DisplayMode } from './types';
|
|
2
3
|
type TranslateStatus = 'idle' | 'pending' | 'processing' | 'completed';
|
|
3
4
|
interface State {
|
|
4
5
|
targetLanguage: string | null;
|
|
5
6
|
autoTranslate: boolean;
|
|
7
|
+
displayMode: DisplayMode;
|
|
6
8
|
editors: Map<LexicalEditor, TranslateStatus>;
|
|
7
9
|
}
|
|
8
10
|
interface Action {
|
|
@@ -11,6 +13,7 @@ interface Action {
|
|
|
11
13
|
setTargetLanguage: (targetLanguage: string) => void;
|
|
12
14
|
setAutoTranslate: (autoTranslate: boolean) => void;
|
|
13
15
|
setStatus: (editor: LexicalEditor, status: TranslateStatus) => void;
|
|
16
|
+
setDisplayMode: (displayMode: DisplayMode) => void;
|
|
14
17
|
registerEditor: (editor: LexicalEditor) => () => void;
|
|
15
18
|
unregisterEditor: (editor: LexicalEditor) => void;
|
|
16
19
|
}
|
|
@@ -19,6 +22,7 @@ export declare const useInlineTranslationStore: import("zustand").UseBoundStore<
|
|
|
19
22
|
setOptions: (options: Partial<import("zustand/middleware").PersistOptions<State & Action, {
|
|
20
23
|
targetLanguage: string | null;
|
|
21
24
|
autoTranslate: boolean;
|
|
25
|
+
displayMode: DisplayMode;
|
|
22
26
|
}>>) => void;
|
|
23
27
|
clearStorage: () => void;
|
|
24
28
|
rehydrate: () => void | Promise<void>;
|
|
@@ -28,10 +32,13 @@ export declare const useInlineTranslationStore: import("zustand").UseBoundStore<
|
|
|
28
32
|
getOptions: () => Partial<import("zustand/middleware").PersistOptions<State & Action, {
|
|
29
33
|
targetLanguage: string | null;
|
|
30
34
|
autoTranslate: boolean;
|
|
35
|
+
displayMode: DisplayMode;
|
|
31
36
|
}>>;
|
|
32
37
|
};
|
|
33
38
|
}>;
|
|
34
39
|
export declare const useStatus: (editor: LexicalEditor) => TranslateStatus | undefined;
|
|
35
40
|
export declare const useIsIdle: () => boolean;
|
|
36
|
-
export declare const
|
|
41
|
+
export declare const useIsTranslating: () => boolean;
|
|
42
|
+
export declare const useIsTranslated: () => boolean;
|
|
43
|
+
export declare const useTargetLanguage: () => any;
|
|
37
44
|
export {};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import { persist } from 'zustand/middleware';
|
|
4
|
+
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
5
|
+
import { useContext } from 'react';
|
|
4
6
|
export const useInlineTranslationStore = create()(persist((set, get) => ({
|
|
5
7
|
targetLanguage: null,
|
|
6
8
|
autoTranslate: false,
|
|
9
|
+
displayMode: 'translationOnly',
|
|
7
10
|
editors: new Map(),
|
|
8
11
|
translate: (editor) => {
|
|
9
12
|
if (editor) {
|
|
@@ -35,6 +38,7 @@ export const useInlineTranslationStore = create()(persist((set, get) => ({
|
|
|
35
38
|
setStatus: (editor, status) => {
|
|
36
39
|
set((state) => ({ editors: new Map(state.editors).set(editor, status) }));
|
|
37
40
|
},
|
|
41
|
+
setDisplayMode: (displayMode) => set({ displayMode }),
|
|
38
42
|
registerEditor: (editor) => {
|
|
39
43
|
set((state) => ({
|
|
40
44
|
editors: new Map(state.editors).set(editor, state.autoTranslate ? 'pending' : 'idle'),
|
|
@@ -53,6 +57,7 @@ export const useInlineTranslationStore = create()(persist((set, get) => ({
|
|
|
53
57
|
partialize: (state) => ({
|
|
54
58
|
targetLanguage: state.targetLanguage,
|
|
55
59
|
autoTranslate: state.autoTranslate,
|
|
60
|
+
displayMode: state.displayMode,
|
|
56
61
|
}),
|
|
57
62
|
}));
|
|
58
63
|
export const useStatus = (editor) => {
|
|
@@ -61,7 +66,17 @@ export const useStatus = (editor) => {
|
|
|
61
66
|
export const useIsIdle = () => {
|
|
62
67
|
return useInlineTranslationStore((state) => Array.from(state.editors.values()).every((x) => x === 'idle'));
|
|
63
68
|
};
|
|
69
|
+
export const useIsTranslating = () => {
|
|
70
|
+
return useInlineTranslationStore((state) => Array.from(state.editors.values()).some((x) => x === 'processing'));
|
|
71
|
+
};
|
|
72
|
+
export const useIsTranslated = () => {
|
|
73
|
+
return useInlineTranslationStore((state) => {
|
|
74
|
+
const statusArr = Array.from(state.editors.values());
|
|
75
|
+
return statusArr.length > 0 && statusArr.every((x) => x === 'completed' || x === 'pending');
|
|
76
|
+
});
|
|
77
|
+
};
|
|
64
78
|
export const useTargetLanguage = () => {
|
|
65
79
|
const { locale } = useLocaleContext();
|
|
66
|
-
|
|
80
|
+
const { session } = useContext(SessionContext);
|
|
81
|
+
return useInlineTranslationStore((state) => state.targetLanguage ?? session?.user?.locale ?? locale);
|
|
67
82
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type DisplayMode = 'inline' | 'translationOnly';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LexicalEditor } from 'lexical';
|
|
2
|
-
|
|
2
|
+
import { DisplayMode } from './types';
|
|
3
|
+
export interface TranslateItem {
|
|
3
4
|
uid: string;
|
|
4
5
|
text: string;
|
|
5
6
|
}
|
|
@@ -7,11 +8,11 @@ export type TranslateService = ({ sourceItems, targetLanguage, }: {
|
|
|
7
8
|
sourceItems: TranslateItem[];
|
|
8
9
|
targetLanguage: string;
|
|
9
10
|
}) => Promise<TranslateItem[]>;
|
|
10
|
-
export declare const translateEditorNodes: ({ editor, targetLanguage, translateService, detectLanguage, }: {
|
|
11
|
+
export declare const translateEditorNodes: ({ editor, targetLanguage, translateService, detectLanguage, displayMode, }: {
|
|
11
12
|
editor: LexicalEditor;
|
|
12
13
|
targetLanguage: string;
|
|
13
14
|
translateService: TranslateService;
|
|
14
15
|
detectLanguage: (text: string) => string | null;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
16
|
+
displayMode: DisplayMode;
|
|
17
|
+
}) => Promise<TranslateItem[] | null | undefined>;
|
|
18
|
+
export declare const applyTranslations: (editor: LexicalEditor, translations: TranslateItem[], displayMode: DisplayMode) => void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { $isParagraphNode, $isTextNode, $isElementNode, $createNodeSelection, $
|
|
1
|
+
import { $isParagraphNode, $isTextNode, $isElementNode, $createNodeSelection, $getNodeByKey, } from 'lexical';
|
|
2
2
|
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
3
3
|
import { $isListNode, $isListItemNode } from '@lexical/list';
|
|
4
4
|
import { $dfs } from '@lexical/utils';
|
|
5
5
|
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
6
6
|
import { $createEmojiNode } from '../../main/nodes/EmojiNode';
|
|
7
|
-
import { $createTranslationNode
|
|
7
|
+
import { $createTranslationNode } from './TranslationNode';
|
|
8
8
|
import { $isExcalidrawNode } from '../../main/nodes/ExcalidrawNode';
|
|
9
9
|
/**
|
|
10
10
|
* Creates a mapping between node keys and sequential IDs
|
|
@@ -41,7 +41,7 @@ const nodeToHtml = (editor, node) => {
|
|
|
41
41
|
const html = $generateHtmlFromNodes(editor, selection);
|
|
42
42
|
return html;
|
|
43
43
|
};
|
|
44
|
-
export const translateEditorNodes = async ({ editor, targetLanguage, translateService, detectLanguage, }) => {
|
|
44
|
+
export const translateEditorNodes = async ({ editor, targetLanguage, translateService, detectLanguage, displayMode, }) => {
|
|
45
45
|
let nodes = [];
|
|
46
46
|
const nodeUniqueIdMap = editor.getEditorState().read(() => getNodeIds());
|
|
47
47
|
editor.update(() => {
|
|
@@ -79,24 +79,31 @@ export const translateEditorNodes = async ({ editor, targetLanguage, translateSe
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
if (translations) {
|
|
82
|
-
applyTranslations(editor, translations);
|
|
82
|
+
applyTranslations(editor, translations, displayMode);
|
|
83
83
|
}
|
|
84
|
+
return translations;
|
|
84
85
|
};
|
|
85
|
-
const applyTranslations = (editor, translations) => {
|
|
86
|
+
export const applyTranslations = (editor, translations, displayMode) => {
|
|
86
87
|
editor.update(() => {
|
|
87
88
|
const nodeUniqueIdMap = getNodeIds(true);
|
|
88
89
|
translations.forEach(({ uid, text }) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
node
|
|
90
|
+
try {
|
|
91
|
+
const node = $getNodeByKey(nodeUniqueIdMap.get(uid));
|
|
92
|
+
if (node) {
|
|
93
|
+
if (displayMode === 'inline') {
|
|
94
|
+
node.append($createTranslationNode({ html: text, displayMode: 'inline' }));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
node.getChildren().forEach((child) => {
|
|
98
|
+
child.remove();
|
|
99
|
+
});
|
|
100
|
+
node.append($createTranslationNode({ html: text, displayMode: 'translationOnly' }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
console.error('Failed to apply translations to editor', e);
|
|
92
106
|
}
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
export const restoreTranslation = (editor) => {
|
|
97
|
-
editor.update(() => {
|
|
98
|
-
$nodesOfType(TranslationNode).forEach((node) => {
|
|
99
|
-
node.remove();
|
|
100
107
|
});
|
|
101
108
|
});
|
|
102
109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.91",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"ufo": "^1.5.4",
|
|
66
66
|
"url-join": "^4.0.1",
|
|
67
67
|
"zustand": "^4.5.5",
|
|
68
|
-
"@blocklet/pdf": "^2.1.
|
|
68
|
+
"@blocklet/pdf": "^2.1.91"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@babel/core": "^7.25.2",
|