123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- /* eslint-disable max-len */
- import React, { ReactNode, CSSProperties, RefObject, ChangeEvent, DragEvent } from 'react';
- import cls from 'classnames';
- import PropTypes from 'prop-types';
- import { noop } from 'lodash-es';
- import UploadFoundation, { BaseFileItem, UploadAdapter, BeforeUploadObjectResult, AfterUploadResult } from '@douyinfe/semi-foundation/upload/foundation';
- import { strings, cssClasses } from '@douyinfe/semi-foundation/upload/constants';
- import FileCard from './fileCard';
- import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
- import LocaleConsumer from '../locale/localeConsumer';
- import { IconUpload } from '@douyinfe/semi-icons';
- import { ArrayElement } from '../_base/base';
- import { Locale } from '../locale/interface';
- import '@douyinfe/semi-foundation/upload/upload.scss';
- const prefixCls = cssClasses.PREFIX;
- export interface FileItem extends BaseFileItem {
- validateMessage?: ReactNode;
- }
- export type UploadListType = ArrayElement<typeof strings.LIST_TYPE>;
- export type PromptPositionType = ArrayElement<typeof strings.PROMPT_POSITION>;
- export interface RenderFileItemProps extends FileItem {
- previewFile: (fileItem: FileItem) => ReactNode;
- listType: UploadListType;
- onRemove: () => void;
- onRetry: () => void;
- onReplace: () => void;
- key: string;
- showRetry: boolean;
- showReplace: boolean;
- style: CSSProperties;
- disabled: boolean;
- onPreviewClick: () => void;
- }
- export interface BeforeUploadProps {
- file: FileItem;
- fileList: Array<FileItem>;
- }
- export interface AfterUploadProps {
- file: FileItem;
- fileList: Array<FileItem>;
- response: any;
- }
- export interface OnChangeProps {
- fileList: Array<FileItem>;
- currentFile: FileItem;
- }
- export interface customRequestArgs {
- fileName: string; // Current file name
- data: Record<string, any>; // User-set props.data
- file: FileItem;
- fileInstance: File; // Original File Object which extends to the blob, the file object actually acquired by the browser (https://developer.mozilla.org/zh-CN/docs/Web/API/File)
- onProgress: (event: { total: number; loaded: number }) => any; // The function that should be called during the upload process, the event needs to contain the total and loaded attributes
- onError: (userXhr: { status?: number }, e: Event) => any; // Functions to call in case of upload error
- onSuccess: (response: any, e: Event) => any; // The function that should be called after the upload is successful, the response is the request result after the upload is successful
- withCredentials: boolean; // User-set props.with Credentials
- action: string; // User-set props.action
- }
- export interface CustomError extends Error {
- status: number;
- method: string;
- url: string;
- }
- export interface UploadProps {
- accept?: string;
- action: string;
- afterUpload?: (object: AfterUploadProps) => AfterUploadResult;
- beforeUpload?: (object: BeforeUploadProps) => BeforeUploadObjectResult | Promise<BeforeUploadObjectResult> | boolean;
- beforeClear?: (fileList: Array<FileItem>) => boolean | Promise<boolean>;
- beforeRemove?: (file: FileItem, fileList: Array<FileItem>) => boolean | Promise<boolean>;
- capture?: boolean | string | undefined;
- children?: ReactNode;
- className?: string;
- customRequest?: (object: customRequestArgs) => void;
- data?: Record<string, any> | ((file: File) => Record<string, unknown>);
- defaultFileList?: Array<FileItem>;
- directory?: boolean;
- disabled?: boolean;
- dragIcon?: ReactNode;
- dragMainText?: ReactNode;
- dragSubText?: ReactNode;
- draggable?: boolean;
- fileList?: Array<FileItem>;
- fileName?: string;
- headers?: Record<string, any> | ((file: File) => Record<string, string>);
- itemStyle?: CSSProperties;
- limit?: number;
- listType?: UploadListType;
- maxSize?: number;
- minSize?: number;
- multiple?: boolean;
- name?: string;
- onAcceptInvalid?: (files: File[]) => void;
- onChange?: (object: OnChangeProps) => void;
- onClear?: () => void;
- onDrop?: (e: Event, files: Array<File>, fileList: Array<FileItem>) => void;
- onError?: (e: CustomError, file: File, fileList: Array<FileItem>, xhr: XMLHttpRequest) => void;
- onExceed?: (fileList: Array<File>) => void;
- onFileChange?: (files: Array<File>) => void;
- onOpenFileDialog?: () => void;
- onPreviewClick?: (fileItem: FileItem) => void;
- onProgress?: (percent: number, file: File, fileList: Array<FileItem>) => void;
- onRemove?: (currentFile: File, fileList: Array<FileItem>, currentFileItem: FileItem) => void;
- onRetry?: (fileItem: FileItem) => void;
- onSizeError?: (file: File, fileList: Array<FileItem>) => void;
- onSuccess?: (responseBody: any, file: File, fileList: Array<FileItem>) => void;
- previewFile?: (fileItem: FileItem) => ReactNode;
- prompt?: ReactNode;
- promptPosition?: PromptPositionType;
- renderFileItem?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
- showClear?: boolean;
- showReplace?: boolean; // Display replacement function
- showRetry?: boolean;
- showUploadList?: boolean;
- style?: CSSProperties;
- timeout?: number;
- transformFile?: (file: File) => FileItem;
- uploadTrigger?: 'auto' | 'custom';
- validateMessage?: ReactNode;
- validateStatus?: ValidateStatus;
- withCredentials?: boolean;
- }
- export interface UploadState {
- dragAreaStatus: 'default' | 'legal' | 'illegal'; // Status of the drag zone
- fileList: Array<FileItem>;
- inputKey: number;
- localUrls: Array<string>;
- replaceIdx: number;
- replaceInputKey: number;
- }
- class Upload extends BaseComponent<UploadProps, UploadState> {
- static propTypes = {
- accept: PropTypes.string, // Limit allowed file types
- action: PropTypes.string.isRequired,
- afterUpload: PropTypes.func,
- beforeClear: PropTypes.func,
- beforeRemove: PropTypes.func,
- beforeUpload: PropTypes.func,
- children: PropTypes.node,
- className: PropTypes.string,
- customRequest: PropTypes.func,
- data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), // Extra parameters attached when uploading
- defaultFileList: PropTypes.array,
- directory: PropTypes.bool, // Support folder upload
- disabled: PropTypes.bool,
- dragIcon: PropTypes.node,
- dragMainText: PropTypes.node,
- dragSubText: PropTypes.node,
- draggable: PropTypes.bool,
- fileList: PropTypes.array, // files had been uploaded
- fileName: PropTypes.string, // same as name, to avoid props conflict in Form.Upload
- headers: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
- itemStyle: PropTypes.object,
- limit: PropTypes.number, // 最大允许上传文件个数
- listType: PropTypes.oneOf<UploadProps['listType']>(strings.LIST_TYPE),
- maxSize: PropTypes.number, // 文件大小限制,单位kb
- minSize: PropTypes.number, // 文件大小限制,单位kb
- multiple: PropTypes.bool,
- name: PropTypes.string, // file name
- onAcceptInvalid: PropTypes.func,
- onChange: PropTypes.func,
- onClear: PropTypes.func,
- onDrop: PropTypes.func,
- onError: PropTypes.func,
- onExceed: PropTypes.func, // Callback exceeding limit
- onFileChange: PropTypes.func, // Callback when file is selected
- onOpenFileDialog: PropTypes.func,
- onPreviewClick: PropTypes.func,
- onProgress: PropTypes.func,
- onRemove: PropTypes.func,
- onRetry: PropTypes.func,
- onSizeError: PropTypes.func, // Callback with invalid file size
- onSuccess: PropTypes.func,
- previewFile: PropTypes.func, // Custom preview
- prompt: PropTypes.node,
- promptPosition: PropTypes.oneOf<UploadProps['promptPosition']>(strings.PROMPT_POSITION),
- renderFileItem: PropTypes.func,
- showClear: PropTypes.bool,
- showReplace: PropTypes.bool,
- showRetry: PropTypes.bool,
- showUploadList: PropTypes.bool, // whether to show fileList
- style: PropTypes.object,
- timeout: PropTypes.number,
- transformFile: PropTypes.func,
- uploadTrigger: PropTypes.oneOf<UploadProps['uploadTrigger']>(strings.UPLOAD_TRIGGER), // auto、custom
- validateMessage: PropTypes.node,
- validateStatus: PropTypes.oneOf<UploadProps['validateStatus']>(strings.VALIDATE_STATUS),
- withCredentials: PropTypes.bool,
- };
- static defaultProps: Partial<UploadProps> = {
- defaultFileList: [],
- disabled: false,
- listType: 'list' as const,
- multiple: false,
- onAcceptInvalid: noop,
- onChange: noop,
- beforeRemove: () => true,
- beforeClear: () => true,
- onClear: noop,
- onDrop: noop,
- onError: noop,
- onExceed: noop,
- onFileChange: noop,
- onOpenFileDialog: noop,
- onProgress: noop,
- onRemove: noop,
- onRetry: noop,
- onSizeError: noop,
- onSuccess: noop,
- promptPosition: 'right' as const,
- showClear: true,
- showReplace: false,
- showRetry: true,
- showUploadList: true,
- uploadTrigger: 'auto' as const,
- withCredentials: false,
- };
- static FileCard = FileCard;
- constructor(props: UploadProps) {
- super(props);
- this.state = {
- fileList: props.defaultFileList || [],
- replaceIdx: -1,
- inputKey: Math.random(),
- replaceInputKey: Math.random(),
- // Status of the drag zone
- dragAreaStatus: 'default',
- localUrls: [],
- };
- this.foundation = new UploadFoundation(this.adapter);
- this.inputRef = React.createRef<HTMLInputElement>();
- this.replaceInputRef = React.createRef<HTMLInputElement>();
- }
- static getDerivedStateFromProps(props: UploadProps): Partial<UploadState> | null {
- const { fileList } = props;
- if ('fileList' in props) {
- return {
- fileList: fileList || []
- };
- }
- return null;
- }
- get adapter(): UploadAdapter<UploadProps, UploadState> {
- return {
- ...super.adapter,
- notifyFileSelect: (files): void => this.props.onFileChange(files),
- notifyError: (error, fileInstance, fileList, xhr): void => this.props.onError(error, fileInstance, fileList, xhr),
- notifySuccess: (responseBody, file, fileList): void => this.props.onSuccess(responseBody, file, fileList),
- notifyProgress: (percent, file, fileList): void => this.props.onProgress(percent, file, fileList),
- notifyRemove: (file, fileList, fileItem): void => this.props.onRemove(file, fileList, fileItem),
- notifySizeError: (file, fileList): void => this.props.onSizeError(file, fileList),
- notifyExceed: (fileList): void => this.props.onExceed(fileList),
- updateFileList: (fileList, cb): void => {
- if (typeof cb === 'function') {
- this.setState({ fileList }, cb);
- } else {
- this.setState({ fileList });
- }
- },
- notifyBeforeUpload: ({ file, fileList }): boolean | BeforeUploadObjectResult | Promise<BeforeUploadObjectResult> => this.props.beforeUpload({ file, fileList }),
- notifyAfterUpload: ({ response, file, fileList }): AfterUploadResult => this.props.afterUpload({ response, file, fileList }),
- resetInput: (): void => {
- this.setState(prevState => ({
- inputKey: Math.random()
- }));
- },
- resetReplaceInput: (): void => {
- this.setState(prevState => ({
- replaceInputKey: Math.random()
- }));
- },
- updateDragAreaStatus: (dragAreaStatus: string): void => this.setState({ dragAreaStatus } as { dragAreaStatus: 'default' | 'legal' | 'illegal' }),
- notifyChange: ({ currentFile, fileList }): void => this.props.onChange({ currentFile, fileList }),
- updateLocalUrls: (urls): void => this.setState({ localUrls: urls }),
- notifyClear: (): void => this.props.onClear(),
- notifyPreviewClick: (file): void => this.props.onPreviewClick(file),
- notifyDrop: (e, files, fileList): void => this.props.onDrop(e, files, fileList),
- notifyAcceptInvalid: (invalidFiles): void => this.props.onAcceptInvalid(invalidFiles),
- notifyBeforeRemove: (file, fileList): boolean | Promise<boolean> => this.props.beforeRemove(file, fileList),
- notifyBeforeClear: (fileList): boolean | Promise<boolean> => this.props.beforeClear(fileList),
- };
- }
- foundation: UploadFoundation;
- inputRef: RefObject<HTMLInputElement> = null;
- replaceInputRef: RefObject<HTMLInputElement> = null;
- componentWillUnmount(): void {
- this.foundation.destroy();
- }
- onClick = (): void => {
- const { inputRef, props } = this;
- const { onOpenFileDialog } = props;
- const isDisabled = Boolean(this.props.disabled);
- if (isDisabled || !inputRef || !inputRef.current) {
- return;
- }
- inputRef.current.click();
- if (onOpenFileDialog && typeof onOpenFileDialog) {
- onOpenFileDialog();
- }
- };
- onChange = (e: ChangeEvent<HTMLInputElement>): void => {
- const { files } = e.target;
- this.foundation.handleChange(files);
- };
- replace = (index: number): void => {
- this.setState({ replaceIdx: index }, () => {
- this.replaceInputRef.current.click();
- });
- };
- onReplaceChange = (e: ChangeEvent<HTMLInputElement>): void => {
- const { files } = e.target;
- this.foundation.handleReplaceChange(files);
- };
- clear = (): void => {
- this.foundation.handleClear();
- };
- remove = (fileItem: FileItem): void => {
- this.foundation.handleRemove(fileItem);
- };
- upload = (): void => {
- const { fileList } = this.state;
- this.foundation.startUpload(fileList);
- };
- renderFile = (file: FileItem, index: number, locale: Locale['Upload']): ReactNode => {
- const { name, status, validateMessage, _sizeInvalid } = file;
- const { previewFile, listType, itemStyle, showRetry, renderFileItem, disabled, onPreviewClick, showReplace } = this.props;
- const onRemove = (): void => this.remove(file);
- const onRetry = (): void => {
- this.foundation.retry(file);
- };
- const onReplace = (): void => {
- this.replace(index);
- };
- const fileCardProps = {
- ...file,
- previewFile,
- listType,
- onRemove,
- onRetry,
- key: `${name}${index}`,
- showRetry: typeof file.showRetry !== 'undefined' ? file.showRetry : showRetry,
- style: itemStyle,
- disabled,
- showReplace: typeof file.showReplace !== 'undefined' ? file.showReplace : showReplace,
- onReplace,
- onPreviewClick: typeof onPreviewClick !== 'undefined' ? (): void => this.foundation.handlePreviewClick(file) : undefined,
- };
- if (status === strings.FILE_STATUS_UPLOAD_FAIL && !validateMessage) {
- fileCardProps.validateMessage = locale.fail;
- }
- if (_sizeInvalid && !validateMessage) {
- fileCardProps.validateMessage = locale.illegalSize;
- }
- if (typeof renderFileItem === 'undefined') {
- return <FileCard {...fileCardProps} />;
- } else {
- return renderFileItem(fileCardProps);
- }
- };
- renderFileList = (): ReactNode => {
- const { showUploadList, listType, limit, disabled, children } = this.props;
- const { fileList: stateFileList } = this.state;
- const fileList = this.props.fileList || stateFileList;
- const isPicType = listType === strings.FILE_LIST_PIC;
- const showAddTriggerInList = isPicType && (limit ? limit > fileList.length : true);
- const uploadAddCls = cls(`${prefixCls }-add`, {
- [`${prefixCls }-picture-add`]: isPicType,
- [`${prefixCls}-picture-add-disabled`]: disabled
- });
- const addContent = (
- <div className={uploadAddCls} onClick={this.onClick}>
- {children}
- </div>
- );
- if (!showUploadList || !fileList.length) {
- if (showAddTriggerInList) {
- return addContent;
- }
- return null;
- }
- const fileListCls = cls(`${prefixCls }-file-list`, {
- [`${prefixCls }-picture-file-list`]: isPicType,
- });
- const titleCls = `${prefixCls }-file-list-title`;
- const mainCls = `${prefixCls }-file-list-main`;
- const showTitle = limit !== 1 && fileList.length && listType !== strings.FILE_LIST_PIC;
- const showClear = this.props.showClear && !disabled;
- return (
- <LocaleConsumer componentName="Upload">
- {(locale: Locale['Upload']): ReactNode => (
- <div className={fileListCls}>
- {showTitle ? (
- <div className={titleCls}>
- <span className={`${titleCls }-choosen`}>{locale.selectedFiles}</span>
- {showClear ? (
- <span onClick={this.clear} className={`${titleCls }-clear`}>
- {locale.clear}
- </span>
- ) : null}
- </div>
- ) : null}
- <div className={mainCls}>
- {fileList.map((file, index) => this.renderFile(file, index, locale))}
- {showAddTriggerInList ? addContent : null}
- </div>
- </div>
- )}
- </LocaleConsumer>
- );
- };
- onDrop = (e: DragEvent<HTMLDivElement>): void => {
- this.foundation.handleDrop(e);
- };
- onDragOver = (e: DragEvent<HTMLDivElement>): void => {
- // When a drag element moves within the target element
- this.foundation.handleDragOver(e);
- };
- onDragLeave = (e: DragEvent<HTMLDivElement>): void => {
- this.foundation.handleDragLeave(e);
- };
- onDragEnter = (e: DragEvent<HTMLDivElement>): void => {
- this.foundation.handleDragEnter(e);
- };
- renderDragArea = (): ReactNode => {
- const { dragAreaStatus } = this.state;
- const { children, dragIcon, dragMainText, dragSubText } = this.props;
- const dragAreaBaseCls = `${prefixCls }-drag-area`;
- const dragAreaCls = cls(dragAreaBaseCls, {
- [`${dragAreaBaseCls }-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
- [`${dragAreaBaseCls }-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL,
- [`${dragAreaBaseCls }-custom`]: children,
- });
- return (
- <LocaleConsumer componentName="Upload">
- {(locale: Locale['Upload']): ReactNode => (
- <div
- className={dragAreaCls}
- onDrop={this.onDrop}
- onDragOver={this.onDragOver}
- onDragLeave={this.onDragLeave}
- onDragEnter={this.onDragEnter}
- onClick={this.onClick}
- >
- {children ? (
- children
- ) : (
- <>
- <div className={`${dragAreaBaseCls }-icon`}>
- {dragIcon || <IconUpload size="extra-large" />}
- </div>
- <div className={`${dragAreaBaseCls }-text`}>
- <div className={`${dragAreaBaseCls }-main-text`}>
- {dragMainText || locale.mainText}
- </div>
- <div className={`${dragAreaBaseCls }-sub-text`}>{dragSubText}</div>
- <div className={`${dragAreaBaseCls }-tips`}>
- {dragAreaStatus === strings.DRAG_AREA_LEGAL && (
- <span className={`${dragAreaBaseCls }-tips-legal`}>{locale.legalTips}</span>
- )}
- {dragAreaStatus === strings.DRAG_AREA_ILLEGAL && (
- <span className={`${dragAreaBaseCls }-tips-illegal`}>
- {locale.illegalTips}
- </span>
- )}
- </div>
- </div>
- </>
- )}
- </div>
- )}
- </LocaleConsumer>
- );
- };
- render(): ReactNode {
- const {
- style,
- className,
- multiple,
- accept,
- disabled,
- children,
- capture,
- listType,
- prompt,
- promptPosition,
- draggable,
- validateMessage,
- validateStatus,
- directory,
- } = this.props;
- const uploadCls = cls(prefixCls, {
- [`${prefixCls }-picture`]: listType === strings.FILE_LIST_PIC,
- [`${prefixCls }-disabled`]: disabled,
- [`${prefixCls }-default`]: validateStatus === 'default',
- [`${prefixCls }-error`]: validateStatus === 'error',
- [`${prefixCls }-warning`]: validateStatus === 'warning',
- [`${prefixCls }-success`]: validateStatus === 'success',
- }, className);
- const uploadAddCls = cls(`${prefixCls }-add`);
- const inputCls = cls(`${prefixCls }-hidden-input`);
- const inputReplaceCls = cls(`${prefixCls }-hidden-input-replace`);
- const promptCls = cls(`${prefixCls }-prompt`);
- const validateMsgCls = cls(`${prefixCls }-validate-message`);
- const dirProps = directory ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {};
- const addContent =
- listType !== strings.FILE_LIST_PIC ? (
- <div className={uploadAddCls} onClick={this.onClick}>
- {children}
- </div>
- ) : null;
- return (
- <div className={uploadCls} style={style} x-prompt-pos={promptPosition}>
- <input
- key={this.state.inputKey}
- capture={capture}
- multiple={multiple}
- accept={accept}
- onChange={this.onChange}
- type="file"
- autoComplete="off"
- tabIndex={-1}
- className={inputCls}
- ref={this.inputRef}
- {...dirProps}
- />
- <input
- key={this.state.replaceInputKey}
- multiple={false}
- accept={accept}
- onChange={this.onReplaceChange}
- type="file"
- autoComplete="off"
- tabIndex={-1}
- className={inputReplaceCls}
- ref={this.replaceInputRef}
- />
- {draggable ? this.renderDragArea() : addContent}
- {prompt ? <div className={promptCls}>{prompt}</div> : null}
- {validateMessage ? <div className={validateMsgCls}>{validateMessage}</div> : null}
- {this.renderFileList()}
- </div>
- );
- }
- }
- export default Upload;
|