@blocklet/editor 2.1.113 → 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.
@@ -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, $isTextNode, $createTextNode, $getNodeByKey, $isElementNode, $copyNode, $createNodeSelection, } from 'lexical';
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 { $isMentionNode } from '../../main/nodes/MentionNode';
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,4 +1,4 @@
1
- import { TranslateService } from './utils';
1
+ import { TranslateService } from './EditorTranslator';
2
2
  interface Props {
3
3
  translateService: TranslateService;
4
4
  detectLanguage: (text: string) => string | null;
@@ -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 { applyTranslations, translateEditorNodes } from './utils';
6
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
7
7
  import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
8
- // 原始 editor 状态, 用于恢复
9
- const originalEditorStates = {};
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 translationsRef = useRef(null);
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
- originalEditorStates[editor.getKey()] = editor.getEditorState();
24
- const translations = await translateEditorNodes({
25
- editor,
30
+ await editorTranslator.translate({
26
31
  translateService,
27
32
  targetLanguage,
28
33
  detectLanguage,
29
- displayMode,
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
- if (originalEditorStates[editor.getKey()]) {
40
- onRestore?.();
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
- // Warning: flushSync was called from inside a lifecycle method
67
- setTimeout(() => {
68
- if (translationsRef.current && originalEditorStates[editor.getKey()]) {
69
- editor.setEditorState(originalEditorStates[editor.getKey()]);
70
- applyTranslations(editor, translationsRef.current, displayMode);
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
- }, 0);
73
- }, [displayMode]);
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: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } });
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
- /// <reference types="react" />
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
- export interface TranslationPayload {
6
- key?: NodeKey;
7
- html: string;
3
+ interface TranslationNodeProps {
4
+ sid: string;
8
5
  displayMode: DisplayMode;
9
6
  }
10
- export type SerializedTranslationNode = Spread<{
11
- html: string;
12
- displayMode: DisplayMode;
13
- }, SerializedLexicalNode>;
14
- export declare class TranslationNode extends DecoratorNode<JSX.Element> {
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
- static importJSON(serializedNode: SerializedTranslationNode): TranslationNode;
20
- exportDOM(): DOMExportOutput;
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({ html, key, displayMode }: TranslationPayload): TranslationNode;
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 { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* eslint-disable @typescript-eslint/no-use-before-define */
3
- import { Box } from '@mui/material';
4
- import { $applyNodeReplacement, DecoratorNode } from 'lexical';
5
- function $convertTranslationElement(domNode) {
6
- const element = domNode;
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);
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 'translation';
10
+ return NODE_TYPE;
26
11
  }
27
12
  static clone(node) {
28
- return new TranslationNode(node.__html, node.__key, node.__displayMode);
13
+ return new TranslationNode(node.__data, node.__key);
29
14
  }
30
- static importJSON(serializedNode) {
31
- const { html, displayMode } = serializedNode;
32
- const node = $createTranslationNode({ html, displayMode });
33
- return node;
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
- exportJSON() {
60
- return {
61
- html: this.__html,
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({ html, key, displayMode }) {
82
- return $applyNodeReplacement(new TranslationNode(html, key, displayMode));
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,4 +1,3 @@
1
1
  export * from './TranslationNode';
2
- export * from './utils';
3
2
  export * from './InlineTranslationPlugin';
4
3
  export * from './store';
@@ -1,4 +1,3 @@
1
1
  export * from './TranslationNode';
2
- export * from './utils';
3
2
  export * from './InlineTranslationPlugin';
4
3
  export * from './store';
@@ -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) => void;
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
- translate: (editor) => {
11
+ editorTranslateStates: new Map(),
12
+ translate: (editor, options) => {
12
13
  if (editor) {
13
- set((state) => ({ editors: new Map(state.editors).set(editor, 'pending') }));
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) => ({ editors: new Map(state.editors).set(editor, 'idle') }));
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
  },
@@ -84,7 +84,7 @@ export function PagesKitComponentRenderer({ id, name, properties, onPropertiesCh
84
84
  }, children: [_jsx(Box, { component: "span", className: "iconify", "data-icon": "tabler:settings-2", sx: { fontSize: 20 } }), _jsx("span", { children: "Properties" })] }) }), _jsx(Divider, { sx: { mt: 0.75, mb: 2 } }), propDefinitions.map((x) => {
85
85
  // @ts-ignore
86
86
  const multiline = !!x.multiline;
87
- return (_jsx(Box, { sx: { mt: 2 }, children: _jsx(PropertyField, { label: getPropertyLabel({ locale, locales: x.locales }), type: x.type, value: pendingProperties[locale]?.[x.id] ??
87
+ return (_jsx(Box, { sx: { mt: 2 }, children: _jsx(PropertyField, { label: getPropertyLabel({ locale, locales: x.locales }) || x.key || '', type: x.type, value: pendingProperties[locale]?.[x.id] ??
88
88
  getLocalizedValue({ key: 'defaultValue', locale, data: x.locales }), onChange: (v) => setPendingProperties({
89
89
  ...pendingProperties,
90
90
  [locale]: { ...pendingProperties[locale], [x.id]: v },
@@ -1,6 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { FormControlLabel, IconButton, InputAdornment, Switch, TextField } from '@mui/material';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, FormControlLabel, FormLabel, IconButton, InputAdornment, Stack, Switch, TextField } from '@mui/material';
3
3
  import UploadIcon from '@mui/icons-material/Upload';
4
+ import { useRef } from 'react';
5
+ import { lazyRetry as lazy } from '@arcblock/ux/lib/Util';
6
+ import ColorPicker from '../../main/ui/ColorPicker';
7
+ const CodeEditor = lazy(() => import('@blocklet/code-editor').then((module) => ({
8
+ default: module.CodeEditor,
9
+ })));
4
10
  function getVideoSize(url) {
5
11
  return new Promise((resolve, reject) => {
6
12
  const video = document.createElement('video');
@@ -38,6 +44,22 @@ export function UploaderButton({ onChange }) {
38
44
  return (_jsx(IconButton, { size: "small", onClick: handleOpen, children: _jsx(UploadIcon, {}) }, "uploader-trigger"));
39
45
  }
40
46
  export function PropertyField({ type, multiline, label, value, onChange, ...rest }) {
47
+ const codeEditorUploadCallback = useRef(null);
48
+ const handleOpen = () => {
49
+ // @ts-ignore
50
+ const uploader = uploaderRef?.current?.getUploader();
51
+ uploader?.open();
52
+ if (codeEditorUploadCallback.current) {
53
+ // rewrite default emitter
54
+ uploader.onceUploadSuccess(({ response }) => {
55
+ let fileName = response?.data?.filename || '';
56
+ if (fileName) {
57
+ fileName = `mediakit://${fileName}`;
58
+ }
59
+ codeEditorUploadCallback.current?.(fileName);
60
+ });
61
+ }
62
+ };
41
63
  const handleChange = (v) => {
42
64
  if (type === 'json') {
43
65
  try {
@@ -72,8 +94,61 @@ export function PropertyField({ type, multiline, label, value, onChange, ...rest
72
94
  } }) })),
73
95
  }, ...rest }));
74
96
  }
75
- return (_jsx(TextField, { sx: { width: 1 }, size: "small", label: label, value: type === 'json' && typeof value !== 'string' ? JSON.stringify(value, null, 2) : value, onChange: (e) => handleChange(e.target.value), ...((multiline || type === 'json') && {
76
- multiline: true,
77
- rows: 5,
78
- }), ...rest }));
97
+ if (type === 'color') {
98
+ return (_jsx(TextField, { sx: { width: 1 }, size: "small", label: label, value: value, onChange: (e) => handleChange(e.target.value), InputProps: {
99
+ endAdornment: (_jsx(InputAdornment, { position: "end", sx: {
100
+ '.popup-item-color': {
101
+ width: 24,
102
+ height: 24,
103
+ borderRadius: '4px',
104
+ cursor: 'pointer',
105
+ backgroundColor: value || '#fff',
106
+ border: '1px solid rgba(0, 0, 0, 0.23)',
107
+ '&:hover': {
108
+ opacity: 0.8,
109
+ },
110
+ },
111
+ }, children: _jsx(ColorPicker, { color: value, onChange: handleChange, buttonClassName: "popup-item spaced popup-item-color" }) })),
112
+ }, ...rest }));
113
+ }
114
+ if (type === 'yaml' || type === 'json') {
115
+ let newValue = value;
116
+ if (type === 'json' && typeof value !== 'string') {
117
+ newValue = JSON.stringify(value, null, 2);
118
+ }
119
+ return (_jsxs(Stack, { sx: {
120
+ width: '100%',
121
+ position: 'relative',
122
+ pt: 1,
123
+ pb: '6px',
124
+ px: '1px',
125
+ minHeight: 50,
126
+ '.monaco-editor,.overflow-guard': { borderRadius: 1 },
127
+ }, children: [_jsx(FormLabel, { sx: {
128
+ position: 'absolute',
129
+ left: 0,
130
+ top: 0,
131
+ transform: 'translate(0px, -7px) scale(0.75)',
132
+ }, children: label }), _jsx(CodeEditor, { keyId: label, locale: "en", language: type === 'yaml' ? 'yaml' : 'json', value: newValue, onChange: (v) => {
133
+ return handleChange(v);
134
+ }, typeScriptNoValidation: false, onUpload: (callback) => {
135
+ codeEditorUploadCallback.current = callback;
136
+ handleOpen();
137
+ } }), _jsx(Box, { component: "fieldset", sx: {
138
+ pointerEvents: 'none',
139
+ position: 'absolute',
140
+ left: 0,
141
+ top: -5,
142
+ width: '100%',
143
+ height: '100%',
144
+ border: 1,
145
+ borderColor: 'rgba(0, 0, 0, 0.23)',
146
+ borderRadius: 1,
147
+ px: 1,
148
+ py: 0,
149
+ zIndex: 1,
150
+ m: 0,
151
+ } })] }));
152
+ }
153
+ return (_jsx(TextField, { sx: { width: 1 }, size: "small", label: label, value: value, onChange: (e) => handleChange(e.target.value), multiline: true, rows: 5, ...rest }));
79
154
  }
@@ -1,5 +1,5 @@
1
1
  export declare const mode: string;
2
- export declare const propertyTypes: readonly ["string", "number", "boolean", "json", "url"];
2
+ export declare const propertyTypes: readonly ["string", "number", "boolean", "json", "url", "yaml", "color"];
3
3
  export type PropertyType = (typeof propertyTypes)[number];
4
4
  export type Properties = {
5
5
  [locale: string]: {
@@ -1,5 +1,5 @@
1
1
  export const mode = process.env.NODE_ENV === 'development' ? 'draft' : 'production';
2
- export const propertyTypes = ['string', 'number', 'boolean', 'json', 'url'];
2
+ export const propertyTypes = ['string', 'number', 'boolean', 'json', 'url', 'yaml', 'color'];
3
3
  export const isValidPropertyType = (type) => {
4
4
  return !type || propertyTypes.includes(type);
5
5
  };
@@ -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
+ };
@@ -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;
@@ -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;
@@ -462,7 +462,10 @@ i.prettier-error {
462
462
  z-index: 10001;
463
463
  display: block;
464
464
  position: fixed;
465
- box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.5);
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
+ }
@@ -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.113",
3
+ "version": "2.1.115",
4
4
  "main": "lib/index.js",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,9 +24,10 @@
24
24
  ]
25
25
  },
26
26
  "dependencies": {
27
- "@blocklet/embed": "^0.2.1",
27
+ "@blocklet/code-editor": "^0.4.201",
28
+ "@blocklet/embed": "^0.2.2",
28
29
  "@blocklet/js-sdk": "1.16.33",
29
- "@blocklet/pages-kit": "^0.3.19",
30
+ "@blocklet/pages-kit": "^0.3.22",
30
31
  "@excalidraw/excalidraw": "^0.14.2",
31
32
  "@iconify/iconify": "^3.1.1",
32
33
  "@iconify/icons-tabler": "^1.2.95",
@@ -65,7 +66,7 @@
65
66
  "ufo": "^1.5.4",
66
67
  "url-join": "^4.0.1",
67
68
  "zustand": "^4.5.5",
68
- "@blocklet/pdf": "^2.1.113"
69
+ "@blocklet/pdf": "^2.1.115"
69
70
  },
70
71
  "devDependencies": {
71
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
- };