UNPKG

antd-img-crop

Version:

An image cropper for Ant Design Upload

271 lines (264 loc) 17.1 kB
'use strict'; var tslib = require('tslib'); var jsxRuntime = require('react/jsx-runtime'); var antd = require('antd'); var AntModal = require('antd/es/modal'); var AntUpload = require('antd/es/upload'); var react = require('react'); var AntButton = require('antd/es/button'); var AntSlider = require('antd/es/slider'); var Cropper = require('react-easy-crop'); const PREFIX = 'img-crop'; const ZOOM_INITIAL = 1; const ZOOM_STEP = 0.1; const ROTATION_INITIAL = 0; const ROTATION_MIN = -180; const ROTATION_MAX = 180; const ROTATION_STEP = 1; const ASPECT_STEP = 0.01; const EasyCrop = react.forwardRef((props, ref) => { const { cropperRef, zoomSlider, rotationSlider, aspectSlider, showReset, resetBtnText, modalImage, aspect: propAspect, minZoom, maxZoom, minAspect, maxAspect, cropShape, showGrid, cropperProps, } = props; const [crop, setCrop] = react.useState({ x: 0, y: 0 }); const [zoom, setZoom] = react.useState(ZOOM_INITIAL); const [rotation, setRotation] = react.useState(ROTATION_INITIAL); const [aspect, setAspect] = react.useState(propAspect); const cropPixelsRef = react.useRef({ width: 0, height: 0, x: 0, y: 0 }); const onCropComplete = react.useCallback((_, croppedAreaPixels) => { cropPixelsRef.current = croppedAreaPixels; }, []); const prevPropAspect = react.useRef(propAspect); if (prevPropAspect.current !== propAspect) { prevPropAspect.current = propAspect; setAspect(propAspect); } const isResetActive = zoom !== ZOOM_INITIAL || rotation !== ROTATION_INITIAL || aspect !== propAspect; const onReset = () => { setZoom(ZOOM_INITIAL); setRotation(ROTATION_INITIAL); setAspect(propAspect); }; react.useImperativeHandle(ref, () => ({ rotation, cropPixelsRef, onReset, })); const wrapperClass = '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]'; 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]'; const sliderClass = '[flex:1]'; 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: { containerClassName: `${PREFIX}-container ![position:relative] [width:100%] [height:40vh] [&~section:first-of-type]:[margin-top:16px] [&~section:last-of-type]:[margin-bottom:16px]`, mediaClassName: `${PREFIX}-media`, } })), 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 }))] })); }); var EasyCrop$1 = react.memo(EasyCrop); 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}}"; (function() { if (typeof document === 'undefined') return; const style = document.createElement('style'); const meta = document.querySelector('meta[name="csp-nonce"]'); if (meta && meta.content) style.setAttribute('nonce', meta.content); style.textContent = css_248z; document.head.appendChild(style); })(); // v1 >= v2 const isGeThan = (v1, v2) => { var _a, _b; const arr1 = v1.split('.').map(Number); const arr2 = v2.split('.').map(Number); const len = Math.max(arr1.length, arr2.length); for (let i = 0; i < len; i++) { const a = (_a = arr1[i]) !== null && _a !== void 0 ? _a : 0; const b = (_b = arr2[i]) !== null && _b !== void 0 ? _b : 0; if (a > b) return true; if (a < b) return false; } return true; }; const ImgCrop = react.forwardRef((props, cropperRef) => { 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; const cb = react.useRef({}); cb.current.onModalOk = onModalOk; cb.current.onModalCancel = onModalCancel; cb.current.beforeCrop = beforeCrop; /** * crop */ const easyCropRef = react.useRef(null); const getCropCanvas = react.useCallback((target) => { var _a; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const context = ((_a = target === null || target === void 0 ? void 0 : target.getRootNode) === null || _a === void 0 ? void 0 : _a.call(target)) || document; const imgSource = context.querySelector(`.${PREFIX}-media`); const { width: cropWidth, height: cropHeight, x: cropX, y: cropY, } = easyCropRef.current.cropPixelsRef.current; if (rotationSlider && easyCropRef.current.rotation !== ROTATION_INITIAL) { const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource; const angle = easyCropRef.current.rotation * (Math.PI / 180); // get container for rotated image const sine = Math.abs(Math.sin(angle)); const cosine = Math.abs(Math.cos(angle)); const squareWidth = imgWidth * cosine + imgHeight * sine; const squareHeight = imgHeight * cosine + imgWidth * sine; canvas.width = squareWidth; canvas.height = squareHeight; ctx.fillStyle = fillColor; ctx.fillRect(0, 0, squareWidth, squareHeight); // rotate container const squareHalfWidth = squareWidth / 2; const squareHalfHeight = squareHeight / 2; ctx.translate(squareHalfWidth, squareHalfHeight); ctx.rotate(angle); ctx.translate(-squareHalfWidth, -squareHalfHeight); // draw rotated image const imgX = (squareWidth - imgWidth) / 2; const imgY = (squareHeight - imgHeight) / 2; ctx.drawImage(imgSource, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight); // crop rotated image const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight); canvas.width = cropWidth; canvas.height = cropHeight; ctx.putImageData(imgData, -cropX, -cropY); } else { canvas.width = cropWidth; canvas.height = cropHeight; ctx.fillStyle = fillColor; ctx.fillRect(0, 0, cropWidth, cropHeight); ctx.drawImage(imgSource, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); } return canvas; }, [fillColor, rotationSlider]); /** * upload */ const [modalOpen, setModalOpen] = react.useState(false); const [modalImage, setModalImage] = react.useState(''); const onCancel = react.useRef(undefined); const onOk = react.useRef(undefined); const runBeforeUpload = react.useCallback((_a) => tslib.__awaiter(void 0, [_a], void 0, function* ({ beforeUpload, file, resolve, reject, }) { const rawFile = file; if (typeof beforeUpload !== 'function') { resolve(rawFile); return; } try { // https://ant.design/components/upload-cn#api // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178 const result = yield beforeUpload(file, [file]); if (result === false) { resolve(false); } else { resolve((result !== true && result) || rawFile); } } catch (err) { reject(err); } }), []); const getNewBeforeUpload = react.useCallback((beforeUpload) => { return ((file, fileList) => { return new Promise((resolve, reject) => tslib.__awaiter(void 0, void 0, void 0, function* () { let processedFile = file; if (typeof cb.current.beforeCrop === 'function') { try { const result = yield cb.current.beforeCrop(file, fileList); if (result === false) { return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal } if (result !== true) { processedFile = result || file; // will open modal } } catch (err) { return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal } } // read file const reader = new FileReader(); reader.addEventListener('load', () => { if (typeof reader.result === 'string') { setModalOpen(true); setTimeout(() => { setModalImage(reader.result); }, 10); } }); reader.readAsDataURL(processedFile); // on modal cancel onCancel.current = () => { var _a, _b; setModalOpen(false); setModalImage(''); easyCropRef.current.onReset(); let hasResolveCalled = false; (_b = (_a = cb.current).onModalCancel) === null || _b === void 0 ? void 0 : _b.call(_a, (LIST_IGNORE) => { resolve(LIST_IGNORE); hasResolveCalled = true; }); if (!hasResolveCalled) { resolve(AntUpload.LIST_IGNORE); } }; // on modal confirm onOk.current = (event) => tslib.__awaiter(void 0, void 0, void 0, function* () { setModalOpen(false); setModalImage(''); easyCropRef.current.onReset(); const canvas = getCropCanvas(event.target); const { type, name, uid } = processedFile; canvas.toBlob((blob) => tslib.__awaiter(void 0, void 0, void 0, function* () { const newFile = new File([blob], name, { type }); Object.assign(newFile, { uid }); runBeforeUpload({ beforeUpload, file: newFile, resolve: (file) => { var _a, _b; resolve(file); (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, file); }, reject: (err) => { var _a, _b; reject(err); (_b = (_a = cb.current).onModalOk) === null || _b === void 0 ? void 0 : _b.call(_a, err); }, }); }), type, quality); }); })); }); }, [getCropCanvas, quality, runBeforeUpload]); const getNewUpload = react.useCallback((children) => { const upload = Array.isArray(children) ? children[0] : children; const _a = upload.props, { beforeUpload, accept } = _a, restUploadProps = tslib.__rest(_a, ["beforeUpload", "accept"]); return Object.assign(Object.assign({}, upload), { props: Object.assign(Object.assign({}, restUploadProps), { accept: accept || 'image/*', beforeUpload: getNewBeforeUpload(beforeUpload) }) }); }, [getNewBeforeUpload]); /** * modal */ const modalBaseProps = react.useMemo(() => { const obj = {}; if (modalWidth !== undefined) obj.width = modalWidth; if (modalOk !== undefined) obj.okText = modalOk; if (modalCancel !== undefined) obj.cancelText = modalCancel; return obj; }, [modalCancel, modalOk, modalWidth]); const wrapClassName = `${PREFIX}-modal${modalClassName ? ` ${modalClassName}` : ''}`; const lang = typeof window === 'undefined' ? '' : window.navigator.language; const isCN = lang === 'zh-CN'; const title = modalTitle || (isCN ? '编辑图片' : 'Edit image'); const resetBtnText = resetText || (isCN ? '重置' : 'Reset'); 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') ? { mask: { closable: false } } : { 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 }) }))] })); }); module.exports = ImgCrop;