index.tsx 27 KB

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