@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 +21 -0
- package/NOTICE.md +25 -0
- package/README.md +30 -0
- package/README.zh-CN.md +30 -0
- package/dist/image-crop.cjs.js +270 -0
- package/dist/image-crop.d.ts +39 -0
- package/dist/image-crop.esm.js +264 -0
- package/package.json +53 -0
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.
|
package/README.zh-CN.md
ADDED
|
@@ -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
|
+
}
|