@skalfa/skalfa-component 1.0.51 → 1.0.53
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/input/InputImage.component.d.ts +2 -0
- package/dist/input/InputImage.component.js +54 -18
- package/dist/supervision/FormSupervision.component.d.ts +1 -1
- package/dist/supervision/FormSupervision.component.js +6 -2
- package/package.json +1 -1
- package/src/input/InputImage.component.tsx +71 -21
- package/src/supervision/FormSupervision.component.tsx +10 -5
|
@@ -17,6 +17,8 @@ export declare const InputImageComponent: React.FC<InputImageProps>;
|
|
|
17
17
|
interface CropperProps {
|
|
18
18
|
src: string;
|
|
19
19
|
aspect: number;
|
|
20
|
+
fileName?: string;
|
|
21
|
+
fileType?: string;
|
|
20
22
|
onDone?: (file: File) => void;
|
|
21
23
|
}
|
|
22
24
|
export declare const CanvasCropper: React.FC<CropperProps>;
|
|
@@ -16,6 +16,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
16
16
|
const [preview, setPreview] = (0, react_1.useState)("");
|
|
17
17
|
const [drag, setDrag] = (0, react_1.useState)(false);
|
|
18
18
|
const [cropSrc, setCropSrc] = (0, react_1.useState)(null);
|
|
19
|
+
const [cropFile, setCropFile] = (0, react_1.useState)(null);
|
|
19
20
|
const [openCrop, setOpenCrop] = (0, react_1.useState)(false);
|
|
20
21
|
const inputHandler = (0, _utils_1.useInputHandler)(name, value, validations, register, true, unregister);
|
|
21
22
|
const [invalidMessage, setInvalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
|
|
@@ -35,6 +36,7 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
35
36
|
}
|
|
36
37
|
}, [value]);
|
|
37
38
|
const openCropper = (file) => {
|
|
39
|
+
setCropFile(file);
|
|
38
40
|
const reader = new FileReader();
|
|
39
41
|
reader.onload = () => {
|
|
40
42
|
setCropSrc(reader.result);
|
|
@@ -68,10 +70,14 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
68
70
|
};
|
|
69
71
|
const onCropCancel = () => {
|
|
70
72
|
setOpenCrop(false);
|
|
73
|
+
setCropFile(null);
|
|
74
|
+
setCropSrc(null);
|
|
71
75
|
inputRef.current.value = "";
|
|
72
76
|
};
|
|
73
77
|
const remove = () => {
|
|
74
78
|
setPreview("");
|
|
79
|
+
setCropFile(null);
|
|
80
|
+
setCropSrc(null);
|
|
75
81
|
onChange?.(null);
|
|
76
82
|
inputRef.current && (inputRef.current.value = "");
|
|
77
83
|
};
|
|
@@ -99,12 +105,11 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invali
|
|
|
99
105
|
}, onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => {
|
|
100
106
|
e.preventDefault();
|
|
101
107
|
setDrag(true);
|
|
102
|
-
}, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
|
|
108
|
+
}, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-fit max-w-[90vw]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), fileName: cropFile?.name, fileType: cropFile?.type, onDone: onCropDone }) })) }))] }));
|
|
103
109
|
};
|
|
104
110
|
exports.InputImageComponent = InputImageComponent;
|
|
105
|
-
const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
111
|
+
const CanvasCropper = ({ src, aspect, fileName, fileType, onDone, }) => {
|
|
106
112
|
const canvasRef = (0, react_1.useRef)(null);
|
|
107
|
-
const previewRef = (0, react_1.useRef)(null);
|
|
108
113
|
const pinchDistRef = (0, react_1.useRef)(0);
|
|
109
114
|
const pinchStartZoomRef = (0, react_1.useRef)(1);
|
|
110
115
|
const [img, setImg] = (0, react_1.useState)(null);
|
|
@@ -130,8 +135,8 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
130
135
|
canvas.width = CROP_W;
|
|
131
136
|
canvas.height = CROP_SIZE;
|
|
132
137
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
133
|
-
const scaledW = img.
|
|
134
|
-
const scaledH = img.
|
|
138
|
+
const scaledW = img.naturalWidth * zoom;
|
|
139
|
+
const scaledH = img.naturalHeight * zoom;
|
|
135
140
|
const x = pos.x - scaledW / 2 + CROP_W / 2;
|
|
136
141
|
const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
|
|
137
142
|
ctx.drawImage(img, x, y, scaledW, scaledH);
|
|
@@ -178,19 +183,50 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
178
183
|
const performCrop = () => {
|
|
179
184
|
if (!img)
|
|
180
185
|
return;
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
const scaleFactor = 1 / zoom;
|
|
187
|
+
const scaledW = img.naturalWidth * zoom;
|
|
188
|
+
const scaledH = img.naturalHeight * zoom;
|
|
189
|
+
const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
|
|
190
|
+
const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
|
|
191
|
+
const sourceX = cropDisplayX * scaleFactor;
|
|
192
|
+
const sourceY = cropDisplayY * scaleFactor;
|
|
193
|
+
const sourceW = CROP_W * scaleFactor;
|
|
194
|
+
const sourceH = CROP_SIZE * scaleFactor;
|
|
195
|
+
const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
|
|
196
|
+
const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
|
|
197
|
+
const sw = Math.min(sourceW, img.naturalWidth - sx);
|
|
198
|
+
const sh = Math.min(sourceH, img.naturalHeight - sy);
|
|
199
|
+
let targetW = Math.round(sw);
|
|
200
|
+
let targetH = Math.round(sh);
|
|
201
|
+
const MAX_DIMENSION = 2560;
|
|
202
|
+
if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
|
|
203
|
+
if (targetW >= targetH) {
|
|
204
|
+
targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
|
|
205
|
+
targetW = MAX_DIMENSION;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
|
|
209
|
+
targetH = MAX_DIMENSION;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const outputCanvas = document.createElement("canvas");
|
|
213
|
+
outputCanvas.width = targetW;
|
|
214
|
+
outputCanvas.height = targetH;
|
|
215
|
+
const octx = outputCanvas.getContext("2d");
|
|
216
|
+
if (!octx)
|
|
217
|
+
return;
|
|
218
|
+
octx.imageSmoothingEnabled = true;
|
|
219
|
+
octx.imageSmoothingQuality = "high";
|
|
220
|
+
octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
221
|
+
const type = fileType || "image/jpeg";
|
|
222
|
+
const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
|
|
223
|
+
const quality = type === "image/png" ? undefined : 0.92;
|
|
224
|
+
outputCanvas.toBlob((blob) => {
|
|
225
|
+
if (!blob)
|
|
226
|
+
return;
|
|
227
|
+
const file = new File([blob], name, { type, lastModified: Date.now() });
|
|
192
228
|
onDone?.(file);
|
|
193
|
-
});
|
|
229
|
+
}, type, quality);
|
|
194
230
|
};
|
|
195
231
|
const handleWheel = (e) => {
|
|
196
232
|
e.preventDefault();
|
|
@@ -276,6 +312,6 @@ const CanvasCropper = ({ src, aspect, onDone, }) => {
|
|
|
276
312
|
setDragging(false);
|
|
277
313
|
pinchDistRef.current = 0;
|
|
278
314
|
};
|
|
279
|
-
return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) })
|
|
315
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { touchAction: "none" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-wrapper", style: { width: CROP_W, height: CROP_SIZE }, onMouseMove: handleMove, onMouseUp: handleUp, onMouseLeave: handleUp, onWheel: handleWheel, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: CROP_W, height: CROP_SIZE, onMouseDown: handleDown, className: "cursor-move" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-image-cropper-grid-container", style: { width: CROP_W, height: CROP_SIZE }, children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-border" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-h input-image-cropper-grid-line-h-2" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-1" }), (0, jsx_runtime_1.jsx)("div", { className: "input-image-cropper-grid-line-v input-image-cropper-grid-line-v-2" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-4 pb-2", children: (0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { label: "Selesai", variant: "outline", onClick: performCrop, block: true }) })] }));
|
|
280
316
|
};
|
|
281
317
|
exports.CanvasCropper = CanvasCropper;
|
|
@@ -197,7 +197,8 @@ function FormSupervisionComponent({ title, fields, submitControl, confirmation,
|
|
|
197
197
|
if (form?.onHide?.(valMap, prefix))
|
|
198
198
|
return null;
|
|
199
199
|
const ws = watchState[name];
|
|
200
|
-
|
|
200
|
+
const isHidden = form.construction?.hidden ?? ws?.hidden;
|
|
201
|
+
if (isHidden)
|
|
201
202
|
return null;
|
|
202
203
|
if (inputType === "cluster") {
|
|
203
204
|
const { name: mapName, fields: innerForms, label, tip, wrap, className, min } = form.construction;
|
|
@@ -247,7 +248,10 @@ function FormSupervisionComponent({ title, fields, submitControl, confirmation,
|
|
|
247
248
|
return ((0, jsx_runtime_1.jsx)("div", { className: (0, _utils_1.cn)(form.className, generateColClass(form.col || "12")), children: customRender?.({ formControl, values, setValues, errors, setErrors, setRegister, prefixName: prefix }) }, key));
|
|
248
249
|
}
|
|
249
250
|
const Component = inputMap[inputType] || __1.InputComponent;
|
|
250
|
-
|
|
251
|
+
const construction = form.construction || {};
|
|
252
|
+
const disabled = construction.disabled ?? ws?.disabled;
|
|
253
|
+
const readOnly = construction.readOnly ?? ws?.readOnly;
|
|
254
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, _utils_1.cn)(form.className, generateColClass(form.col || "12")), children: (0, jsx_runtime_1.jsx)(Component, { ...construction, ...formControl(name), disabled: disabled, readOnly: readOnly, name: name }) }, key));
|
|
251
255
|
};
|
|
252
256
|
(0, react_1.useEffect)(() => {
|
|
253
257
|
(modal == "failed") && setModal(false);
|
package/package.json
CHANGED
|
@@ -54,6 +54,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
54
54
|
const [preview, setPreview] = useState("");
|
|
55
55
|
const [drag, setDrag] = useState(false);
|
|
56
56
|
const [cropSrc, setCropSrc] = useState<string | null>(null);
|
|
57
|
+
const [cropFile, setCropFile] = useState<File | null>(null);
|
|
57
58
|
const [openCrop, setOpenCrop] = useState(false);
|
|
58
59
|
|
|
59
60
|
const inputHandler = useInputHandler(name, value, validations, register, true, unregister);
|
|
@@ -77,6 +78,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
77
78
|
}, [value]);
|
|
78
79
|
|
|
79
80
|
const openCropper = (file: File) => {
|
|
81
|
+
setCropFile(file);
|
|
80
82
|
const reader = new FileReader();
|
|
81
83
|
reader.onload = () => {
|
|
82
84
|
setCropSrc(reader.result as string);
|
|
@@ -113,11 +115,15 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
113
115
|
|
|
114
116
|
const onCropCancel = () => {
|
|
115
117
|
setOpenCrop(false);
|
|
118
|
+
setCropFile(null);
|
|
119
|
+
setCropSrc(null);
|
|
116
120
|
inputRef.current!.value = "";
|
|
117
121
|
};
|
|
118
122
|
|
|
119
123
|
const remove = () => {
|
|
120
124
|
setPreview("");
|
|
125
|
+
setCropFile(null);
|
|
126
|
+
setCropSrc(null);
|
|
121
127
|
onChange?.(null);
|
|
122
128
|
inputRef.current && (inputRef.current.value = "");
|
|
123
129
|
};
|
|
@@ -243,6 +249,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
243
249
|
<CanvasCropper
|
|
244
250
|
src={cropSrc}
|
|
245
251
|
aspect={eval(currentAspect.replace(":", "/"))}
|
|
252
|
+
fileName={cropFile?.name}
|
|
253
|
+
fileType={cropFile?.type}
|
|
246
254
|
onDone={onCropDone}
|
|
247
255
|
/>
|
|
248
256
|
</div>
|
|
@@ -255,6 +263,8 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
255
263
|
<CanvasCropper
|
|
256
264
|
src={cropSrc}
|
|
257
265
|
aspect={eval(currentAspect.replace(":", "/"))}
|
|
266
|
+
fileName={cropFile?.name}
|
|
267
|
+
fileType={cropFile?.type}
|
|
258
268
|
onDone={onCropDone}
|
|
259
269
|
/>
|
|
260
270
|
</div>
|
|
@@ -269,18 +279,21 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
|
|
|
269
279
|
|
|
270
280
|
|
|
271
281
|
interface CropperProps {
|
|
272
|
-
src
|
|
273
|
-
aspect
|
|
274
|
-
|
|
282
|
+
src : string;
|
|
283
|
+
aspect : number;
|
|
284
|
+
fileName ?: string;
|
|
285
|
+
fileType ?: string;
|
|
286
|
+
onDone ?: (file: File) => void;
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
export const CanvasCropper: React.FC<CropperProps> = ({
|
|
278
290
|
src,
|
|
279
291
|
aspect,
|
|
292
|
+
fileName,
|
|
293
|
+
fileType,
|
|
280
294
|
onDone,
|
|
281
295
|
}) => {
|
|
282
296
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
283
|
-
const previewRef = useRef<HTMLCanvasElement>(null);
|
|
284
297
|
const pinchDistRef = useRef(0);
|
|
285
298
|
const pinchStartZoomRef = useRef(1);
|
|
286
299
|
|
|
@@ -314,8 +327,8 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
314
327
|
|
|
315
328
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
316
329
|
|
|
317
|
-
const scaledW = img.
|
|
318
|
-
const scaledH = img.
|
|
330
|
+
const scaledW = img.naturalWidth * zoom;
|
|
331
|
+
const scaledH = img.naturalHeight * zoom;
|
|
319
332
|
|
|
320
333
|
const x = pos.x - scaledW / 2 + CROP_W / 2;
|
|
321
334
|
const y = pos.y - scaledH / 2 + CROP_SIZE / 2;
|
|
@@ -376,24 +389,63 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
376
389
|
const performCrop = () => {
|
|
377
390
|
if (!img) return;
|
|
378
391
|
|
|
379
|
-
const
|
|
380
|
-
const pctx = preview.getContext("2d")!;
|
|
392
|
+
const scaleFactor = 1 / zoom;
|
|
381
393
|
|
|
382
|
-
|
|
383
|
-
|
|
394
|
+
const scaledW = img.naturalWidth * zoom;
|
|
395
|
+
const scaledH = img.naturalHeight * zoom;
|
|
384
396
|
|
|
385
|
-
const
|
|
386
|
-
const
|
|
397
|
+
const cropDisplayX = (scaledW - CROP_W) / 2 - pos.x;
|
|
398
|
+
const cropDisplayY = (scaledH - CROP_SIZE) / 2 - pos.y;
|
|
387
399
|
|
|
388
|
-
const
|
|
389
|
-
const
|
|
400
|
+
const sourceX = cropDisplayX * scaleFactor;
|
|
401
|
+
const sourceY = cropDisplayY * scaleFactor;
|
|
402
|
+
const sourceW = CROP_W * scaleFactor;
|
|
403
|
+
const sourceH = CROP_SIZE * scaleFactor;
|
|
390
404
|
|
|
391
|
-
|
|
405
|
+
const sx = Math.max(0, Math.min(img.naturalWidth - 1, sourceX));
|
|
406
|
+
const sy = Math.max(0, Math.min(img.naturalHeight - 1, sourceY));
|
|
407
|
+
const sw = Math.min(sourceW, img.naturalWidth - sx);
|
|
408
|
+
const sh = Math.min(sourceH, img.naturalHeight - sy);
|
|
392
409
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
410
|
+
let targetW = Math.round(sw);
|
|
411
|
+
let targetH = Math.round(sh);
|
|
412
|
+
|
|
413
|
+
const MAX_DIMENSION = 2560;
|
|
414
|
+
if (targetW > MAX_DIMENSION || targetH > MAX_DIMENSION) {
|
|
415
|
+
if (targetW >= targetH) {
|
|
416
|
+
targetH = Math.round((targetH * MAX_DIMENSION) / targetW);
|
|
417
|
+
targetW = MAX_DIMENSION;
|
|
418
|
+
} else {
|
|
419
|
+
targetW = Math.round((targetW * MAX_DIMENSION) / targetH);
|
|
420
|
+
targetH = MAX_DIMENSION;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const outputCanvas = document.createElement("canvas");
|
|
425
|
+
outputCanvas.width = targetW;
|
|
426
|
+
outputCanvas.height = targetH;
|
|
427
|
+
|
|
428
|
+
const octx = outputCanvas.getContext("2d");
|
|
429
|
+
if (!octx) return;
|
|
430
|
+
|
|
431
|
+
octx.imageSmoothingEnabled = true;
|
|
432
|
+
octx.imageSmoothingQuality = "high";
|
|
433
|
+
|
|
434
|
+
octx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
|
|
435
|
+
|
|
436
|
+
const type = fileType || "image/jpeg";
|
|
437
|
+
const name = fileName || (type === "image/png" ? "cropped.png" : "cropped.jpg");
|
|
438
|
+
const quality = type === "image/png" ? undefined : 0.92;
|
|
439
|
+
|
|
440
|
+
outputCanvas.toBlob(
|
|
441
|
+
(blob) => {
|
|
442
|
+
if (!blob) return;
|
|
443
|
+
const file = new File([blob], name, { type, lastModified: Date.now() });
|
|
444
|
+
onDone?.(file);
|
|
445
|
+
},
|
|
446
|
+
type,
|
|
447
|
+
quality
|
|
448
|
+
);
|
|
397
449
|
};
|
|
398
450
|
|
|
399
451
|
|
|
@@ -557,8 +609,6 @@ export const CanvasCropper: React.FC<CropperProps> = ({
|
|
|
557
609
|
block
|
|
558
610
|
/>
|
|
559
611
|
</div>
|
|
560
|
-
|
|
561
|
-
<canvas ref={previewRef} className="hidden" />
|
|
562
612
|
</div>
|
|
563
613
|
);
|
|
564
614
|
};
|
|
@@ -107,7 +107,7 @@ export type WatchAction = {
|
|
|
107
107
|
hidden ?: boolean
|
|
108
108
|
value ?: any
|
|
109
109
|
required ?: boolean
|
|
110
|
-
|
|
110
|
+
readOnly ?: boolean
|
|
111
111
|
reset ?: boolean
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -389,7 +389,8 @@ export function FormSupervisionComponent({
|
|
|
389
389
|
if (form?.onHide?.(valMap, prefix)) return null;
|
|
390
390
|
|
|
391
391
|
const ws = watchState[name];
|
|
392
|
-
|
|
392
|
+
const isHidden = (form.construction as any)?.hidden ?? ws?.hidden;
|
|
393
|
+
if (isHidden) return null;
|
|
393
394
|
|
|
394
395
|
if (inputType === "cluster") {
|
|
395
396
|
const { name: mapName, fields: innerForms, label, tip, wrap, className, min } = form.construction as ClusterConstruction;
|
|
@@ -492,13 +493,17 @@ export function FormSupervisionComponent({
|
|
|
492
493
|
}
|
|
493
494
|
|
|
494
495
|
const Component = inputMap[inputType] || InputComponent;
|
|
496
|
+
const construction = (form.construction as any) || {};
|
|
497
|
+
const disabled = construction.disabled ?? ws?.disabled;
|
|
498
|
+
const readOnly = construction.readOnly ?? ws?.readOnly;
|
|
499
|
+
|
|
495
500
|
return (
|
|
496
501
|
<div key={key} className={cn(form.className, generateColClass(form.col || "12"))}>
|
|
497
502
|
<Component
|
|
498
|
-
{...
|
|
503
|
+
{...construction}
|
|
499
504
|
{...formControl(name)}
|
|
500
|
-
disabled={
|
|
501
|
-
readOnly={
|
|
505
|
+
disabled={disabled}
|
|
506
|
+
readOnly={readOnly}
|
|
502
507
|
name={name}
|
|
503
508
|
/>
|
|
504
509
|
</div>
|