@blocklet/editor 2.1.126 → 2.1.128

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,6 @@ 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;
37
39
  }
38
40
  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,9 @@ 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
+ }
171
178
  }
172
179
  export function useEditorTranslator(options) {
173
180
  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,24 @@
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, useState } from 'react';
4
+ import { Box, 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';
7
8
  import { useInlineTranslationStore, useStatus, useTargetLanguage } from './store';
8
9
  import { useEditorTranslator } from './EditorTranslator';
9
10
  import { NODE_TYPE } from './TranslationNode';
10
11
  const translations = {
11
- en: 'AI-Translated Content',
12
- zh: 'AI 翻译内容',
12
+ translatedContent: {
13
+ en: 'AI-Translated Content',
14
+ zh: 'AI 翻译内容',
15
+ },
16
+ original: {
17
+ en: 'Original',
18
+ zh: '原文',
19
+ },
13
20
  };
14
- function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore }) {
21
+ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onTranslate, onRestore, showTranslationBadge = true, }) {
15
22
  const [editor] = useLexicalComposerContext();
16
23
  const [inViewport] = useInViewport(editor.getRootElement());
17
24
  const setStatus = useInlineTranslationStore((s) => s.setStatus);
@@ -22,7 +29,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
22
29
  const status = useStatus(editor);
23
30
  const previousStatus = usePrevious(status);
24
31
  const editorTranslator = useEditorTranslator({ displayMode });
25
- const hoveringTranslationElementRef = useHoveringTranslationElement();
26
32
  const { locale } = useLocaleContext();
27
33
  const handleTranslate = async () => {
28
34
  try {
@@ -61,17 +67,6 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
61
67
  useEffect(() => {
62
68
  editorTranslator.updateOptions({ displayMode });
63
69
  }, [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
70
  // 对于翻译后的内容,禁止 checkbox 点击交互
76
71
  useEffect(() => {
77
72
  const onClick = (event) => {
@@ -93,17 +88,18 @@ function InternalInlineTranslationPlugin({ translateService, detectLanguage, onT
93
88
  if (editor.isEditable()) {
94
89
  return null;
95
90
  }
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' } }));
91
+ 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: {
92
+ display: 'inline-block',
93
+ px: 1,
94
+ fontSize: 12,
95
+ fontWeight: 'medium',
96
+ color: 'text.secondary',
97
+ bgcolor: 'grey.50',
98
+ border: 1,
99
+ borderColor: 'divider',
100
+ borderRadius: 0.5,
101
+ }, children: ["\uD83E\uDD16", ' ', translations.translatedContent[locale] ||
102
+ translations.translatedContent.en] }) })) : (_jsx(Box, { sx: { position: 'absolute', top: 0, bottom: 0, left: 0, width: '1px' } }))] }));
107
103
  }
108
104
  export function InlineTranslationPlugin(props) {
109
105
  const [editor] = useLexicalComposerContext();
@@ -112,23 +108,86 @@ export function InlineTranslationPlugin(props) {
112
108
  }
113
109
  return _jsx(InternalInlineTranslationPlugin, { ...props });
114
110
  }
