@blocklet/editor 2.1.114 → 2.1.115
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/AITranslationPlugin/utils.d.ts +0 -10
- package/lib/ext/AITranslationPlugin/utils.js +2 -37
- package/lib/ext/InlineTranslationPlugin/EditorTranslator.d.ts +36 -0
- package/lib/ext/InlineTranslationPlugin/EditorTranslator.js +143 -0
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.d.ts +1 -1
- package/lib/ext/InlineTranslationPlugin/InlineTranslationPlugin.js +59 -29
- package/lib/ext/InlineTranslationPlugin/TranslationNode.d.ts +15 -22
- package/lib/ext/InlineTranslationPlugin/TranslationNode.js +32 -73
- package/lib/ext/InlineTranslationPlugin/index.d.ts +0 -1
- package/lib/ext/InlineTranslationPlugin/index.js +0 -1
- package/lib/ext/InlineTranslationPlugin/store.d.ts +7 -1
- package/lib/ext/InlineTranslationPlugin/store.js +13 -3
- package/lib/ext/translation/translation-utils.d.ts +16 -0
- package/lib/ext/translation/translation-utils.js +63 -0
- package/lib/main/editor.d.ts +2 -1
- package/lib/main/editor.js +3 -3
- package/lib/main/index.css +17 -1
- package/lib/main/index.d.ts +1 -0
- package/lib/main/index.js +2 -2
- package/package.json +2 -2
- package/lib/ext/InlineTranslationPlugin/utils.d.ts +0 -18
- package/lib/ext/InlineTranslationPlugin/utils.js +0 -124
|
@@ -4,16 +4,6 @@ import type { TranslationBlock, NonTextNodeMatch, TranslateAPI, TranslationBlock
|
|
|
4
4
|
export declare const defaultTranslateAPI: TranslateAPI;
|
|
5
5
|
export declare function $copyNodeDeep<T extends LexicalNode>(node: T): T;
|
|
6
6
|
export declare const insertNodesAfter: (node: LexicalNode, nodesToInsert: LexicalNode[]) => void;
|
|
7
|
-
/**
|
|
8
|
-
* 判断 node 是否是有效的 TextNode
|
|
9
|
-
* - $isTextNode => true
|
|
10
|
-
* - format 不是 code
|
|
11
|
-
*/
|
|
12
|
-
export declare const $isValidTextNode: (node: LexicalNode) => boolean | "";
|
|
13
|
-
/**
|
|
14
|
-
* 判断一个 top level node 是否含有效的 TextNode 的 child
|
|
15
|
-
*/
|
|
16
|
-
export declare const $hasValidTextNode: (elementNode: ElementNode) => boolean;
|
|
17
7
|
/**
|
|
18
8
|
* 遍历顶层结点,获取需要翻译的结点列表
|
|
19
9
|
* - HeadingNode
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { $isParagraphNode, $
|
|
1
|
+
import { $isParagraphNode, $createTextNode, $getNodeByKey, $isElementNode, $copyNode, } from 'lexical';
|
|
2
2
|
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
3
3
|
import { $isListNode, $isListItemNode } from '@lexical/list';
|
|
4
4
|
import { $dfs, $findMatchingParent } from '@lexical/utils';
|
|
5
5
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
6
6
|
import { joinURL } from 'ufo';
|
|
7
7
|
import { getCSRFToken } from '@blocklet/js-sdk';
|
|
8
|
-
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
|
|
9
8
|
import { blockletExists, getBlockletMountPointInfo, isValidUrl } from '../utils';
|
|
10
9
|
import { $getDepth } from '../../lexical-utils';
|
|
11
|
-
import { $
|
|
10
|
+
import { $htmlToNodes, $nodeToHtml, $hasValidTextNode, $isValidTextNode } from '../translation/translation-utils';
|
|
12
11
|
const REGEX_INVALID_SEQUENCE = /^[#$%^&*()_+\-=\\[\]{};':"\\|,.<>/?]+$/;
|
|
13
12
|
export const defaultTranslateAPI = async ({ texts, sourceLanguage, targetLanguage }, onData) => {
|
|
14
13
|
if (!blockletExists('did-comments')) {
|
|
@@ -79,20 +78,6 @@ export const insertNodesAfter = (node, nodesToInsert) => {
|
|
|
79
78
|
currentNode = item;
|
|
80
79
|
});
|
|
81
80
|
};
|
|
82
|
-
/**
|
|
83
|
-
* 判断 node 是否是有效的 TextNode
|
|
84
|
-
* - $isTextNode => true
|
|
85
|
-
* - format 不是 code
|
|
86
|
-
*/
|
|
87
|
-
export const $isValidTextNode = (node) => {
|
|
88
|
-
return $isTextNode(node) && node.getTextContent().trim() && !$isMentionNode(node);
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* 判断一个 top level node 是否含有效的 TextNode 的 child
|
|
92
|
-
*/
|
|
93
|
-
export const $hasValidTextNode = (elementNode) => {
|
|
94
|
-
return elementNode.getChildren().some($isValidTextNode);
|
|
95
|
-
};
|
|
96
81
|
/**
|
|
97
82
|
* 遍历顶层结点,获取需要翻译的结点列表
|
|
98
83
|
* - HeadingNode
|
|
@@ -143,16 +128,6 @@ export const $extractTextContentFromTranslationNode = (translationNode, selected
|
|
|
143
128
|
});
|
|
144
129
|
return text;
|
|
145
130
|
};
|
|
146
|
-
const $nodeToHtml = (editor, node) => {
|
|
147
|
-
const selection = $createNodeSelection();
|
|
148
|
-
const children = node.getChildren();
|
|
149
|
-
// 剔除嵌套 list 节点
|
|
150
|
-
children
|
|
151
|
-
.filter((x) => !$isListNode(x))
|
|
152
|
-
.forEach((child) => $dfs(child).forEach((x) => selection.add(x.node.getKey())));
|
|
153
|
-
const html = $generateHtmlFromNodes(editor, selection);
|
|
154
|
-
return html;
|
|
155
|
-
};
|
|
156
131
|
// translation nodes => translation blocks (若未传入 nodes, 默认为 editor 内所有的顶层结点)
|
|
157
132
|
export const $extractTranslationBlocks = (editor, translationNodes) => {
|
|
158
133
|
const _translationNodes = translationNodes ||
|
|
@@ -215,16 +190,6 @@ export const matchNonTextNodes = (text) => {
|
|
|
215
190
|
}
|
|
216
191
|
return matches;
|
|
217
192
|
};
|
|
218
|
-
function $htmlToNodes(editor, text) {
|
|
219
|
-
try {
|
|
220
|
-
const dom = new DOMParser().parseFromString(text, 'text/html');
|
|
221
|
-
return $generateNodesFromDOM(editor, dom);
|
|
222
|
-
}
|
|
223
|
-
catch (e) {
|
|
224
|
-
console.error(e, text);
|
|
225
|
-
return [$createTextNode(text)];
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
193
|
export const $replaceTranslation = (translationBlock, editor) => {
|
|
229
194
|
try {
|
|
230
195
|
const { text, key } = translationBlock;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { LexicalEditor } from 'lexical';
|
|
2
|
+
import { DisplayMode } from './types';
|
|
3
|
+
export interface TranslateItem {
|
|
4
|
+
sid: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export type TranslateService = ({ sourceItems, targetLanguage, useCache, }: {
|
|
8
|
+
sourceItems: TranslateItem[];
|
|
9
|
+
targetLanguage: string;
|
|
10
|
+
useCache?: boolean;
|
|
11
|
+
}) => Promise<TranslateItem[]>;
|
|
12
|
+
export interface EditorTranslatorOptions {
|
|
13
|
+
displayMode: DisplayMode;
|
|
14
|
+
}
|
|
15
|
+
export declare class EditorTranslator {
|
|
16
|
+
private editor;
|
|
17
|
+
private options;
|
|
18
|
+
private originalEditorState;
|
|
19
|
+
private nodeKeyToSidMap;
|
|
20
|
+
private sidToNodeKeyMap;
|
|
21
|
+
private translations;
|
|
22
|
+
constructor(editor: LexicalEditor, options: EditorTranslatorOptions);
|
|
23
|
+
get isTranslated(): boolean;
|
|
24
|
+
setEditor(editor: LexicalEditor): void;
|
|
25
|
+
updateOptions(options: EditorTranslatorOptions): void;
|
|
26
|
+
updateDisplayMode(): void;
|
|
27
|
+
translate({ targetLanguage, translateService, detectLanguage, useCache, }: {
|
|
28
|
+
targetLanguage: string;
|
|
29
|
+
translateService: TranslateService;
|
|
30
|
+
detectLanguage: (text: string) => string | null;
|
|
31
|
+
useCache?: boolean;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
applyTranslations(): void;
|
|
34
|
+
restore(sid?: string): void;
|
|
35
|
+
}
|
|
36
|
+
export declare function useEditorTranslator(options: EditorTranslatorOptions): EditorTranslator;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
2
|
+
import { $getNodeByKey } from 'lexical';
|
|
3
|
+
import { useRef, useEffect } from 'react';
|
|
4
|
+
import { $createNodeKeyToSidMap, $createSidToNodeKeyMap, $nodeToHtml, $findTranslatableElementNodes, $htmlToNodes, } from '../translation/translation-utils';
|
|
5
|
+
import { $createEmojiNode } from '../../main/nodes/EmojiNode';
|
|
6
|
+
import { $createTranslationNode } from './TranslationNode';
|
|
7
|
+
export class EditorTranslator {
|
|
8
|
+
editor;
|
|
9
|
+
options;
|
|
10
|
+
originalEditorState;
|
|
11
|
+
nodeKeyToSidMap;
|
|
12
|
+
sidToNodeKeyMap;
|
|
13
|
+
translations = [];
|
|
14
|
+
constructor(editor, options) {
|
|
15
|
+
this.editor = editor;
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.originalEditorState = editor.getEditorState().clone();
|
|
18
|
+
this.nodeKeyToSidMap = this.editor.getEditorState().read(() => $createNodeKeyToSidMap());
|
|
19
|
+
this.sidToNodeKeyMap = this.editor.getEditorState().read(() => $createSidToNodeKeyMap());
|
|
20
|
+
}
|
|
21
|
+
get isTranslated() {
|
|
22
|
+
return this.translations.length > 0;
|
|
23
|
+
}
|
|
24
|
+
setEditor(editor) {
|
|
25
|
+
this.editor = editor;
|
|
26
|
+
}
|
|
27
|
+
updateOptions(options) {
|
|
28
|
+
const previousDisplayMode = this.options.displayMode;
|
|
29
|
+
this.options = options;
|
|
30
|
+
if (this.isTranslated && previousDisplayMode !== this.options.displayMode) {
|
|
31
|
+
this.updateDisplayMode();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
updateDisplayMode() {
|
|
35
|
+
// Warning: flushSync was called from inside a lifecycle method
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
this.editor.setEditorState(this.originalEditorState);
|
|
38
|
+
this.applyTranslations();
|
|
39
|
+
}, 10);
|
|
40
|
+
}
|
|
41
|
+
async translate({ targetLanguage, translateService, detectLanguage, useCache, }) {
|
|
42
|
+
let nodes = [];
|
|
43
|
+
this.editor.update(() => {
|
|
44
|
+
nodes = $findTranslatableElementNodes()
|
|
45
|
+
.filter((node) => detectLanguage(node.getTextContent()) !== targetLanguage)
|
|
46
|
+
.map((node) => {
|
|
47
|
+
const html = $nodeToHtml(this.editor, node);
|
|
48
|
+
return { node, html };
|
|
49
|
+
});
|
|
50
|
+
nodes.forEach(({ node }) => {
|
|
51
|
+
node.append($createEmojiNode('global-loading', ''));
|
|
52
|
+
});
|
|
53
|
+
}, { discrete: true });
|
|
54
|
+
if (nodes.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
this.translations = await translateService({
|
|
59
|
+
sourceItems: nodes.map(({ node, html }) => ({
|
|
60
|
+
sid: this.nodeKeyToSidMap.get(node.getKey()),
|
|
61
|
+
text: html,
|
|
62
|
+
})),
|
|
63
|
+
targetLanguage,
|
|
64
|
+
useCache,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(err);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
nodes.forEach(({ node }, index) => {
|
|
72
|
+
this.editor.update(() => {
|
|
73
|
+
const lastChild = node.getLastChild();
|
|
74
|
+
if (lastChild) {
|
|
75
|
+
lastChild.remove();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (this.isTranslated) {
|
|
81
|
+
this.applyTranslations();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
applyTranslations() {
|
|
85
|
+
this.editor.update(() => {
|
|
86
|
+
this.translations.forEach(({ sid, text }) => {
|
|
87
|
+
try {
|
|
88
|
+
const node = $getNodeByKey(this.sidToNodeKeyMap.get(sid));
|
|
89
|
+
if (node) {
|
|
90
|
+
const translationNode = $createTranslationNode({
|
|
91
|
+
translatedNodes: $htmlToNodes(this.editor, text),
|
|
92
|
+
data: { sid, displayMode: this.options.displayMode },
|
|
93
|
+
});
|
|
94
|
+
if (this.options.displayMode === 'inline') {
|
|
95
|
+
node.append(translationNode);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
node.append(translationNode, $createTranslationNode({
|
|
99
|
+
translatedNodes: node.getChildren(),
|
|
100
|
+
data: { sid, displayMode: this.options.displayMode },
|
|
101
|
+
}) // 利用一个隐藏的 TranslationNode 来存储 original nodes, 用于单个结点恢复
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
console.error('Failed to apply translations to editor', e);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
restore(sid) {
|
|
113
|
+
this.editor.update(() => {
|
|
114
|
+
if (!sid) {
|
|
115
|
+
this.editor.setEditorState(this.originalEditorState);
|
|
116
|
+
}
|
|
117
|
+
else if (this.options.displayMode === 'translationOnly') {
|
|
118
|
+
const nodeKey = this.sidToNodeKeyMap.get(sid);
|
|
119
|
+
if (!nodeKey) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// TranslationNode 容器结点
|
|
123
|
+
const node = $getNodeByKey(nodeKey);
|
|
124
|
+
if (node && node.getChildren().length === 2) {
|
|
125
|
+
const firstTranslationNode = node.getFirstChild();
|
|
126
|
+
const secondTranslationNode = node.getLastChild();
|
|
127
|
+
node.append(secondTranslationNode, firstTranslationNode);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function useEditorTranslator(options) {
|
|
134
|
+
const [editor] = useLexicalComposerContext();
|
|
135
|
+
const ref = useRef(new EditorTranslator(editor, options));
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
ref.current.setEditor(editor);
|
|
138
|
+
}, [editor]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
ref.current.updateOptions(options);
|
|
141
|
+
}, [...Object.values(options)]);
|
|
142
|
+
return ref.current;
|
|
143
|
+
}
|
|
@@ -1,34 +1,38 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { Box } from '@mui/material';
|
|
5
5
|
import { useInViewport } from 'ahooks';
|
|
6
|
-
import {
|
|
6
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
7
7
|
import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { useEditorTranslator } from './EditorTranslator';
|
|
9
|
+
import { NODE_TYPE } from './TranslationNode';
|
|
10
|
+
const translations = {
|
|
11
|
+
en: 'AI-Translated Content',
|
|
12
|
+
zh: 'AI 翻译内容',
|
|
13
|
+
};
|
|
10
14
|
function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore }) {
|
|
11
15
|
const [editor] = useLexicalComposerContext();
|
|
12
16
|
const [inViewport] = useInViewport(editor.getRootElement());
|
|
13
17
|
const setStatus = useInlineTranslationStore((s) => s.setStatus);
|
|
14
18
|
const registerEditor = useInlineTranslationStore((s) => s.registerEditor);
|
|
15
19
|
const displayMode = useInlineTranslationStore((s) => s.displayMode);
|
|
20
|
+
const useCache = useInlineTranslationStore((s) => s.editorTranslateStates.get(editor)?.useCache);
|
|
16
21
|
const targetLanguage = useTargetLanguage();
|
|
17
22
|
const status = useStatus(editor);
|
|
18
|
-
const
|
|
23
|
+
const editorTranslator = useEditorTranslator({ displayMode });
|
|
24
|
+
const hoveringTranslationElementRef = useHoveringTranslationElement();
|
|
25
|
+
const { locale } = useLocaleContext();
|
|
19
26
|
const handleTranslate = async () => {
|
|
20
27
|
try {
|
|
21
28
|
setStatus(editor, 'processing');
|
|
22
29
|
onTranslate?.({ targetLanguage });
|
|
23
|
-
|
|
24
|
-
const translations = await translateEditorNodes({
|
|
25
|
-
editor,
|
|
30
|
+
await editorTranslator.translate({
|
|
26
31
|
translateService,
|
|
27
32
|
targetLanguage,
|
|
28
33
|
detectLanguage,
|
|
29
|
-
|
|
34
|
+
useCache,
|
|
30
35
|
});
|
|
31
|
-
translationsRef.current = translations ?? null;
|
|
32
36
|
setStatus(editor, 'completed');
|
|
33
37
|
}
|
|
34
38
|
catch (err) {
|
|
@@ -36,22 +40,13 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
36
40
|
}
|
|
37
41
|
};
|
|
38
42
|
const handleRestore = () => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
editor.setEditorState(originalEditorStates[editor.getKey()]);
|
|
42
|
-
delete originalEditorStates[editor.getKey()];
|
|
43
|
-
}
|
|
43
|
+
onRestore?.();
|
|
44
|
+
editorTranslator.restore();
|
|
44
45
|
setStatus(editor, 'idle');
|
|
45
46
|
};
|
|
46
47
|
useEffect(() => {
|
|
47
48
|
return registerEditor(editor);
|
|
48
49
|
}, [editor]);
|
|
49
|
-
// 清理备份的 editor 状态
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
return () => {
|
|
52
|
-
delete originalEditorStates[editor.getKey()];
|
|
53
|
-
};
|
|
54
|
-
}, [editor]);
|
|
55
50
|
useEffect(() => {
|
|
56
51
|
if (inViewport && status === 'pending') {
|
|
57
52
|
handleTranslate();
|
|
@@ -63,18 +58,33 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
|
|
|
63
58
|
}
|
|
64
59
|
}, [editor, status]);
|
|
65
60
|
useEffect(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
editorTranslator.updateOptions({ displayMode });
|
|
62
|
+
}, [editorTranslator, displayMode]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handler = (event) => {
|
|
65
|
+
if (event.ctrlKey && hoveringTranslationElementRef.current?.dataset?.sid) {
|
|
66
|
+
editorTranslator.restore(hoveringTranslationElementRef.current.dataset.sid);
|
|
71
67
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener('keydown', handler);
|
|
70
|
+
return () => {
|
|
71
|
+
document.removeEventListener('keydown', handler);
|
|
72
|
+
};
|
|
73
|
+
}, [editor, editorTranslator]);
|
|
74
74
|
if (editor.isEditable()) {
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
return _jsx(Box, { sx: {
|
|
77
|
+
return status === 'completed' ? (_jsx(Box, { sx: { mb: 0.5 }, children: _jsxs(Box, { sx: {
|
|
78
|
+
display: 'inline-block',
|
|
79
|
+
px: 1,
|
|
80
|
+
fontSize: 12,
|
|
81
|
+
fontWeight: 'medium',
|
|
82
|
+
color: 'text.secondary',
|
|
83
|
+
bgcolor: 'grey.50',
|
|
84
|
+
border: 1,
|
|
85
|
+
borderColor: 'divider',
|
|
86
|
+
borderRadius: 0.5,
|
|
87
|
+
}, children: ["\uD83E\uDD16 ", translations[locale] || translations.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }));
|
|
78
88
|
}
|
|
79
89
|
export function InlineTranslationPlugin(props) {
|
|
80
90
|
const [editor] = useLexicalComposerContext();
|
|
@@ -83,3 +93,23 @@ export function InlineTranslationPlugin(props) {
|
|
|
83
93
|
}
|
|
84
94
|
return _jsx(InternalInlineTranslationPlugin, { ...props });
|
|
85
95
|
}
|
|
96
|
+
function useHoveringTranslationElement() {
|
|
97
|
+
const [editor] = useLexicalComposerContext();
|
|
98
|
+
const hoveringElementRef = useRef(null);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const handler = (event) => {
|
|
101
|
+
const translationElement = event.target.closest(`.${NODE_TYPE}`);
|
|
102
|
+
if (translationElement && editor.getRootElement()?.contains(translationElement)) {
|
|
103
|
+
hoveringElementRef.current = translationElement;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
hoveringElementRef.current = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
document.addEventListener('mouseover', handler);
|
|
110
|
+
return () => {
|
|
111
|
+
document.removeEventListener('mouseover', handler);
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
return hoveringElementRef;
|
|
115
|
+
}
|
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from 'lexical';
|
|
3
|
-
import { DecoratorNode } from 'lexical';
|
|
1
|
+
import { EditorConfig, ElementNode, LexicalEditor, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from 'lexical';
|
|
4
2
|
import { DisplayMode } from './types';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
html: string;
|
|
3
|
+
interface TranslationNodeProps {
|
|
4
|
+
sid: string;
|
|
8
5
|
displayMode: DisplayMode;
|
|
9
6
|
}
|
|
10
|
-
export type SerializedTranslationNode = Spread<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
__html: string;
|
|
16
|
-
__displayMode: DisplayMode;
|
|
7
|
+
export type SerializedTranslationNode = Spread<TranslationNodeProps, SerializedLexicalNode>;
|
|
8
|
+
export declare const NODE_TYPE = "inline-translation-node";
|
|
9
|
+
export declare class TranslationNode extends ElementNode {
|
|
10
|
+
__data: TranslationNodeProps;
|
|
11
|
+
constructor(data: TranslationNodeProps, key?: NodeKey);
|
|
17
12
|
static getType(): string;
|
|
18
13
|
static clone(node: TranslationNode): TranslationNode;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
static importDOM(): DOMConversionMap | null;
|
|
22
|
-
constructor(html: string, key?: NodeKey, displayMode?: DisplayMode);
|
|
23
|
-
exportJSON(): SerializedTranslationNode;
|
|
24
|
-
createDOM(config: EditorConfig): HTMLElement;
|
|
25
|
-
updateDOM(): false;
|
|
26
|
-
decorate(): JSX.Element;
|
|
14
|
+
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement;
|
|
15
|
+
updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean;
|
|
27
16
|
}
|
|
28
|
-
export declare function $createTranslationNode({
|
|
17
|
+
export declare function $createTranslationNode({ translatedNodes, data, }: {
|
|
18
|
+
translatedNodes: LexicalNode[];
|
|
19
|
+
data: TranslationNodeProps;
|
|
20
|
+
}): TranslationNode;
|
|
29
21
|
export declare function $isTranslationNode(node: LexicalNode | null | undefined): node is TranslationNode;
|
|
22
|
+
export {};
|
|
@@ -1,85 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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);
|
|
1
|
+
import { $isElementNode, ElementNode, } from 'lexical';
|
|
2
|
+
export const NODE_TYPE = 'inline-translation-node';
|
|
3
|
+
export class TranslationNode extends ElementNode {
|
|
4
|
+
__data;
|
|
5
|
+
constructor(data, key) {
|
|
6
|
+
super(key);
|
|
7
|
+
this.__data = data;
|
|
17
8
|
}
|
|
18
|
-
const node = $createTranslationNode({ html: element.innerHTML, displayMode });
|
|
19
|
-
return { node };
|
|
20
|
-
}
|
|
21
|
-
export class TranslationNode extends DecoratorNode {
|
|
22
|
-
__html;
|
|
23
|
-
__displayMode;
|
|
24
9
|
static getType() {
|
|
25
|
-
return
|
|
10
|
+
return NODE_TYPE;
|
|
26
11
|
}
|
|
27
12
|
static clone(node) {
|
|
28
|
-
return new TranslationNode(node.
|
|
13
|
+
return new TranslationNode(node.__data, node.__key);
|
|
29
14
|
}
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
exportDOM() {
|
|
36
|
-
const element = document.createElement('span');
|
|
37
|
-
element.innerHTML = this.__html;
|
|
38
|
-
element.setAttribute('data-lexical-inline-translation', JSON.stringify({ displayMode: this.__displayMode }));
|
|
39
|
-
return { element };
|
|
40
|
-
}
|
|
41
|
-
static importDOM() {
|
|
42
|
-
return {
|
|
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
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
constructor(html, key, displayMode = 'translationOnly') {
|
|
55
|
-
super(key);
|
|
56
|
-
this.__html = html;
|
|
57
|
-
this.__displayMode = displayMode;
|
|
15
|
+
createDOM(config, editor) {
|
|
16
|
+
const dom = document.createElement('div');
|
|
17
|
+
dom.classList.add(NODE_TYPE, `${NODE_TYPE}-${this.__data.displayMode}`);
|
|
18
|
+
dom.dataset.sid = this.__data.sid;
|
|
19
|
+
return dom;
|
|
58
20
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
displayMode: this.__displayMode,
|
|
63
|
-
type: 'translation',
|
|
64
|
-
version: 1,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
createDOM(config) {
|
|
68
|
-
const span = document.createElement('span');
|
|
69
|
-
span.className = 'lexical-editor-inline-translation';
|
|
70
|
-
return span;
|
|
71
|
-
}
|
|
72
|
-
updateDOM() {
|
|
21
|
+
updateDOM(prevNode, dom, config) {
|
|
22
|
+
// Returning false tells Lexical that this node does not need its
|
|
23
|
+
// DOM element replacing with a new copy from createDOM.
|
|
73
24
|
return false;
|
|
74
25
|
}
|
|
75
|
-
decorate() {
|
|
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 } })] }));
|
|
79
|
-
}
|
|
80
26
|
}
|
|
81
|
-
export function $createTranslationNode({
|
|
82
|
-
|
|
27
|
+
export function $createTranslationNode({ translatedNodes, data, }) {
|
|
28
|
+
const node = new TranslationNode(data);
|
|
29
|
+
if (translatedNodes.length === 1) {
|
|
30
|
+
const first = translatedNodes[0];
|
|
31
|
+
if ($isElementNode(first)) {
|
|
32
|
+
node.append(...first.getChildren());
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
node.append(first);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
node.append(...translatedNodes);
|
|
40
|
+
}
|
|
41
|
+
return node;
|
|
83
42
|
}
|
|
84
43
|
export function $isTranslationNode(node) {
|
|
85
44
|
return node instanceof TranslationNode;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { LexicalEditor } from 'lexical';
|
|
2
2
|
import { DisplayMode } from './types';
|
|
3
3
|
type TranslateStatus = 'idle' | 'pending' | 'processing' | 'completed';
|
|
4
|
+
interface EditorTranslateState {
|
|
5
|
+
useCache: boolean;
|
|
6
|
+
}
|
|
4
7
|
interface State {
|
|
5
8
|
targetLanguage: string | null;
|
|
6
9
|
autoTranslate: boolean;
|
|
7
10
|
displayMode: DisplayMode;
|
|
8
11
|
editors: Map<LexicalEditor, TranslateStatus>;
|
|
12
|
+
editorTranslateStates: Map<LexicalEditor, EditorTranslateState>;
|
|
9
13
|
}
|
|
10
14
|
interface Action {
|
|
11
|
-
translate: (editor?: LexicalEditor
|
|
15
|
+
translate: (editor?: LexicalEditor, options?: {
|
|
16
|
+
useCache?: boolean;
|
|
17
|
+
}) => void;
|
|
12
18
|
showOriginal: (editor?: LexicalEditor) => void;
|
|
13
19
|
setTargetLanguage: (targetLanguage: string) => void;
|
|
14
20
|
setAutoTranslate: (autoTranslate: boolean) => void;
|
|
@@ -8,9 +8,15 @@ export const useInlineTranslationStore = create()(persist((set, get) => ({
|
|
|
8
8
|
autoTranslate: false,
|
|
9
9
|
displayMode: 'translationOnly',
|
|
10
10
|
editors: new Map(),
|
|
11
|
-
|
|
11
|
+
editorTranslateStates: new Map(),
|
|
12
|
+
translate: (editor, options) => {
|
|
12
13
|
if (editor) {
|
|
13
|
-
set((state) => ({
|
|
14
|
+
set((state) => ({
|
|
15
|
+
editors: new Map(state.editors).set(editor, 'pending'),
|
|
16
|
+
...(options?.useCache !== undefined && {
|
|
17
|
+
editorTranslateStates: new Map(state.editorTranslateStates).set(editor, { useCache: options.useCache }),
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
14
20
|
}
|
|
15
21
|
else {
|
|
16
22
|
set((state) => ({
|
|
@@ -20,11 +26,15 @@ export const useInlineTranslationStore = create()(persist((set, get) => ({
|
|
|
20
26
|
},
|
|
21
27
|
showOriginal: (editor) => {
|
|
22
28
|
if (editor) {
|
|
23
|
-
set((state) => ({
|
|
29
|
+
set((state) => ({
|
|
30
|
+
editors: new Map(state.editors).set(editor, 'idle'),
|
|
31
|
+
editorTranslateStates: new Map(Array.from(state.editorTranslateStates.entries()).filter(([k]) => k !== editor)),
|
|
32
|
+
}));
|
|
24
33
|
}
|
|
25
34
|
else {
|
|
26
35
|
set((state) => ({
|
|
27
36
|
editors: new Map(Array.from(state.editors.entries()).map(([k]) => [k, 'idle'])),
|
|
37
|
+
editorTranslateStates: new Map(),
|
|
28
38
|
}));
|
|
29
39
|
}
|
|
30
40
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ElementNode, LexicalNode, LexicalEditor } from 'lexical';
|
|
2
|
+
export declare const $nodeToHtml: (editor: LexicalEditor, node: ElementNode) => string;
|
|
3
|
+
export declare const $htmlToNodes: (editor: LexicalEditor, text: string) => LexicalNode[] | import("lexical").TextNode[];
|
|
4
|
+
/**
|
|
5
|
+
* 判断 node 是否是有效的 TextNode
|
|
6
|
+
* - $isTextNode => true
|
|
7
|
+
* - format 不是 code
|
|
8
|
+
*/
|
|
9
|
+
export declare const $isValidTextNode: (node: LexicalNode) => boolean | "";
|
|
10
|
+
/**
|
|
11
|
+
* 判断一个 ElementNode 是否含有效的 TextNode 的 child
|
|
12
|
+
*/
|
|
13
|
+
export declare const $hasValidTextNode: (elementNode: ElementNode) => boolean;
|
|
14
|
+
export declare const $findTranslatableElementNodes: () => ElementNode[];
|
|
15
|
+
export declare const $createNodeKeyToSidMap: () => Map<string, string>;
|
|
16
|
+
export declare const $createSidToNodeKeyMap: () => Map<string, string>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { $isParagraphNode, $isTextNode, $isElementNode, $createNodeSelection, $createTextNode, } from 'lexical';
|
|
2
|
+
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
3
|
+
import { $isListNode, $isListItemNode } from '@lexical/list';
|
|
4
|
+
import { $dfs } from '@lexical/utils';
|
|
5
|
+
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
|
|
6
|
+
import { $isExcalidrawNode } from '../../main/nodes/ExcalidrawNode';
|
|
7
|
+
import { $isMentionNode } from '../../main/nodes/MentionNode';
|
|
8
|
+
export const $nodeToHtml = (editor, node) => {
|
|
9
|
+
const selection = $createNodeSelection();
|
|
10
|
+
const children = node.getChildren();
|
|
11
|
+
// 剔除嵌套 list 节点
|
|
12
|
+
children
|
|
13
|
+
.filter((x) => !$isListNode(x))
|
|
14
|
+
.forEach((child) => $dfs(child)
|
|
15
|
+
.filter((x) => !$isExcalidrawNode(x.node))
|
|
16
|
+
.forEach((x) => selection.add(x.node.getKey())));
|
|
17
|
+
const html = $generateHtmlFromNodes(editor, selection);
|
|
18
|
+
return html;
|
|
19
|
+
};
|
|
20
|
+
export const $htmlToNodes = (editor, text) => {
|
|
21
|
+
try {
|
|
22
|
+
const dom = new DOMParser().parseFromString(text, 'text/html');
|
|
23
|
+
return $generateNodesFromDOM(editor, dom);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(e, text);
|
|
27
|
+
return [$createTextNode(text)];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 判断 node 是否是有效的 TextNode
|
|
32
|
+
* - $isTextNode => true
|
|
33
|
+
* - format 不是 code
|
|
34
|
+
*/
|
|
35
|
+
export const $isValidTextNode = (node) => {
|
|
36
|
+
return $isTextNode(node) && node.getTextContent().trim() && !$isMentionNode(node);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 判断一个 ElementNode 是否含有效的 TextNode 的 child
|
|
40
|
+
*/
|
|
41
|
+
export const $hasValidTextNode = (elementNode) => {
|
|
42
|
+
return elementNode.getChildren().some($isValidTextNode);
|
|
43
|
+
};
|
|
44
|
+
export const $findTranslatableElementNodes = () => {
|
|
45
|
+
const nodes = $dfs()
|
|
46
|
+
.map((x) => x.node)
|
|
47
|
+
.filter((node) => $isElementNode(node) &&
|
|
48
|
+
($isHeadingNode(node) || $isParagraphNode(node) || $isQuoteNode(node) || $isListItemNode(node)))
|
|
49
|
+
.filter($hasValidTextNode);
|
|
50
|
+
return nodes;
|
|
51
|
+
};
|
|
52
|
+
// 创建映射表: node key -> node sequential ID
|
|
53
|
+
export const $createNodeKeyToSidMap = () => {
|
|
54
|
+
const nodes = $dfs();
|
|
55
|
+
let counter = 0;
|
|
56
|
+
return new Map(nodes.map((x) => [x.node.getKey(), `${counter++}`]));
|
|
57
|
+
};
|
|
58
|
+
// 创建映射表: node sequential ID -> node key
|
|
59
|
+
export const $createSidToNodeKeyMap = () => {
|
|
60
|
+
const nodes = $dfs();
|
|
61
|
+
let counter = 0;
|
|
62
|
+
return new Map(nodes.map((x) => [`${counter++}`, x.node.getKey()]));
|
|
63
|
+
};
|
package/lib/main/editor.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { MutableRefObject, ReactNode } from 'react';
|
|
|
9
9
|
import { EditorState, LexicalEditor } from 'lexical';
|
|
10
10
|
export interface EditorProps {
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
prepend?: ReactNode;
|
|
12
13
|
placeholder?: ReactNode;
|
|
13
14
|
onChange?: (editorState: EditorState, editor: LexicalEditor) => void;
|
|
14
15
|
autoFocus?: boolean;
|
|
@@ -17,4 +18,4 @@ export interface EditorProps {
|
|
|
17
18
|
onReady?: () => void;
|
|
18
19
|
enableHeadingsIdPlugin?: boolean;
|
|
19
20
|
}
|
|
20
|
-
export default function Editor({ children, placeholder, onChange, autoFocus, showToolbar, editorRef, onReady, enableHeadingsIdPlugin, }: EditorProps): JSX.Element;
|
|
21
|
+
export default function Editor({ children, prepend, placeholder, onChange, autoFocus, showToolbar, editorRef, onReady, enableHeadingsIdPlugin, }: EditorProps): JSX.Element;
|
package/lib/main/editor.js
CHANGED
|
@@ -83,7 +83,7 @@ import { useResponsiveTable } from './hooks/useResponsiveTable';
|
|
|
83
83
|
import { useTranslationListener } from './hooks/useTranslationListener';
|
|
84
84
|
import { PagesKitComponentPlugin } from '../ext/PagesKitComponent/PagesKitComponentPlugin';
|
|
85
85
|
import { EditorHolderPlugin } from '../ext/EditorHolderPlugin';
|
|
86
|
-
export default function Editor({ children, placeholder, onChange, autoFocus = true, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, }) {
|
|
86
|
+
export default function Editor({ children, prepend, placeholder, onChange, autoFocus = true, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, }) {
|
|
87
87
|
const [editor] = useLexicalComposerContext();
|
|
88
88
|
const [editable, setEditable] = useState(false);
|
|
89
89
|
const config = useEditorConfig();
|
|
@@ -112,9 +112,9 @@ export default function Editor({ children, placeholder, onChange, autoFocus = tr
|
|
|
112
112
|
}
|
|
113
113
|
}, [hasNodes('image'), hasUploader]);
|
|
114
114
|
if (minimalMode) {
|
|
115
|
-
return (_jsxs(_Fragment, { children: [isRichText && editable && showToolbar && _jsx(ToolbarPlugin, {}), hasNodes('image') && hasUploader && _jsx(DragDropPaste, {}), autoFocus && _jsx(AutoFocusPlugin, { defaultSelection: "rootEnd" }), _jsx(ClearEditorPlugin, {}), !!editable && _jsx(ComponentPickerPlugin, {}), !!editable && _jsx(MentionsPlugin, {}), hasNodes('link') && _jsx(AutoEmbedPlugin, {}), _jsx(EditorContent, { ref: onRef, className: cx('be-content', editable && 'editable'), children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cx('be-editable', 'notranslate') }), placeholder: _jsx(Placeholder, { className: "be-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), hasNodes('code', 'code-highlight') && _jsx(CodeHighlightPlugin, {}), hasNodes('image') && hasUploader && _jsx(ImagesPlugin, {}), hasNodes('video') && _jsx(VideoPlugin, {}), hasNodes('link') && _jsx(LinkPlugin, {}), hasNodes('tweet') && _jsx(TwitterPlugin, {}), hasNodes('youtube') && _jsx(YouTubePlugin, {}), hasNodes('figma') && _jsx(FigmaPlugin, {}), _jsx(BilibiliPlugin, {}), _jsx(PostLinkEmbedPlugin, {}), _jsx(BookmarkPlugin, {}), editable && _jsx(CustomOnChangePlugin, { placeholder: placeholder }), editable && _jsx(TemplatePlugin, {}), !editable && _jsx(BlurTextPlugin, {}), hasNodes('pdf') && _jsx(PdfPlugin, {}), hasNodes('file') && _jsx(FilePlugin, {}), hasNodes('horizontalrule') && _jsx(HorizontalRulePlugin, {}), hasNodes('excalidraw') && _jsx(ExcalidrawPlugin, {}), hasNodes('alert') && _jsx(AlertPlugin, {}), hasNodes('pages-kit-component') && _jsx(PagesKitComponentPlugin, {}), _jsx(TabFocusPlugin, {}), onChange && _jsx(OnChangePlugin, { onChange: onChange }), floatingAnchorElem && editable && (_jsxs(_Fragment, { children: [hasNodes('code') && _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), hasNodes('link') && _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem })] })), _jsx(AiImagePlugin, {}), editorRef && _jsx(EditorRefPlugin, { editorRef: editorRef }), children] }));
|
|
115
|
+
return (_jsxs(_Fragment, { children: [prepend, isRichText && editable && showToolbar && _jsx(ToolbarPlugin, {}), hasNodes('image') && hasUploader && _jsx(DragDropPaste, {}), autoFocus && _jsx(AutoFocusPlugin, { defaultSelection: "rootEnd" }), _jsx(ClearEditorPlugin, {}), !!editable && _jsx(ComponentPickerPlugin, {}), !!editable && _jsx(MentionsPlugin, {}), hasNodes('link') && _jsx(AutoEmbedPlugin, {}), _jsx(EditorContent, { ref: onRef, className: cx('be-content', editable && 'editable'), children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cx('be-editable', 'notranslate') }), placeholder: _jsx(Placeholder, { className: "be-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), hasNodes('code', 'code-highlight') && _jsx(CodeHighlightPlugin, {}), hasNodes('image') && hasUploader && _jsx(ImagesPlugin, {}), hasNodes('video') && _jsx(VideoPlugin, {}), hasNodes('link') && _jsx(LinkPlugin, {}), hasNodes('tweet') && _jsx(TwitterPlugin, {}), hasNodes('youtube') && _jsx(YouTubePlugin, {}), hasNodes('figma') && _jsx(FigmaPlugin, {}), _jsx(BilibiliPlugin, {}), _jsx(PostLinkEmbedPlugin, {}), _jsx(BookmarkPlugin, {}), editable && _jsx(CustomOnChangePlugin, { placeholder: placeholder }), editable && _jsx(TemplatePlugin, {}), !editable && _jsx(BlurTextPlugin, {}), hasNodes('pdf') && _jsx(PdfPlugin, {}), hasNodes('file') && _jsx(FilePlugin, {}), hasNodes('horizontalrule') && _jsx(HorizontalRulePlugin, {}), hasNodes('excalidraw') && _jsx(ExcalidrawPlugin, {}), hasNodes('alert') && _jsx(AlertPlugin, {}), hasNodes('pages-kit-component') && _jsx(PagesKitComponentPlugin, {}), _jsx(TabFocusPlugin, {}), onChange && _jsx(OnChangePlugin, { onChange: onChange }), floatingAnchorElem && editable && (_jsxs(_Fragment, { children: [hasNodes('code') && _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), hasNodes('link') && _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem })] })), _jsx(AiImagePlugin, {}), editorRef && _jsx(EditorRefPlugin, { editorRef: editorRef }), children] }));
|
|
116
116
|
}
|
|
117
|
-
return (_jsxs(_Fragment, { children: [isRichText && editable && showToolbar && _jsx(ToolbarPlugin, {}), isMaxLength && _jsx(MaxLengthPlugin, { maxLength: 30 }), hasNodes('image') && hasUploader && _jsx(DragDropPaste, {}), autoFocus && _jsx(AutoFocusPlugin, { defaultSelection: "rootEnd" }), _jsx(ClearEditorPlugin, {}), !!editable && _jsx(ComponentPickerPlugin, {}), !!editable && _jsx(EmojiPickerPlugin, {}), hasNodes('link') && _jsx(AutoEmbedPlugin, {}), !!editable && _jsx(MentionsPlugin, {}), hasNodes('emoji') && _jsx(EmojisPlugin, {}), hasNodes('hashtag') && _jsx(HashtagPlugin, {}), _jsx(SpeechToTextPlugin, {}), hasNodes('autolink') && _jsx(AutoLinkPlugin, {}), isRichText ? (_jsxs(_Fragment, { children: [_jsx(HistoryPlugin, { externalHistoryState: historyState }), _jsx(EditorContent, { ref: onRef, className: cx('be-content', editable && 'editable'), children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cx('be-editable', 'notranslate') }), placeholder: _jsx(Placeholder, { className: "be-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), _jsx(MarkdownShortcutPlugin, {}), _jsx(TabIndentationPlugin, {}), hasNodes('code', 'code-highlight') && _jsx(CodeHighlightPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListPlugin, {}), hasNodes('list', 'listitem') && _jsx(CheckListPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListMaxIndentLevelPlugin, { maxDepth: 7 }), hasNodes('table', 'tablerow', 'tablecell') && editable && (_jsx(TablePlugin, { hasCellMerge: true, hasCellBackgroundColor: true })), hasNodes('table', 'tablerow', 'tablecell') && _jsx(TableCellResizer, {}), hasNodes('image') && hasUploader && _jsx(ImagesPlugin, {}), hasNodes('video') && _jsx(VideoPlugin, {}), hasNodes('link') && _jsx(LinkPlugin, {}), hasNodes('tweet') && _jsx(TwitterPlugin, {}), hasNodes('youtube') && _jsx(YouTubePlugin, {}), hasNodes('figma') && _jsx(FigmaPlugin, {}), _jsx(BilibiliPlugin, {}), _jsx(BlockletEmbedPlugin, {}), _jsx(PostLinkEmbedPlugin, {}), _jsx(BookmarkPlugin, {}), _jsx(AidePlugin, {}), editable && _jsx(CustomOnChangePlugin, { placeholder: placeholder }), editable && _jsx(TemplatePlugin, {}), !editable && _jsx(BlurTextPlugin, {}), hasNodes('pdf') && _jsx(PdfPlugin, {}), hasNodes('file') && _jsx(FilePlugin, {}), hasNodes('link') && _jsx(ClickableLinkPlugin, {}), hasNodes('horizontalrule') && _jsx(HorizontalRulePlugin, {}), hasNodes('excalidraw') && _jsx(ExcalidrawPlugin, {}), hasNodes('alert') && _jsx(AlertPlugin, {}), hasNodes('pages-kit-component') && _jsx(PagesKitComponentPlugin, {}), _jsx(TabFocusPlugin, {}), hasNodes('collapsible-container', 'collapsible-content', 'collapsible-title') && _jsx(CollapsiblePlugin, {}), onChange && _jsx(OnChangePlugin, { onChange: onChange }), floatingAnchorElem && editable && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), hasNodes('code') && _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), hasNodes('link') && _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem }), hasNodes('table') && _jsx(TableCellActionMenuPlugin, { anchorElem: floatingAnchorElem, cellMerge: true }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem })] })), enableHeadingsIdPlugin && _jsx(HeadingsIdPlugin, {})] })) : (_jsxs(_Fragment, { children: [_jsx(PlainTextPlugin, { contentEditable: _jsx(ContentEditable, {}), placeholder: _jsx(Placeholder, { children: "placeholder" }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, { externalHistoryState: historyState })] })), (isCharLimit || isCharLimitUtf8) && (_jsx(CharacterLimitPlugin, { charset: isCharLimit ? 'UTF-16' : 'UTF-8', maxLength: 5 })), isAutocomplete && hasNodes('autocomplete') && _jsx(AutocompletePlugin, {}), _jsx("div", { children: showTableOfContents && _jsx(TableOfContentsPlugin, {}) }), _jsx(SelectBlockPlugin, {}), _jsx(RemoveListPlugin, {}), _jsx(MarkdownHeadTextPlugin, {}), _jsx(AiImagePlugin, {}), _jsx(EditorHolderPlugin, {}), editorRef && _jsx(EditorRefPlugin, { editorRef: editorRef }), onReady && _jsx(EditorReadyPlugin, { onReady: onReady }), children] }));
|
|
117
|
+
return (_jsxs(_Fragment, { children: [prepend, isRichText && editable && showToolbar && _jsx(ToolbarPlugin, {}), isMaxLength && _jsx(MaxLengthPlugin, { maxLength: 30 }), hasNodes('image') && hasUploader && _jsx(DragDropPaste, {}), autoFocus && _jsx(AutoFocusPlugin, { defaultSelection: "rootEnd" }), _jsx(ClearEditorPlugin, {}), !!editable && _jsx(ComponentPickerPlugin, {}), !!editable && _jsx(EmojiPickerPlugin, {}), hasNodes('link') && _jsx(AutoEmbedPlugin, {}), !!editable && _jsx(MentionsPlugin, {}), hasNodes('emoji') && _jsx(EmojisPlugin, {}), hasNodes('hashtag') && _jsx(HashtagPlugin, {}), _jsx(SpeechToTextPlugin, {}), hasNodes('autolink') && _jsx(AutoLinkPlugin, {}), isRichText ? (_jsxs(_Fragment, { children: [_jsx(HistoryPlugin, { externalHistoryState: historyState }), _jsx(EditorContent, { ref: onRef, className: cx('be-content', editable && 'editable'), children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cx('be-editable', 'notranslate') }), placeholder: _jsx(Placeholder, { className: "be-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), _jsx(MarkdownShortcutPlugin, {}), _jsx(TabIndentationPlugin, {}), hasNodes('code', 'code-highlight') && _jsx(CodeHighlightPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListPlugin, {}), hasNodes('list', 'listitem') && _jsx(CheckListPlugin, {}), hasNodes('list', 'listitem') && _jsx(ListMaxIndentLevelPlugin, { maxDepth: 7 }), hasNodes('table', 'tablerow', 'tablecell') && editable && (_jsx(TablePlugin, { hasCellMerge: true, hasCellBackgroundColor: true })), hasNodes('table', 'tablerow', 'tablecell') && _jsx(TableCellResizer, {}), hasNodes('image') && hasUploader && _jsx(ImagesPlugin, {}), hasNodes('video') && _jsx(VideoPlugin, {}), hasNodes('link') && _jsx(LinkPlugin, {}), hasNodes('tweet') && _jsx(TwitterPlugin, {}), hasNodes('youtube') && _jsx(YouTubePlugin, {}), hasNodes('figma') && _jsx(FigmaPlugin, {}), _jsx(BilibiliPlugin, {}), _jsx(BlockletEmbedPlugin, {}), _jsx(PostLinkEmbedPlugin, {}), _jsx(BookmarkPlugin, {}), _jsx(AidePlugin, {}), editable && _jsx(CustomOnChangePlugin, { placeholder: placeholder }), editable && _jsx(TemplatePlugin, {}), !editable && _jsx(BlurTextPlugin, {}), hasNodes('pdf') && _jsx(PdfPlugin, {}), hasNodes('file') && _jsx(FilePlugin, {}), hasNodes('link') && _jsx(ClickableLinkPlugin, {}), hasNodes('horizontalrule') && _jsx(HorizontalRulePlugin, {}), hasNodes('excalidraw') && _jsx(ExcalidrawPlugin, {}), hasNodes('alert') && _jsx(AlertPlugin, {}), hasNodes('pages-kit-component') && _jsx(PagesKitComponentPlugin, {}), _jsx(TabFocusPlugin, {}), hasNodes('collapsible-container', 'collapsible-content', 'collapsible-title') && _jsx(CollapsiblePlugin, {}), onChange && _jsx(OnChangePlugin, { onChange: onChange }), floatingAnchorElem && editable && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), hasNodes('code') && _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), hasNodes('link') && _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem }), hasNodes('table') && _jsx(TableCellActionMenuPlugin, { anchorElem: floatingAnchorElem, cellMerge: true }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem })] })), enableHeadingsIdPlugin && _jsx(HeadingsIdPlugin, {})] })) : (_jsxs(_Fragment, { children: [_jsx(PlainTextPlugin, { contentEditable: _jsx(ContentEditable, {}), placeholder: _jsx(Placeholder, { children: "placeholder" }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, { externalHistoryState: historyState })] })), (isCharLimit || isCharLimitUtf8) && (_jsx(CharacterLimitPlugin, { charset: isCharLimit ? 'UTF-16' : 'UTF-8', maxLength: 5 })), isAutocomplete && hasNodes('autocomplete') && _jsx(AutocompletePlugin, {}), _jsx("div", { children: showTableOfContents && _jsx(TableOfContentsPlugin, {}) }), _jsx(SelectBlockPlugin, {}), _jsx(RemoveListPlugin, {}), _jsx(MarkdownHeadTextPlugin, {}), _jsx(AiImagePlugin, {}), _jsx(EditorHolderPlugin, {}), editorRef && _jsx(EditorRefPlugin, { editorRef: editorRef }), onReady && _jsx(EditorReadyPlugin, { onReady: onReady }), children] }));
|
|
118
118
|
}
|
|
119
119
|
const EditorContent = styled.div `
|
|
120
120
|
position: relative;
|
package/lib/main/index.css
CHANGED
|
@@ -462,7 +462,10 @@ i.prettier-error {
|
|
|
462
462
|
z-index: 10001;
|
|
463
463
|
display: block;
|
|
464
464
|
position: fixed;
|
|
465
|
-
box-shadow:
|
|
465
|
+
box-shadow:
|
|
466
|
+
0 12px 28px 0 rgba(0, 0, 0, 0.2),
|
|
467
|
+
0 2px 4px 0 rgba(0, 0, 0, 0.1),
|
|
468
|
+
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
|
|
466
469
|
border-radius: 8px;
|
|
467
470
|
min-height: 40px;
|
|
468
471
|
background-color: #fff;
|
|
@@ -1327,3 +1330,16 @@ li.embed-option-disabled > span:after {
|
|
|
1327
1330
|
.medium-zoom-image--opened {
|
|
1328
1331
|
z-index: 99999;
|
|
1329
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
.inline-translation-node {
|
|
1335
|
+
position: relative;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/* 紧挨的 2 个 TranslationNode 容器结点,分别用于包裹翻译结果结点和原文结点, 第 2 个 TranslationNode 是隐藏状态, 通过调换顺序来实现单个结点的原文/翻译切换 */
|
|
1339
|
+
.inline-translation-node + .inline-translation-node {
|
|
1340
|
+
display: none;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.inline-translation-node-inline > * {
|
|
1344
|
+
border-bottom: 2px dashed #3890f9;
|
|
1345
|
+
}
|
package/lib/main/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface BlockletEditorProps extends Omit<React.DetailedHTMLProps<React.
|
|
|
12
12
|
editable?: boolean;
|
|
13
13
|
editorState?: string;
|
|
14
14
|
children?: ReactNode;
|
|
15
|
+
prepend?: ReactNode;
|
|
15
16
|
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
|
16
17
|
onChange?: (editorState: EditorState, editor: LexicalEditor) => void;
|
|
17
18
|
autoFocus?: boolean;
|
package/lib/main/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export default function BlockletEditor({ editorState, nodes = PlaygroundNodes, e
|
|
|
34
34
|
};
|
|
35
35
|
return (_jsx(SettingsContext, { children: _jsx(SharedHistoryContext, { children: _jsx(SharedAutocompleteContext, { children: _jsx(TableContext, { children: _jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(EditorShell, { ...props, editable: editable }) }) }) }) }) }));
|
|
36
36
|
}
|
|
37
|
-
function EditorShell({ placeholder, children, editable, onChange, autoFocus, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, ...props }) {
|
|
37
|
+
function EditorShell({ placeholder, children, prepend, editable, onChange, autoFocus, showToolbar = true, editorRef, onReady, enableHeadingsIdPlugin, ...props }) {
|
|
38
38
|
const [editor] = useLexicalComposerContext();
|
|
39
39
|
useEffect(() => {
|
|
40
40
|
editor.setEditable(editable ?? true);
|
|
@@ -45,7 +45,7 @@ function EditorShell({ placeholder, children, editable, onChange, autoFocus, sho
|
|
|
45
45
|
editor.focus();
|
|
46
46
|
}
|
|
47
47
|
}, []);
|
|
48
|
-
return (_jsx(EditorRoot, { ...props, className: cx(props.className, 'be-shell'), ref: shellRef, onClick: onShellClick, children: _jsx(Editor, { onChange: onChange, placeholder: placeholder, autoFocus: autoFocus, showToolbar: showToolbar, editorRef: editorRef, onReady: onReady, enableHeadingsIdPlugin: enableHeadingsIdPlugin, children: children }) }));
|
|
48
|
+
return (_jsx(EditorRoot, { ...props, className: cx(props.className, 'be-shell'), ref: shellRef, onClick: onShellClick, children: _jsx(Editor, { onChange: onChange, placeholder: placeholder, autoFocus: autoFocus, showToolbar: showToolbar, editorRef: editorRef, onReady: onReady, enableHeadingsIdPlugin: enableHeadingsIdPlugin, prepend: prepend, children: children }) }));
|
|
49
49
|
}
|
|
50
50
|
const EditorRoot = styled.div `
|
|
51
51
|
container: blocklet-editor / inline-size;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.115",
|
|
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.115"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@babel/core": "^7.25.2",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { LexicalEditor } from 'lexical';
|
|
2
|
-
import { DisplayMode } from './types';
|
|
3
|
-
export interface TranslateItem {
|
|
4
|
-
uid: string;
|
|
5
|
-
text: string;
|
|
6
|
-
}
|
|
7
|
-
export type TranslateService = ({ sourceItems, targetLanguage, }: {
|
|
8
|
-
sourceItems: TranslateItem[];
|
|
9
|
-
targetLanguage: string;
|
|
10
|
-
}) => Promise<TranslateItem[]>;
|
|
11
|
-
export declare const translateEditorNodes: ({ editor, targetLanguage, translateService, detectLanguage, displayMode, }: {
|
|
12
|
-
editor: LexicalEditor;
|
|
13
|
-
targetLanguage: string;
|
|
14
|
-
translateService: TranslateService;
|
|
15
|
-
detectLanguage: (text: string) => string | null;
|
|
16
|
-
displayMode: DisplayMode;
|
|
17
|
-
}) => Promise<TranslateItem[] | null | undefined>;
|
|
18
|
-
export declare const applyTranslations: (editor: LexicalEditor, translations: TranslateItem[], displayMode: DisplayMode) => void;
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { $isParagraphNode, $isTextNode, $isElementNode, $createNodeSelection, $getNodeByKey, } from 'lexical';
|
|
2
|
-
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
3
|
-
import { $isListNode, $isListItemNode } from '@lexical/list';
|
|
4
|
-
import { $dfs } from '@lexical/utils';
|
|
5
|
-
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
6
|
-
import { $createEmojiNode } from '../../main/nodes/EmojiNode';
|
|
7
|
-
import { $createTranslationNode } from './TranslationNode';
|
|
8
|
-
import { $isExcalidrawNode } from '../../main/nodes/ExcalidrawNode';
|
|
9
|
-
import { $isMentionNode } from '../../main/nodes/MentionNode';
|
|
10
|
-
/**
|
|
11
|
-
* Creates a mapping between node keys and sequential IDs
|
|
12
|
-
* @param reversed If true, maps ID -> key. If false, maps key -> ID
|
|
13
|
-
* @returns Map of either [key -> ID] or [ID -> key]
|
|
14
|
-
*/
|
|
15
|
-
const getNodeIds = (reversed = false) => {
|
|
16
|
-
const nodes = $dfs();
|
|
17
|
-
let counter = 0;
|
|
18
|
-
if (reversed) {
|
|
19
|
-
return new Map(nodes.map((x) => [`${counter++}`, x.node.getKey()]));
|
|
20
|
-
}
|
|
21
|
-
return new Map(nodes.map((x) => [x.node.getKey(), `${counter++}`]));
|
|
22
|
-
};
|
|
23
|
-
const collectTranslatableNodes = () => {
|
|
24
|
-
const nodes = $dfs()
|
|
25
|
-
.map((x) => x.node)
|
|
26
|
-
.filter((node) => $isElementNode(node) &&
|
|
27
|
-
($isHeadingNode(node) || $isParagraphNode(node) || $isQuoteNode(node) || $isListItemNode(node)))
|
|
28
|
-
.filter((node) => {
|
|
29
|
-
return $hasValidTextNode(node) && !!node.getTextContent().trim();
|
|
30
|
-
});
|
|
31
|
-
return nodes;
|
|
32
|
-
};
|
|
33
|
-
const nodeToHtml = (editor, node) => {
|
|
34
|
-
const selection = $createNodeSelection();
|
|
35
|
-
const children = node.getChildren();
|
|
36
|
-
// 剔除嵌套 list 节点
|
|
37
|
-
children
|
|
38
|
-
.filter((x) => !$isListNode(x))
|
|
39
|
-
.forEach((child) => $dfs(child)
|
|
40
|
-
.filter((x) => !$isExcalidrawNode(x.node))
|
|
41
|
-
.forEach((x) => selection.add(x.node.getKey())));
|
|
42
|
-
const html = $generateHtmlFromNodes(editor, selection);
|
|
43
|
-
return html;
|
|
44
|
-
};
|
|
45
|
-
export const translateEditorNodes = async ({ editor, targetLanguage, translateService, detectLanguage, displayMode, }) => {
|
|
46
|
-
let nodes = [];
|
|
47
|
-
const nodeUniqueIdMap = editor.getEditorState().read(() => getNodeIds());
|
|
48
|
-
editor.update(() => {
|
|
49
|
-
nodes = collectTranslatableNodes()
|
|
50
|
-
.filter((node) => detectLanguage(node.getTextContent()) !== targetLanguage)
|
|
51
|
-
.map((node) => {
|
|
52
|
-
const html = nodeToHtml(editor, node);
|
|
53
|
-
return { node, html };
|
|
54
|
-
});
|
|
55
|
-
nodes.forEach(({ node }) => {
|
|
56
|
-
node.append($createEmojiNode('global-loading', ''));
|
|
57
|
-
});
|
|
58
|
-
}, { discrete: true });
|
|
59
|
-
if (nodes.length === 0) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
let translations = null;
|
|
63
|
-
try {
|
|
64
|
-
translations = await translateService({
|
|
65
|
-
sourceItems: nodes.map(({ node, html }) => ({ uid: nodeUniqueIdMap.get(node.getKey()), text: html })),
|
|
66
|
-
targetLanguage,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
console.error(err);
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
nodes.forEach(({ node }, index) => {
|
|
74
|
-
editor.update(() => {
|
|
75
|
-
const lastChild = node.getLastChild();
|
|
76
|
-
if (lastChild) {
|
|
77
|
-
lastChild.remove();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (translations) {
|
|
83
|
-
applyTranslations(editor, translations, displayMode);
|
|
84
|
-
}
|
|
85
|
-
return translations;
|
|
86
|
-
};
|
|
87
|
-
export const applyTranslations = (editor, translations, displayMode) => {
|
|
88
|
-
editor.update(() => {
|
|
89
|
-
const nodeUniqueIdMap = getNodeIds(true);
|
|
90
|
-
translations.forEach(({ uid, text }) => {
|
|
91
|
-
try {
|
|
92
|
-
const node = $getNodeByKey(nodeUniqueIdMap.get(uid));
|
|
93
|
-
if (node) {
|
|
94
|
-
if (displayMode === 'inline') {
|
|
95
|
-
node.append($createTranslationNode({ html: text, displayMode: 'inline' }));
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
node.getChildren().forEach((child) => {
|
|
99
|
-
child.remove();
|
|
100
|
-
});
|
|
101
|
-
node.append($createTranslationNode({ html: text, displayMode: 'translationOnly' }));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
console.error('Failed to apply translations to editor', e);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* 判断 node 是否是有效的 TextNode
|
|
113
|
-
* - $isTextNode => true
|
|
114
|
-
* - format 不是 code
|
|
115
|
-
*/
|
|
116
|
-
const $isValidTextNode = (node) => {
|
|
117
|
-
return $isTextNode(node) && node.getTextContent().trim() && !$isMentionNode(node);
|
|
118
|
-
};
|
|
119
|
-
/**
|
|
120
|
-
* 判断一个 top level node 是否含有效的 TextNode 的 child
|
|
121
|
-
*/
|
|
122
|
-
const $hasValidTextNode = (elementNode) => {
|
|
123
|
-
return elementNode.getChildren().some($isValidTextNode);
|
|
124
|
-
};
|