@inshapardaz/likhari-react 0.1.6 → 0.1.7-dev.75
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/dist/EditorRoot.d.ts +43 -1
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +77 -6
- package/dist/PortalTargetContext.d.ts +16 -0
- package/dist/PortalTargetContext.d.ts.map +1 -0
- package/dist/PortalTargetContext.js +18 -0
- package/dist/components/LinkDialog.d.ts.map +1 -1
- package/dist/components/LinkDialog.js +7 -3
- package/dist/components/Toolbar.d.ts +4 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +246 -33
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/strings.d.ts +160 -0
- package/dist/i18n/strings.d.ts.map +1 -0
- package/dist/i18n/strings.js +428 -0
- package/dist/i18n/useStrings.d.ts +10 -0
- package/dist/i18n/useStrings.d.ts.map +1 -0
- package/dist/i18n/useStrings.js +14 -0
- package/dist/image/ImageComponent.d.ts +23 -0
- package/dist/image/ImageComponent.d.ts.map +1 -0
- package/dist/image/ImageComponent.js +171 -0
- package/dist/image/ImageCropDialog.d.ts +22 -0
- package/dist/image/ImageCropDialog.d.ts.map +1 -0
- package/dist/image/ImageCropDialog.js +120 -0
- package/dist/image/ImageCropper.d.ts +15 -0
- package/dist/image/ImageCropper.d.ts.map +1 -0
- package/dist/image/ImageCropper.js +120 -0
- package/dist/image/ImageDialog.d.ts +37 -0
- package/dist/image/ImageDialog.d.ts.map +1 -0
- package/dist/image/ImageDialog.js +176 -0
- package/dist/image/ImageNode.d.ts +64 -0
- package/dist/image/ImageNode.d.ts.map +1 -0
- package/dist/image/ImageNode.js +133 -0
- package/dist/image/ImageOptionsContext.d.ts +24 -0
- package/dist/image/ImageOptionsContext.d.ts.map +1 -0
- package/dist/image/ImageOptionsContext.js +10 -0
- package/dist/image/cropMath.d.ts +35 -0
- package/dist/image/cropMath.d.ts.map +1 -0
- package/dist/image/cropMath.js +92 -0
- package/dist/image/imageEdit.d.ts +49 -0
- package/dist/image/imageEdit.d.ts.map +1 -0
- package/dist/image/imageEdit.js +132 -0
- package/dist/image/imageUrl.d.ts +14 -0
- package/dist/image/imageUrl.d.ts.map +1 -0
- package/dist/image/imageUrl.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +2 -0
- package/dist/persistence/draftStorage.d.ts +26 -0
- package/dist/persistence/draftStorage.d.ts.map +1 -0
- package/dist/persistence/draftStorage.js +71 -0
- package/dist/plugins/AutosaveRestorePlugin.d.ts +16 -0
- package/dist/plugins/AutosaveRestorePlugin.d.ts.map +1 -0
- package/dist/plugins/AutosaveRestorePlugin.js +33 -0
- package/dist/theme/EditorThemeProvider.d.ts +17 -1
- package/dist/theme/EditorThemeProvider.d.ts.map +1 -1
- package/dist/theme/EditorThemeProvider.js +24 -2
- package/dist/theme/accentColor.d.ts +43 -0
- package/dist/theme/accentColor.d.ts.map +1 -0
- package/dist/theme/accentColor.js +84 -0
- package/dist/theme/editor.css +125 -0
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ImageCropDialogValue {
|
|
2
|
+
src: string;
|
|
3
|
+
width: number | null;
|
|
4
|
+
height: number | null;
|
|
5
|
+
}
|
|
6
|
+
export interface ImageCropDialogProps {
|
|
7
|
+
opened: boolean;
|
|
8
|
+
/** The embedded image being edited — only images with pixels of their own
|
|
9
|
+
* (not linked ones) can be cropped, rotated or resized here. */
|
|
10
|
+
initial: {
|
|
11
|
+
src: string;
|
|
12
|
+
width: number | null;
|
|
13
|
+
height: number | null;
|
|
14
|
+
};
|
|
15
|
+
onSubmit: (value: ImageCropDialogValue) => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Crop, rotate, flip and resize dialog for an embedded image, reached from
|
|
19
|
+
* its context menu. Edits that change pixels are re-embedded as a data URI,
|
|
20
|
+
* or stored through the host's `onImageUpload` when there is one. */
|
|
21
|
+
export declare function ImageCropDialog({ opened, initial, onSubmit, onClose }: ImageCropDialogProps): import("react").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=ImageCropDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageCropDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropDialog.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAqBD;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,oBAAoB,+BAqK3F"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Checkbox, Divider, Group, Modal, NumberInput, Stack, Text } from '@mantine/core';
|
|
4
|
+
import { normalizeImageUrl } from './imageUrl.js';
|
|
5
|
+
import { dataUrlBytes, dataUrlToFile, fitDimension, mimeFromSrc } from './imageEdit.js';
|
|
6
|
+
import { ImageCropper } from './ImageCropper.js';
|
|
7
|
+
import { useImageOptions } from './ImageOptionsContext.js';
|
|
8
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
9
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
10
|
+
/** Natural pixel size of an image URL (for the aspect ratio and size presets). */
|
|
11
|
+
function useNaturalSize(src) {
|
|
12
|
+
const [size, setSize] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setSize(null);
|
|
15
|
+
if (!src)
|
|
16
|
+
return;
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
const img = new Image();
|
|
19
|
+
img.onload = () => {
|
|
20
|
+
if (!cancelled && img.naturalWidth)
|
|
21
|
+
setSize({ width: img.naturalWidth, height: img.naturalHeight });
|
|
22
|
+
};
|
|
23
|
+
img.src = src;
|
|
24
|
+
return () => {
|
|
25
|
+
cancelled = true;
|
|
26
|
+
};
|
|
27
|
+
}, [src]);
|
|
28
|
+
return size;
|
|
29
|
+
}
|
|
30
|
+
/** Crop, rotate, flip and resize dialog for an embedded image, reached from
|
|
31
|
+
* its context menu. Edits that change pixels are re-embedded as a data URI,
|
|
32
|
+
* or stored through the host's `onImageUpload` when there is one. */
|
|
33
|
+
export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
|
|
34
|
+
const strings = useUiStrings();
|
|
35
|
+
const portalTarget = usePortalTarget();
|
|
36
|
+
const { maxSizeMB, onImageUpload } = useImageOptions();
|
|
37
|
+
const [pixelSrc, setPixelSrc] = useState(null);
|
|
38
|
+
const [width, setWidth] = useState(null);
|
|
39
|
+
const [height, setHeight] = useState(null);
|
|
40
|
+
const [lockAspect, setLockAspect] = useState(true);
|
|
41
|
+
const [error, setError] = useState(null);
|
|
42
|
+
const [busy, setBusy] = useState(false);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!opened)
|
|
45
|
+
return;
|
|
46
|
+
setPixelSrc(null);
|
|
47
|
+
setWidth(initial.width);
|
|
48
|
+
setHeight(initial.height);
|
|
49
|
+
setLockAspect(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
setBusy(false);
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [opened]);
|
|
54
|
+
const effectiveSrc = pixelSrc ?? initial.src;
|
|
55
|
+
const natural = useNaturalSize(effectiveSrc);
|
|
56
|
+
const resetSize = () => {
|
|
57
|
+
setWidth(null);
|
|
58
|
+
setHeight(null);
|
|
59
|
+
};
|
|
60
|
+
const onSizeChange = (changed, value) => {
|
|
61
|
+
const n = typeof value === 'number' ? value : null;
|
|
62
|
+
if (n === null) {
|
|
63
|
+
changed === 'width' ? setWidth(null) : setHeight(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (lockAspect && natural) {
|
|
67
|
+
const fitted = fitDimension(changed, n, natural.width, natural.height);
|
|
68
|
+
setWidth(fitted.width);
|
|
69
|
+
setHeight(fitted.height);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
changed === 'width' ? setWidth(n) : setHeight(n);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handleSubmit = async (e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setError(null);
|
|
78
|
+
try {
|
|
79
|
+
let src = effectiveSrc;
|
|
80
|
+
if (pixelSrc) {
|
|
81
|
+
if (dataUrlBytes(pixelSrc) > maxSizeMB * 1024 * 1024) {
|
|
82
|
+
setError(strings.imageCropDialog.errors.largerThan(maxSizeMB));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (onImageUpload) {
|
|
86
|
+
setBusy(true);
|
|
87
|
+
const ext = mimeFromSrc(pixelSrc).split('/')[1];
|
|
88
|
+
src = await onImageUpload(dataUrlToFile(pixelSrc, `image.${ext}`));
|
|
89
|
+
if (!normalizeImageUrl(src)) {
|
|
90
|
+
setError(strings.imageCropDialog.errors.uploadUnusable);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
onSubmit({ src, width, height });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// ImageCropper's own pixel-edit error message (imageEdit.ts,
|
|
99
|
+
// applyPixelOp) is shown as-is when present — see the note in
|
|
100
|
+
// ImageDialog.tsx for why that pure utility's messages stay English.
|
|
101
|
+
setError(err instanceof Error ? err.message : strings.imageCropDialog.errors.couldNotSave);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
setBusy(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: strings.imageCropDialog.title, centered: true, size: "lg", portalProps: { target: portalTarget }, children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "md", children: [_jsx(ImageCropper, { src: effectiveSrc, canReset: pixelSrc !== null, onApply: (edited) => {
|
|
108
|
+
setPixelSrc(edited);
|
|
109
|
+
resetSize();
|
|
110
|
+
}, onReset: () => {
|
|
111
|
+
setPixelSrc(null);
|
|
112
|
+
setWidth(initial.width);
|
|
113
|
+
setHeight(initial.height);
|
|
114
|
+
} }), _jsx(Divider, { label: strings.imageCropDialog.sizeLabel, labelPosition: "left" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { grow: true, align: "flex-end", children: [_jsx(NumberInput, { label: strings.imageCropDialog.widthLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.width) : strings.imageCropDialog.autoPlaceholder, value: width ?? '', onChange: (v) => onSizeChange('width', v) }), _jsx(NumberInput, { label: strings.imageCropDialog.heightLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.height) : strings.imageCropDialog.autoPlaceholder, value: height ?? '', onChange: (v) => onSizeChange('height', v) })] }), _jsx(Checkbox, { label: strings.imageCropDialog.keepProportions, checked: lockAspect, onChange: (e) => setLockAspect(e.currentTarget.checked) }), _jsxs(Group, { gap: "xs", children: [[25, 50, 75, 100].map((pct) => (_jsxs(Button, { size: "xs", variant: "default", disabled: !natural, onClick: () => {
|
|
115
|
+
if (!natural)
|
|
116
|
+
return;
|
|
117
|
+
setWidth(Math.round((natural.width * pct) / 100));
|
|
118
|
+
setHeight(Math.round((natural.height * pct) / 100));
|
|
119
|
+
}, children: [pct, "%"] }, pct))), _jsx(Button, { size: "xs", variant: "subtle", onClick: resetSize, children: strings.imageCropDialog.originalSize })] }), _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageCropDialog.dragHint(natural) })] }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: strings.common.save })] })] }) }) }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ImageCropperProps {
|
|
2
|
+
/** The image as it is now (original, or already edited). */
|
|
3
|
+
src: string;
|
|
4
|
+
/** Called with the edited image as a data URI. */
|
|
5
|
+
onApply: (edited: string) => void;
|
|
6
|
+
/** Whether there are edits to undo. */
|
|
7
|
+
canReset: boolean;
|
|
8
|
+
onReset: () => void;
|
|
9
|
+
}
|
|
10
|
+
/** Rotate, flip and crop. Drag on the preview to choose the area to keep, then
|
|
11
|
+
* drag its handles to resize it or drag inside it to move it, then "Apply crop".
|
|
12
|
+
* Each operation re-renders the pixels, so it needs an image the canvas may read
|
|
13
|
+
* (an embedded image). */
|
|
14
|
+
export declare function ImageCropper({ src, onApply, canReset, onReset, }: ImageCropperProps): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=ImageCropper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageCropper.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropper.tsx"],"names":[],"mappings":"AAgDA,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,uCAAuC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;0BAG0B;AAC1B,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,OAAO,EACP,QAAQ,EACR,OAAO,GACR,EAAE,iBAAiB,+BA0OnB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, } from "react";
|
|
3
|
+
import { ActionIcon, Alert, Button, Group, SegmentedControl, Stack, Text, Tooltip, } from "@mantine/core";
|
|
4
|
+
import { IconCrop, IconFlipHorizontal, IconFlipVertical, IconRestore, IconRotate2, IconRotateClockwise2, } from "@tabler/icons-react";
|
|
5
|
+
import { applyPixelOp } from "./imageEdit.js";
|
|
6
|
+
import { MIN_CROP, moveRect, rectFromDrag, resizeRect, } from "./cropMath.js";
|
|
7
|
+
import { usePortalTarget } from "../PortalTargetContext.js";
|
|
8
|
+
const ASPECTS = {
|
|
9
|
+
free: null,
|
|
10
|
+
"1:1": 1,
|
|
11
|
+
"4:3": 4 / 3,
|
|
12
|
+
"16:9": 16 / 9,
|
|
13
|
+
};
|
|
14
|
+
const HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
15
|
+
const clamp = (v) => Math.min(Math.max(v, 0), 1);
|
|
16
|
+
/** Rotate, flip and crop. Drag on the preview to choose the area to keep, then
|
|
17
|
+
* drag its handles to resize it or drag inside it to move it, then "Apply crop".
|
|
18
|
+
* Each operation re-renders the pixels, so it needs an image the canvas may read
|
|
19
|
+
* (an embedded image). */
|
|
20
|
+
export function ImageCropper({ src, onApply, canReset, onReset, }) {
|
|
21
|
+
const portalTarget = usePortalTarget();
|
|
22
|
+
const imgRef = useRef(null);
|
|
23
|
+
const drag = useRef(null);
|
|
24
|
+
const [rect, setRect] = useState(null);
|
|
25
|
+
const [aspect, setAspect] = useState("free");
|
|
26
|
+
const [natural, setNatural] = useState(null);
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const run = async (op) => {
|
|
30
|
+
setBusy(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
try {
|
|
33
|
+
onApply(await applyPixelOp(src, op));
|
|
34
|
+
setRect(null);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err.message : "Could not edit the image");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
setBusy(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const pointer = (event) => {
|
|
44
|
+
const box = imgRef.current?.getBoundingClientRect();
|
|
45
|
+
if (!box || !box.width || !box.height)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
point: {
|
|
49
|
+
x: clamp((event.clientX - box.left) / box.width),
|
|
50
|
+
y: clamp((event.clientY - box.top) / box.height),
|
|
51
|
+
},
|
|
52
|
+
box: { width: box.width, height: box.height },
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const onDown = (event) => {
|
|
56
|
+
const p = pointer(event);
|
|
57
|
+
if (!p)
|
|
58
|
+
return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
61
|
+
const target = event.target;
|
|
62
|
+
const handle = target.dataset.handle;
|
|
63
|
+
if (handle && rect) {
|
|
64
|
+
drag.current = { mode: "resize", handle, origin: rect };
|
|
65
|
+
}
|
|
66
|
+
else if (target.dataset.move && rect) {
|
|
67
|
+
drag.current = { mode: "move", start: p.point, origin: rect };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
drag.current = { mode: "new", anchor: p.point };
|
|
71
|
+
setRect({ x: p.point.x, y: p.point.y, w: 0, h: 0 });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const onMove = (event) => {
|
|
75
|
+
const current = drag.current;
|
|
76
|
+
const p = pointer(event);
|
|
77
|
+
if (!current || !p)
|
|
78
|
+
return;
|
|
79
|
+
const ratio = ASPECTS[aspect];
|
|
80
|
+
if (current.mode === "new")
|
|
81
|
+
setRect(rectFromDrag(current.anchor, p.point, ratio, p.box));
|
|
82
|
+
else if (current.mode === "move")
|
|
83
|
+
setRect(moveRect(current.origin, p.point.x - current.start.x, p.point.y - current.start.y));
|
|
84
|
+
else
|
|
85
|
+
setRect(resizeRect(current.origin, current.handle, p.point, ratio, p.box));
|
|
86
|
+
};
|
|
87
|
+
const onUp = () => {
|
|
88
|
+
const wasNew = drag.current?.mode === "new";
|
|
89
|
+
drag.current = null;
|
|
90
|
+
// A click without a drag isn't a selection.
|
|
91
|
+
if (wasNew)
|
|
92
|
+
setRect((r) => (r && (r.w < MIN_CROP || r.h < MIN_CROP) ? null : r));
|
|
93
|
+
};
|
|
94
|
+
const selectionSize = rect && natural && rect.w > 0 && rect.h > 0
|
|
95
|
+
? `${Math.max(1, Math.round(rect.w * natural.width))} × ${Math.max(1, Math.round(rect.h * natural.height))} px`
|
|
96
|
+
: null;
|
|
97
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { gap: "xs", children: [_jsx(Tooltip, { label: "Rotate left", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate left", disabled: busy, onClick: () => run({ type: "rotate", direction: "ccw" }), children: _jsx(IconRotate2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Rotate right", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate right", disabled: busy, onClick: () => run({ type: "rotate", direction: "cw" }), children: _jsx(IconRotateClockwise2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip horizontally", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip horizontally", disabled: busy, onClick: () => run({ type: "flip", axis: "horizontal" }), children: _jsx(IconFlipHorizontal, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip vertically", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip vertically", disabled: busy, onClick: () => run({ type: "flip", axis: "vertical" }), children: _jsx(IconFlipVertical, { size: 16 }) }) }), _jsx(Button, { ml: "auto", size: "xs", variant: "subtle", leftSection: _jsx(IconRestore, { size: 14 }), disabled: !canReset || busy, onClick: () => {
|
|
98
|
+
setRect(null);
|
|
99
|
+
setError(null);
|
|
100
|
+
onReset();
|
|
101
|
+
}, children: "Undo edits" })] }), _jsxs(Group, { gap: "xs", align: "center", children: [_jsx(Text, { size: "sm", children: "Crop shape" }), _jsx(SegmentedControl, { size: "xs", value: aspect, onChange: (v) => {
|
|
102
|
+
setAspect(v);
|
|
103
|
+
setRect(null);
|
|
104
|
+
}, data: [
|
|
105
|
+
{ value: "free", label: "Free" },
|
|
106
|
+
{ value: "1:1", label: "1:1" },
|
|
107
|
+
{ value: "4:3", label: "4:3" },
|
|
108
|
+
{ value: "16:9", label: "16:9" },
|
|
109
|
+
] })] }), _jsx("div", { className: "likhari-cropper-stage", onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp, children: _jsxs("div", { className: "likhari-cropper-frame", children: [_jsx("img", { ref: imgRef, src: src, alt: "", draggable: false, className: "likhari-cropper-image", onLoad: (e) => setNatural({
|
|
110
|
+
width: e.currentTarget.naturalWidth,
|
|
111
|
+
height: e.currentTarget.naturalHeight,
|
|
112
|
+
}) }), rect && rect.w > 0 && rect.h > 0 && (_jsx("div", { className: "likhari-cropper-selection", "data-move": "true", style: {
|
|
113
|
+
left: `${rect.x * 100}%`,
|
|
114
|
+
top: `${rect.y * 100}%`,
|
|
115
|
+
width: `${rect.w * 100}%`,
|
|
116
|
+
height: `${rect.h * 100}%`,
|
|
117
|
+
}, children: HANDLES.map((handle) => (_jsx("span", { className: "likhari-cropper-handle", "data-handle": handle }, handle))) }))] }) }), _jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", c: "dimmed", children: selectionSize
|
|
118
|
+
? `Selection: ${selectionSize}. Drag the handles to resize it, or drag inside to move it.`
|
|
119
|
+
: "Drag on the image to choose the area to keep." }), _jsx(Button, { size: "xs", leftSection: _jsx(IconCrop, { size: 14 }), disabled: !rect || busy, loading: busy, onClick: () => rect && run({ type: "crop", rect }), children: "Apply crop" })] }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error }))] }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ImageLinkType } from './ImageNode.js';
|
|
2
|
+
export interface ImageDialogValue {
|
|
3
|
+
src: string;
|
|
4
|
+
altText: string;
|
|
5
|
+
/** null = no caption */
|
|
6
|
+
caption: string | null;
|
|
7
|
+
linkType: ImageLinkType;
|
|
8
|
+
/** Whether the pixels changed (a new URL/upload, or a conversion) — the
|
|
9
|
+
* display size no longer matches, so the caller should reset it to auto. */
|
|
10
|
+
sourceChanged: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ImageDialogProps {
|
|
13
|
+
/** `insert`: pick an image. `edit`: also change its source, alt text and caption. */
|
|
14
|
+
mode: 'insert' | 'edit';
|
|
15
|
+
opened: boolean;
|
|
16
|
+
/** The image being edited (edit mode). */
|
|
17
|
+
initial?: {
|
|
18
|
+
src: string;
|
|
19
|
+
altText: string;
|
|
20
|
+
caption: string | null;
|
|
21
|
+
linkType: ImageLinkType;
|
|
22
|
+
};
|
|
23
|
+
/** What to do as the dialog opens: focus the caption field, or start converting
|
|
24
|
+
* a linked image to an embedded one. */
|
|
25
|
+
intent?: 'caption' | 'convert';
|
|
26
|
+
onSubmit: (image: ImageDialogValue) => void;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Insert-image dialog (UI spec §6) and, in edit mode, the image's details:
|
|
31
|
+
* replace the source (URL or upload), alt text, caption, and — for a linked
|
|
32
|
+
* image — convert it to an embedded one. Cropping, rotating and resizing an
|
|
33
|
+
* embedded image is a separate dialog (`ImageCropDialog`), reached from the
|
|
34
|
+
* image's context menu once it has pixels of its own to edit.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }: ImageDialogProps): import("react").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=ImageDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB;gFAC4E;IAC5E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5F;4CACwC;IACxC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAaD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAwQjG"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Checkbox, FileInput, Group, Modal, SegmentedControl, Stack, Text, TextInput } from '@mantine/core';
|
|
4
|
+
import { IconPhotoDown } from '@tabler/icons-react';
|
|
5
|
+
import { ACCEPTED_IMAGE_TYPES, normalizeImageUrl, validateImageFile } from './imageUrl.js';
|
|
6
|
+
import { dataUrlBytes, dataUrlToFile, fetchImageAsDataUrl, mimeFromSrc } from './imageEdit.js';
|
|
7
|
+
import { useImageOptions } from './ImageOptionsContext.js';
|
|
8
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
9
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
10
|
+
function readAsDataUrl(file) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const reader = new FileReader();
|
|
13
|
+
reader.onload = () => resolve(String(reader.result));
|
|
14
|
+
reader.onerror = () => reject(new Error('READ_FAILED'));
|
|
15
|
+
reader.readAsDataURL(file);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Insert-image dialog (UI spec §6) and, in edit mode, the image's details:
|
|
20
|
+
* replace the source (URL or upload), alt text, caption, and — for a linked
|
|
21
|
+
* image — convert it to an embedded one. Cropping, rotating and resizing an
|
|
22
|
+
* embedded image is a separate dialog (`ImageCropDialog`), reached from the
|
|
23
|
+
* image's context menu once it has pixels of its own to edit.
|
|
24
|
+
*/
|
|
25
|
+
export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }) {
|
|
26
|
+
const strings = useUiStrings();
|
|
27
|
+
const portalTarget = usePortalTarget();
|
|
28
|
+
const { allowLinked, allowEmbedded, allowCaption, maxSizeMB, onImageUpload, fetchImage } = useImageOptions();
|
|
29
|
+
const editing = mode === 'edit' && initial !== undefined;
|
|
30
|
+
const defaultSource = editing ? 'keep' : allowLinked ? 'linked' : 'embedded';
|
|
31
|
+
const [source, setSource] = useState(defaultSource);
|
|
32
|
+
const [url, setUrl] = useState('');
|
|
33
|
+
const [embedCopy, setEmbedCopy] = useState(false);
|
|
34
|
+
const [file, setFile] = useState(null);
|
|
35
|
+
const [fileSrc, setFileSrc] = useState(null);
|
|
36
|
+
const [altText, setAltText] = useState('');
|
|
37
|
+
const [caption, setCaption] = useState('');
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const [busy, setBusy] = useState(false);
|
|
40
|
+
const [converting, setConverting] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!opened)
|
|
43
|
+
return;
|
|
44
|
+
setSource(defaultSource);
|
|
45
|
+
setUrl('');
|
|
46
|
+
setEmbedCopy(false);
|
|
47
|
+
setFile(null);
|
|
48
|
+
setFileSrc(null);
|
|
49
|
+
setAltText(initial?.altText ?? '');
|
|
50
|
+
setCaption(initial?.caption ?? '');
|
|
51
|
+
setError(null);
|
|
52
|
+
setBusy(false);
|
|
53
|
+
setConverting(false);
|
|
54
|
+
if (intent === 'convert' && initial?.linkType === 'linked')
|
|
55
|
+
void convert(initial.src);
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [opened]);
|
|
58
|
+
const baseSrc = source === 'keep' ? (initial?.src ?? null) : source === 'linked' ? normalizeImageUrl(url) : fileSrc;
|
|
59
|
+
const effectiveLinkType = source === 'keep' ? (initial?.linkType ?? 'linked') : source === 'linked' ? 'linked' : 'embedded';
|
|
60
|
+
const convertedFromUrl = source === 'embedded' && fileSrc !== null && file === null;
|
|
61
|
+
const changeSource = (next) => {
|
|
62
|
+
setSource(next);
|
|
63
|
+
setFile(null);
|
|
64
|
+
setFileSrc(null);
|
|
65
|
+
setError(null);
|
|
66
|
+
};
|
|
67
|
+
const changeFile = async (next) => {
|
|
68
|
+
setFile(next);
|
|
69
|
+
setFileSrc(null);
|
|
70
|
+
setError(null);
|
|
71
|
+
if (!next)
|
|
72
|
+
return;
|
|
73
|
+
const problem = validateImageFile(next, maxSizeMB);
|
|
74
|
+
if (problem) {
|
|
75
|
+
setError(problem);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
setFileSrc(await readAsDataUrl(next));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
setError(strings.imageDialog.errors.couldNotReadFile);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** Downloads a linked image and switches the dialog to its embedded copy. */
|
|
86
|
+
const convert = async (src) => {
|
|
87
|
+
if (!src)
|
|
88
|
+
return;
|
|
89
|
+
setConverting(true);
|
|
90
|
+
setError(null);
|
|
91
|
+
try {
|
|
92
|
+
const dataUrl = await fetchImageAsDataUrl(src, fetchImage);
|
|
93
|
+
if (dataUrlBytes(dataUrl) > maxSizeMB * 1024 * 1024)
|
|
94
|
+
throw new Error(strings.imageDialog.errors.largerThan(maxSizeMB));
|
|
95
|
+
setSource('embedded');
|
|
96
|
+
setFile(null);
|
|
97
|
+
setFileSrc(dataUrl);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// fetchImageAsDataUrl's own errors (network/CORS failures, unsupported
|
|
101
|
+
// type) are English (packages/react/src/image/imageEdit.ts) — shown
|
|
102
|
+
// as-is since threading locale into that pure utility isn't worth the
|
|
103
|
+
// refactor here; only the size-limit message above is ours to localize.
|
|
104
|
+
setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotConvert);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setConverting(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const handleSubmit = async (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
setError(null);
|
|
113
|
+
if (!baseSrc) {
|
|
114
|
+
setError(source === 'linked'
|
|
115
|
+
? strings.imageDialog.errors.enterUrl
|
|
116
|
+
: source === 'embedded'
|
|
117
|
+
? strings.imageDialog.errors.chooseFile
|
|
118
|
+
: strings.imageDialog.errors.noImage);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
let src = baseSrc;
|
|
123
|
+
let linkType = effectiveLinkType;
|
|
124
|
+
// Pixels the document doesn't reference yet (an upload, or a downloaded
|
|
125
|
+
// copy of a linked image) are stored: through the host's handler, or
|
|
126
|
+
// embedded as a data URI.
|
|
127
|
+
let newPixels = source === 'embedded' ? fileSrc : null;
|
|
128
|
+
if (!newPixels && source === 'linked' && embedCopy) {
|
|
129
|
+
setBusy(true);
|
|
130
|
+
newPixels = await fetchImageAsDataUrl(baseSrc, fetchImage);
|
|
131
|
+
}
|
|
132
|
+
if (newPixels) {
|
|
133
|
+
if (dataUrlBytes(newPixels) > maxSizeMB * 1024 * 1024) {
|
|
134
|
+
setError(strings.imageDialog.errors.largerThan(maxSizeMB));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
linkType = 'embedded';
|
|
138
|
+
if (onImageUpload) {
|
|
139
|
+
setBusy(true);
|
|
140
|
+
const ext = mimeFromSrc(newPixels).split('/')[1];
|
|
141
|
+
const upload = !file ? dataUrlToFile(newPixels, `image.${ext}`) : file;
|
|
142
|
+
src = await onImageUpload(upload);
|
|
143
|
+
if (!normalizeImageUrl(src)) {
|
|
144
|
+
setError(strings.imageDialog.errors.uploadUnusable);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
src = newPixels;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
onSubmit({
|
|
153
|
+
src,
|
|
154
|
+
altText: altText.trim(),
|
|
155
|
+
caption: allowCaption ? caption.trim() || null : (initial?.caption ?? null),
|
|
156
|
+
linkType,
|
|
157
|
+
sourceChanged: source !== 'keep',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
// fetchImageAsDataUrl's own network/CORS error message (imageEdit.ts)
|
|
162
|
+
// is shown as-is when present — see note in `convert` above.
|
|
163
|
+
setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotSave);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setBusy(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const convertButton = (_jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPhotoDown, { size: 14 }), loading: converting, disabled: !baseSrc, onClick: () => convert(baseSrc), children: strings.imageDialog.convertButton }));
|
|
170
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: editing ? strings.imageDialog.titleEdit : strings.imageDialog.titleInsert, centered: true, size: "sm", portalProps: { target: portalTarget }, children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [(editing || (allowLinked && allowEmbedded)) && (_jsx(SegmentedControl, { fullWidth: true, value: source, onChange: (v) => changeSource(v), data: [
|
|
171
|
+
...(editing ? [{ value: 'keep', label: strings.imageDialog.currentImage }] : []),
|
|
172
|
+
...(allowLinked ? [{ value: 'linked', label: strings.imageDialog.fromUrl }] : []),
|
|
173
|
+
...(allowEmbedded ? [{ value: 'embedded', label: strings.imageDialog.upload }] : []),
|
|
174
|
+
] })), source === 'keep' && (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", c: "dimmed", lineClamp: 2, style: { wordBreak: 'break-all' }, children: initial?.linkType === 'embedded' ? strings.imageDialog.embeddedInDocument : strings.imageDialog.linkedFrom(initial?.src ?? '') }), initial?.linkType === 'linked' && allowEmbedded && (_jsxs(Group, { gap: "xs", children: [convertButton, _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageDialog.convertHint })] }))] })), source === 'linked' && (_jsxs(_Fragment, { children: [_jsx(TextInput, { label: strings.imageDialog.imageUrlLabel, placeholder: strings.imageDialog.imageUrlPlaceholder, value: url, onChange: (e) => setUrl(e.currentTarget.value), "data-autofocus": intent !== 'caption' ? true : undefined }), allowEmbedded && (_jsx(Checkbox, { label: strings.imageDialog.embedCopyLabel, description: strings.imageDialog.embedCopyDescription, checked: embedCopy, onChange: (e) => setEmbedCopy(e.currentTarget.checked) }))] })), source === 'embedded' &&
|
|
175
|
+
(convertedFromUrl ? (_jsx(Text, { size: "sm", c: "dimmed", children: strings.imageDialog.convertedFromUrl })) : (_jsx(FileInput, { label: strings.imageDialog.fileLabel, placeholder: strings.imageDialog.filePlaceholder, accept: ACCEPTED_IMAGE_TYPES.join(','), value: file, onChange: changeFile, description: strings.imageDialog.fileDescription(maxSizeMB, !onImageUpload) }))), _jsx(TextInput, { label: strings.imageDialog.altTextLabel, description: strings.imageDialog.altTextDescription, value: altText, onChange: (e) => setAltText(e.currentTarget.value) }), allowCaption && (_jsx(TextInput, { label: strings.imageDialog.captionLabel, description: strings.imageDialog.captionDescription, value: caption, onChange: (e) => setCaption(e.currentTarget.value), "data-autofocus": intent === 'caption' ? true : undefined })), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: editing ? strings.common.save : strings.common.insert })] })] }) }) }));
|
|
176
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { DecoratorNode, type DOMExportOutput, type EditorConfig, type LexicalNode, type NodeKey, type SerializedLexicalNode, type Spread } from 'lexical';
|
|
3
|
+
/** How the image's bytes are held: an external URL, or embedded in the
|
|
4
|
+
* document (a base64 data URI, or a URL returned by the host's upload handler). */
|
|
5
|
+
export type ImageLinkType = 'linked' | 'embedded';
|
|
6
|
+
export interface ImagePayload {
|
|
7
|
+
src: string;
|
|
8
|
+
altText?: string;
|
|
9
|
+
/** null = no caption; '' = caption enabled but empty */
|
|
10
|
+
caption?: string | null;
|
|
11
|
+
linkType: ImageLinkType;
|
|
12
|
+
width?: number | null;
|
|
13
|
+
height?: number | null;
|
|
14
|
+
key?: NodeKey;
|
|
15
|
+
}
|
|
16
|
+
export type SerializedImageNode = Spread<{
|
|
17
|
+
src: string;
|
|
18
|
+
altText: string;
|
|
19
|
+
caption: string | null;
|
|
20
|
+
linkType: ImageLinkType;
|
|
21
|
+
width: number | null;
|
|
22
|
+
height: number | null;
|
|
23
|
+
}, SerializedLexicalNode>;
|
|
24
|
+
/**
|
|
25
|
+
* Block-level image (editor-architecture-design.md §3.5). The caption is a
|
|
26
|
+
* plain string (null = no caption) edited through the image's right-click menu
|
|
27
|
+
* / edit dialog — not the nested sub-editor the architecture doc leaves open —
|
|
28
|
+
* which keeps every edit in the parent editor's undo history.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ImageNode extends DecoratorNode<ReactElement> {
|
|
31
|
+
__src: string;
|
|
32
|
+
__altText: string;
|
|
33
|
+
__caption: string | null;
|
|
34
|
+
__linkType: ImageLinkType;
|
|
35
|
+
__width: number | null;
|
|
36
|
+
__height: number | null;
|
|
37
|
+
static getType(): string;
|
|
38
|
+
static clone(node: ImageNode): ImageNode;
|
|
39
|
+
static importJSON(json: SerializedImageNode): ImageNode;
|
|
40
|
+
constructor(src: string, altText?: string, caption?: string | null, linkType?: ImageLinkType, width?: number | null, height?: number | null, key?: NodeKey);
|
|
41
|
+
exportJSON(): SerializedImageNode;
|
|
42
|
+
/** `<figure><img><figcaption>` — the figure/figcaption semantics from spec §4.7. */
|
|
43
|
+
exportDOM(): DOMExportOutput;
|
|
44
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
45
|
+
updateDOM(): false;
|
|
46
|
+
isInline(): false;
|
|
47
|
+
getSrc(): string;
|
|
48
|
+
getAltText(): string;
|
|
49
|
+
getCaption(): string | null;
|
|
50
|
+
getLinkType(): ImageLinkType;
|
|
51
|
+
getWidth(): number | null;
|
|
52
|
+
getHeight(): number | null;
|
|
53
|
+
setCaption(caption: string | null): this;
|
|
54
|
+
setAltText(altText: string): this;
|
|
55
|
+
/** Points the node at new image data (a replaced URL/upload, or edited pixels). */
|
|
56
|
+
setSource(src: string, linkType: ImageLinkType): this;
|
|
57
|
+
/** Display size in px; null = the image's natural size. */
|
|
58
|
+
setDimensions(width: number | null, height: number | null): this;
|
|
59
|
+
getTextContent(): string;
|
|
60
|
+
decorate(): ReactElement;
|
|
61
|
+
}
|
|
62
|
+
export declare function $createImageNode({ src, altText, caption, linkType, width, height, key }: ImagePayload): ImageNode;
|
|
63
|
+
export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
|
|
64
|
+
//# sourceMappingURL=ImageNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageNode.d.ts","sourceRoot":"","sources":["../../src/image/ImageNode.tsx"],"names":[],"mappings":"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"}
|