@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.
- package/lib/main/hooks/hooks.d.ts +15 -0
- package/lib/main/hooks/hooks.js +43 -0
- package/lib/main/hooks/medium-zoom.d.ts +5 -0
- package/lib/main/hooks/medium-zoom.js +30 -0
- package/lib/main/index.css +6 -0
- package/lib/main/nodes/ImageComponent.d.ts +6 -1
- package/lib/main/nodes/ImageComponent.js +52 -23
- package/lib/main/nodes/ImageNode.d.ts +16 -2
- package/lib/main/nodes/ImageNode.js +30 -6
- package/lib/main/ui/ImageEnhancer.d.ts +27 -0
- package/lib/main/ui/ImageEnhancer.js +76 -0
- package/lib/main/utils/device-frame.d.ts +2 -0
- package/lib/main/utils/device-frame.js +15 -0
- package/lib/main/utils/images.d.ts +9 -0
- package/lib/main/utils/images.js +13 -0
- package/lib/types.d.ts +1 -0
- package/package.json +6 -3
|
@@ -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,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
|
+
};
|
package/lib/main/index.css
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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,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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "1.6.
|
|
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.
|
|
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": "
|
|
116
|
+
"gitHead": "3c8c1ef53a2022d046862c57ba938b252d264525"
|
|
114
117
|
}
|