index.tsx 29 KB

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