@blocklet/editor 1.6.247 → 1.6.248

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.
@@ -0,0 +1,15 @@
1
+ import { ImageSizeMode } from '../../types';
2
+ export declare const useEditorSize: () => {
3
+ width: number;
4
+ height: number;
5
+ } | undefined;
6
+ export declare const useMaxImageWidth: () => number;
7
+ export declare const useImageNaturalSize: (src: string) => {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ export declare const useImageDisplayWidth: ({ src, sizeMode, width, }: {
12
+ src: string;
13
+ sizeMode?: ImageSizeMode | undefined;
14
+ width: number | 'inherit';
15
+ }) => number;
@@ -0,0 +1,43 @@
1
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2
+ import { useSize } from 'ahooks';
3
+ import { useEffect, useState } from 'react';
4
+ import { scaleImage } from '../utils/images';
5
+ export const useEditorSize = () => {
6
+ const [editor] = useLexicalComposerContext();
7
+ return useSize(() => editor.getRootElement());
8
+ };
9
+ export const useMaxImageWidth = () => {
10
+ const size = useEditorSize();
11
+ if (size?.width) {
12
+ return size.width - 54;
13
+ }
14
+ return document.documentElement.clientWidth;
15
+ };
16
+ export const useImageNaturalSize = (src) => {
17
+ const [size, setSize] = useState({ width: 0, height: 0 });
18
+ useEffect(() => {
19
+ const img = new Image();
20
+ img.onload = () => setSize({ width: img.width, height: img.height });
21
+ img.src = src;
22
+ }, [src]);
23
+ return size;
24
+ };
25
+ export const useImageDisplayWidth = ({ src, sizeMode = 'best-fit', width, }) => {
26
+ const naturalSize = useImageNaturalSize(src);
27
+ const maxImageWidth = useMaxImageWidth();
28
+ // 显式设置过宽度
29
+ if (width !== 'inherit') {
30
+ return Math.min(width, maxImageWidth);
31
+ }
32
+ if (!naturalSize) {
33
+ return 0;
34
+ }
35
+ if (sizeMode === 'small') {
36
+ return scaleImage({ width: naturalSize.width, height: naturalSize.height, maxSize: 200 }).width;
37
+ }
38
+ if (sizeMode === 'original') {
39
+ return Math.min(maxImageWidth, naturalSize.width);
40
+ }
41
+ return scaleImage({ width: naturalSize.width, height: naturalSize.height, maxSize: Math.min(400, maxImageWidth) })
42
+ .width;
43
+ };
@@ -0,0 +1,5 @@
1
+ import 'medium-zoom/dist/style.css';
2
+ export declare const useMediumZoom: (imageRef: React.RefObject<HTMLImageElement>) => {
3
+ zoomOpened: boolean;
4
+ openedImageRef: import("react").MutableRefObject<HTMLImageElement | null>;
5
+ };
@@ -0,0 +1,30 @@
1
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import mediumZoom from 'medium-zoom/dist/pure';
4
+ import 'medium-zoom/dist/style.css';
5
+ export const useMediumZoom = (imageRef) => {
6
+ const [editor] = useLexicalComposerContext();
7
+ const editable = editor.isEditable();
8
+ const openedImageRef = useRef(null);
9
+ const [zoomOpened, setZoomOpened] = useState(false);
10
+ useEffect(() => {
11
+ if (!editable && imageRef.current) {
12
+ const zoom = mediumZoom(imageRef.current, { background: 'rgba(0,0,0,.5)' });
13
+ zoom.on('opened', (e) => {
14
+ const openedImage = document.querySelector('.medium-zoom-image--opened');
15
+ if (openedImage) {
16
+ openedImageRef.current = openedImage;
17
+ setZoomOpened(true);
18
+ }
19
+ });
20
+ zoom.on('close', (e) => {
21
+ openedImageRef.current = null;
22
+ setZoomOpened(false);
23
+ });
24
+ return () => {
25
+ zoom.detach();
26
+ };
27
+ }
28
+ }, [editable, imageRef.current]);
29
+ return { zoomOpened, openedImageRef };
30
+ };
@@ -1321,3 +1321,9 @@ li.embed-option-disabled > span:after {
1321
1321
  .be-editable h6[id]:hover a {
1322
1322
  opacity: 1;
1323
1323
  }
1324
+
1325
+ /* https://github.com/francoischalifour/medium-zoom */
1326
+ .medium-zoom-overlay,
1327
+ .medium-zoom-image--opened {
1328
+ z-index: 99999;
1329
+ }
@@ -8,7 +8,9 @@
8
8
  /// <reference types="react" />
9
9
  import { LexicalEditor, NodeKey } from 'lexical';
10
10
  import './ImageNode.css';
11
- export default function ImageComponent({ file, src, altText, nodeKey, width, height, maxWidth, resizable, showCaption, caption, captionsEnabled, }: {
11
+ import { type DeviceFrame } from '../utils/device-frame';
12
+ import type { ImageSizeMode } from '../../types';
13
+ export default function ImageComponent({ file, src, altText, nodeKey, width, height, maxWidth, resizable, showCaption, caption, captionsEnabled, markerState, frame, sizeMode, }: {
12
14
  file?: File;
13
15
  altText: string;
14
16
  caption: LexicalEditor;
@@ -20,4 +22,7 @@ export default function ImageComponent({ file, src, altText, nodeKey, width, hei
20
22
  src?: string;
21
23
  width: 'inherit' | number;
22
24
  captionsEnabled: boolean;
25
+ markerState?: string;
26
+ frame?: DeviceFrame;
27
+ sizeMode?: ImageSizeMode;
23
28
  }): JSX.Element;
@@ -20,6 +20,7 @@ import { mergeRegister } from '@lexical/utils';
20
20
  import { Alert, Button, LinearProgress } from '@mui/material';
21
21
  import { $getNodeByKey, $getSelection, $isNodeSelection, $setSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical';
22
22
  import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
23
+ import { createPortal } from 'react-dom';
23
24
  import { useEditorConfig } from '../../config';
24
25
  import { useSettings } from '../context/SettingsContext';
25
26
  import { useSharedHistoryContext } from '../context/SharedHistoryContext';
@@ -31,6 +32,9 @@ import ImageResizer from '../ui/ImageResizer';
31
32
  import Placeholder from '../ui/Placeholder';
32
33
  import { $isImageNode } from './ImageNode';
33
34
  import './ImageNode.css';
35
+ import { ImageAnnotation, ImageAnnotationView, ImageEnhancer } from '../ui/ImageEnhancer';
36
+ import { useImageDisplayWidth } from '../hooks/hooks';
37
+ import { useMediumZoom } from '../hooks/medium-zoom';
34
38
  const imageCache = new Set();
35
39
  // Min size that require user confirm to upload image
36
40
  const REQUIRE_CONFIRM_UPLOAD_SIZE = 10 << 20;
@@ -46,37 +50,19 @@ function useSuspenseImage(src) {
46
50
  });
47
51
  }
48
52
  }
49
- function LazyImage({ altText, className, imageRef, src, width, height, maxWidth, }) {
53
+ function LazyImage({ altText, className, imageRef, src, width, height, }) {
50
54
  useSuspenseImage(src);
51
- const [editor] = useLexicalComposerContext();
52
- const editable = editor.isEditable();
53
55
  const imgProps = {
54
56
  style: {
55
57
  height,
56
- maxWidth,
57
58
  width,
58
59
  // 图片为 svg 时设置最小宽度以避免图片显示尺寸为 0 (https://github.com/blocklet/image-bin/issues/142)
59
60
  ...(src?.endsWith('.svg') && { minWidth: 200 }),
60
61
  },
61
62
  };
62
- if (!editable) {
63
- imgProps.onClick = () => {
64
- try {
65
- // 点击图片时去除 imageFilter 参数以查看原图 (#1201)
66
- const url = src.startsWith('/') ? new URL(src, window.location.origin) : new URL(src);
67
- url.searchParams.delete('imageFilter');
68
- window.open(url.href);
69
- }
70
- catch (e) {
71
- window.open(src);
72
- }
73
- };
74
- imgProps.role = 'presentation';
75
- imgProps.style.cursor = 'pointer';
76
- }
77
63
  return (_jsx("img", { className: className || undefined, src: src, alt: altText, ref: imageRef, draggable: "false", ...imgProps }));
78
64
  }
79
- export default function ImageComponent({ file, src, altText, nodeKey, width, height, maxWidth, resizable, showCaption, caption, captionsEnabled, }) {
65
+ export default function ImageComponent({ file, src, altText, nodeKey, width, height, maxWidth, resizable, showCaption, caption, captionsEnabled, markerState, frame, sizeMode, }) {
80
66
  const imageRef = useRef(null);
81
67
  const buttonRef = useRef(null);
82
68
  const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
@@ -167,6 +153,31 @@ export default function ImageComponent({ file, src, altText, nodeKey, width, hei
167
153
  }
168
154
  });
169
155
  };
156
+ const setMarkerState = (state) => {
157
+ editor.update(() => {
158
+ const node = $getNodeByKey(nodeKey);
159
+ if ($isImageNode(node)) {
160
+ node.setMarkerState(state);
161
+ }
162
+ });
163
+ };
164
+ const setFrame = (deviceFrame) => {
165
+ editor.update(() => {
166
+ const node = $getNodeByKey(nodeKey);
167
+ if ($isImageNode(node)) {
168
+ node.setFrame(deviceFrame);
169
+ }
170
+ });
171
+ };
172
+ const setSizeMode = (imageSizeMode) => {
173
+ editor.update(() => {
174
+ const node = $getNodeByKey(nodeKey);
175
+ if ($isImageNode(node)) {
176
+ node.setSizeMode(imageSizeMode);
177
+ node.setWidthAndHeight('inherit', 'inherit');
178
+ }
179
+ });
180
+ };
170
181
  const onResizeEnd = (nextWidth, nextHeight) => {
171
182
  // Delay hiding the resize bars for click case
172
183
  setTimeout(() => {
@@ -176,6 +187,7 @@ export default function ImageComponent({ file, src, altText, nodeKey, width, hei
176
187
  const node = $getNodeByKey(nodeKey);
177
188
  if ($isImageNode(node)) {
178
189
  node.setWidthAndHeight(nextWidth, nextHeight);
190
+ node.setSizeMode(undefined);
179
191
  }
180
192
  });
181
193
  };
@@ -227,9 +239,26 @@ export default function ImageComponent({ file, src, altText, nodeKey, width, hei
227
239
  }
228
240
  }, [src, file, confirmText]);
229
241
  const placeholder = objectUrl ? (_jsx(ImageContainer, { draggable: draggable, success: !!src, loading: loading, error: error, confirmText: confirmText, uploadImage: uploadImage, placeholder: objectUrl, placeholderProps: { style: { width, height, maxWidth } } })) : null;
230
- return (_jsx(Suspense, { fallback: placeholder, children: _jsxs(_Fragment, { children: [src ? (_jsx(ImageContainer, { draggable: draggable, children: _jsx(LazyImage, { className: isFocused ? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}` : null, src: src, altText: altText, imageRef: imageRef, width: width, height: height, maxWidth: maxWidth }) })) : (placeholder), showCaption && (_jsx("div", { className: "image-caption-container", children: _jsxs(LexicalNestedComposer, { initialEditor: caption, children: [_jsx(AutoFocusPlugin, {}), _jsx(MentionsPlugin, {}), _jsx(LinkPlugin, {}), _jsx(EmojisPlugin, {}), _jsx(HashtagPlugin, {}), _jsx(KeywordsPlugin, {}), _jsx(HistoryPlugin, { externalHistoryState: historyState }), _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "ImageNode__contentEditable" }), placeholder: editor.isEditable() ? (_jsx(Placeholder, { className: "ImageNode__placeholder", children: "Enter a caption..." })) : null, ErrorBoundary: LexicalErrorBoundary })] }) })), resizable && $isNodeSelection(selection) && isFocused && (_jsx(ImageResizer, { showCaption: showCaption, setShowCaption: setShowCaption, editor: editor, buttonRef: buttonRef, imageRef: imageRef,
231
- // maxWidth={maxWidth}
232
- onResizeStart: onResizeStart, onResizeEnd: onResizeEnd, captionsEnabled: captionsEnabled }))] }) }));
242
+ const imageDisplayWidth = useImageDisplayWidth({ src: src || objectUrl, sizeMode, width });
243
+ const { zoomOpened, openedImageRef } = useMediumZoom(imageRef);
244
+ const renderImageAnnotationView = () => {
245
+ if (!markerState)
246
+ return null;
247
+ const element = (_jsx(ImageAnnotationView, { imageRef: zoomOpened ? openedImageRef : imageRef, markerState: markerState, sx: {
248
+ ...(zoomOpened && {
249
+ zIndex: 999999,
250
+ // transform: openedImageRef.current?.style.transform,
251
+ '.__markerjslive_': {
252
+ // ...pick(openedImageRef.current?.style, ['width', 'height', 'top', 'left']),
253
+ transform: openedImageRef.current?.style.transform,
254
+ },
255
+ }),
256
+ } }));
257
+ return zoomOpened ? createPortal(element, document.body) : element;
258
+ };
259
+ return (_jsx(Suspense, { fallback: placeholder, children: _jsxs(_Fragment, { children: [src ? (_jsxs(ImageContainer, { draggable: draggable, children: [_jsx(LazyImage, { className: isFocused ? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}` : null, src: src, altText: altText, imageRef: imageRef, width: imageDisplayWidth, height: height }), renderImageAnnotationView(), editor.isEditable() && (_jsx(ImageAnnotation, { imageRef: imageRef, markerState: markerState, onChange: setMarkerState }))] })) : (placeholder), showCaption && (_jsx("div", { className: "image-caption-container", children: _jsxs(LexicalNestedComposer, { initialEditor: caption, children: [_jsx(AutoFocusPlugin, {}), _jsx(MentionsPlugin, {}), _jsx(LinkPlugin, {}), _jsx(EmojisPlugin, {}), _jsx(HashtagPlugin, {}), _jsx(KeywordsPlugin, {}), _jsx(HistoryPlugin, { externalHistoryState: historyState }), _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "ImageNode__contentEditable" }), placeholder: editor.isEditable() ? (_jsx(Placeholder, { className: "ImageNode__placeholder", children: "Enter a caption..." })) : null, ErrorBoundary: LexicalErrorBoundary })] }) })), resizable && $isNodeSelection(selection) && isFocused && (_jsxs(_Fragment, { children: [_jsx(ImageResizer, { showCaption: showCaption, setShowCaption: setShowCaption, editor: editor, buttonRef: buttonRef, imageRef: imageRef,
260
+ // maxWidth={maxWidth}
261
+ onResizeStart: onResizeStart, onResizeEnd: onResizeEnd, captionsEnabled: captionsEnabled }), _jsx(ImageEnhancer, { deviceFrame: frame, sizeMode: width === 'inherit' ? sizeMode || 'best-fit' : undefined, onSizeModeChange: setSizeMode, onFrameChange: setFrame })] }))] }) }));
233
262
  }
