@996-design/996-image-crop 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 nanxiaobei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,25 @@
1
+ # Upstream attribution
2
+
3
+ The bundled runtime and declarations in `dist/` are derived from
4
+ `antd-img-crop` 4.30.0.
5
+
6
+ - Upstream: https://github.com/nanxiaobei/antd-img-crop/tree/4.30.0
7
+ - License: MIT. See `LICENSE`.
8
+
9
+ Local changes are limited to the package identity, workspace scripts and
10
+ metadata, and rewriting Ant Design imports and peer dependency to
11
+ `@996-design/996-components`. The four Ant Design component subpath imports are
12
+ consolidated into equivalent named imports from the scoped core entry because
13
+ the scoped source package does not expose extensionless directory subpaths.
14
+ The image-crop behavior is otherwise kept aligned with the published 4.30.0
15
+ package.
16
+
17
+ ## Mechanical parity source
18
+
19
+ - npm tarball: https://registry.npmjs.org/antd-img-crop/-/antd-img-crop-4.30.0.tgz
20
+ - Integrity: `sha512-fjpwyCNKtKr22AQcENBix5Y+P8ECM80Ivk/Yn7kF9D0XltYBGJHg+3yycxcGq7ATgnl7pgPYDvN2VzKleogCLQ==`
21
+ - Guard inventory: 3 published runtime/declaration files; sorted-path SHA-256
22
+ `0977ce3f5d3434fb0623d9748d00b53a65e61684536e9cbee6620e4396181ae4`.
23
+
24
+ The guard models the scoped core import consolidation explicitly before hashing.
25
+ Package metadata, local build products, `node_modules`, and caches are excluded.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # 996 Design Image Crop
2
+
3
+ Image cropping extension for upload workflows in 996 Design applications.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @996-design/996-image-crop
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```tsx
14
+ import ImageCrop from "@996-design/996-image-crop";
15
+ import { Upload } from "@996-design/996-components";
16
+
17
+ export function AvatarUpload() {
18
+ return (
19
+ <ImageCrop rotationSlider>
20
+ <Upload listType="picture-card">Upload</Upload>
21
+ </ImageCrop>
22
+ );
23
+ }
24
+ ```
25
+
26
+ The package provides ESM, CJS and TypeScript declaration entry points.
27
+
28
+ ## License
29
+
30
+ MIT. See `NOTICE.md` for license and source attribution.
@@ -0,0 +1,30 @@
1
+ # 996 Design 图片裁剪
2
+
3
+ 面向 996 Design 上传流程的图片裁剪扩展。
4
+
5
+ ## 安装
6
+
7
+ ```sh
8
+ pnpm add @996-design/996-image-crop
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```tsx
14
+ import ImageCrop from "@996-design/996-image-crop";
15
+ import { Upload } from "@996-design/996-components";
16
+
17
+ export function AvatarUpload() {
18
+ return (
19
+ <ImageCrop rotationSlider>
20
+ <Upload listType="picture-card">上传</Upload>
21
+ </ImageCrop>
22
+ );
23
+ }
24
+ ```
25
+
26
+ 包内提供 ESM、CJS 与 TypeScript 类型入口。
27
+
28
+ ## 许可证
29
+
30
+ MIT。许可与来源归因见 `NOTICE.md`。
@@ -0,0 +1,270 @@
1
+ 'use strict';
2
+
3
+ var tslib = require('tslib');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var antd = require('@996-design/996-components');
6
+ var AntModal = antd.Modal;
7
+ var AntUpload = antd.Upload;
8
+ var react = require('react');
9
+ var AntButton = antd.Button;
10
+ var AntSlider = antd.Slider;
11
+ var Cropper = require('react-easy-crop');
12
+
13
+ const PREFIX = 'img-crop';
14
+ const ZOOM_INITIAL = 1;
15
+ const ZOOM_STEP = 0.1;
16
+ const ROTATION_INITIAL = 0;
17
+ const ROTATION_MIN = -180;
18
+ const ROTATION_MAX = 180;
19
+ const ROTATION_STEP = 1;
20
+ const ASPECT_STEP = 0.01;
21
+
22
+ const EasyCrop = react.forwardRef((props, ref) => {
23
+ const { cropperRef, zoomSlider, rotationSlider, aspectSlider, showReset, resetBtnText, modalImage, aspect: propAspect, minZoom, maxZoom, minAspect, maxAspect, cropShape, showGrid, cropperProps, } = props;
24
+ const [crop, setCrop] = react.useState({ x: 0, y: 0 });
25
+ const [zoom, setZoom] = react.useState(ZOOM_INITIAL);
26
+ const [rotation, setRotation] = react.useState(ROTATION_INITIAL);
27
+ const [aspect, setAspect] = react.useState(propAspect);
28
+ const cropPixelsRef = react.useRef({ width: 0, height: 0, x: 0, y: 0 });
29
+ const onCropComplete = react.useCallback((_, croppedAreaPixels) => {
30
+ cropPixelsRef.current = croppedAreaPixels;
31
+ }, []);
32
+ const prevPropAspect = react.useRef(propAspect);
33
+ if (prevPropAspect.current !== propAspect) {
34
+ prevPropAspect.current = propAspect;
35
+ setAspect(propAspect);
36
+ }
37
+ const isResetActive = zoom !== ZOOM_INITIAL ||
38
+ rotation !== ROTATION_INITIAL ||
39
+ aspect !== propAspect;
40
+ const onReset = () => {
41
+ setZoom(ZOOM_INITIAL);
42
+ setRotation(ROTATION_INITIAL);
43
+ setAspect(propAspect);
44
+ };
45
+ react.useImperativeHandle(ref, () => ({
46
+ rotation,
47
+ cropPixelsRef,
48
+ onReset,
49
+ }));
50
+ const wrapperClass = '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]';
51
+ const buttonClass = '[display:flex] [align-items:center] [justify-content:center] [height:32px] [width:32px] [background:transparent] [border:0] [font-family:inherit] [font-size:18px] [cursor:pointer] disabled:[opacity:20%] disabled:[cursor:default]';
52
+ const sliderClass = '[flex:1]';
53
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Cropper, Object.assign({}, cropperProps, { ref: cropperRef, image: modalImage, crop: crop, zoom: zoom, rotation: rotation, aspect: aspect, minZoom: minZoom, maxZoom: maxZoom, zoomWithScroll: zoomSlider, cropShape: cropShape, showGrid: showGrid, onCropChange: setCrop, onZoomChange: setZoom, onRotationChange: setRotation, onCropComplete: onCropComplete, classes: {
54
+ containerClassName: `${PREFIX}-container ![position:relative] [width:100%] [height:40vh] [&~section:first-of-type]:[margin-top:16px] [&~section:last-of-type]:[margin-bottom:16px]`,
55
+ mediaClassName: `${PREFIX}-media`,
56
+ } })), zoomSlider && (jsxRuntime.jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-zoom ${wrapperClass}`, children: [jsxRuntime.jsx("button", { className: buttonClass, onClick: () => setZoom(+(zoom - ZOOM_STEP).toFixed(1)), disabled: zoom - ZOOM_STEP < minZoom, children: "\uFF0D" }), jsxRuntime.jsx(AntSlider, { className: sliderClass, min: minZoom, max: maxZoom, step: ZOOM_STEP, value: zoom, onChange: setZoom }), jsxRuntime.jsx("button", { className: buttonClass, onClick: () => setZoom(+(zoom + ZOOM_STEP).toFixed(1)), disabled: zoom + ZOOM_STEP > maxZoom, children: "\uFF0B" })] })), rotationSlider && (jsxRuntime.jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-rotation ${wrapperClass}`, children: [jsxRuntime.jsx("button", { className: `${buttonClass} [font-size:16px]`, onClick: () => setRotation(rotation - ROTATION_STEP), disabled: rotation === ROTATION_MIN, children: "\u21BA" }), jsxRuntime.jsx(AntSlider, { className: sliderClass, min: ROTATION_MIN, max: ROTATION_MAX, step: ROTATION_STEP, value: rotation, onChange: setRotation }), jsxRuntime.jsx("button", { className: `${buttonClass} [font-size:16px]`, onClick: () => setRotation(rotation + ROTATION_STEP), disabled: rotation === ROTATION_MAX, children: "\u21BB" })] })), aspectSlider && (jsxRuntime.jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-aspect ${wrapperClass}`, children: [jsxRuntime.jsx("button", { className: buttonClass, onClick: () => setAspect(+(aspect - ASPECT_STEP).toFixed(2)), disabled: aspect - ASPECT_STEP < minAspect, children: "\u2195" }), jsxRuntime.jsx(AntSlider, { className: sliderClass, min: minAspect, max: maxAspect, step: ASPECT_STEP, value: aspect, onChange: setAspect }), jsxRuntime.jsx("button", { className: buttonClass, onClick: () => setAspect(+(aspect + ASPECT_STEP).toFixed(2)), disabled: aspect + ASPECT_STEP > maxAspect, children: "\u2194" })] })), showReset && (zoomSlider || rotationSlider || aspectSlider) && (jsxRuntime.jsx(AntButton, { className: "[position:absolute] [bottom:20px]", style: isResetActive ? {} : { opacity: 0.3, pointerEvents: 'none' }, onClick: onReset, children: resetBtnText }))] }));
57
+ });
58
+ var EasyCrop$1 = react.memo(EasyCrop);
59
+
60
+ var css_248z = "/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */.visible{visibility:visible}.\\!\\[position\\:relative\\]{position:relative!important}.\\[position\\:absolute\\]{position:absolute}.\\[bottom\\:20px\\]{bottom:20px}.container{width:100%}.\\[margin-inline\\:auto\\]{margin-inline:auto}.\\[display\\:flex\\]{display:flex}.grid{display:grid}.\\[height\\:32px\\]{height:32px}.\\[height\\:40vh\\]{height:40vh}.\\[width\\:32px\\]{width:32px}.\\[width\\:60\\%\\]{width:60%}.\\[width\\:100\\%\\]{width:100%}.\\[flex\\:1\\]{flex:1}.\\[cursor\\:pointer\\]{cursor:pointer}.\\[align-items\\:center\\]{align-items:center}.\\[justify-content\\:center\\]{justify-content:center}.\\[font-family\\:inherit\\]{font-family:inherit}.\\[font-size\\:16px\\]{font-size:16px}.\\[font-size\\:18px\\]{font-size:18px}.\\[background\\:transparent\\]{background:transparent}.\\[border\\:0\\]{border:0}.disabled\\:\\[cursor\\:default\\]{&:disabled{cursor:default}}.disabled\\:\\[opacity\\:20\\%\\]{&:disabled{opacity:20%}}.\\[\\&\\~section\\:first-of-type\\]\\:\\[margin-top\\:16px\\]{&~section:first-of-type{margin-top:16px}}.\\[\\&\\~section\\:last-of-type\\]\\:\\[margin-bottom\\:16px\\]{&~section:last-of-type{margin-bottom:16px}}";
61
+ (function() {
62
+ if (typeof document === 'undefined') return;
63
+ const style = document.createElement('style');
64
+ const meta = document.querySelector('meta[name="csp-nonce"]');
65
+ if (meta && meta.content) style.setAttribute('nonce', meta.content);
66
+ style.textContent = css_248z;
67
+ document.head.appendChild(style);
68
+ })();
69
+
70
+ // v1 >= v2
71
+ const isGeThan = (v1, v2) => {
72
+ var _a, _b;
73
+ const arr1 = v1.split('.').map(Number);
74
+ const arr2 = v2.split('.').map(Number);
75
+ const len = Math.max(arr1.length, arr2.length);
76
+ for (let i = 0; i < len; i++) {
77
+ const a = (_a = arr1[i]) !== null && _a !== void 0 ? _a : 0;
78
+ const b = (_b = arr2[i]) !== null && _b !== void 0 ? _b : 0;
79
+ if (a > b)
80
+ return true;
81
+ if (a < b)
82
+ return false;
83
+ }
84
+ return true;
85
+ };
86
+ const ImgCrop = react.forwardRef((props, cropperRef) => {
87
+ const { quality = 0.4, fillColor = 'white', zoomSlider = true, rotationSlider = false, aspectSlider = false, showReset = false, resetText, aspect = 1, minZoom = 1, maxZoom = 3, minAspect = 0.5, maxAspect = 2, cropShape = 'rect', showGrid = false, cropperProps, modalClassName, modalTitle, modalWidth, modalOk, modalCancel, onModalOk, onModalCancel, modalProps, beforeCrop, children, } = props;
88
+ const cb = react.useRef({});
89
+ cb.current.onModalOk = onModalOk;
90
+ cb.current.onModalCancel = onModalCancel;
91
+ cb.current.beforeCrop = beforeCrop;
92
+ /**
93
+ * crop
94
+ */
95
+ const easyCropRef = react.useRef(null);
96
+ const getCropCanvas = react.useCallback((target) => {
97
+ var _a;
98
+ const canvas = document.createElement('canvas');
99
+ const ctx = canvas.getContext('2d');
100
+ const context = ((_a = target === null || target === void 0 ? void 0 : target.getRootNode) === null || _a === void 0 ? void 0 : _a.call(target)) || document;
101
+ const imgSource = context.querySelector(`.${PREFIX}-media`);
102
+ const { width: cropWidth, height: cropHeight, x: cropX, y: cropY, } = easyCropRef.current.cropPixelsRef.current;
103
+ if (rotationSlider &&
104
+ easyCropRef.current.rotation !== ROTATION_INITIAL) {
105
+ const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource;
106
+ const angle = easyCropRef.current.rotation * (Math.PI / 180);
107
+ // get container for rotated image
108
+ const sine = Math.abs(Math.sin(angle));
109
+ const cosine = Math.abs(Math.cos(angle));
110
+ const squareWidth = imgWidth * cosine + imgHeight * sine;
111
+ const squareHeight = imgHeight * cosine + imgWidth * sine;
112
+ canvas.width = squareWidth;
113
+ canvas.height = squareHeight;
114
+ ctx.fillStyle = fillColor;
115
+ ctx.fillRect(0, 0, squareWidth, squareHeight);
116
+ // rotate container
117
+ const squareHalfWidth = squareWidth / 2;
118
+ const squareHalfHeight = squareHeight / 2;
119
+ ctx.translate(squareHalfWidth, squareHalfHeight);
120
+ ctx.rotate(angle);
121
+ ctx.translate(-squareHalfWidth, -squareHalfHeight);
122
+ // draw rotated image
123
+ const imgX = (squareWidth - imgWidth) / 2;
124
+ const imgY = (squareHeight - imgHeight) / 2;
125
+ ctx.drawImage(imgSource, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight);
126
+ // crop rotated image
127
+ const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight);
128
+ canvas.width = cropWidth;
129
+ canvas.height = cropHeight;
130
+ ctx.putImageData(imgData, -cropX, -cropY);
131
+ }
132
+ else {
133
+ canvas.width = cropWidth;
134
+ canvas.height = cropHeight;
135
+ ctx.fillStyle = fillColor;
136
+ ctx.fillRect(0, 0, cropWidth, cropHeight);
137
+ ctx.drawImage(imgSource, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
138
+ }
139
+ return canvas;
140
+ }, [fillColor, rotationSlider]);
141
+ /**
142
+ * upload
143
+ */
144
+ const [modalOpen, setModalOpen] = react.useState(false);
145
+ const [modalImage, setModalImage] = react.useState('');
146
+ const onCancel = react.useRef(undefined);
147
+ const onOk = react.useRef(undefined);
148
+ const runBeforeUpload = react.useCallback((_a) => tslib.__awaiter(void 0, [_a], void 0, function* ({ beforeUpload, file, resolve, reject, }) {
149
+ const rawFile = file;
150
+ if (typeof beforeUpload !== 'function') {
151
+ resolve(rawFile);
152
+ return;
153
+ }
154
+ try {
155
+ // https://ant.design/components/upload-cn#api
156
+ // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178
157
+ const result = yield beforeUpload(file, [file]);
158
+ if (result === false) {
159
+ resolve(false);
160
+ }
161
+ else {
162
+ resolve((result !== true && result) || rawFile);
163
+ }
164
+ }
165
+ catch (err) {
166
+ reject(err);
167
+ }
168
+ }), []);
169
+ const getNewBeforeUpload = react.useCallback((beforeUpload) => {
170
+ return ((file, fileList) => {
171
+ return new Promise((resolve, reject) => tslib.__awaiter(void 0, void 0, void 0, function* () {
172
+ let processedFile = file;
173
+ if (typeof cb.current.beforeCrop === 'function') {
174
+ try {
175
+ const result = yield cb.current.beforeCrop(file, fileList);
176
+ if (result === false) {
177
+ return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
178
+ }
179
+ if (result !== true) {
180
+ processedFile = result || file; // will open modal
181
+ }
182
+ }
183
+ catch (err) {
184
+ return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
185
+ }
186
+ }
187
+ // read file
188
+ const reader = new FileReader();
189
+ reader.addEventListener('load', () => {
190
+ if (typeof reader.result === 'string') {
191
+ setModalOpen(true);
192
+ setTimeout(() => {
193
+ setModalImage(reader.result);
194
+ }, 10);
195
+ }
196
+ });
197
+ reader.readAsDataURL(processedFile);
198
+ // on modal cancel
199
+ onCancel.current = () => {
200
+ var _a, _b;
201
+ setModalOpen(false);
202
+ setModalImage('');
203
+ easyCropRef.current.onReset();
204
+ let hasResolveCalled = false;
205
+ (_b = (_a = cb.current).onModalCancel) === null || _b === void 0 ? void 0 : _b.call(_a, (LIST_IGNORE) => {
206
+ resolve(LIST_IGNORE);
207
+ hasResolveCalled = true;
208
+ });
209
+ if (!hasResolveCalled) {
210
+ resolve(AntUpload.LIST_IGNORE);
211
+ }
212
+ };
213
+ // on modal confirm
214
+ onOk.current = (event) => tslib.__awaiter(void 0, void 0, void 0, function* () {
215
+ setModalOpen(false);
216
+ setModalImage('');
217
+ easyCropRef.current.onReset();
218
+ const canvas = getCropCanvas(event.target);
219
+ const { type, name, uid } = processedFile;
220
+ canvas.toBlob((blob) => tslib.__awaiter(void 0, void 0, void 0, function* () {
221
+ const newFile = new File([blob], name, { type });
222
+ Object.assign(newFile, { uid });
223
+ runBeforeUpload({
224
+ beforeUpload,
225
+ file: newFile,
226
+ resolve: (file) => {
227
+ var _a, _b;
228
+ resolve(file);
229
+ (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, file);
230
+ },
231
+ reject: (err) => {
232
+ var _a, _b;
233
+ reject(err);
234
+ (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, err);
235
+ },
236
+ });
237
+ }), type, quality);
238
+ });
239
+ }));
240
+ });
241
+ }, [getCropCanvas, quality, runBeforeUpload]);
242
+ const getNewUpload = react.useCallback((children) => {
243
+ const upload = Array.isArray(children) ? children[0] : children;
244
+ const _a = upload.props, { beforeUpload, accept } = _a, restUploadProps = tslib.__rest(_a, ["beforeUpload", "accept"]);
245
+ return Object.assign(Object.assign({}, upload), { props: Object.assign(Object.assign({}, restUploadProps), { accept: accept || 'image/*', beforeUpload: getNewBeforeUpload(beforeUpload) }) });
246
+ }, [getNewBeforeUpload]);
247
+ /**
248
+ * modal
249
+ */
250
+ const modalBaseProps = react.useMemo(() => {
251
+ const obj = {};
252
+ if (modalWidth !== undefined)
253
+ obj.width = modalWidth;
254
+ if (modalOk !== undefined)
255
+ obj.okText = modalOk;
256
+ if (modalCancel !== undefined)
257
+ obj.cancelText = modalCancel;
258
+ return obj;
259
+ }, [modalCancel, modalOk, modalWidth]);
260
+ const wrapClassName = `${PREFIX}-modal${modalClassName ? ` ${modalClassName}` : ''}`;
261
+ const lang = typeof window === 'undefined' ? '' : window.navigator.language;
262
+ const isCN = lang === 'zh-CN';
263
+ const title = modalTitle || (isCN ? '编辑图片' : 'Edit image');
264
+ const resetBtnText = resetText || (isCN ? '重置' : 'Reset');
265
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [getNewUpload(children), jsxRuntime.jsx(AntModal, Object.assign({}, modalProps, modalBaseProps, { open: modalOpen, title: title, onCancel: onCancel.current, onOk: onOk.current, wrapClassName: wrapClassName, destroyOnHidden: true }, (isGeThan(antd.version, '6.3.1')
266
+ ? { mask: { closable: false } }
267
+ : { maskClosable: true }), { children: jsxRuntime.jsx(EasyCrop$1, { ref: easyCropRef, cropperRef: cropperRef, zoomSlider: zoomSlider, rotationSlider: rotationSlider, aspectSlider: aspectSlider, showReset: showReset, resetBtnText: resetBtnText, modalImage: modalImage, aspect: aspect, minZoom: minZoom, maxZoom: maxZoom, minAspect: minAspect, maxAspect: maxAspect, cropShape: cropShape, showGrid: showGrid, cropperProps: cropperProps }) }))] }));
268
+ });
269
+
270
+ module.exports = ImgCrop;
@@ -0,0 +1,39 @@
1
+ import * as react from 'react';
2
+ import { JSX } from 'react';
3
+ import Cropper, { CropperProps } from 'react-easy-crop';
4
+ import { UploadProps, ModalProps } from '@996-design/996-components';
5
+
6
+ type BeforeUpload = Exclude<UploadProps['beforeUpload'], undefined>;
7
+ type BeforeUploadReturnType = ReturnType<BeforeUpload>;
8
+ type ImgCropProps = {
9
+ quality?: number;
10
+ fillColor?: string;
11
+ zoomSlider?: boolean;
12
+ rotationSlider?: boolean;
13
+ aspectSlider?: boolean;
14
+ showReset?: boolean;
15
+ resetText?: string;
16
+ aspect?: number;
17
+ minZoom?: number;
18
+ maxZoom?: number;
19
+ minAspect?: number;
20
+ maxAspect?: number;
21
+ cropShape?: 'rect' | 'round';
22
+ showGrid?: boolean;
23
+ cropperProps?: Omit<CropperProps, 'image' | 'crop' | 'zoom' | 'rotation' | 'aspect' | 'minZoom' | 'maxZoom' | 'minAspect' | 'maxAspect' | 'zoomWithScroll' | 'cropShape' | 'showGrid' | 'onCropChange' | 'onZoomChange' | 'onRotationChange' | 'onCropComplete' | 'classes' | 'keyboardStep'> & Partial<Pick<CropperProps, 'keyboardStep'>>;
24
+ modalClassName?: string;
25
+ modalTitle?: string;
26
+ modalWidth?: number | string;
27
+ modalOk?: string;
28
+ modalCancel?: string;
29
+ onModalOk?: (value: BeforeUploadReturnType) => void;
30
+ onModalCancel?: (resolve: (value: BeforeUploadReturnType) => void) => void;
31
+ modalProps?: Omit<ModalProps, 'className' | 'title' | 'width' | 'okText' | 'cancelText' | 'onOk' | 'onCancel' | 'open' | 'visible' | 'wrapClassName' | 'maskClosable' | 'destroyOnHidden'>;
32
+ beforeCrop?: BeforeUpload;
33
+ children: JSX.Element;
34
+ };
35
+
36
+ declare const ImgCrop: react.ForwardRefExoticComponent<ImgCropProps & react.RefAttributes<Cropper>>;
37
+
38
+ export { ImgCrop as default };
39
+ export type { ImgCropProps };
@@ -0,0 +1,264 @@
1
+ import { __awaiter, __rest } from 'tslib';
2
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
+ import { Button as AntButton, Modal as AntModal, Slider as AntSlider, Upload as AntUpload, version } from '@996-design/996-components';
4
+ import { forwardRef, useState, useRef, useCallback, useImperativeHandle, memo, useMemo } from 'react';
5
+ import Cropper from 'react-easy-crop';
6
+
7
+ const PREFIX = 'img-crop';
8
+ const ZOOM_INITIAL = 1;
9
+ const ZOOM_STEP = 0.1;
10
+ const ROTATION_INITIAL = 0;
11
+ const ROTATION_MIN = -180;
12
+ const ROTATION_MAX = 180;
13
+ const ROTATION_STEP = 1;
14
+ const ASPECT_STEP = 0.01;
15
+
16
+ const EasyCrop = forwardRef((props, ref) => {
17
+ const { cropperRef, zoomSlider, rotationSlider, aspectSlider, showReset, resetBtnText, modalImage, aspect: propAspect, minZoom, maxZoom, minAspect, maxAspect, cropShape, showGrid, cropperProps, } = props;
18
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
19
+ const [zoom, setZoom] = useState(ZOOM_INITIAL);
20
+ const [rotation, setRotation] = useState(ROTATION_INITIAL);
21
+ const [aspect, setAspect] = useState(propAspect);
22
+ const cropPixelsRef = useRef({ width: 0, height: 0, x: 0, y: 0 });
23
+ const onCropComplete = useCallback((_, croppedAreaPixels) => {
24
+ cropPixelsRef.current = croppedAreaPixels;
25
+ }, []);
26
+ const prevPropAspect = useRef(propAspect);
27
+ if (prevPropAspect.current !== propAspect) {
28
+ prevPropAspect.current = propAspect;
29
+ setAspect(propAspect);
30
+ }
31
+ const isResetActive = zoom !== ZOOM_INITIAL ||
32
+ rotation !== ROTATION_INITIAL ||
33
+ aspect !== propAspect;
34
+ const onReset = () => {
35
+ setZoom(ZOOM_INITIAL);
36
+ setRotation(ROTATION_INITIAL);
37
+ setAspect(propAspect);
38
+ };
39
+ useImperativeHandle(ref, () => ({
40
+ rotation,
41
+ cropPixelsRef,
42
+ onReset,
43
+ }));
44
+ const wrapperClass = '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]';
45
+ const buttonClass = '[display:flex] [align-items:center] [justify-content:center] [height:32px] [width:32px] [background:transparent] [border:0] [font-family:inherit] [font-size:18px] [cursor:pointer] disabled:[opacity:20%] disabled:[cursor:default]';
46
+ const sliderClass = '[flex:1]';
47
+ return (jsxs(Fragment, { children: [jsx(Cropper, Object.assign({}, cropperProps, { ref: cropperRef, image: modalImage, crop: crop, zoom: zoom, rotation: rotation, aspect: aspect, minZoom: minZoom, maxZoom: maxZoom, zoomWithScroll: zoomSlider, cropShape: cropShape, showGrid: showGrid, onCropChange: setCrop, onZoomChange: setZoom, onRotationChange: setRotation, onCropComplete: onCropComplete, classes: {
48
+ containerClassName: `${PREFIX}-container ![position:relative] [width:100%] [height:40vh] [&~section:first-of-type]:[margin-top:16px] [&~section:last-of-type]:[margin-bottom:16px]`,
49
+ mediaClassName: `${PREFIX}-media`,
50
+ } })), zoomSlider && (jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-zoom ${wrapperClass}`, children: [jsx("button", { className: buttonClass, onClick: () => setZoom(+(zoom - ZOOM_STEP).toFixed(1)), disabled: zoom - ZOOM_STEP < minZoom, children: "\uFF0D" }), jsx(AntSlider, { className: sliderClass, min: minZoom, max: maxZoom, step: ZOOM_STEP, value: zoom, onChange: setZoom }), jsx("button", { className: buttonClass, onClick: () => setZoom(+(zoom + ZOOM_STEP).toFixed(1)), disabled: zoom + ZOOM_STEP > maxZoom, children: "\uFF0B" })] })), rotationSlider && (jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-rotation ${wrapperClass}`, children: [jsx("button", { className: `${buttonClass} [font-size:16px]`, onClick: () => setRotation(rotation - ROTATION_STEP), disabled: rotation === ROTATION_MIN, children: "\u21BA" }), jsx(AntSlider, { className: sliderClass, min: ROTATION_MIN, max: ROTATION_MAX, step: ROTATION_STEP, value: rotation, onChange: setRotation }), jsx("button", { className: `${buttonClass} [font-size:16px]`, onClick: () => setRotation(rotation + ROTATION_STEP), disabled: rotation === ROTATION_MAX, children: "\u21BB" })] })), aspectSlider && (jsxs("section", { className: `${PREFIX}-control ${PREFIX}-control-aspect ${wrapperClass}`, children: [jsx("button", { className: buttonClass, onClick: () => setAspect(+(aspect - ASPECT_STEP).toFixed(2)), disabled: aspect - ASPECT_STEP < minAspect, children: "\u2195" }), jsx(AntSlider, { className: sliderClass, min: minAspect, max: maxAspect, step: ASPECT_STEP, value: aspect, onChange: setAspect }), jsx("button", { className: buttonClass, onClick: () => setAspect(+(aspect + ASPECT_STEP).toFixed(2)), disabled: aspect + ASPECT_STEP > maxAspect, children: "\u2194" })] })), showReset && (zoomSlider || rotationSlider || aspectSlider) && (jsx(AntButton, { className: "[position:absolute] [bottom:20px]", style: isResetActive ? {} : { opacity: 0.3, pointerEvents: 'none' }, onClick: onReset, children: resetBtnText }))] }));
51
+ });
52
+ var EasyCrop$1 = memo(EasyCrop);
53
+
54
+ var css_248z = "/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */.visible{visibility:visible}.\\!\\[position\\:relative\\]{position:relative!important}.\\[position\\:absolute\\]{position:absolute}.\\[bottom\\:20px\\]{bottom:20px}.container{width:100%}.\\[margin-inline\\:auto\\]{margin-inline:auto}.\\[display\\:flex\\]{display:flex}.grid{display:grid}.\\[height\\:32px\\]{height:32px}.\\[height\\:40vh\\]{height:40vh}.\\[width\\:32px\\]{width:32px}.\\[width\\:60\\%\\]{width:60%}.\\[width\\:100\\%\\]{width:100%}.\\[flex\\:1\\]{flex:1}.\\[cursor\\:pointer\\]{cursor:pointer}.\\[align-items\\:center\\]{align-items:center}.\\[justify-content\\:center\\]{justify-content:center}.\\[font-family\\:inherit\\]{font-family:inherit}.\\[font-size\\:16px\\]{font-size:16px}.\\[font-size\\:18px\\]{font-size:18px}.\\[background\\:transparent\\]{background:transparent}.\\[border\\:0\\]{border:0}.disabled\\:\\[cursor\\:default\\]{&:disabled{cursor:default}}.disabled\\:\\[opacity\\:20\\%\\]{&:disabled{opacity:20%}}.\\[\\&\\~section\\:first-of-type\\]\\:\\[margin-top\\:16px\\]{&~section:first-of-type{margin-top:16px}}.\\[\\&\\~section\\:last-of-type\\]\\:\\[margin-bottom\\:16px\\]{&~section:last-of-type{margin-bottom:16px}}";
55
+ (function() {
56
+ if (typeof document === 'undefined') return;
57
+ const style = document.createElement('style');
58
+ const meta = document.querySelector('meta[name="csp-nonce"]');
59
+ if (meta && meta.content) style.setAttribute('nonce', meta.content);
60
+ style.textContent = css_248z;
61
+ document.head.appendChild(style);
62
+ })();
63
+
64
+ // v1 >= v2
65
+ const isGeThan = (v1, v2) => {
66
+ var _a, _b;
67
+ const arr1 = v1.split('.').map(Number);
68
+ const arr2 = v2.split('.').map(Number);
69
+ const len = Math.max(arr1.length, arr2.length);
70
+ for (let i = 0; i < len; i++) {
71
+ const a = (_a = arr1[i]) !== null && _a !== void 0 ? _a : 0;
72
+ const b = (_b = arr2[i]) !== null && _b !== void 0 ? _b : 0;
73
+ if (a > b)
74
+ return true;
75
+ if (a < b)
76
+ return false;
77
+ }
78
+ return true;
79
+ };
80
+ const ImgCrop = forwardRef((props, cropperRef) => {
81
+ const { quality = 0.4, fillColor = 'white', zoomSlider = true, rotationSlider = false, aspectSlider = false, showReset = false, resetText, aspect = 1, minZoom = 1, maxZoom = 3, minAspect = 0.5, maxAspect = 2, cropShape = 'rect', showGrid = false, cropperProps, modalClassName, modalTitle, modalWidth, modalOk, modalCancel, onModalOk, onModalCancel, modalProps, beforeCrop, children, } = props;
82
+ const cb = useRef({});
83
+ cb.current.onModalOk = onModalOk;
84
+ cb.current.onModalCancel = onModalCancel;
85
+ cb.current.beforeCrop = beforeCrop;
86
+ /**
87
+ * crop
88
+ */
89
+ const easyCropRef = useRef(null);
90
+ const getCropCanvas = useCallback((target) => {
91
+ var _a;
92
+ const canvas = document.createElement('canvas');
93
+ const ctx = canvas.getContext('2d');
94
+ const context = ((_a = target === null || target === void 0 ? void 0 : target.getRootNode) === null || _a === void 0 ? void 0 : _a.call(target)) || document;
95
+ const imgSource = context.querySelector(`.${PREFIX}-media`);
96
+ const { width: cropWidth, height: cropHeight, x: cropX, y: cropY, } = easyCropRef.current.cropPixelsRef.current;
97
+ if (rotationSlider &&
98
+ easyCropRef.current.rotation !== ROTATION_INITIAL) {
99
+ const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource;
100
+ const angle = easyCropRef.current.rotation * (Math.PI / 180);
101
+ // get container for rotated image
102
+ const sine = Math.abs(Math.sin(angle));
103
+ const cosine = Math.abs(Math.cos(angle));
104
+ const squareWidth = imgWidth * cosine + imgHeight * sine;
105
+ const squareHeight = imgHeight * cosine + imgWidth * sine;
106
+ canvas.width = squareWidth;
107
+ canvas.height = squareHeight;
108
+ ctx.fillStyle = fillColor;
109
+ ctx.fillRect(0, 0, squareWidth, squareHeight);
110
+ // rotate container
111
+ const squareHalfWidth = squareWidth / 2;
112
+ const squareHalfHeight = squareHeight / 2;
113
+ ctx.translate(squareHalfWidth, squareHalfHeight);
114
+ ctx.rotate(angle);
115
+ ctx.translate(-squareHalfWidth, -squareHalfHeight);
116
+ // draw rotated image
117
+ const imgX = (squareWidth - imgWidth) / 2;
118
+ const imgY = (squareHeight - imgHeight) / 2;
119
+ ctx.drawImage(imgSource, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight);
120
+ // crop rotated image
121
+ const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight);
122
+ canvas.width = cropWidth;
123
+ canvas.height = cropHeight;
124
+ ctx.putImageData(imgData, -cropX, -cropY);
125
+ }
126
+ else {
127
+ canvas.width = cropWidth;
128
+ canvas.height = cropHeight;
129
+ ctx.fillStyle = fillColor;
130
+ ctx.fillRect(0, 0, cropWidth, cropHeight);
131
+ ctx.drawImage(imgSource, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
132
+ }
133
+ return canvas;
134
+ }, [fillColor, rotationSlider]);
135
+ /**
136
+ * upload
137
+ */
138
+ const [modalOpen, setModalOpen] = useState(false);
139
+ const [modalImage, setModalImage] = useState('');
140
+ const onCancel = useRef(undefined);
141
+ const onOk = useRef(undefined);
142
+ const runBeforeUpload = useCallback((_a) => __awaiter(void 0, [_a], void 0, function* ({ beforeUpload, file, resolve, reject, }) {
143
+ const rawFile = file;
144
+ if (typeof beforeUpload !== 'function') {
145
+ resolve(rawFile);
146
+ return;
147
+ }
148
+ try {
149
+ // https://ant.design/components/upload-cn#api
150
+ // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178
151
+ const result = yield beforeUpload(file, [file]);
152
+ if (result === false) {
153
+ resolve(false);
154
+ }
155
+ else {
156
+ resolve((result !== true && result) || rawFile);
157
+ }
158
+ }
159
+ catch (err) {
160
+ reject(err);
161
+ }
162
+ }), []);
163
+ const getNewBeforeUpload = useCallback((beforeUpload) => {
164
+ return ((file, fileList) => {
165
+ return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
166
+ let processedFile = file;
167
+ if (typeof cb.current.beforeCrop === 'function') {
168
+ try {
169
+ const result = yield cb.current.beforeCrop(file, fileList);
170
+ if (result === false) {
171
+ return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
172
+ }
173
+ if (result !== true) {
174
+ processedFile = result || file; // will open modal
175
+ }
176
+ }
177
+ catch (err) {
178
+ return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal
179
+ }
180
+ }
181
+ // read file
182
+ const reader = new FileReader();
183
+ reader.addEventListener('load', () => {
184
+ if (typeof reader.result === 'string') {
185
+ setModalOpen(true);
186
+ setTimeout(() => {
187
+ setModalImage(reader.result);
188
+ }, 10);
189
+ }
190
+ });
191
+ reader.readAsDataURL(processedFile);
192
+ // on modal cancel
193
+ onCancel.current = () => {
194
+ var _a, _b;
195
+ setModalOpen(false);
196
+ setModalImage('');
197
+ easyCropRef.current.onReset();
198
+ let hasResolveCalled = false;
199
+ (_b = (_a = cb.current).onModalCancel) === null || _b === void 0 ? void 0 : _b.call(_a, (LIST_IGNORE) => {
200
+ resolve(LIST_IGNORE);
201
+ hasResolveCalled = true;
202
+ });
203
+ if (!hasResolveCalled) {
204
+ resolve(AntUpload.LIST_IGNORE);
205
+ }
206
+ };
207
+ // on modal confirm
208
+ onOk.current = (event) => __awaiter(void 0, void 0, void 0, function* () {
209
+ setModalOpen(false);
210
+ setModalImage('');
211
+ easyCropRef.current.onReset();
212
+ const canvas = getCropCanvas(event.target);
213
+ const { type, name, uid } = processedFile;
214
+ canvas.toBlob((blob) => __awaiter(void 0, void 0, void 0, function* () {
215
+ const newFile = new File([blob], name, { type });
216
+ Object.assign(newFile, { uid });
217
+ runBeforeUpload({
218
+ beforeUpload,
219
+ file: newFile,
220
+ resolve: (file) => {
221
+ var _a, _b;
222
+ resolve(file);
223
+ (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, file);
224
+ },
225
+ reject: (err) => {
226
+ var _a, _b;
227
+ reject(err);
228
+ (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, err);
229
+ },
230
+ });
231
+ }), type, quality);
232
+ });
233
+ }));
234
+ });
235
+ }, [getCropCanvas, quality, runBeforeUpload]);
236
+ const getNewUpload = useCallback((children) => {
237
+ const upload = Array.isArray(children) ? children[0] : children;
238
+ const _a = upload.props, { beforeUpload, accept } = _a, restUploadProps = __rest(_a, ["beforeUpload", "accept"]);
239
+ return Object.assign(Object.assign({}, upload), { props: Object.assign(Object.assign({}, restUploadProps), { accept: accept || 'image/*', beforeUpload: getNewBeforeUpload(beforeUpload) }) });
240
+ }, [getNewBeforeUpload]);
241
+ /**
242
+ * modal
243
+ */
244
+ const modalBaseProps = useMemo(() => {
245
+ const obj = {};
246
+ if (modalWidth !== undefined)
247
+ obj.width = modalWidth;
248
+ if (modalOk !== undefined)
249
+ obj.okText = modalOk;
250
+ if (modalCancel !== undefined)
251
+ obj.cancelText = modalCancel;
252
+ return obj;
253
+ }, [modalCancel, modalOk, modalWidth]);
254
+ const wrapClassName = `${PREFIX}-modal${modalClassName ? ` ${modalClassName}` : ''}`;
255
+ const lang = typeof window === 'undefined' ? '' : window.navigator.language;
256
+ const isCN = lang === 'zh-CN';
257
+ const title = modalTitle || (isCN ? '编辑图片' : 'Edit image');
258
+ const resetBtnText = resetText || (isCN ? '重置' : 'Reset');
259
+ return (jsxs(Fragment, { children: [getNewUpload(children), jsx(AntModal, Object.assign({}, modalProps, modalBaseProps, { open: modalOpen, title: title, onCancel: onCancel.current, onOk: onOk.current, wrapClassName: wrapClassName, destroyOnHidden: true }, (isGeThan(version, '6.3.1')
260
+ ? { mask: { closable: false } }
261
+ : { maskClosable: true }), { children: jsx(EasyCrop$1, { ref: easyCropRef, cropperRef: cropperRef, zoomSlider: zoomSlider, rotationSlider: rotationSlider, aspectSlider: aspectSlider, showReset: showReset, resetBtnText: resetBtnText, modalImage: modalImage, aspect: aspect, minZoom: minZoom, maxZoom: maxZoom, minAspect: minAspect, maxAspect: maxAspect, cropShape: cropShape, showGrid: showGrid, cropperProps: cropperProps }) }))] }));
262
+ });
263
+
264
+ export { ImgCrop as default };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@996-design/996-image-crop",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Image cropping extension for 996 Design upload workflows",
6
+ "keywords": [
7
+ "996-design",
8
+ "react",
9
+ "upload",
10
+ "image-crop"
11
+ ],
12
+ "license": "MIT",
13
+ "types": "./dist/image-crop.d.ts",
14
+ "module": "./dist/image-crop.esm.js",
15
+ "main": "./dist/image-crop.cjs.js",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/image-crop.d.ts",
19
+ "import": "./dist/image-crop.esm.js",
20
+ "require": "./dist/image-crop.cjs.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "NOTICE.md",
26
+ "README.md",
27
+ "README.zh-CN.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "check": "bun -e \"await import('./dist/image-crop.esm.js')\"",
32
+ "build": "bun run check",
33
+ "check:standalone": "bun scripts/check-standalone.ts"
34
+ },
35
+ "peerDependencies": {
36
+ "@996-design/996-components": "^1.0.0",
37
+ "react": ">=16.8.0",
38
+ "react-dom": ">=16.8.0"
39
+ },
40
+ "dependencies": {
41
+ "react-easy-crop": "^5.5.6",
42
+ "tslib": "^2.8.1"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "registry": "https://registry.npmjs.org"
47
+ },
48
+ "packageManager": "pnpm@9.15.0",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+ssh://git@gitlab.asdnc.cn/996-design/996-image-crop.git"
52
+ }
53
+ }