1 | import { __awaiter } from "tslib";
|
2 | import React, { forwardRef, useRef, useState, useImperativeHandle } from 'react';
|
3 | import { AddOutline, CloseOutline } from 'antd-mobile-icons';
|
4 | import { mergeProps } from '../../utils/with-default-props';
|
5 | import ImageViewer from '../image-viewer';
|
6 | import PreviewItem from './preview-item';
|
7 | import { usePropsValue } from '../../utils/use-props-value';
|
8 | import { useIsomorphicLayoutEffect, useUnmount, useSize } from 'ahooks';
|
9 | import Space from '../space';
|
10 | import { withNativeProps } from '../../utils/native-props';
|
11 | import { measureCSSLength } from '../../utils/measure-css-length';
|
12 | import { useConfig } from '../config-provider';
|
13 | import Grid from '../grid';
|
14 | const classPrefix = `adm-image-uploader`;
|
15 | const defaultProps = {
|
16 | disableUpload: false,
|
17 | deletable: true,
|
18 | deleteIcon: React.createElement(CloseOutline, {
|
19 | className: `${classPrefix}-cell-delete-icon`
|
20 | }),
|
21 | showUpload: true,
|
22 | multiple: false,
|
23 | maxCount: 0,
|
24 | defaultValue: [],
|
25 | accept: 'image/*',
|
26 | preview: true,
|
27 | showFailed: true,
|
28 | imageFit: 'cover'
|
29 | };
|
30 | export const ImageUploader = forwardRef((p, ref) => {
|
31 | const {
|
32 | locale
|
33 | } = useConfig();
|
34 | const props = mergeProps(defaultProps, p);
|
35 | const {
|
36 | columns
|
37 | } = props;
|
38 | const [value, setValue] = usePropsValue(props);
|
39 | const [tasks, setTasks] = useState([]);
|
40 | const containerRef = useRef(null);
|
41 | const containerSize = useSize(containerRef);
|
42 | const gapMeasureRef = useRef(null);
|
43 | const [cellSize, setCellSize] = useState(80);
|
44 | const inputRef = useRef(null);
|
45 | useIsomorphicLayoutEffect(() => {
|
46 | const gapMeasure = gapMeasureRef.current;
|
47 | if (columns && containerSize && gapMeasure) {
|
48 | const width = containerSize.width;
|
49 | const gap = measureCSSLength(window.getComputedStyle(gapMeasure).getPropertyValue('height'));
|
50 | setCellSize((width - gap * (columns - 1)) / columns);
|
51 | }
|
52 | }, [containerSize === null || containerSize === void 0 ? void 0 : containerSize.width]);
|
53 | const style = {
|
54 | '--cell-size': cellSize + 'px'
|
55 | };
|
56 | useIsomorphicLayoutEffect(() => {
|
57 | setTasks(prev => prev.filter(task => {
|
58 | if (task.url === undefined) return true;
|
59 | return !value.some(fileItem => fileItem.url === task.url);
|
60 | }));
|
61 | }, [value]);
|
62 | useIsomorphicLayoutEffect(() => {
|
63 | var _a;
|
64 | (_a = props.onUploadQueueChange) === null || _a === void 0 ? void 0 : _a.call(props, tasks.map(item => ({
|
65 | id: item.id,
|
66 | status: item.status
|
67 | })));
|
68 | }, [tasks]);
|
69 | const idCountRef = useRef(0);
|
70 | const {
|
71 | maxCount,
|
72 | onPreview,
|
73 | renderItem
|
74 | } = props;
|
75 | function processFile(file, fileList) {
|
76 | return __awaiter(this, void 0, void 0, function* () {
|
77 | const {
|
78 | beforeUpload
|
79 | } = props;
|
80 | let transformedFile = file;
|
81 | transformedFile = yield beforeUpload === null || beforeUpload === void 0 ? void 0 : beforeUpload(file, fileList);
|
82 | return transformedFile;
|
83 | });
|
84 | }
|
85 | function getFinalTasks(tasks) {
|
86 | return props.showFailed ? tasks : tasks.filter(task => task.status !== 'fail');
|
87 | }
|
88 | function onChange(e) {
|
89 | var _a;
|
90 | return __awaiter(this, void 0, void 0, function* () {
|
91 | e.persist();
|
92 | const {
|
93 | files: rawFiles
|
94 | } = e.target;
|
95 | if (!rawFiles) return;
|
96 | let files = [].slice.call(rawFiles);
|
97 | e.target.value = '';
|
98 | if (props.beforeUpload) {
|
99 | const postFiles = files.map(file => processFile(file, files));
|
100 | yield Promise.all(postFiles).then(filesList => {
|
101 | files = filesList.filter(Boolean);
|
102 | });
|
103 | }
|
104 | if (files.length === 0) {
|
105 | return;
|
106 | }
|
107 | if (maxCount > 0) {
|
108 | const exceed = value.length + files.length - maxCount;
|
109 | if (exceed > 0) {
|
110 | files = files.slice(0, files.length - exceed);
|
111 | (_a = props.onCountExceed) === null || _a === void 0 ? void 0 : _a.call(props, exceed);
|
112 | }
|
113 | }
|
114 | const newTasks = files.map(file => ({
|
115 | id: idCountRef.current++,
|
116 | status: 'pending',
|
117 | file
|
118 | }));
|
119 | setTasks(prev => [...getFinalTasks(prev), ...newTasks]);
|
120 | const newVal = [];
|
121 | yield Promise.all(newTasks.map((currentTask, index) => __awaiter(this, void 0, void 0, function* () {
|
122 | try {
|
123 | const result = yield props.upload(currentTask.file);
|
124 | newVal[index] = result;
|
125 | setTasks(prev => {
|
126 | return prev.map(task => {
|
127 | if (task.id === currentTask.id) {
|
128 | return Object.assign(Object.assign({}, task), {
|
129 | status: 'success',
|
130 | url: result.url
|
131 | });
|
132 | }
|
133 | return task;
|
134 | });
|
135 | });
|
136 | } catch (e) {
|
137 | setTasks(prev => {
|
138 | return prev.map(task => {
|
139 | if (task.id === currentTask.id) {
|
140 | return Object.assign(Object.assign({}, task), {
|
141 | status: 'fail'
|
142 | });
|
143 | }
|
144 | return task;
|
145 | });
|
146 | });
|
147 | throw e;
|
148 | }
|
149 | }))).catch(error => console.error(error));
|
150 | setValue(prev => prev.concat(newVal));
|
151 | });
|
152 | }
|
153 | const imageViewerHandlerRef = useRef(null);
|
154 | function previewImage(index) {
|
155 | imageViewerHandlerRef.current = ImageViewer.Multi.show({
|
156 | images: value.map(fileItem => fileItem.url),
|
157 | defaultIndex: index,
|
158 | onClose: () => {
|
159 | imageViewerHandlerRef.current = null;
|
160 | }
|
161 | });
|
162 | }
|
163 | useUnmount(() => {
|
164 | var _a;
|
165 | (_a = imageViewerHandlerRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
166 | });
|
167 | const finalTasks = getFinalTasks(tasks);
|
168 | const showUpload = props.showUpload && (maxCount === 0 || value.length + finalTasks.length < maxCount);
|
169 | const renderImages = () => {
|
170 | return value.map((fileItem, index) => {
|
171 | var _a, _b;
|
172 | const originNode = React.createElement(PreviewItem, {
|
173 | key: (_a = fileItem.key) !== null && _a !== void 0 ? _a : index,
|
174 | url: (_b = fileItem.thumbnailUrl) !== null && _b !== void 0 ? _b : fileItem.url,
|
175 | deletable: props.deletable,
|
176 | deleteIcon: props.deleteIcon,
|
177 | imageFit: props.imageFit,
|
178 | onClick: () => {
|
179 | if (props.preview) {
|
180 | previewImage(index);
|
181 | }
|
182 | onPreview && onPreview(index, fileItem);
|
183 | },
|
184 | onDelete: () => __awaiter(void 0, void 0, void 0, function* () {
|
185 | var _c;
|
186 | const canDelete = yield (_c = props.onDelete) === null || _c === void 0 ? void 0 : _c.call(props, fileItem);
|
187 | if (canDelete === false) return;
|
188 | setValue(value.filter((x, i) => i !== index));
|
189 | })
|
190 | });
|
191 | return renderItem ? renderItem(originNode, fileItem, value) : originNode;
|
192 | });
|
193 | };
|
194 | const contentNode = React.createElement(React.Fragment, null, renderImages(), tasks.map(task => {
|
195 | if (!props.showFailed && task.status === 'fail') {
|
196 | return null;
|
197 | }
|
198 | return React.createElement(PreviewItem, {
|
199 | key: task.id,
|
200 | file: task.file,
|
201 | deletable: task.status !== 'pending',
|
202 | deleteIcon: props.deleteIcon,
|
203 | status: task.status,
|
204 | imageFit: props.imageFit,
|
205 | onDelete: () => {
|
206 | setTasks(tasks.filter(x => x.id !== task.id));
|
207 | }
|
208 | });
|
209 | }), React.createElement("div", {
|
210 | className: `${classPrefix}-upload-button-wrap`,
|
211 | style: showUpload ? undefined : {
|
212 | display: 'none'
|
213 | }
|
214 | }, props.children || React.createElement("span", {
|
215 | className: `${classPrefix}-cell ${classPrefix}-upload-button`,
|
216 | role: 'button',
|
217 | "aria-label": locale.ImageUploader.upload
|
218 | }, React.createElement("span", {
|
219 | className: `${classPrefix}-upload-button-icon`
|
220 | }, React.createElement(AddOutline, null))), !props.disableUpload && React.createElement("input", {
|
221 | ref: inputRef,
|
222 | capture: props.capture,
|
223 | accept: props.accept,
|
224 | multiple: props.multiple,
|
225 | type: 'file',
|
226 | className: `${classPrefix}-input`,
|
227 | onChange: onChange,
|
228 | "aria-hidden": true
|
229 | })));
|
230 | useImperativeHandle(ref, () => ({
|
231 | get nativeElement() {
|
232 | return inputRef.current;
|
233 | }
|
234 | }));
|
235 | return withNativeProps(props, React.createElement("div", {
|
236 | className: classPrefix,
|
237 | ref: containerRef
|
238 | }, columns ? React.createElement(Grid, {
|
239 | className: `${classPrefix}-grid`,
|
240 | columns: columns,
|
241 | style: style
|
242 | }, React.createElement("div", {
|
243 | className: `${classPrefix}-gap-measure`,
|
244 | ref: gapMeasureRef
|
245 | }), contentNode.props.children) : React.createElement(Space, {
|
246 | className: `${classPrefix}-space`,
|
247 | wrap: true,
|
248 | block: true
|
249 | }, contentNode.props.children)));
|
250 | }); |
\ | No newline at end of file |