@mittwald/flow-react-components 1.2.0-next.49 → 1.2.0-next.50
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/MIGRATION.md +39 -0
- package/dist/assets/component-index.json +1 -1
- package/dist/assets/doc-properties.json +1 -1
- package/dist/css/all-layered.css +1 -1
- package/dist/css/all.css +1 -1
- package/dist/js/_virtual/_19.locale.mjs +8 -2
- package/dist/js/packages/components/src/components/ImageCropper/ImageCropper.mjs +38 -5
- package/dist/js/packages/components/src/components/ImageCropper/ImageCropper.mjs.map +1 -1
- package/dist/js/packages/components/src/components/ImageCropper/ImageCropper.module.mjs +4 -2
- package/dist/js/packages/components/src/components/ImageCropper/ImageCropper.module.mjs.map +1 -1
- package/dist/js/packages/components/src/components/ImageCropper/lib/getCroppedImageFile.mjs +12 -2
- package/dist/js/packages/components/src/components/ImageCropper/lib/getCroppedImageFile.mjs.map +1 -1
- package/dist/types/components/ImageCropper/ImageCropper.d.ts +22 -1
- package/dist/types/components/ImageCropper/index.d.ts +1 -1
- package/package.json +9 -9
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
/* */
|
|
3
3
|
//#region \0.locale.json@849e6f494125ee27df10a461562fa893
|
|
4
4
|
var __locale_default = {
|
|
5
|
-
"de-DE": {
|
|
6
|
-
|
|
5
|
+
"de-DE": {
|
|
6
|
+
"error.text": "Das Bild konnte nicht geladen werden.",
|
|
7
|
+
"zoom": "Zoom"
|
|
8
|
+
},
|
|
9
|
+
"en-US": {
|
|
10
|
+
"error.text": "The image could not be loaded.",
|
|
11
|
+
"zoom": "zoom"
|
|
12
|
+
}
|
|
7
13
|
};
|
|
8
14
|
//#endregion
|
|
9
15
|
export { __locale_default as default };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
/* */
|
|
3
|
+
import { IconDanger } from "../Icon/components/icons/IconDanger.mjs";
|
|
3
4
|
import { useLocalizedStringFormatter } from "../TranslationProvider/useLocalizedStringFormatter.mjs";
|
|
5
|
+
import { Text } from "../Text/Text.mjs";
|
|
6
|
+
import { IllustratedMessage } from "../IllustratedMessage/IllustratedMessage.mjs";
|
|
4
7
|
import ImageCropper_module_default from "./ImageCropper.module.mjs";
|
|
5
8
|
import { Slider } from "../Slider/Slider.mjs";
|
|
6
9
|
import { getCroppedImageFile } from "./lib/getCroppedImageFile.mjs";
|
|
7
10
|
import __locale_default from "../../../../../_virtual/_19.locale.mjs";
|
|
8
11
|
import { useImageSrc } from "../../lib/hooks/useImageSrc.mjs";
|
|
9
12
|
import clsx from "clsx";
|
|
10
|
-
import { useEffect, useState } from "react";
|
|
13
|
+
import { useEffect, useRef, useState } from "react";
|
|
11
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
15
|
import Cropper from "react-easy-crop";
|
|
13
16
|
import { useDebouncedCallback } from "use-debounce";
|
|
@@ -17,31 +20,60 @@ import { useDebouncedCallback } from "use-debounce";
|
|
|
17
20
|
* @flowStatus new
|
|
18
21
|
*/
|
|
19
22
|
var ImageCropper = (props) => {
|
|
20
|
-
const { image, className, onCropComplete, width = 300, height = 300, aspectRatio, ...rest } = props;
|
|
23
|
+
const { image, className, onCropComplete, onError, errorView, width = 300, height = 300, aspectRatio, ...rest } = props;
|
|
21
24
|
const imageSrc = useImageSrc(image);
|
|
22
25
|
const [mediaLoaded, setMediaLoaded] = useState(false);
|
|
26
|
+
const [error, setError] = useState();
|
|
23
27
|
const [crop, setCrop] = useState({
|
|
24
28
|
x: 0,
|
|
25
29
|
y: 0
|
|
26
30
|
});
|
|
27
31
|
const [zoom, setZoom] = useState(1);
|
|
32
|
+
/** Reports every image at most once, no matter how many crops fail on it. */
|
|
33
|
+
const errorReported = useRef(false);
|
|
28
34
|
const stringFormatter = useLocalizedStringFormatter(__locale_default, "ImageCropper");
|
|
29
35
|
const rootClassName = clsx(ImageCropper_module_default.imageCropper, className);
|
|
36
|
+
const reportError = (reason, message) => {
|
|
37
|
+
if (errorReported.current) return;
|
|
38
|
+
errorReported.current = true;
|
|
39
|
+
const imageCropperError = {
|
|
40
|
+
reason,
|
|
41
|
+
message
|
|
42
|
+
};
|
|
43
|
+
setError(imageCropperError);
|
|
44
|
+
onError?.(imageCropperError);
|
|
45
|
+
};
|
|
30
46
|
const debouncedCropComplete = useDebouncedCallback(async (croppedAreaPixels) => {
|
|
31
47
|
if (!croppedAreaPixels) return;
|
|
32
|
-
|
|
48
|
+
let croppedImageFile;
|
|
49
|
+
try {
|
|
50
|
+
croppedImageFile = await getCroppedImageFile(imageSrc, image ?? "", croppedAreaPixels);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
reportError("crop", cause instanceof Error ? cause.message : String(cause));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
33
55
|
onCropComplete?.(croppedImageFile);
|
|
34
56
|
}, 500, { leading: true });
|
|
35
57
|
useEffect(() => {
|
|
36
58
|
setMediaLoaded(false);
|
|
59
|
+
setError(void 0);
|
|
60
|
+
errorReported.current = false;
|
|
37
61
|
}, [imageSrc]);
|
|
62
|
+
const defaultErrorView = /* @__PURE__ */ jsxs(IllustratedMessage, {
|
|
63
|
+
color: "danger",
|
|
64
|
+
children: [/* @__PURE__ */ jsx(IconDanger, {}), /* @__PURE__ */ jsx(Text, { children: stringFormatter.format("error.text") })]
|
|
65
|
+
});
|
|
66
|
+
const errorElement = error ? errorView ?? defaultErrorView : void 0;
|
|
38
67
|
return /* @__PURE__ */ jsxs("div", {
|
|
39
68
|
className: rootClassName,
|
|
40
69
|
style: { width },
|
|
41
70
|
children: [/* @__PURE__ */ jsx("div", {
|
|
42
71
|
className: ImageCropper_module_default.cropperContainer,
|
|
43
72
|
style: { height },
|
|
44
|
-
children: /* @__PURE__ */ jsx(
|
|
73
|
+
children: errorElement ? /* @__PURE__ */ jsx("div", {
|
|
74
|
+
className: ImageCropper_module_default.errorViewContainer,
|
|
75
|
+
children: errorElement
|
|
76
|
+
}) : /* @__PURE__ */ jsx(Cropper, {
|
|
45
77
|
style: { containerStyle: { borderRadius: "calc(var(--image-cropper--corner-radius) - 1px)" } },
|
|
46
78
|
aspect: aspectRatio,
|
|
47
79
|
crop,
|
|
@@ -49,13 +81,14 @@ var ImageCropper = (props) => {
|
|
|
49
81
|
onCropChange: setCrop,
|
|
50
82
|
zoom,
|
|
51
83
|
onZoomChange: setZoom,
|
|
84
|
+
mediaProps: { onError: () => reportError("load", `Failed to load image "${imageSrc}"`) },
|
|
52
85
|
onMediaLoaded: () => setMediaLoaded(true),
|
|
53
86
|
onCropComplete: (_, croppedAreaPixels) => {
|
|
54
87
|
if (mediaLoaded) debouncedCropComplete(croppedAreaPixels);
|
|
55
88
|
},
|
|
56
89
|
...rest
|
|
57
90
|
})
|
|
58
|
-
}), /* @__PURE__ */ jsx(Slider, {
|
|
91
|
+
}), !errorElement && /* @__PURE__ */ jsx(Slider, {
|
|
59
92
|
minValue: 1,
|
|
60
93
|
maxValue: 3,
|
|
61
94
|
step: .01,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageCropper.mjs","names":[],"sources":["../../../../../../../src/components/ImageCropper/ImageCropper.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"ImageCropper.mjs","names":[],"sources":["../../../../../../../src/components/ImageCropper/ImageCropper.tsx"],"sourcesContent":["import {\n type CSSProperties,\n type FC,\n type ReactNode,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport Cropper, { type Area, type CropperProps } from \"react-easy-crop\";\nimport type { PropsWithClassName } from \"@/lib/types/props\";\nimport clsx from \"clsx\";\nimport styles from \"./ImageCropper.module.scss\";\nimport { Slider } from \"@/components/Slider\";\nimport { getCroppedImageFile } from \"@/components/ImageCropper/lib/getCroppedImageFile\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider/useLocalizedStringFormatter\";\nimport locales from \"./locales/*.locale.json\";\nimport { useImageSrc } from \"@/lib/hooks/useImageSrc\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { IllustratedMessage } from \"@/components/IllustratedMessage\";\nimport { IconDanger } from \"@/components/Icon/components/icons\";\nimport { Text } from \"@/components/Text\";\n\n/** Why an {@link ImageCropper} cannot work with the given image. */\nexport interface ImageCropperError {\n /**\n * `\"load\"` — the image itself could not be loaded. `\"crop\"` — the image is\n * displayed, but reading it into a canvas to produce the cropped file failed\n * (a cross-origin image without CORS headers, for example).\n */\n reason: \"load\" | \"crop\";\n /** Technical message, meant for logging — not for display. */\n message: string;\n}\n\nexport interface ImageCropperProps\n extends PropsWithClassName, Partial<Pick<CropperProps, \"cropShape\">> {\n /** The image file to crop. */\n image?: File | string;\n /** Callback on crop complete. */\n onCropComplete?: (croppedImage: File) => void;\n /**\n * Callback when the image cannot be cropped, because it failed to load or\n * could not be read.\n */\n onError?: (error: ImageCropperError) => void;\n /**\n * View that is shown instead of the cropper when the image cannot be cropped.\n * Defaults to a localized error message.\n */\n errorView?: ReactNode;\n /** The width of the component. @default 300 */\n width?: CSSProperties[\"width\"];\n /** The height of the component. @default 300 */\n height?: CSSProperties[\"height\"];\n /** The aspect ratio of the crop shape. */\n aspectRatio?: number;\n}\n\n/**\n * @flr-generate all\n * @flowStatus new\n */\nexport const ImageCropper: FC<ImageCropperProps> = (props) => {\n const {\n image,\n className,\n onCropComplete,\n onError,\n errorView,\n width = 300,\n height = 300,\n aspectRatio,\n ...rest\n } = props;\n\n const imageSrc = useImageSrc(image);\n\n const [mediaLoaded, setMediaLoaded] = useState(false);\n const [error, setError] = useState<ImageCropperError>();\n const [crop, setCrop] = useState({ x: 0, y: 0 });\n const [zoom, setZoom] = useState(1);\n\n /** Reports every image at most once, no matter how many crops fail on it. */\n const errorReported = useRef(false);\n\n const stringFormatter = useLocalizedStringFormatter(locales, \"ImageCropper\");\n const rootClassName = clsx(styles.imageCropper, className);\n\n const reportError = (\n reason: ImageCropperError[\"reason\"],\n message: string,\n ): void => {\n if (errorReported.current) {\n return;\n }\n errorReported.current = true;\n\n const imageCropperError: ImageCropperError = { reason, message };\n setError(imageCropperError);\n onError?.(imageCropperError);\n };\n\n const debouncedCropComplete = useDebouncedCallback(\n async (croppedAreaPixels: Area) => {\n if (!croppedAreaPixels) {\n return;\n }\n\n let croppedImageFile: File;\n\n try {\n croppedImageFile = await getCroppedImageFile(\n imageSrc,\n image ?? \"\",\n croppedAreaPixels,\n );\n } catch (cause) {\n reportError(\n \"crop\",\n cause instanceof Error ? cause.message : String(cause),\n );\n return;\n }\n\n onCropComplete?.(croppedImageFile);\n },\n 500,\n {\n leading: true,\n },\n );\n\n useEffect(() => {\n setMediaLoaded(false);\n setError(undefined);\n errorReported.current = false;\n }, [imageSrc]);\n\n const defaultErrorView = (\n <IllustratedMessage color=\"danger\">\n <IconDanger />\n <Text>{stringFormatter.format(\"error.text\")}</Text>\n </IllustratedMessage>\n );\n\n const errorElement = error ? (errorView ?? defaultErrorView) : undefined;\n\n return (\n <div className={rootClassName} style={{ width }}>\n <div className={styles.cropperContainer} style={{ height }}>\n {errorElement ? (\n <div className={styles.errorViewContainer}>{errorElement}</div>\n ) : (\n <Cropper\n style={{\n containerStyle: {\n borderRadius: \"calc(var(--image-cropper--corner-radius) - 1px)\",\n },\n }}\n aspect={aspectRatio}\n crop={crop}\n image={imageSrc}\n onCropChange={setCrop}\n zoom={zoom}\n onZoomChange={setZoom}\n mediaProps={{\n onError: () =>\n reportError(\"load\", `Failed to load image \"${imageSrc}\"`),\n }}\n onMediaLoaded={() => setMediaLoaded(true)}\n onCropComplete={(_, croppedAreaPixels) => {\n if (mediaLoaded) {\n debouncedCropComplete(croppedAreaPixels);\n }\n }}\n {...rest}\n />\n )}\n </div>\n {!errorElement && (\n <Slider\n minValue={1}\n maxValue={3}\n step={0.01}\n value={zoom}\n sliderOnly\n onChange={(zoom) => setZoom(zoom as number)}\n aria-label={stringFormatter.format(\"zoom\")}\n />\n )}\n </div>\n );\n};\n\nexport default ImageCropper;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA8DA,IAAa,gBAAuC,UAAU;CAC5D,MAAM,EACJ,OACA,WACA,gBACA,SACA,WACA,QAAQ,KACR,SAAS,KACT,aACA,GAAG,SACD;CAEJ,MAAM,WAAW,YAAY,KAAK;CAElC,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CACpD,MAAM,CAAC,OAAO,YAAY,SAA4B;CACtD,MAAM,CAAC,MAAM,WAAW,SAAS;EAAE,GAAG;EAAG,GAAG;CAAE,CAAC;CAC/C,MAAM,CAAC,MAAM,WAAW,SAAS,CAAC;;CAGlC,MAAM,gBAAgB,OAAO,KAAK;CAElC,MAAM,kBAAkB,4BAA4B,kBAAS,cAAc;CAC3E,MAAM,gBAAgB,KAAK,4BAAO,cAAc,SAAS;CAEzD,MAAM,eACJ,QACA,YACS;EACT,IAAI,cAAc,SAChB;EAEF,cAAc,UAAU;EAExB,MAAM,oBAAuC;GAAE;GAAQ;EAAQ;EAC/D,SAAS,iBAAiB;EAC1B,UAAU,iBAAiB;CAC7B;CAEA,MAAM,wBAAwB,qBAC5B,OAAO,sBAA4B;EACjC,IAAI,CAAC,mBACH;EAGF,IAAI;EAEJ,IAAI;GACF,mBAAmB,MAAM,oBACvB,UACA,SAAS,IACT,iBACF;EACF,SAAS,OAAO;GACd,YACE,QACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD;GACA;EACF;EAEA,iBAAiB,gBAAgB;CACnC,GACA,KACA,EACE,SAAS,KACX,CACF;CAEA,gBAAgB;EACd,eAAe,KAAK;EACpB,SAAS,KAAA,CAAS;EAClB,cAAc,UAAU;CAC1B,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,mBACJ,qBAAC,oBAAD;EAAoB,OAAM;EAA1B,UAAA,CACE,oBAAC,YAAD,CAAa,CAAA,GACb,oBAAC,MAAD,EAAA,UAAO,gBAAgB,OAAO,YAAY,EAAQ,CAAA,CAChC;;CAGtB,MAAM,eAAe,QAAS,aAAa,mBAAoB,KAAA;CAE/D,OACE,qBAAC,OAAD;EAAK,WAAW;EAAe,OAAO,EAAE,MAAM;EAA9C,UAAA,CACE,oBAAC,OAAD;GAAK,WAAW,4BAAO;GAAkB,OAAO,EAAE,OAAO;GACtD,UAAA,eACC,oBAAC,OAAD;IAAK,WAAW,4BAAO;IAAqB,UAAA;GAAkB,CAAA,IAE9D,oBAAC,SAAD;IACE,OAAO,EACL,gBAAgB,EACd,cAAc,kDAChB,EACF;IACA,QAAQ;IACF;IACN,OAAO;IACP,cAAc;IACR;IACN,cAAc;IACd,YAAY,EACV,eACE,YAAY,QAAQ,yBAAyB,SAAS,EAAE,EAC5D;IACA,qBAAqB,eAAe,IAAI;IACxC,iBAAiB,GAAG,sBAAsB;KACxC,IAAI,aACF,sBAAsB,iBAAiB;IAE3C;IACA,GAAI;GACL,CAAA;EAEA,CAAA,GACJ,CAAC,gBACA,oBAAC,QAAD;GACE,UAAU;GACV,UAAU;GACV,MAAM;GACN,OAAO;GACP,YAAA;GACA,WAAW,SAAS,QAAQ,IAAc;GAC1C,cAAY,gBAAgB,OAAO,MAAM;EAC1C,CAAA,CAEA;;AAET"}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
//#region src/components/ImageCropper/ImageCropper.module.scss
|
|
4
4
|
var imageCropper = "flow--image-cropper";
|
|
5
5
|
var cropperContainer = "flow--image-cropper--cropper-container";
|
|
6
|
+
var errorViewContainer = "flow--image-cropper--error-view-container";
|
|
6
7
|
var ImageCropper_module_default = {
|
|
7
8
|
imageCropper,
|
|
8
|
-
cropperContainer
|
|
9
|
+
cropperContainer,
|
|
10
|
+
errorViewContainer
|
|
9
11
|
};
|
|
10
12
|
//#endregion
|
|
11
|
-
export { cropperContainer, ImageCropper_module_default as default, imageCropper };
|
|
13
|
+
export { cropperContainer, ImageCropper_module_default as default, errorViewContainer, imageCropper };
|
|
12
14
|
|
|
13
15
|
//# sourceMappingURL=ImageCropper.module.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageCropper.module.mjs","names":[],"sources":["../../../../../../../src/components/ImageCropper/ImageCropper.module.scss"],"sourcesContent":[".imageCropper {\n display: flex;\n flex-direction: column;\n gap: var(--image-cropper--spacing);\n max-width: 100%;\n\n .cropperContainer {\n position: relative;\n width: 100%;\n border-radius: var(--image-cropper--corner-radius);\n border-color: var(--image-cropper--border-color);\n border-style: var(--image-cropper--border-style);\n border-width: var(--image-cropper--border-width);\n }\n\n}\n\n/*\n * react-easy-crop injects its stylesheet as an unlayered `<style>` element at\n * runtime, outside of Flow's build, with hard coded rgba values for the crop\n * mask and the grid. Unlayered CSS beats layered CSS regardless of specificity,\n * so these token based overrides only apply in the layered stylesheet variant\n * if they leave the cascade layer as well.\n */\n@layer flow.unlayered {\n .imageCropper {\n :global(.reactEasyCrop_CropArea) {\n color: var(--image-cropper--mask-color);\n border-color: var(--image-cropper--mask-border-color);\n box-sizing: content-box;\n }\n\n :global(.reactEasyCrop_CropAreaGrid::before) {\n border-color: var(--image-cropper--grid-color);\n border-width: var(--image-cropper--grid-width);\n }\n\n :global(.reactEasyCrop_Container) {\n border-radius: var(--image-cropper--corner-radius);\n }\n }\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"ImageCropper.module.mjs","names":[],"sources":["../../../../../../../src/components/ImageCropper/ImageCropper.module.scss"],"sourcesContent":[".imageCropper {\n display: flex;\n flex-direction: column;\n gap: var(--image-cropper--spacing);\n max-width: 100%;\n\n .cropperContainer {\n position: relative;\n width: 100%;\n border-radius: var(--image-cropper--corner-radius);\n border-color: var(--image-cropper--border-color);\n border-style: var(--image-cropper--border-style);\n border-width: var(--image-cropper--border-width);\n }\n\n .errorViewContainer {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n }\n}\n\n/*\n * react-easy-crop injects its stylesheet as an unlayered `<style>` element at\n * runtime, outside of Flow's build, with hard coded rgba values for the crop\n * mask and the grid. Unlayered CSS beats layered CSS regardless of specificity,\n * so these token based overrides only apply in the layered stylesheet variant\n * if they leave the cascade layer as well.\n */\n@layer flow.unlayered {\n .imageCropper {\n :global(.reactEasyCrop_CropArea) {\n color: var(--image-cropper--mask-color);\n border-color: var(--image-cropper--mask-border-color);\n box-sizing: content-box;\n }\n\n :global(.reactEasyCrop_CropAreaGrid::before) {\n border-color: var(--image-cropper--grid-color);\n border-width: var(--image-cropper--grid-width);\n }\n\n :global(.reactEasyCrop_Container) {\n border-radius: var(--image-cropper--corner-radius);\n }\n }\n}\n"],"mappings":""}
|
|
@@ -6,7 +6,7 @@ function getCroppedImageFile(imageSrc, sourceImage, pixelCrop) {
|
|
|
6
6
|
const image = new Image();
|
|
7
7
|
image.crossOrigin = "anonymous";
|
|
8
8
|
image.src = imageSrc;
|
|
9
|
-
|
|
9
|
+
const cropOntoCanvas = () => {
|
|
10
10
|
const canvas = document.createElement("canvas");
|
|
11
11
|
const ctx = canvas.getContext("2d");
|
|
12
12
|
canvas.width = pixelCrop.width;
|
|
@@ -21,10 +21,20 @@ function getCroppedImageFile(imageSrc, sourceImage, pixelCrop) {
|
|
|
21
21
|
const sourceImageType = isSourceImageJsFile ? sourceImage.type : "image/png";
|
|
22
22
|
const quality = sourceImageType === "image/jpeg" ? .86 : sourceImageType === "image/webp" ? .82 : void 0;
|
|
23
23
|
canvas.toBlob((blob) => {
|
|
24
|
-
if (!blob)
|
|
24
|
+
if (!blob) {
|
|
25
|
+
reject(/* @__PURE__ */ new Error("Failed to read the cropped image from canvas"));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
25
28
|
resolve(new File([blob], sourceImageName, { type: sourceImageType }));
|
|
26
29
|
}, sourceImageType, quality);
|
|
27
30
|
};
|
|
31
|
+
image.onload = () => {
|
|
32
|
+
try {
|
|
33
|
+
cropOntoCanvas();
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
reject(cause instanceof Error ? cause : new Error(String(cause)));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
28
38
|
image.onerror = () => {
|
|
29
39
|
reject(/* @__PURE__ */ new Error("Failed to load image"));
|
|
30
40
|
};
|
package/dist/js/packages/components/src/components/ImageCropper/lib/getCroppedImageFile.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getCroppedImageFile.mjs","names":[],"sources":["../../../../../../../../src/components/ImageCropper/lib/getCroppedImageFile.ts"],"sourcesContent":["import type { Area } from \"react-easy-crop\";\n\nexport function getCroppedImageFile(\n imageSrc: string,\n sourceImage: File | string,\n pixelCrop: Area,\n): Promise<File> {\n return new Promise((resolve, reject) => {\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.src = imageSrc;\n\n
|
|
1
|
+
{"version":3,"file":"getCroppedImageFile.mjs","names":[],"sources":["../../../../../../../../src/components/ImageCropper/lib/getCroppedImageFile.ts"],"sourcesContent":["import type { Area } from \"react-easy-crop\";\n\nexport function getCroppedImageFile(\n imageSrc: string,\n sourceImage: File | string,\n pixelCrop: Area,\n): Promise<File> {\n return new Promise((resolve, reject) => {\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.src = imageSrc;\n\n const cropOntoCanvas = (): void => {\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n\n canvas.width = pixelCrop.width;\n canvas.height = pixelCrop.height;\n\n if (!ctx) {\n reject(new Error(\"Failed to get canvas context\"));\n return;\n }\n\n ctx.drawImage(\n image,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n pixelCrop.width,\n pixelCrop.height,\n );\n\n const isSourceImageJsFile = sourceImage instanceof File;\n const sourceImageName = isSourceImageJsFile\n ? sourceImage.name\n : \"cropped-image.png\";\n const sourceImageType = isSourceImageJsFile\n ? sourceImage.type\n : \"image/png\";\n\n const quality =\n sourceImageType === \"image/jpeg\"\n ? 0.86\n : sourceImageType === \"image/webp\"\n ? 0.82\n : undefined;\n\n canvas.toBlob(\n (blob) => {\n if (!blob) {\n reject(new Error(\"Failed to read the cropped image from canvas\"));\n return;\n }\n\n resolve(\n new File([blob], sourceImageName, {\n type: sourceImageType,\n }),\n );\n },\n sourceImageType,\n quality,\n );\n };\n\n image.onload = () => {\n try {\n cropOntoCanvas();\n } catch (cause) {\n // A tainted canvas makes `drawImage`/`toBlob` throw a SecurityError.\n reject(cause instanceof Error ? cause : new Error(String(cause)));\n }\n };\n\n image.onerror = () => {\n reject(new Error(\"Failed to load image\"));\n };\n });\n}\n"],"mappings":";;;AAEA,SAAgB,oBACd,UACA,aACA,WACe;CACf,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,IAAI,MAAM;EACxB,MAAM,cAAc;EACpB,MAAM,MAAM;EAEZ,MAAM,uBAA6B;GACjC,MAAM,SAAS,SAAS,cAAc,QAAQ;GAC9C,MAAM,MAAM,OAAO,WAAW,IAAI;GAElC,OAAO,QAAQ,UAAU;GACzB,OAAO,SAAS,UAAU;GAE1B,IAAI,CAAC,KAAK;IACR,uBAAO,IAAI,MAAM,8BAA8B,CAAC;IAChD;GACF;GAEA,IAAI,UACF,OACA,UAAU,GACV,UAAU,GACV,UAAU,OACV,UAAU,QACV,GACA,GACA,UAAU,OACV,UAAU,MACZ;GAEA,MAAM,sBAAsB,uBAAuB;GACnD,MAAM,kBAAkB,sBACpB,YAAY,OACZ;GACJ,MAAM,kBAAkB,sBACpB,YAAY,OACZ;GAEJ,MAAM,UACJ,oBAAoB,eAChB,MACA,oBAAoB,eAClB,MACA,KAAA;GAER,OAAO,QACJ,SAAS;IACR,IAAI,CAAC,MAAM;KACT,uBAAO,IAAI,MAAM,8CAA8C,CAAC;KAChE;IACF;IAEA,QACE,IAAI,KAAK,CAAC,IAAI,GAAG,iBAAiB,EAChC,MAAM,gBACR,CAAC,CACH;GACF,GACA,iBACA,OACF;EACF;EAEA,MAAM,eAAe;GACnB,IAAI;IACF,eAAe;GACjB,SAAS,OAAO;IAEd,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;GAClE;EACF;EAEA,MAAM,gBAAgB;GACpB,uBAAO,IAAI,MAAM,sBAAsB,CAAC;EAC1C;CACF,CAAC;AACH"}
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import { CSSProperties, FC } from 'react';
|
|
1
|
+
import { CSSProperties, FC, ReactNode } from 'react';
|
|
2
2
|
import { CropperProps } from 'react-easy-crop';
|
|
3
3
|
import { PropsWithClassName } from '../../lib/types/props';
|
|
4
|
+
/** Why an {@link ImageCropper} cannot work with the given image. */
|
|
5
|
+
export interface ImageCropperError {
|
|
6
|
+
/**
|
|
7
|
+
* `"load"` — the image itself could not be loaded. `"crop"` — the image is
|
|
8
|
+
* displayed, but reading it into a canvas to produce the cropped file failed
|
|
9
|
+
* (a cross-origin image without CORS headers, for example).
|
|
10
|
+
*/
|
|
11
|
+
reason: "load" | "crop";
|
|
12
|
+
/** Technical message, meant for logging — not for display. */
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
4
15
|
export interface ImageCropperProps extends PropsWithClassName, Partial<Pick<CropperProps, "cropShape">> {
|
|
5
16
|
/** The image file to crop. */
|
|
6
17
|
image?: File | string;
|
|
7
18
|
/** Callback on crop complete. */
|
|
8
19
|
onCropComplete?: (croppedImage: File) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Callback when the image cannot be cropped, because it failed to load or
|
|
22
|
+
* could not be read.
|
|
23
|
+
*/
|
|
24
|
+
onError?: (error: ImageCropperError) => void;
|
|
25
|
+
/**
|
|
26
|
+
* View that is shown instead of the cropper when the image cannot be cropped.
|
|
27
|
+
* Defaults to a localized error message.
|
|
28
|
+
*/
|
|
29
|
+
errorView?: ReactNode;
|
|
9
30
|
/** The width of the component. @default 300 */
|
|
10
31
|
width?: CSSProperties["width"];
|
|
11
32
|
/** The height of the component. @default 300 */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type ImageCropperProps, ImageCropper } from './ImageCropper';
|
|
1
|
+
export { type ImageCropperError, type ImageCropperProps, ImageCropper, } from './ImageCropper';
|
|
2
2
|
export { default } from './ImageCropper';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-react-components",
|
|
3
|
-
"version": "1.2.0-next.
|
|
3
|
+
"version": "1.2.0-next.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A React implementation of Flow, mittwald’s design system",
|
|
6
6
|
"homepage": "https://flow.mittwald.de",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"@internationalized/string-compiler": "^3.4.1",
|
|
80
80
|
"@lezer/common": "^1.5.2",
|
|
81
81
|
"@lezer/highlight": "^1.2.3",
|
|
82
|
-
"@mittwald/flow-icons": "1.2.0-next.
|
|
82
|
+
"@mittwald/flow-icons": "1.2.0-next.50",
|
|
83
83
|
"@mittwald/password-tools-js": "^3.0.0",
|
|
84
|
-
"@mittwald/react-tunnel": "1.2.0-next.
|
|
84
|
+
"@mittwald/react-tunnel": "1.2.0-next.50",
|
|
85
85
|
"@mittwald/react-use-promise": "^4.2.2",
|
|
86
86
|
"@react-aria/form": "^3.2.1",
|
|
87
87
|
"@react-aria/i18n": "^3.13.1",
|
|
@@ -134,12 +134,12 @@
|
|
|
134
134
|
"@internationalized/date": "^3.12.3",
|
|
135
135
|
"@lezer/generator": "^1.8.0",
|
|
136
136
|
"@lezer/lr": "^1.4.10",
|
|
137
|
-
"@mittwald/flow-core": "1.2.0-next.
|
|
138
|
-
"@mittwald/flow-design-tokens": "1.2.0-next.
|
|
139
|
-
"@mittwald/flow-icons-base": "1.2.0-next.
|
|
137
|
+
"@mittwald/flow-core": "1.2.0-next.50",
|
|
138
|
+
"@mittwald/flow-design-tokens": "1.2.0-next.50",
|
|
139
|
+
"@mittwald/flow-icons-base": "1.2.0-next.50",
|
|
140
140
|
"@mittwald/react-use-promise": "^4.2.2",
|
|
141
141
|
"@mittwald/remote-dom-react": "1.2.2-mittwald.10",
|
|
142
|
-
"@mittwald/typescript-config": "1.2.0-next.
|
|
142
|
+
"@mittwald/typescript-config": "1.2.0-next.50",
|
|
143
143
|
"@nx/storybook": "^22.7.6",
|
|
144
144
|
"@storybook/addon-a11y": "^10.5.10",
|
|
145
145
|
"@storybook/addon-actions": "^9.0.8",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
},
|
|
192
192
|
"peerDependencies": {
|
|
193
193
|
"@internationalized/date": "^3.12.2",
|
|
194
|
-
"@mittwald/flow-icons-pro": "1.2.0-next.
|
|
194
|
+
"@mittwald/flow-icons-pro": "1.2.0-next.50",
|
|
195
195
|
"@mittwald/react-use-promise": "^4.2.2",
|
|
196
196
|
"next": "^16.2.3",
|
|
197
197
|
"react": "^19.2.0",
|
|
@@ -215,5 +215,5 @@
|
|
|
215
215
|
"browserslist": [
|
|
216
216
|
"baseline widely available"
|
|
217
217
|
],
|
|
218
|
-
"gitHead": "
|
|
218
|
+
"gitHead": "8d45eb915e3f41d17452ca3166035212b3213082"
|
|
219
219
|
}
|