index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. import React, { ReactNode, CSSProperties, RefObject, ChangeEvent, DragEvent } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { noop, pick } from 'lodash';
  5. import UploadFoundation from '@douyinfe/semi-foundation/upload/foundation';
  6. import { strings, cssClasses } from '@douyinfe/semi-foundation/upload/constants';
  7. import FileCard from './fileCard';
  8. import BaseComponent from '../_base/baseComponent';
  9. import LocaleConsumer from '../locale/localeConsumer';
  10. import { IconUpload } from '@douyinfe/semi-icons';
  11. import type {
  12. FileItem,
  13. RenderFileItemProps,
  14. UploadListType,
  15. PromptPositionType,
  16. BeforeUploadProps,
  17. AfterUploadProps,
  18. OnChangeProps,
  19. customRequestArgs,
  20. CustomError,
  21. RenderPictureCloseProps,
  22. } from './interface';
  23. import { Locale } from '../locale/interface';
  24. import '@douyinfe/semi-foundation/upload/upload.scss';
  25. import type {
  26. CustomFile,
  27. UploadAdapter,
  28. BeforeUploadObjectResult,
  29. AfterUploadResult,
  30. FileItemStatus
  31. } from '@douyinfe/semi-foundation/upload/foundation';
  32. import type { ValidateStatus } from '../_base/baseComponent';
  33. const prefixCls = cssClasses.PREFIX;
  34. export type {
  35. FileItem,
  36. FileItemStatus,
  37. RenderFileItemProps,
  38. UploadListType,
  39. PromptPositionType,
  40. BeforeUploadProps,
  41. AfterUploadProps,
  42. OnChangeProps,
  43. customRequestArgs,
  44. CustomError,
  45. BeforeUploadObjectResult,
  46. AfterUploadResult,
  47. };
  48. export interface UploadProps {
  49. accept?: string;
  50. action: string;
  51. afterUpload?: (object: AfterUploadProps) => AfterUploadResult;
  52. beforeUpload?: (
  53. object: BeforeUploadProps
  54. ) => BeforeUploadObjectResult | Promise<BeforeUploadObjectResult> | boolean;
  55. beforeClear?: (fileList: Array<FileItem>) => boolean | Promise<boolean>;
  56. beforeRemove?: (file: FileItem, fileList: Array<FileItem>) => boolean | Promise<boolean>;
  57. capture?: boolean | 'user' | 'environment' | undefined;
  58. children?: ReactNode;
  59. className?: string;
  60. customRequest?: (object: customRequestArgs) => void;
  61. data?: Record<string, any> | ((file: File) => Record<string, unknown>);
  62. defaultFileList?: Array<FileItem>;
  63. directory?: boolean;
  64. disabled?: boolean;
  65. dragIcon?: ReactNode;
  66. dragMainText?: ReactNode;
  67. dragSubText?: ReactNode;
  68. draggable?: boolean;
  69. addOnPasting?: boolean;
  70. fileList?: Array<FileItem>;
  71. fileName?: string;
  72. headers?: Record<string, any> | ((file: File) => Record<string, string>);
  73. hotSpotLocation?: 'start' | 'end';
  74. itemStyle?: CSSProperties;
  75. limit?: number;
  76. listType?: UploadListType;
  77. maxSize?: number;
  78. minSize?: number;
  79. multiple?: boolean;
  80. name?: string;
  81. onAcceptInvalid?: (files: File[]) => void;
  82. onChange?: (object: OnChangeProps) => void;
  83. onClear?: () => void;
  84. onDrop?: (e: Event, files: Array<File>, fileList: Array<FileItem>) => void;
  85. onError?: (e: CustomError, file: File, fileList: Array<FileItem>, xhr: XMLHttpRequest) => void;
  86. onPastingError?: (error: Error | PermissionStatus) => void;
  87. onExceed?: (fileList: Array<File>) => void;
  88. onFileChange?: (files: Array<File>) => void;
  89. onOpenFileDialog?: () => void;
  90. onPreviewClick?: (fileItem: FileItem) => void;
  91. onProgress?: (percent: number, file: File, fileList: Array<FileItem>) => void;
  92. onRemove?: (currentFile: File, fileList: Array<FileItem>, currentFileItem: FileItem) => void;
  93. onRetry?: (fileItem: FileItem) => void;
  94. onSizeError?: (file: File, fileList: Array<FileItem>) => void;
  95. onSuccess?: (responseBody: any, file: File, fileList: Array<FileItem>) => void;
  96. previewFile?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
  97. prompt?: ReactNode;
  98. promptPosition?: PromptPositionType;
  99. picHeight?: string | number;
  100. picWidth?: string | number;
  101. renderFileItem?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
  102. renderPicInfo?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
  103. renderThumbnail?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
  104. renderPicPreviewIcon?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
  105. renderPicClose?: (renderPicCloseProps: RenderPictureCloseProps) => ReactNode;
  106. renderFileOperation?: (fileItem: RenderFileItemProps) => ReactNode;
  107. showClear?: boolean;
  108. showPicInfo?: boolean; // Show pic info in picture wall
  109. showReplace?: boolean; // Display replacement function
  110. showRetry?: boolean;
  111. showUploadList?: boolean;
  112. style?: CSSProperties;
  113. timeout?: number;
  114. transformFile?: (file: File) => FileItem;
  115. uploadTrigger?: 'auto' | 'custom';
  116. validateMessage?: ReactNode;
  117. validateStatus?: ValidateStatus;
  118. withCredentials?: boolean
  119. }
  120. export interface UploadState {
  121. dragAreaStatus: 'default' | 'legal' | 'illegal'; // Status of the drag zone
  122. fileList: Array<FileItem>;
  123. inputKey: number;
  124. localUrls: Array<string>;
  125. replaceIdx: number;
  126. replaceInputKey: number
  127. }
  128. class Upload extends BaseComponent<UploadProps, UploadState> {
  129. static propTypes = {
  130. accept: PropTypes.string, // Limit allowed file types
  131. action: PropTypes.string.isRequired,
  132. addOnPasting: PropTypes.bool,
  133. afterUpload: PropTypes.func,
  134. beforeClear: PropTypes.func,
  135. beforeRemove: PropTypes.func,
  136. beforeUpload: PropTypes.func,
  137. children: PropTypes.node,
  138. className: PropTypes.string,
  139. customRequest: PropTypes.func,
  140. data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), // Extra parameters attached when uploading
  141. defaultFileList: PropTypes.array,
  142. directory: PropTypes.bool, // Support folder upload
  143. disabled: PropTypes.bool,
  144. dragIcon: PropTypes.node,
  145. dragMainText: PropTypes.node,
  146. dragSubText: PropTypes.node,
  147. draggable: PropTypes.bool,
  148. fileList: PropTypes.array, // files had been uploaded
  149. fileName: PropTypes.string, // same as name, to avoid props conflict in Form.Upload
  150. headers: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  151. hotSpotLocation: PropTypes.oneOf(['start', 'end']),
  152. itemStyle: PropTypes.object,
  153. limit: PropTypes.number, // 最大允许上传文件个数
  154. listType: PropTypes.oneOf<UploadProps['listType']>(strings.LIST_TYPE),
  155. maxSize: PropTypes.number, // 文件大小限制,单位kb
  156. minSize: PropTypes.number, // 文件大小限制,单位kb
  157. multiple: PropTypes.bool,
  158. name: PropTypes.string, // file name
  159. onAcceptInvalid: PropTypes.func,
  160. onChange: PropTypes.func,
  161. onClear: PropTypes.func,
  162. onDrop: PropTypes.func,
  163. onError: PropTypes.func,
  164. onExceed: PropTypes.func, // Callback exceeding limit
  165. onFileChange: PropTypes.func, // Callback when file is selected
  166. onOpenFileDialog: PropTypes.func,
  167. onPreviewClick: PropTypes.func,
  168. onProgress: PropTypes.func,
  169. onRemove: PropTypes.func,
  170. onRetry: PropTypes.func,
  171. onSizeError: PropTypes.func, // Callback with invalid file size
  172. onSuccess: PropTypes.func,
  173. onPastingError: PropTypes.func,
  174. previewFile: PropTypes.func, // Custom preview
  175. prompt: PropTypes.node,
  176. promptPosition: PropTypes.oneOf<UploadProps['promptPosition']>(strings.PROMPT_POSITION),
  177. picWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  178. picHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  179. renderFileItem: PropTypes.func,
  180. renderPicPreviewIcon: PropTypes.func,
  181. renderFileOperation: PropTypes.func,
  182. renderPicClose: PropTypes.func,
  183. renderPicInfo: PropTypes.func,
  184. renderThumbnail: PropTypes.func,
  185. showClear: PropTypes.bool,
  186. showPicInfo: PropTypes.bool,
  187. showReplace: PropTypes.bool,
  188. showRetry: PropTypes.bool,
  189. showUploadList: PropTypes.bool, // whether to show fileList
  190. style: PropTypes.object,
  191. timeout: PropTypes.number,
  192. transformFile: PropTypes.func,
  193. uploadTrigger: PropTypes.oneOf<UploadProps['uploadTrigger']>(strings.UPLOAD_TRIGGER), // auto、custom
  194. validateMessage: PropTypes.node,
  195. validateStatus: PropTypes.oneOf<UploadProps['validateStatus']>(strings.VALIDATE_STATUS),
  196. withCredentials: PropTypes.bool,
  197. };
  198. static defaultProps: Partial<UploadProps> = {
  199. defaultFileList: [],
  200. disabled: false,
  201. listType: 'list' as const,
  202. hotSpotLocation: 'end',
  203. multiple: false,
  204. onAcceptInvalid: noop,
  205. onChange: noop,
  206. beforeRemove: () => true,
  207. beforeClear: () => true,
  208. onClear: noop,
  209. onDrop: noop,
  210. onError: noop,
  211. onExceed: noop,
  212. onFileChange: noop,
  213. onOpenFileDialog: noop,
  214. onProgress: noop,
  215. onRemove: noop,
  216. onRetry: noop,
  217. onSizeError: noop,
  218. onSuccess: noop,
  219. onPastingError: noop,
  220. promptPosition: 'right' as const,
  221. showClear: true,
  222. showPicInfo: false,
  223. showReplace: false,
  224. showRetry: true,
  225. showUploadList: true,
  226. uploadTrigger: 'auto' as const,
  227. withCredentials: false,
  228. };
  229. static FileCard = FileCard;
  230. constructor(props: UploadProps) {
  231. super(props);
  232. this.state = {
  233. fileList: props.defaultFileList || [],
  234. replaceIdx: -1,
  235. inputKey: Math.random(),
  236. replaceInputKey: Math.random(),
  237. // Status of the drag zone
  238. dragAreaStatus: 'default',
  239. localUrls: [],
  240. };
  241. this.foundation = new UploadFoundation(this.adapter);
  242. this.inputRef = React.createRef<HTMLInputElement>();
  243. this.replaceInputRef = React.createRef<HTMLInputElement>();
  244. }
  245. /**
  246. * Notes:
  247. * The input parameter and return value here do not declare the type, otherwise tsc may report an error in form/fields.tsx when wrap after withField
  248. * `The types of the parameters "props" and "nextProps" are incompatible.
  249. The attribute "action" is missing in the type "Readonly<any>", but it is required in the type "UploadProps".`
  250. * which seems to be a bug, remove props type declare here
  251. */
  252. static getDerivedStateFromProps(props) {
  253. const { fileList } = props;
  254. if ('fileList' in props) {
  255. return {
  256. fileList: fileList || [],
  257. };
  258. }
  259. return null;
  260. }
  261. get adapter(): UploadAdapter<UploadProps, UploadState> {
  262. return {
  263. ...super.adapter,
  264. notifyFileSelect: (files): void => this.props.onFileChange(files),
  265. notifyError: (error, fileInstance, fileList, xhr): void =>
  266. this.props.onError(error, fileInstance, fileList, xhr),
  267. notifySuccess: (responseBody, file, fileList): void => this.props.onSuccess(responseBody, file, fileList),
  268. notifyProgress: (percent, file, fileList): void => this.props.onProgress(percent, file, fileList),
  269. notifyRemove: (file, fileList, fileItem): void => this.props.onRemove(file, fileList, fileItem),
  270. notifySizeError: (file, fileList): void => this.props.onSizeError(file, fileList),
  271. notifyExceed: (fileList): void => this.props.onExceed(fileList),
  272. updateFileList: (fileList, cb): void => {
  273. if (typeof cb === 'function') {
  274. this.setState({ fileList }, cb);
  275. } else {
  276. this.setState({ fileList });
  277. }
  278. },
  279. notifyBeforeUpload: ({
  280. file,
  281. fileList,
  282. }): boolean | BeforeUploadObjectResult | Promise<BeforeUploadObjectResult> =>
  283. this.props.beforeUpload({ file, fileList }),
  284. notifyAfterUpload: ({ response, file, fileList }): AfterUploadResult =>
  285. this.props.afterUpload({ response, file, fileList }),
  286. resetInput: (): void => {
  287. this.setState(prevState => ({
  288. inputKey: Math.random(),
  289. }));
  290. },
  291. resetReplaceInput: (): void => {
  292. this.setState(prevState => ({
  293. replaceInputKey: Math.random(),
  294. }));
  295. },
  296. isMac: (): boolean => {
  297. return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
  298. },
  299. registerPastingHandler: (cb?: (e: KeyboardEvent) => void): void => {
  300. document.body.addEventListener('keydown', cb);
  301. this.pastingCb = cb;
  302. },
  303. unRegisterPastingHandler: (): void => {
  304. if (this.pastingCb) {
  305. document.body.removeEventListener('keydown', this.pastingCb);
  306. }
  307. },
  308. notifyPastingError: (error): void => this.props.onPastingError(error),
  309. updateDragAreaStatus: (dragAreaStatus: string): void =>
  310. this.setState({ dragAreaStatus } as { dragAreaStatus: 'default' | 'legal' | 'illegal' }),
  311. notifyChange: ({ currentFile, fileList }): void => this.props.onChange({ currentFile, fileList }),
  312. updateLocalUrls: (urls): void => this.setState({ localUrls: urls }),
  313. notifyClear: (): void => this.props.onClear(),
  314. notifyPreviewClick: (file): void => this.props.onPreviewClick(file),
  315. notifyDrop: (e, files, fileList): void => this.props.onDrop(e, files, fileList),
  316. notifyAcceptInvalid: (invalidFiles): void => this.props.onAcceptInvalid(invalidFiles),
  317. notifyBeforeRemove: (file, fileList): boolean | Promise<boolean> => this.props.beforeRemove(file, fileList),
  318. notifyBeforeClear: (fileList): boolean | Promise<boolean> => this.props.beforeClear(fileList),
  319. };
  320. }
  321. foundation: UploadFoundation;
  322. inputRef: RefObject<HTMLInputElement> = null;
  323. replaceInputRef: RefObject<HTMLInputElement> = null;
  324. pastingCb: null | ((params: any) => void);
  325. componentDidMount(): void {
  326. this.foundation.init();
  327. }
  328. componentWillUnmount(): void {
  329. this.foundation.destroy();
  330. }
  331. onClick = (): void => {
  332. const { inputRef, props } = this;
  333. const { onOpenFileDialog } = props;
  334. const isDisabled = Boolean(this.props.disabled);
  335. if (isDisabled || !inputRef || !inputRef.current) {
  336. return;
  337. }
  338. inputRef.current.click();
  339. if (onOpenFileDialog && typeof onOpenFileDialog) {
  340. onOpenFileDialog();
  341. }
  342. };
  343. onChange = (e: ChangeEvent<HTMLInputElement>): void => {
  344. const { files } = e.target;
  345. this.foundation.handleChange(files);
  346. };
  347. replace = (index: number): void => {
  348. this.setState({ replaceIdx: index }, () => {
  349. this.replaceInputRef.current.click();
  350. });
  351. };
  352. onReplaceChange = (e: ChangeEvent<HTMLInputElement>): void => {
  353. const { files } = e.target;
  354. this.foundation.handleReplaceChange(files);
  355. };
  356. clear = (): void => {
  357. this.foundation.handleClear();
  358. };
  359. remove = (fileItem: FileItem): void => {
  360. this.foundation.handleRemove(fileItem);
  361. };
  362. /**
  363. * ref method
  364. * insert files at index
  365. * @param files Array<CustomFile>
  366. * @param index number
  367. * @returns
  368. */
  369. insert = (files: Array<CustomFile>, index?: number): void => {
  370. return this.foundation.insertFileToList(files, index);
  371. };
  372. /**
  373. * ref method
  374. * manual upload by user
  375. */
  376. upload = (): void => {
  377. this.foundation.manualUpload();
  378. };
  379. /**
  380. * ref method
  381. * manual open file select dialog
  382. */
  383. openFileDialog = (): void => {
  384. this.onClick();
  385. };
  386. renderFile = (file: FileItem, index: number, locale: Locale['Upload']): ReactNode => {
  387. const { name, status, validateMessage, _sizeInvalid, uid } = file;
  388. const {
  389. previewFile,
  390. listType,
  391. itemStyle,
  392. showPicInfo,
  393. renderPicInfo,
  394. renderPicClose,
  395. renderPicPreviewIcon,
  396. renderFileOperation,
  397. renderFileItem,
  398. renderThumbnail,
  399. disabled,
  400. onPreviewClick,
  401. picWidth,
  402. picHeight,
  403. } = this.props;
  404. const onRemove = (): void => this.remove(file);
  405. const onRetry = (): void => {
  406. this.foundation.retry(file);
  407. };
  408. const onReplace = (): void => {
  409. this.replace(index);
  410. };
  411. const fileCardProps = {
  412. ...pick(this.props, ['showRetry', 'showReplace', '']),
  413. ...file,
  414. previewFile,
  415. listType,
  416. onRemove,
  417. onRetry,
  418. index,
  419. key: uid || `${name}${index}`,
  420. style: itemStyle,
  421. disabled,
  422. showPicInfo,
  423. renderPicInfo,
  424. renderPicPreviewIcon,
  425. renderPicClose,
  426. renderFileOperation,
  427. renderThumbnail,
  428. onReplace,
  429. onPreviewClick:
  430. typeof onPreviewClick !== 'undefined'
  431. ? (): void => this.foundation.handlePreviewClick(file)
  432. : undefined,
  433. picWidth,
  434. picHeight
  435. };
  436. if (status === strings.FILE_STATUS_UPLOAD_FAIL && !validateMessage) {
  437. fileCardProps.validateMessage = locale.fail;
  438. }
  439. if (_sizeInvalid && !validateMessage) {
  440. fileCardProps.validateMessage = locale.illegalSize;
  441. }
  442. if (typeof renderFileItem === 'undefined') {
  443. return <FileCard {...fileCardProps} />;
  444. } else {
  445. return renderFileItem(fileCardProps);
  446. }
  447. };
  448. renderFileList = (): ReactNode => {
  449. const { listType } = this.props;
  450. if (listType === strings.FILE_LIST_PIC) {
  451. return this.renderFileListPic();
  452. }
  453. if (listType === strings.FILE_LIST_DEFAULT) {
  454. return this.renderFileListDefault();
  455. }
  456. return null;
  457. };
  458. renderFileListPic = () => {
  459. const { showUploadList, limit, disabled, children, draggable, hotSpotLocation, picHeight, picWidth } = this.props;
  460. const { fileList: stateFileList, dragAreaStatus } = this.state;
  461. const fileList = this.props.fileList || stateFileList;
  462. const showAddTriggerInList = limit ? limit > fileList.length : true;
  463. const dragAreaBaseCls = `${prefixCls}-drag-area`;
  464. const uploadAddCls = cls(`${prefixCls}-add`, {
  465. [`${prefixCls}-picture-add`]: true,
  466. [`${prefixCls}-picture-add-disabled`]: disabled,
  467. });
  468. const fileListCls = cls(`${prefixCls}-file-list`, {
  469. [`${prefixCls}-picture-file-list`]: true,
  470. });
  471. const dragAreaCls = cls({
  472. [`${dragAreaBaseCls}-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
  473. [`${dragAreaBaseCls}-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL,
  474. });
  475. const mainCls = `${prefixCls}-file-list-main`;
  476. const addContentProps = {
  477. role: 'button',
  478. className: uploadAddCls,
  479. onClick: this.onClick,
  480. style: {
  481. height: picHeight,
  482. width: picWidth
  483. }
  484. };
  485. const containerProps = {
  486. className: fileListCls,
  487. };
  488. const draggableProps = {
  489. onDrop: this.onDrop,
  490. onDragOver: this.onDragOver,
  491. onDragLeave: this.onDragLeave,
  492. onDragEnter: this.onDragEnter,
  493. };
  494. if (draggable) {
  495. Object.assign(addContentProps, draggableProps, { className: cls(uploadAddCls, dragAreaCls) });
  496. }
  497. const addContent = (
  498. <div {...addContentProps} x-semi-prop="children">
  499. {children}
  500. </div>
  501. );
  502. if (!showUploadList || !fileList.length) {
  503. if (showAddTriggerInList) {
  504. return addContent;
  505. }
  506. return null;
  507. }
  508. return (
  509. <LocaleConsumer componentName="Upload">
  510. {(locale: Locale['Upload']) => (
  511. <div {...containerProps}>
  512. <div className={mainCls} role="list" aria-label="picture list">
  513. {showAddTriggerInList && hotSpotLocation === 'start' ? addContent : null}
  514. {fileList.map((file, index) => this.renderFile(file, index, locale))}
  515. {showAddTriggerInList && hotSpotLocation === 'end' ? addContent : null}
  516. </div>
  517. </div>
  518. )}
  519. </LocaleConsumer>
  520. );
  521. };
  522. renderFileListDefault = () => {
  523. const { showUploadList, limit, disabled } = this.props;
  524. const { fileList: stateFileList } = this.state;
  525. const fileList = this.props.fileList || stateFileList;
  526. const fileListCls = cls(`${prefixCls}-file-list`);
  527. const titleCls = `${prefixCls}-file-list-title`;
  528. const mainCls = `${prefixCls}-file-list-main`;
  529. const showTitle = limit !== 1 && fileList.length;
  530. const showClear = this.props.showClear && !disabled;
  531. const containerProps = {
  532. className: fileListCls,
  533. };
  534. if (!showUploadList || !fileList.length) {
  535. return null;
  536. }
  537. return (
  538. <LocaleConsumer componentName="Upload">
  539. {(locale: Locale['Upload']) => (
  540. <div {...containerProps}>
  541. {showTitle ? (
  542. <div className={titleCls}>
  543. <span className={`${titleCls}-choosen`}>{locale.selectedFiles}</span>
  544. {showClear ? (
  545. <span
  546. role="button"
  547. tabIndex={0}
  548. onClick={this.clear}
  549. className={`${titleCls}-clear`}
  550. >
  551. {locale.clear}
  552. </span>
  553. ) : null}
  554. </div>
  555. ) : null}
  556. <div className={mainCls} role="list" aria-label="file list">
  557. {fileList.map((file, index) => this.renderFile(file, index, locale))}
  558. </div>
  559. </div>
  560. )}
  561. </LocaleConsumer>
  562. );
  563. };
  564. onDrop = (e: DragEvent<HTMLDivElement>): void => {
  565. this.foundation.handleDrop(e);
  566. };
  567. onDragOver = (e: DragEvent<HTMLDivElement>): void => {
  568. // When a drag element moves within the target element
  569. this.foundation.handleDragOver(e);
  570. };
  571. onDragLeave = (e: DragEvent<HTMLDivElement>): void => {
  572. this.foundation.handleDragLeave(e);
  573. };
  574. onDragEnter = (e: DragEvent<HTMLDivElement>): void => {
  575. this.foundation.handleDragEnter(e);
  576. };
  577. renderAddContent = () => {
  578. const { draggable, children, listType, disabled } = this.props;
  579. const uploadAddCls = cls(`${prefixCls}-add`);
  580. if (listType === strings.FILE_LIST_PIC) {
  581. return null;
  582. }
  583. if (draggable) {
  584. return this.renderDragArea();
  585. }
  586. return (
  587. <div role="button" tabIndex={0} aria-disabled={disabled} className={uploadAddCls} onClick={this.onClick}>
  588. {children}
  589. </div>
  590. );
  591. };
  592. renderDragArea = (): ReactNode => {
  593. const { dragAreaStatus } = this.state;
  594. const { children, dragIcon, dragMainText, dragSubText, disabled } = this.props;
  595. const dragAreaBaseCls = `${prefixCls}-drag-area`;
  596. const dragAreaCls = cls(dragAreaBaseCls, {
  597. [`${dragAreaBaseCls}-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
  598. [`${dragAreaBaseCls}-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL,
  599. [`${dragAreaBaseCls}-custom`]: children,
  600. });
  601. return (
  602. <LocaleConsumer componentName="Upload">
  603. {(locale: Locale['Upload']): ReactNode => (
  604. <div
  605. role="button"
  606. tabIndex={0}
  607. aria-disabled={disabled}
  608. className={dragAreaCls}
  609. onDrop={this.onDrop}
  610. onDragOver={this.onDragOver}
  611. onDragLeave={this.onDragLeave}
  612. onDragEnter={this.onDragEnter}
  613. onClick={this.onClick}
  614. >
  615. {children ? (
  616. children
  617. ) : (
  618. <>
  619. <div className={`${dragAreaBaseCls}-icon`} x-semi-prop="dragIcon">
  620. {dragIcon || <IconUpload size="extra-large" />}
  621. </div>
  622. <div className={`${dragAreaBaseCls}-text`}>
  623. <div className={`${dragAreaBaseCls}-main-text`} x-semi-prop="dragMainText">
  624. {dragMainText || locale.mainText}
  625. </div>
  626. <div className={`${dragAreaBaseCls}-sub-text`} x-semi-prop="dragSubText">
  627. {dragSubText}
  628. </div>
  629. <div className={`${dragAreaBaseCls}-tips`}>
  630. {dragAreaStatus === strings.DRAG_AREA_LEGAL && (
  631. <span className={`${dragAreaBaseCls}-tips-legal`}>{locale.legalTips}</span>
  632. )}
  633. {dragAreaStatus === strings.DRAG_AREA_ILLEGAL && (
  634. <span className={`${dragAreaBaseCls}-tips-illegal`}>
  635. {locale.illegalTips}
  636. </span>
  637. )}
  638. </div>
  639. </div>
  640. </>
  641. )}
  642. </div>
  643. )}
  644. </LocaleConsumer>
  645. );
  646. };
  647. render(): ReactNode {
  648. const {
  649. style,
  650. className,
  651. multiple,
  652. accept,
  653. disabled,
  654. children,
  655. capture,
  656. listType,
  657. prompt,
  658. promptPosition,
  659. draggable,
  660. validateMessage,
  661. validateStatus,
  662. directory,
  663. ...rest
  664. } = this.props;
  665. const uploadCls = cls(
  666. prefixCls,
  667. {
  668. [`${prefixCls}-picture`]: listType === strings.FILE_LIST_PIC,
  669. [`${prefixCls}-disabled`]: disabled,
  670. [`${prefixCls}-default`]: validateStatus === 'default',
  671. [`${prefixCls}-error`]: validateStatus === 'error',
  672. [`${prefixCls}-warning`]: validateStatus === 'warning',
  673. [`${prefixCls}-success`]: validateStatus === 'success',
  674. },
  675. className
  676. );
  677. const inputCls = cls(`${prefixCls}-hidden-input`);
  678. const inputReplaceCls = cls(`${prefixCls}-hidden-input-replace`);
  679. const promptCls = cls(`${prefixCls}-prompt`);
  680. const validateMsgCls = cls(`${prefixCls}-validate-message`);
  681. const dirProps = directory ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {};
  682. return (
  683. <div className={uploadCls} style={style} x-prompt-pos={promptPosition} {...this.getDataAttr(rest)}>
  684. <input
  685. key={this.state.inputKey}
  686. capture={capture}
  687. multiple={multiple}
  688. accept={accept}
  689. onChange={this.onChange}
  690. type="file"
  691. autoComplete="off"
  692. tabIndex={-1}
  693. className={inputCls}
  694. ref={this.inputRef}
  695. {...dirProps}
  696. />
  697. <input
  698. key={this.state.replaceInputKey}
  699. multiple={false}
  700. accept={accept}
  701. onChange={this.onReplaceChange}
  702. type="file"
  703. autoComplete="off"
  704. tabIndex={-1}
  705. className={inputReplaceCls}
  706. ref={this.replaceInputRef}
  707. />
  708. {this.renderAddContent()}
  709. {prompt ? (
  710. <div className={promptCls} x-semi-prop="prompt">
  711. {prompt}
  712. </div>
  713. ) : null}
  714. {validateMessage ? (
  715. <div className={validateMsgCls} x-semi-prop="validateMessage">
  716. {validateMessage}
  717. </div>
  718. ) : null}
  719. {this.renderFileList()}
  720. </div>
  721. );
  722. }
  723. }
  724. export default Upload;