234
263
  const ImageContainer = ({ success, loading, error, confirmText, uploadImage, placeholder, placeholderProps, ...props }) => {
235
264
  const [showMask, setShowMask] = useState(false);
@@ -8,6 +8,8 @@
8
8
  /// <reference types="react" />
9
9
  import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, NodeKey, SerializedEditor, SerializedLexicalNode, Spread } from 'lexical';
10
10
  import { DecoratorNode } from 'lexical';
11
+ import type { DeviceFrame } from '../utils/device-frame';
12
+ import type { ImageSizeMode } from '../../types';
11
13
  export interface ImagePayload {
12
14
  file?: File;
13
15
  altText: string;
@@ -19,6 +21,9 @@ export interface ImagePayload {
19
21
  src?: string;
20
22
  width?: number;
21
23
  captionsEnabled?: boolean;
24
+ markerState?: string;
25
+ frame?: DeviceFrame;
26
+ sizeMode?: ImageSizeMode;
22
27
  }
23
28
  export type SerializedImageNode = Spread<{
24
29
  altText: string;
@@ -28,6 +33,9 @@ export type SerializedImageNode = Spread<{
28
33
  showCaption: boolean;
29
34
  src?: string;
30
35
  width?: number;
36
+ markerState?: string;
37
+ frame?: DeviceFrame;
38
+ sizeMode?: ImageSizeMode;
31
39
  type: 'image';
32
40
  version: 1;
33
41
  }, SerializedLexicalNode>;
@@ -40,16 +48,22 @@ export declare class ImageNode extends DecoratorNode<JSX.Element> {
40
48
  __maxWidth: number;
41
49
  __showCaption: boolean;
42
50
  __caption: LexicalEditor;
51
+ __markerState?: string;
52
+ __frame?: DeviceFrame;
53
+ __sizeMode?: ImageSizeMode;
43
54
  __captionsEnabled: boolean;
44
55
  static getType(): string;
45
56
  static clone(node: ImageNode): ImageNode;
46
57
  static importJSON(serializedNode: SerializedImageNode): ImageNode;
47
58
  exportDOM(): DOMExportOutput;
48
59
  static importDOM(): DOMConversionMap | null;
49
- constructor(src: string | undefined, altText: string, maxWidth: number, width?: 'inherit' | number, height?: 'inherit' | number, showCaption?: boolean, caption?: LexicalEditor, captionsEnabled?: boolean, key?: NodeKey, file?: File);
60
+ constructor(src: string | undefined, altText: string, maxWidth: number, width?: 'inherit' | number, height?: 'inherit' | number, showCaption?: boolean, caption?: LexicalEditor, captionsEnabled?: boolean, key?: NodeKey, file?: File, markerState?: string, frame?: DeviceFrame, sizeMode?: ImageSizeMode);
50
61
  exportJSON(): SerializedImageNode;
51
62
  setWidthAndHeight(width: 'inherit' | number, height: 'inherit' | number): void;
52
63
  setShowCaption(showCaption: boolean): void;
64
+ setMarkerState(markerState: string): void;
65
+ setFrame(frame?: DeviceFrame): void;
66
+ setSizeMode(sizeMode?: ImageSizeMode): void;
53
67
  createDOM(config: EditorConfig): HTMLElement;
54
68
  updateDOM(): false;
55
69
  getSrc(): string | undefined;
@@ -57,5 +71,5 @@ export declare class ImageNode extends DecoratorNode<JSX.Element> {
57
71
  getAltText(): string;
58
72
  decorate(): JSX.Element;
59
73
  }
60
- export declare function $createImageNode({ altText, height, maxWidth, captionsEnabled, src, width, showCaption, caption, key, file, }: ImagePayload): ImageNode;
74
+ export declare function $createImageNode({ altText, height, maxWidth, captionsEnabled, src, width, showCaption, caption, key, file, markerState, frame, sizeMode, }: ImagePayload): ImageNode;
61
75
  export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
@@ -19,16 +19,19 @@ export class ImageNode extends DecoratorNode {
19
19
  __maxWidth;
20
20
  __showCaption;
21
21
  __caption;
22
+ __markerState;
23
+ __frame;
24
+ __sizeMode;
22
25
  // Captions cannot yet be used within editor cells
23
26
  __captionsEnabled;
24
27
  static getType() {
25
28
  return 'image';
26
29
  }
27
30
  static clone(node) {
28
- return new ImageNode(node.__src, node.__altText, node.__maxWidth, node.__width, node.__height, node.__showCaption, node.__caption, node.__captionsEnabled, node.__key, node.file);
31
+ return new ImageNode(node.__src, node.__altText, node.__maxWidth, node.__width, node.__height, node.__showCaption, node.__caption, node.__captionsEnabled, node.__key, node.file, node.__markerState, node.__frame, node.__sizeMode);
29
32
  }
30
33
  static importJSON(serializedNode) {
31
- const { altText, height, width, maxWidth, caption, src, showCaption } = serializedNode;
34
+ const { altText, height, width, maxWidth, caption, src, showCaption, markerState, frame, sizeMode } = serializedNode;
32
35
  const node = $createImageNode({
33
36
  altText,
34
37
  height,
@@ -36,6 +39,9 @@ export class ImageNode extends DecoratorNode {
36
39
  showCaption,
37
40
  src,
38
41
  width,
42
+ markerState,
43
+ frame,
44
+ sizeMode,
39
45
  });
40
46
  const nestedEditor = node.__caption;
41
47
  const editorState = nestedEditor.parseEditorState(caption.editorState);
@@ -58,7 +64,7 @@ export class ImageNode extends DecoratorNode {
58
64
  }),
59
65
  };
60
66
  }
61
- constructor(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file) {
67
+ constructor(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file, markerState, frame, sizeMode) {
62
68
  super(key);
63
69
  this.__src = src;
64
70
  this.__altText = altText;
@@ -69,6 +75,9 @@ export class ImageNode extends DecoratorNode {
69
75
  this.__caption = caption || createEditor();
70
76
  this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined;
71
77
  this.file = file;
78
+ this.__markerState = markerState;
79
+ this.__frame = frame;
80
+ this.__sizeMode = sizeMode;
72
81
  }
73
82
  exportJSON() {
74
83
  return {
@@ -81,6 +90,9 @@ export class ImageNode extends DecoratorNode {
81
90
  type: 'image',
82
91
  version: 1,
83
92
  width: this.__width === 'inherit' ? 0 : this.__width,
93
+ markerState: this.__markerState,
94
+ frame: this.__frame,
95
+ sizeMode: this.__sizeMode,
84
96
  };
85
97
  }
86
98
  setWidthAndHeight(width, height) {
@@ -92,6 +104,18 @@ export class ImageNode extends DecoratorNode {
92
104
  const writable = this.getWritable();
93
105
  writable.__showCaption = showCaption;
94
106
  }
107
+ setMarkerState(markerState) {
108
+ const writable = this.getWritable();
109
+ writable.__markerState = markerState;
110
+ }
111
+ setFrame(frame) {
112
+ const writable = this.getWritable();
113
+ writable.__frame = frame;
114
+ }
115
+ setSizeMode(sizeMode) {
116
+ const writable = this.getWritable();
117
+ writable.__sizeMode = sizeMode;
118
+ }
95
119
  // View
96
120
  createDOM(config) {
97
121
  const span = document.createElement('span');
@@ -116,11 +140,11 @@ export class ImageNode extends DecoratorNode {
116
140
  return this.__altText;
117
141
  }
118
142
  decorate() {
119
- return (_jsx(Suspense, { fallback: null, children: _jsx(ImageComponent, { src: this.__src, file: this.file, altText: this.__altText, width: this.__width, height: this.__height, maxWidth: this.__maxWidth, nodeKey: this.getKey(), showCaption: this.__showCaption, caption: this.__caption, captionsEnabled: this.__captionsEnabled, resizable: true }) }));
143
+ return (_jsx(Suspense, { fallback: null, children: _jsx(ImageComponent, { src: this.__src, file: this.file, altText: this.__altText, width: this.__width, height: this.__height, maxWidth: this.__maxWidth, nodeKey: this.getKey(), showCaption: this.__showCaption, caption: this.__caption, captionsEnabled: this.__captionsEnabled, markerState: this.__markerState, frame: this.__frame, sizeMode: this.__sizeMode, resizable: true }) }));
120
144
  }
121
145
  }
122
- export function $createImageNode({ altText, height, maxWidth = 500, captionsEnabled, src, width, showCaption, caption, key, file, }) {
123
- return new ImageNode(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file);
146
+ export function $createImageNode({ altText, height, maxWidth = 500, captionsEnabled, src, width, showCaption, caption, key, file, markerState, frame, sizeMode, }) {
147
+ return new ImageNode(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key, file, markerState, frame, sizeMode);
124
148
  }
125
149
  export function $isImageNode(node) {
126
150
  return node instanceof ImageNode;
@@ -0,0 +1,27 @@
1
+ import { type BoxProps } from '@mui/material';
2
+ import { ImageSizeMode } from '../../types';
3
+ import { type DeviceFrame } from '../utils/device-frame';
4
+ interface ImageAnnotationProps {
5
+ imageRef: {
6
+ current: null | HTMLImageElement;
7
+ };
8
+ markerState?: string;
9
+ onChange: (markerState: string) => void;
10
+ }
11
+ export declare function ImageAnnotation({ imageRef, markerState, onChange }: ImageAnnotationProps): import("react/jsx-runtime").JSX.Element;
12
+ interface ImageAnnotationViewProps {
13
+ imageRef: {
14
+ current: null | HTMLImageElement;
15
+ };
16
+ markerState?: string;
17
+ sx?: BoxProps['sx'];
18
+ }
19
+ export declare function ImageAnnotationView({ imageRef, markerState, sx, ...rest }: ImageAnnotationViewProps & BoxProps): import("react/jsx-runtime").JSX.Element;
20
+ interface ImageEnhancerProps {
21
+ sizeMode?: ImageSizeMode;
22
+ deviceFrame?: DeviceFrame;
23
+ onSizeModeChange: (sizeMode: ImageSizeMode) => void;
24
+ onFrameChange: (deviceFrame?: DeviceFrame) => void;
25
+ }
26
+ export declare function ImageEnhancer({ sizeMode, onSizeModeChange }: ImageEnhancerProps): import("react/jsx-runtime").JSX.Element;
27
+ export {};
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, IconButton, ToggleButtonGroup, ToggleButton } from '@mui/material';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import * as markerjs2 from 'markerjs2';
5
+ import * as mjslive from 'markerjs-live';
6
+ const parseMarkerState = (markerState) => {
7
+ if (!markerState)
8
+ return null;
9
+ try {
10
+ return JSON.parse(markerState);
11
+ }
12
+ catch (e) {
13
+ console.error(e);
14
+ return null;
15
+ }
16
+ };
17
+ export function ImageAnnotation({ imageRef, markerState, onChange }) {
18
+ const [editing, setEditing] = useState(false);
19
+ useEffect(() => {
20
+ if (editing && imageRef.current) {
21
+ const markerArea = new markerjs2.MarkerArea(imageRef.current);
22
+ markerArea.addEventListener('render', (event) => {
23
+ if (imageRef.current) {
24
+ onChange(JSON.stringify(event.state));
25
+ }
26
+ });
27
+ markerArea.addEventListener('close', () => setEditing(false));
28
+ markerArea.settings.displayMode = 'popup';
29
+ markerArea.uiStyleSettings.zIndex = '99999999';
30
+ markerArea.show();
31
+ const parsed = parseMarkerState(markerState);
32
+ if (parsed) {
33
+ markerArea.restoreState(parsed);
34
+ }
35
+ return () => {
36
+ markerArea.close();
37
+ };
38
+ }
39
+ }, [editing]);
40
+ return (_jsx(Box, { sx: { position: 'absolute', bottom: 16, right: 16 }, children: _jsx(IconButton, { onClick: () => setEditing(!editing), color: "primary", sx: { borderRadius: 0.5, ':hover': { bgcolor: 'grey.200' } }, children: _jsx("i", { className: "iconify", "data-icon": "tabler:photo-edit" }) }) }));
41
+ }
42
+ export function ImageAnnotationView({ imageRef, markerState, sx, ...rest }) {
43
+ const viewRef = useRef(null);
44
+ const mergedSx = [
45
+ {
46
+ position: 'absolute',
47
+ left: 0,
48
+ right: 0,
49
+ top: 0,
50
+ bottom: 0,
51
+ zIndex: 0,
52
+ pointerEvents: 'none',
53
+ // fix: 点击 markerView 时可以正常触发原图片的点击, 进而进入 resize 模式
54
+ svg: { pointerEvents: 'none!important' },
55
+ },
56
+ ...(Array.isArray(sx) ? sx : [sx]),
57
+ ];
58
+ useEffect(() => {
59
+ const parsed = parseMarkerState(markerState);
60
+ if (parsed && imageRef.current && viewRef.current) {
61
+ const markerView = new mjslive.MarkerView(imageRef.current);
62
+ // fix error - "Failed to execute 'setRotate' on 'SVGTransform'"
63
+ setTimeout(() => {
64
+ markerView.targetRoot = viewRef.current;
65
+ markerView.show(parsed);
66
+ }, 10);
67
+ return () => {
68
+ markerView.close();
69
+ };
70
+ }
71
+ }, [markerState, imageRef]);
72
+ return _jsx(Box, { sx: mergedSx, ref: viewRef, ...rest });
73
+ }
74
+ export function ImageEnhancer({ sizeMode, onSizeModeChange }) {
75
+ return (_jsx(Box, { sx: { position: 'relative', zIndex: 'tooltip', whiteSpace: 'nowrap' }, children: _jsx(Box, { sx: { position: 'absolute', bgcolor: '#fff' }, children: _jsxs(ToggleButtonGroup, { color: "standard", value: sizeMode, size: "small", exclusive: true, onChange: (_, v) => onSizeModeChange(v), "aria-label": "Platform", children: [_jsx(ToggleButton, { value: "small", children: "Small" }), _jsx(ToggleButton, { value: "best-fit", children: "Best fit" }), _jsx(ToggleButton, { value: "original", children: "Original size" })] }) }) }));
76
+ }
@@ -0,0 +1,2 @@
1
+ export declare const deviceFrames: readonly ["iphone-x", "iphone-8", "ipad-pro", "imac-pro", "macbook", "macbook-pro", "surface-pro", "surface-book", "surface-studio", "galaxy-s8", "google-pixel", "google-pixel-2-xl", "apple-watch"];
2
+ export type DeviceFrame = typeof deviceFrames[number];
@@ -0,0 +1,15 @@
1
+ export const deviceFrames = [
2
+ 'iphone-x',
3
+ 'iphone-8',
4
+ 'ipad-pro',
5
+ 'imac-pro',
6
+ 'macbook',
7
+ 'macbook-pro',
8
+ 'surface-pro',
9
+ 'surface-book',
10
+ 'surface-studio',
11
+ 'galaxy-s8',
12
+ 'google-pixel',
13
+ 'google-pixel-2-xl',
14
+ 'apple-watch',
15
+ ];
@@ -0,0 +1,9 @@
1
+ export declare const scaleImage: ({ width, height, maxSize }: {
2
+ width: number;
3
+ height: number;
4
+ maxSize: number;
5
+ }) => {
6
+ width: number;
7
+ height: number;
8
+ scaleFactor: number;
9
+ };
@@ -0,0 +1,13 @@
1
+ export const scaleImage = ({ width, height, maxSize }) => {
2
+ const longAxis = Math.max(width, height);
3
+ const scaleFactor = maxSize / longAxis;
4
+ let newWidth = maxSize;
5
+ let newHeight = maxSize;
6
+ if (width !== longAxis) {
7
+ newWidth = Math.floor(scaleFactor * width);
8
+ }
9
+ if (height !== longAxis) {
10
+ newHeight = Math.floor(scaleFactor * height);
11
+ }
12
+ return { width: newWidth, height: newHeight, scaleFactor };
13
+ };
package/lib/types.d.ts CHANGED
@@ -16,3 +16,4 @@ export interface AI {
16
16
  checkAvailable: () => Promise<boolean> | boolean;
17
17
  imageGenerations?: any;
18
18
  }
19
+ export type ImageSizeMode = 'small' | 'best-fit' | 'original';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/editor",
3
- "version": "1.6.247",
3
+ "version": "1.6.248",
4
4
  "main": "lib/index.js",
5
5
  "scripts": {
6
6
  "dev": "npm run storybook",
@@ -40,7 +40,7 @@
40
40
  "@arcblock/ux": "^2.9.77",
41
41
  "@blocklet/embed": "^0.1.11",
42
42
  "@blocklet/pages-kit": "^0.2.302",
43
- "@blocklet/pdf": "1.6.247",
43
+ "@blocklet/pdf": "1.6.248",
44
44
  "@excalidraw/excalidraw": "^0.14.2",
45
45
  "@iconify/iconify": "^3.0.1",
46
46
  "@iconify/icons-tabler": "^1.2.95",
@@ -70,6 +70,9 @@
70
70
  "lexical": "0.13.1",
71
71
  "lodash": "^4.17.21",
72
72
  "lottie-react": "^2.4.0",
73
+ "markerjs-live": "^1.2.1",
74
+ "markerjs2": "^2.32.1",
75
+ "medium-zoom": "^1.1.0",
73
76
  "path-parser": "^6.1.0",
74
77
  "react-player": "^2.14.1",
75
78
  "react-popper": "^2.3.0",
@@ -110,5 +113,5 @@
110
113
  "react": "*",
111
114
  "react-dom": "*"
112
115
  },
113
- "gitHead": "792d8513fb2038b0cca372f8a921624e0906cd70"
116
+ "gitHead": "3c8c1ef53a2022d046862c57ba938b252d264525"
114
117
  }