index.tsx 29 KB

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