115
- function useHoveringTranslationElement() {
111
+ function TranslationElementPopper({ editorTranslator }) {
116
112
  const [editor] = useLexicalComposerContext();
117
- const hoveringElementRef = useRef(null);
113
+ const [popperElement, setPopperElement] = useState(null);
114
+ const [hoveringTranslationElement, setHoveringTranslationElement] = useState(null);
115
+ const [originalText, setOriginalText] = useState(null);
116
+ const { locale } = useLocaleContext();
117
+ const reset = () => {
118
+ setHoveringTranslationElement(null);
119
+ setOriginalText(null);
120
+ };
121
+ const isInsidePopper = (element) => {
122
+ return popperElement && (popperElement === element || popperElement.contains(element));
123
+ };
124
+ const { styles, attributes } = usePopper(hoveringTranslationElement, popperElement, {
125
+ strategy: 'fixed',
126
+ placement: 'bottom-start',
127
+ });
118
128
  useEffect(() => {
119
- const handler = (event) => {
120
- const translationElement = event.target.closest(`.${NODE_TYPE}`);
121
- if (translationElement && editor.getRootElement()?.contains(translationElement)) {
122
- hoveringElementRef.current = translationElement;
129
+ return editor.registerRootListener((rootElement, prevRootElement) => {
130
+ const handleMouseOver = (event) => {
131
+ const target = event.target;
132
+ if (isInsidePopper(target)) {
133
+ return;
134
+ }
135
+ const translationElement = target.closest(`.${NODE_TYPE}`);
136
+ if (translationElement) {
137
+ setHoveringTranslationElement(translationElement);
138
+ }
139
+ else {
140
+ reset();
141
+ }
142
+ };
143
+ let timer = null;
144
+ const handleMouseOut = (event) => {
145
+ const relatedTarget = event.relatedTarget;
146
+ if (timer) {
147
+ clearTimeout(timer);
148
+ }
149
+ timer = setTimeout(() => {
150
+ const editorContainer = rootElement?.closest('.be-shell');
151
+ // - popper 在 editorContainer 与 editor root 之间, 所以使用 `editorContainer.contains(relatedTarget)` 来检测
152
+ // - `editorContainer === relatedTarget` 也表示从 editor 移出
153
+ if (rootElement && (editorContainer === relatedTarget || !editorContainer?.contains(relatedTarget))) {
154
+ reset();
155
+ }
156
+ }, 100);
157
+ };
158
+ if (prevRootElement) {
159
+ prevRootElement.removeEventListener('mouseover', handleMouseOver);
160
+ prevRootElement.removeEventListener('mouseout', handleMouseOut);
123
161
  }
124
- else {
125
- hoveringElementRef.current = null;
162
+ if (rootElement) {
163
+ rootElement.addEventListener('mouseover', handleMouseOver);
164
+ rootElement.addEventListener('mouseout', handleMouseOut);
126
165
  }
127
- };
128
- document.addEventListener('mouseover', handler);
129
- return () => {
130
- document.removeEventListener('mouseover', handler);
131
- };
132
- }, []);
133
- return hoveringElementRef;
166
+ });
167
+ }, [editor]);
168
+ if (!hoveringTranslationElement) {
169
+ return null;
170
+ }
171
+ return (_jsx(Box, { ref: setPopperElement, style: styles.popper, ...attributes.popper, sx: { zIndex: 'modal', mt: '-4px' }, onMouseLeave: reset, children: originalText ? (_jsxs(Box, { sx: {
172
+ position: 'relative',
173
+ zIndex: 'modal',
174
+ maxWidth: 600,
175
+ maxHeight: 400,
176
+ p: 2,
177
+ border: 1,
178
+ borderColor: 'divider',
179
+ borderRadius: 1,
180
+ fontSize: 13,
181
+ bgcolor: '#fff',
182
+ boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
183
+ 'h1,h2,h3,h4,h5,h6': {
184
+ margin: 0,
185
+ },
186
+ }, children: [_jsx(Box, { sx: { color: 'text.secondary' }, children: translations.original[locale] || translations.original.en }), _jsx(Box, { dangerouslySetInnerHTML: { __html: originalText } })] })) : (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 0.5, sx: {
187
+ height: 28,
188
+ pr: 2,
189
+ color: 'text.secondary',
190
+ fontSize: 12,
191
+ cursor: 'pointer',
192
+ }, onClick: () => setOriginalText(originalText ? null : editorTranslator.getOriginalText(hoveringTranslationElement.dataset.sid) || ''), children: [_jsx("i", { className: "iconify", "data-icon": "tabler:tooltip", "data-height": 16 }), translations.original[locale] || translations.original.en] })) }));
134
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/editor",
3
- "version": "2.1.126",
3
+ "version": "2.1.128",
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.126"
69
+ "@blocklet/pdf": "^2.1.128"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/core": "^7.25.2",