@blocklet/editor 2.1.127 → 2.1.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -85,9 +85,9 @@ const Root = styled.a `
85
85
  .be-bookmark-image {
86
86
  flex: 0 0 auto;
87
87
  margin-left: auto;
88
+ // The recommended size for an Open Graph (OG) image is 1200 x 630 pixels with an aspect ratio of 1.91:1
88
89
  width: 300px;
89
- min-height: 128px;
90
- max-height: 144px;
90
+ height: 158px;
91
91
  background-size: cover;
92
92
  background-position: center center;
93
93
  }
@@ -95,6 +95,7 @@ const Root = styled.a `
95
95
  @media (max-width: 768px) {
96
96
  .be-bookmark-image {
97
97
  width: 200px;
98
+ height: 105px;
98
99
  }
99
100
  }
100
101
 
@@ -20,6 +20,7 @@ export declare class EditorTranslator {
20
20
  private nodeKeyToSidMap;
21
21
  private sidToNodeKeyMap;
22
22
  private translations;
23
+ private sourceItems;
23
24
  constructor(editor: LexicalEditor, options: EditorTranslatorOptions);
24
25
  get isTranslated(): boolean;
25
26
  setEditor(editor: LexicalEditor): void;
@@ -34,5 +35,7 @@ export declare class EditorTranslator {
34
35
  applyTranslations(): void;
35
36
  restore(sid?: string): void;
36
37
  preprocessNodes(): Promise<void>;
38
+ getOriginalText(sid: string): string | undefined;
39
+ getTranslationText(sid: string): string | undefined;
37
40
  }
38
41
  export declare function useEditorTranslator(options: EditorTranslatorOptions): EditorTranslator;
@@ -15,6 +15,7 @@ export class EditorTranslator {
15
15
  nodeKeyToSidMap = new Map();
16
16
  sidToNodeKeyMap = new Map();
17
17
  translations = [];
18
+ sourceItems = [];
18
19
  constructor(editor, options) {
19
20
  this.editor = editor;
20
21
  this.options = options;
@@ -62,11 +63,12 @@ export class EditorTranslator {
62
63
  return;
63
64
  }
64
65
  try {
66
+ this.sourceItems = nodes.map(({ node, html }) => ({
67
+ sid: this.nodeKeyToSidMap.get(node.getKey()),
68
+ text: html,
69
+ }));
65
70
  this.translations = await translateService({
66
- sourceItems: nodes.map(({ node, html }) => ({
67
- sid: this.nodeKeyToSidMap.get(node.getKey()),
68
- text: html,
69
- })),
71
+ sourceItems: this.sourceItems,
70
72
  targetLanguage,
71
73
  useCache,
72
74
  });
@@ -102,10 +104,12 @@ export class EditorTranslator {
102
104
  node.append(translationNode);
103
105
  }
104
106
  else {
105
- node.append(translationNode, $createTranslationNode({
106
- translatedNodes: node.getChildren(),
107
- data: { sid, displayMode: this.options.displayMode },
108
- }) // 利用一个隐藏的 TranslationNode 来存储 original nodes, 用于单个结点恢复
107
+ node.getChildren().forEach((child) => child.remove());
108
+ node.append(translationNode
109
+ // $createTranslationNode({
110
+ // translatedNodes: node.getChildren(),
111
+ // data: { sid, displayMode: this.options.displayMode },
112
+ // }) // 利用一个隐藏的 TranslationNode 来存储 original nodes, 用于单个结点恢复
109
113
  );
110
114
  }
111
115
  }
@@ -168,6 +172,12 @@ export class EditorTranslator {
168
172
  await new Promise((resolve) => setTimeout(resolve, 10));
169
173
  this.preprocessEditorState = this.editor.getEditorState().clone();
170
174
  }
175
+ getOriginalText(sid) {
176
+ return this.sourceItems.find((item) => item.sid === sid)?.text;
177
+ }
178
+ getTranslationText(sid) {
179
+ return this.translations.find((item) => item.sid === sid)?.text;
180
+ }
171
181
  }
172
182
  export function useEditorTranslator(options) {
173
183
  const [editor] = useLexicalComposerContext();
@@ -6,6 +6,7 @@ interface Props {
6
6
  targetLanguage: string;
7
7
  }) => void;
8
8
  onRestore?: () => void;
9
+ showTranslationBadge?: boolean;
9
10
  }
10
11
  export declare function InlineTranslationPlugin(props: Props): import("react/jsx-runtime").JSX.Element | null;
11
12
  export {};
@@ -1,17 +1,31 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
- import { useEffect, useRef } from 'react';
4
- import { Box } from '@mui/material';
3
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
4
+ import { Box, ClickAwayListener, Stack } from '@mui/material';
5
5
  import { useInViewport, usePrevious } from 'ahooks';
6
6
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
7
+ import { usePopper } from 'react-popper';
8
+ import { createPortal } from 'react-dom';
9
+ import { css, Global } from '@emotion/react';
10
+ import blue from '@mui/material/colors/blue';
7
11
  import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
8
12
  import { useEditorTranslator } from './EditorTranslator';
9
13
  import { NODE_TYPE } from './TranslationNode';
10
14
  const translations = {
11
- en: 'AI-Translated Content',
12
- zh: 'AI 翻译内容',
15
+ translatedContent: {
16
+ en: 'AI-Translated Content',
17
+ zh: 'AI 翻译内容',
18
+ },
19
+ original: {
20
+ en: 'Original',
21
+ zh: '原文',
22
+ },
23
+ translation: {
24
+ en: 'Translation',
25
+ zh: '翻译',
26
+ },
13
27
  };
14
- function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore }) {
28
+ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore, showTranslationBadge = true, }) {
15
29
  const [editor] = useLexicalComposerContext();
16
30
  const [inViewport] = useInViewport(editor.getRootElement());
17
31
  const setStatus = useInlineTranslationStore((s) => s.setStatus);
@@ -22,7 +36,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
22
36
  const status = useStatus(editor);
23
37
  const previousStatus = usePrevious(status);
24
38
  const editorTranslator = useEditorTranslator({ displayMode });
25
- const hoveringTranslationElementRef = useHoveringTranslationElement();
26
39
  const { locale } = useLocaleContext();
27
40
  const handleTranslate = async () => {
28
41
  try {
@@ -61,17 +74,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
61
74
  useEffect(() => {
62
75
  editorTranslator.updateOptions({ displayMode });
63
76
  }, [editorTranslator, displayMode]);
64
- useEffect(() => {
65
- const handler = (event) => {
66
- if (event.ctrlKey && hoveringTranslationElementRef.current?.dataset?.sid) {
67
- editorTranslator.restore(hoveringTranslationElementRef.current.dataset.sid);
68
- }
69
- };
70
- document.addEventListener('keydown', handler);
71
- return () => {
72
- document.removeEventListener('keydown', handler);
73
- };
74
- }, [editor, editorTranslator]);
75
77
  // 对于翻译后的内容,禁止 checkbox 点击交互
76
78
  useEffect(() => {
77
79
  const onClick = (event) => {
@@ -93,17 +95,18 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
93
95
  if (editor.isEditable()) {
94
96
  return null;
95
97
  }
96
- return status === 'completed' && editorTranslator.isTranslated ? (_jsx(Box, { sx: { mb: 0.5 }, children: _jsxs(Box, { className: "inline-translation-badge", sx: {
97
- display: 'inline-block',
98
- px: 1,
99
- fontSize: 12,
100
- fontWeight: 'medium',
101
- color: 'text.secondary',
102
- bgcolor: 'grey.50',
103
- border: 1,
104
- borderColor: 'divider',
105
- borderRadius: 0.5,
106
- }, children: ["\uD83E\uDD16 ", translations[locale] || translations.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }));
98
+ return (_jsxs(_Fragment, { children: [_jsx(TranslationElementPopper, { editorTranslator: editorTranslator }), showTranslationBadge && status === 'completed' && editorTranslator.isTranslated ? (_jsx(Box, { sx: { mb: 0.5 }, children: _jsxs(Box, { className: "inline-translation-badge", sx: {
99
+ display: 'inline-block',
100
+ px: 1,
101
+ fontSize: 12,
102
+ fontWeight: 'medium',
103
+ color: 'text.secondary',
104
+ bgcolor: 'grey.50',
105
+ border: 1,
106
+ borderColor: 'divider',
107
+ borderRadius: 0.5,
108
+ }, children: ["\uD83E\uDD16", ' ', translations.translatedContent[locale] ||
109
+ translations.translatedContent.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }))] }));
107
110
  }
108
111
  export function InlineTranslationPlugin(props) {
109
112
  const [editor] = useLexicalComposerContext();
@@ -112,23 +115,114 @@ export function InlineTranslationPlugin(props) {
112
115
  }
113
116
  return _jsx(InternalInlineTranslationPlugin, { ...props });
114
117
  }
115
- function useHoveringTranslationElement() {
118
+ function TranslationElementPopper({ editorTranslator }) {
116
119
  const [editor] = useLexicalComposerContext();
117
- const hoveringElementRef = useRef(null);
120
+ const [popperElement, setPopperElement] = useState(null);
121
+ const popperElementRef = useRef(null);
122
+ const [hoveringTranslationElement, setHoveringTranslationElement] = useState(null);
123
+ const [originalText, setOriginalText] = useState(null);
124
+ const { locale } = useLocaleContext();
125
+ useLayoutEffect(() => {
126
+ popperElementRef.current = popperElement;
127
+ }, [popperElement]);
128
+ const reset = () => {
129
+ setHoveringTranslationElement(null);
130
+ setOriginalText(null);
131
+ };
132
+ const isInsidePopper = (element) => {
133
+ return (popperElementRef.current && (popperElementRef.current === element || popperElementRef.current.contains(element)));
134
+ };
135
+ const hoveringTranslationElementPopper = usePopper(hoveringTranslationElement, popperElement, {
136
+ strategy: 'fixed',
137
+ placement: 'left-start',
138
+ modifiers: [
139
+ {
140
+ name: 'flip',
141
+ options: {
142
+ fallbackPlacements: ['bottom-start'],
143
+ },
144
+ },
145
+ ],
146
+ });
147
+ const translationPreviewPopper = usePopper(hoveringTranslationElement, popperElement, {
148
+ strategy: 'fixed',
149
+ placement: 'bottom-start',
150
+ modifiers: [
151
+ {
152
+ name: 'flip',
153
+ },
154
+ ],
155
+ });
118
156
  useEffect(() => {
119
- const handler = (event) => {
120
- const translationElement = event.target.closest(`.${NODE_TYPE}`);
121
- if (translationElement && editor.getRootElement()?.contains(translationElement)) {
122
- hoveringElementRef.current = translationElement;
157
+ return editor.registerRootListener((rootElement, prevRootElement) => {
158
+ const handleMouseOver = (event) => {
159
+ const target = event.target;
160
+ if (isInsidePopper(target)) {
161
+ return;
162
+ }
163
+ const translationElement = target.closest(`.${NODE_TYPE}`);
164
+ if (translationElement) {
165
+ setHoveringTranslationElement(translationElement);
166
+ }
167
+ else {
168
+ reset();
169
+ }
170
+ };
171
+ let timer = null;
172
+ const handleMouseOut = (event) => {
173
+ const relatedTarget = event.relatedTarget;
174
+ if (timer) {
175
+ clearTimeout(timer);
176
+ }
177
+ timer = setTimeout(() => {
178
+ if (rootElement && !rootElement?.contains(relatedTarget) && !isInsidePopper(relatedTarget)) {
179
+ reset();
180
+ }
181
+ }, 100);
182
+ };
183
+ if (prevRootElement) {
184
+ prevRootElement.removeEventListener('mouseover', handleMouseOver);
185
+ prevRootElement.removeEventListener('mouseout', handleMouseOut);
123
186
  }
124
- else {
125
- hoveringElementRef.current = null;
187
+ if (rootElement) {
188
+ rootElement.addEventListener('mouseover', handleMouseOver);
189
+ rootElement.addEventListener('mouseout', handleMouseOut);
126
190
  }
127
- };
128
- document.addEventListener('mouseover', handler);
129
- return () => {
130
- document.removeEventListener('mouseover', handler);
131
- };
132
- }, []);
133
- return hoveringElementRef;
191
+ });
192
+ }, [editor]);
193
+ if (!hoveringTranslationElement) {
194
+ return null;
195
+ }
196
+ const globalStyles = css `
197
+ .inline-translation-node[data-eid='${editor.getKey()}'][data-sid='${hoveringTranslationElement.dataset.sid}'] > * {
198
+ background-color: ${blue[50]};
199
+ border-radius: 4px;
200
+ }
201
+ `;
202
+ return createPortal(_jsxs(_Fragment, { children: [!!originalText && (_jsxs(_Fragment, { children: [_jsx(Global, { styles: globalStyles }), _jsx(ClickAwayListener, { onClickAway: reset, children: _jsxs(Box, { style: translationPreviewPopper.styles.popper, ...translationPreviewPopper.attributes.popper, sx: {
203
+ position: 'relative',
204
+ zIndex: 'modal',
205
+ maxWidth: 600,
206
+ maxHeight: 400,
207
+ p: 2,
208
+ border: 1,
209
+ borderColor: 'divider',
210
+ borderRadius: 1,
211
+ fontSize: 13,
212
+ bgcolor: '#fff',
213
+ boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
214
+ 'h1,h2,h3,h4,h5,h6': {
215
+ margin: 0,
216
+ },
217
+ }, children: [_jsxs(Box, { children: [_jsx(Box, { sx: { color: 'text.secondary', fontWeight: 'medium' }, children: translations.original[locale] || translations.original.en }), _jsx(Box, { dangerouslySetInnerHTML: { __html: originalText } })] }), _jsxs(Box, { sx: { mt: 1.5 }, children: [_jsx(Box, { sx: { color: 'text.secondary', fontWeight: 'medium' }, children: translations.translation[locale] ||
218
+ translations.translation.en }), _jsx(Box, { dangerouslySetInnerHTML: {
219
+ __html: editorTranslator.getTranslationText(hoveringTranslationElement.dataset.sid) || '',
220
+ } })] })] }) })] })), _jsx(Box, { ref: setPopperElement, style: hoveringTranslationElementPopper.styles.popper, ...hoveringTranslationElementPopper.attributes.popper, sx: { zIndex: 'modal' }, onMouseLeave: reset, children: !originalText && (_jsx(Stack, { direction: "row", alignItems: hoveringTranslationElement.clientHeight > 100 ? 'center' : 'flex-start', sx: {
221
+ pr: 1,
222
+ pt: 0.5,
223
+ color: 'text.secondary',
224
+ fontSize: 12,
225
+ cursor: 'pointer',
226
+ height: hoveringTranslationElement.clientHeight,
227
+ }, onClick: () => setOriginalText(originalText ? null : editorTranslator.getOriginalText(hoveringTranslationElement.dataset.sid) || ''), children: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 0.25, children: [_jsx("i", { className: "iconify", "data-icon": "tabler:tooltip", "data-height": 16 }), translations.original[locale] || translations.original.en] }) })) })] }), document.body);
134
228
  }
@@ -15,6 +15,7 @@ export class TranslationNode extends ElementNode {
15
15
  createDOM(config, editor) {
16
16
  const dom = document.createElement('div');
17
17
  dom.classList.add(NODE_TYPE, `${NODE_TYPE}-${this.__data.displayMode}`);
18
+ dom.dataset.eid = editor.getKey();
18
19
  dom.dataset.sid = this.__data.sid;
19
20
  return dom;
20
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/editor",
3
- "version": "2.1.127",
3
+ "version": "2.1.129",
4
4
  "main": "lib/index.js",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -66,7 +66,7 @@
66
66
  "ufo": "^1.5.4",
67
67
  "url-join": "^4.0.1",
68
68
  "zustand": "^4.5.5",
69
- "@blocklet/pdf": "^2.1.127"
69
+ "@blocklet/pdf": "^2.1.129"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/core": "^7.25.2",