>>;
+
+export default defineComponent({
+ name: 'ListItem',
+ inheritAttrs: false,
+ props: listItemProps(),
+ setup(props, { slots, attrs }) {
+ const showProgress = ref(false);
+ const progressRafRef = ref();
+ onMounted(() => {
+ progressRafRef.value = setTimeout(() => {
+ showProgress.value = true;
+ }, 300);
+ });
+ onBeforeUnmount(() => {
+ clearTimeout(progressRafRef.value);
+ });
+ const { rootPrefixCls } = useConfigInject('upload', props);
+ const transitionProps = computed(() => getTransitionProps(`${rootPrefixCls.value}-fade`));
+ return () => {
+ const {
+ prefixCls,
+ locale,
+ listType,
+ file,
+ items,
+ progress: progressProps,
+ iconRender,
+ actionIconRender,
+ itemRender = slots.itemRender,
+ isImgUrl,
+ showPreviewIcon,
+ showRemoveIcon,
+ showDownloadIcon,
+ previewIcon: customPreviewIcon = slots.previewIcon,
+ removeIcon: customRemoveIcon = slots.removeIcon,
+ downloadIcon: customDownloadIcon = slots.downloadIcon,
+ onPreview,
+ onDownload,
+ onClose,
+ } = props;
+ const { class: className, style } = attrs;
+ // This is used for legacy span make scrollHeight the wrong value.
+ // We will force these to be `display: block` with non `picture-card`
+ const spanClassName = `${prefixCls}-span`;
+
+ const iconNode = iconRender({ file });
+ let icon = {iconNode}
;
+ if (listType === 'picture' || listType === 'picture-card') {
+ if (file.status === 'uploading' || (!file.thumbUrl && !file.url)) {
+ const uploadingClassName = {
+ [`${prefixCls}-list-item-thumbnail`]: true,
+ [`${prefixCls}-list-item-file`]: file.status !== 'uploading',
+ };
+ icon = {iconNode}
;
+ } else {
+ const thumbnail = isImgUrl?.(file) ? (
+
+ ) : (
+ iconNode
+ );
+ const aClassName = {
+ [`${prefixCls}-list-item-thumbnail`]: true,
+ [`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file),
+ };
+ icon = (
+ onPreview(file, e)}
+ href={file.url || file.thumbUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {thumbnail}
+
+ );
+ }
+ }
+
+ const infoUploadingClass = {
+ [`${prefixCls}-list-item`]: true,
+ [`${prefixCls}-list-item-${file.status}`]: true,
+ [`${prefixCls}-list-item-list-type-${listType}`]: true,
+ };
+ const linkProps =
+ typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
+
+ const removeIcon = showRemoveIcon
+ ? actionIconRender({
+ customIcon: customRemoveIcon ? customRemoveIcon({ file }) : ,
+ callback: () => onClose(file),
+ prefixCls,
+ title: locale.removeFile,
+ })
+ : null;
+
+ const downloadIcon =
+ showDownloadIcon && file.status === 'done'
+ ? actionIconRender({
+ customIcon: customDownloadIcon ? customDownloadIcon({ file }) : ,
+ callback: () => onDownload(file),
+ prefixCls,
+ title: locale.downloadFile,
+ })
+ : null;
+ const downloadOrDelete = listType !== 'picture-card' && (
+
+ {downloadIcon}
+ {removeIcon}
+
+ );
+ const listItemNameClass = `${prefixCls}-list-item-name`;
+ const preview = file.url
+ ? [
+ onPreview(file, e)}
+ >
+ {file.name}
+ ,
+ downloadOrDelete,
+ ]
+ : [
+ onPreview(file, e)}
+ title={file.name}
+ >
+ {file.name}
+ ,
+ downloadOrDelete,
+ ];
+ const previewStyle: CSSProperties = {
+ pointerEvents: 'none',
+ opacity: 0.5,
+ };
+ const previewIcon = showPreviewIcon ? (
+ onPreview(file, e)}
+ title={locale.previewFile}
+ >
+ {customPreviewIcon ? customPreviewIcon({ file }) : }
+
+ ) : null;
+
+ const actions = listType === 'picture-card' && file.status !== 'uploading' && (
+
+ {previewIcon}
+ {file.status === 'done' && downloadIcon}
+ {removeIcon}
+
+ );
+
+ let message;
+ if (file.response && typeof file.response === 'string') {
+ message = file.response;
+ } else {
+ message = file.error?.statusText || file.error?.message || locale.uploadError;
+ }
+ const iconAndPreview = (
+
+ {icon}
+ {preview}
+
+ );
+
+ const dom = (
+
+
{iconAndPreview}
+ {actions}
+ {showProgress.value && (
+
+
+ {'percent' in file ? (
+
+ ) : null}
+
+
+ )}
+
+ );
+ const listContainerNameClass = {
+ [`${prefixCls}-list-${listType}-container`]: true,
+ [`${className}`]: !!className,
+ };
+ const item =
+ file.status === 'error' ? (
+ node.parentNode as HTMLElement}>
+ {dom}
+
+ ) : (
+ dom
+ );
+
+ return (
+
+ {itemRender
+ ? itemRender({
+ originNode: item,
+ file,
+ fileList: items,
+ actions: {
+ download: onDownload.bind(null, file),
+ preview: onPreview.bind(null, file),
+ remove: onClose.bind(null, file),
+ },
+ })
+ : item}
+
+ );
+ };
+ },
+});
diff --git a/components/upload copy/UploadList/index.tsx b/components/upload copy/UploadList/index.tsx
new file mode 100644
index 000000000..89955c212
--- /dev/null
+++ b/components/upload copy/UploadList/index.tsx
@@ -0,0 +1,261 @@
+import * as React from 'react';
+import CSSMotion, { CSSMotionList, CSSMotionListProps } from 'rc-motion';
+import classNames from 'classnames';
+import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
+import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined';
+import PictureTwoTone from '@ant-design/icons/PictureTwoTone';
+import FileTwoTone from '@ant-design/icons/FileTwoTone';
+import { cloneElement, isValidElement } from '../../_util/reactNode';
+import { UploadListProps, UploadFile, UploadListType, InternalUploadFile } from '../interface';
+import { previewImage, isImageUrl } from '../utils';
+import collapseMotion from '../../_util/motion';
+import { ConfigContext } from '../../config-provider';
+import Button, { ButtonProps } from '../../button';
+import useForceUpdate from '../../_util/hooks/useForceUpdate';
+import ListItem from './ListItem';
+
+const listItemMotion: Partial = {
+ ...collapseMotion,
+};
+
+delete listItemMotion.onAppearEnd;
+delete listItemMotion.onEnterEnd;
+delete listItemMotion.onLeaveEnd;
+
+const InternalUploadList: React.ForwardRefRenderFunction = (
+ {
+ listType,
+ previewFile,
+ onPreview,
+ onDownload,
+ onRemove,
+ locale,
+ iconRender,
+ isImageUrl: isImgUrl,
+ prefixCls: customizePrefixCls,
+ items = [],
+ showPreviewIcon,
+ showRemoveIcon,
+ showDownloadIcon,
+ removeIcon,
+ previewIcon,
+ downloadIcon,
+ progress,
+ appendAction,
+ itemRender,
+ },
+ ref,
+) => {
+ const forceUpdate = useForceUpdate();
+ const [motionAppear, setMotionAppear] = React.useState(false);
+
+ // ============================= Effect =============================
+ React.useEffect(() => {
+ if (listType !== 'picture' && listType !== 'picture-card') {
+ return;
+ }
+ (items || []).forEach((file: InternalUploadFile) => {
+ if (
+ typeof document === 'undefined' ||
+ typeof window === 'undefined' ||
+ !(window as any).FileReader ||
+ !(window as any).File ||
+ !(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) ||
+ file.thumbUrl !== undefined
+ ) {
+ return;
+ }
+ file.thumbUrl = '';
+ if (previewFile) {
+ previewFile(file.originFileObj as File).then((previewDataUrl: string) => {
+ // Need append '' to avoid dead loop
+ file.thumbUrl = previewDataUrl || '';
+ forceUpdate();
+ });
+ }
+ });
+ }, [listType, items, previewFile]);
+
+ React.useEffect(() => {
+ setMotionAppear(true);
+ }, []);
+
+ // ============================= Events =============================
+ const onInternalPreview = (file: UploadFile, e?: React.SyntheticEvent) => {
+ if (!onPreview) {
+ return;
+ }
+ e?.preventDefault();
+ return onPreview(file);
+ };
+
+ const onInternalDownload = (file: UploadFile) => {
+ if (typeof onDownload === 'function') {
+ onDownload(file);
+ } else if (file.url) {
+ window.open(file.url);
+ }
+ };
+
+ const onInternalClose = (file: UploadFile) => {
+ onRemove?.(file);
+ };
+
+ const internalIconRender = (file: UploadFile) => {
+ if (iconRender) {
+ return iconRender(file, listType);
+ }
+ const isLoading = file.status === 'uploading';
+ const fileIcon = isImgUrl && isImgUrl(file) ? : ;
+ let icon: React.ReactNode = isLoading ? : ;
+ if (listType === 'picture') {
+ icon = isLoading ? : fileIcon;
+ } else if (listType === 'picture-card') {
+ icon = isLoading ? locale.uploading : fileIcon;
+ }
+ return icon;
+ };
+
+ const actionIconRender = (
+ customIcon: React.ReactNode,
+ callback: () => void,
+ prefixCls: string,
+ title?: string,
+ ) => {
+ const btnProps: ButtonProps = {
+ type: 'text',
+ size: 'small',
+ title,
+ onClick: (e: React.MouseEvent) => {
+ callback();
+ if (isValidElement(customIcon) && customIcon.props.onClick) {
+ customIcon.props.onClick(e);
+ }
+ },
+ className: `${prefixCls}-list-item-card-actions-btn`,
+ };
+ if (isValidElement(customIcon)) {
+ const btnIcon = cloneElement(customIcon, {
+ ...customIcon.props,
+ onClick: () => {},
+ });
+
+ return ;
+ }
+ return (
+
+ );
+ };
+
+ // ============================== Ref ===============================
+ // Test needs
+ React.useImperativeHandle(ref, () => ({
+ handlePreview: onInternalPreview,
+ handleDownload: onInternalDownload,
+ }));
+
+ const { getPrefixCls, direction } = React.useContext(ConfigContext);
+
+ // ============================= Render =============================
+ const prefixCls = getPrefixCls('upload', customizePrefixCls);
+
+ const listClassNames = classNames({
+ [`${prefixCls}-list`]: true,
+ [`${prefixCls}-list-${listType}`]: true,
+ [`${prefixCls}-list-rtl`]: direction === 'rtl',
+ });
+
+ // >>> Motion config
+ const motionKeyList = [
+ ...items.map(file => ({
+ key: file.uid,
+ file,
+ })),
+ ];
+
+ const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate';
+ // const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`;
+
+ let motionConfig: Omit = {
+ motionDeadline: 2000,
+ motionName: `${prefixCls}-${animationDirection}`,
+ keys: motionKeyList,
+ motionAppear,
+ };
+
+ if (listType !== 'picture-card') {
+ motionConfig = {
+ ...listItemMotion,
+ ...motionConfig,
+ };
+ }
+
+ return (
+
+
+ {({ key, file, className: motionClassName, style: motionStyle }) => (
+
+ )}
+
+
+ {/* Append action */}
+ {appendAction && (
+
+ {({ className: motionClassName, style: motionStyle }) =>
+ cloneElement(appendAction, oriProps => ({
+ className: classNames(oriProps.className, motionClassName),
+ style: {
+ ...motionStyle,
+ ...oriProps.style,
+ },
+ }))
+ }
+
+ )}
+
+ );
+};
+
+const UploadList = React.forwardRef(InternalUploadList);
+
+UploadList.displayName = 'UploadList';
+
+UploadList.defaultProps = {
+ listType: 'text' as UploadListType, // or picture
+ progress: {
+ strokeWidth: 2,
+ showInfo: false,
+ },
+ showRemoveIcon: true,
+ showDownloadIcon: false,
+ showPreviewIcon: true,
+ previewFile: previewImage,
+ isImageUrl,
+};
+
+export default UploadList;
diff --git a/components/upload copy/__tests__/__snapshots__/demo.test.js.snap b/components/upload copy/__tests__/__snapshots__/demo.test.js.snap
new file mode 100644
index 000000000..924cda504
--- /dev/null
+++ b/components/upload copy/__tests__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,126 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders ./components/upload/demo/avatar.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/upload/demo/basic.vue correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/upload/demo/defaultFileList.vue correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/upload/demo/directory.vue correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/upload/demo/drag.vue correctly 1`] = `
+
+
Click or drag file to this area to upload
+
Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files
+
+
+`;
+
+exports[`renders ./components/upload/demo/fileList.vue correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/upload/demo/picture-card.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/upload/demo/picture-style.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/upload/demo/preview-file.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/upload/demo/transform-file.vue correctly 1`] = `
+
+`;
diff --git a/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap
new file mode 100644
index 000000000..9ec7b50d1
--- /dev/null
+++ b/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Upload List handle error 1`] = `
+
+`;
+
+exports[`Upload List handle error 2`] = `
+
+`;
+
+exports[`Upload List should be uploading when upload a file 1`] = `
`;
+
+exports[`Upload List should non-image format file preview 1`] = `
+
+`;
diff --git a/components/upload copy/__tests__/demo.test.js b/components/upload copy/__tests__/demo.test.js
new file mode 100644
index 000000000..8fd9b81ab
--- /dev/null
+++ b/components/upload copy/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('upload', { skip: ['upload-manually'] });
diff --git a/components/upload copy/__tests__/mock.js b/components/upload copy/__tests__/mock.js
new file mode 100644
index 000000000..1f2e57c8a
--- /dev/null
+++ b/components/upload copy/__tests__/mock.js
@@ -0,0 +1,14 @@
+import mock from 'xhr-mock';
+
+export function setup() {
+ mock.setup();
+ mock.post('http://upload.com/', (req, res) => {
+ req.headers({
+ 'content-length': 100,
+ });
+ req.body('thisisbody');
+ return res;
+ });
+}
+
+export const teardown = mock.teardown.bind(mock);
diff --git a/components/upload copy/__tests__/requests.js b/components/upload copy/__tests__/requests.js
new file mode 100644
index 000000000..d90ed171f
--- /dev/null
+++ b/components/upload copy/__tests__/requests.js
@@ -0,0 +1,11 @@
+export const successRequest = ({ onSuccess, file }) => {
+ setTimeout(() => {
+ onSuccess(null, file);
+ });
+};
+
+export const errorRequest = ({ onError }) => {
+ setTimeout(() => {
+ onError();
+ });
+};
diff --git a/components/upload copy/__tests__/upload.test.js b/components/upload copy/__tests__/upload.test.js
new file mode 100644
index 000000000..e82a8907e
--- /dev/null
+++ b/components/upload copy/__tests__/upload.test.js
@@ -0,0 +1,331 @@
+import { mount } from '@vue/test-utils';
+import Upload from '..';
+import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from '../utils';
+import PropsTypes from '../../_util/vue-types';
+import { uploadListProps } from '../interface';
+import { setup, teardown } from './mock';
+
+uploadListProps.items = PropsTypes.any;
+
+describe('Upload', () => {
+ beforeEach(() => setup());
+ afterEach(() => teardown());
+ it('should get refs inside Upload in componentDidMount', () => {
+ let ref = null;
+ const APP = {
+ mounted() {
+ ref = this.$refs.input;
+ },
+ render() {
+ return (
+
+
+
+ );
+ },
+ };
+ mount(APP);
+ expect(ref).toBeDefined();
+ });
+
+ xit('return promise in beforeUpload', done => {
+ const data = jest.fn();
+ const props = {
+ props: {
+ action: 'http://upload.com',
+ beforeUpload: () => new Promise(resolve => setTimeout(() => resolve('success'), 100)),
+ data,
+ },
+ listeners: {
+ change: ({ file }) => {
+ if (file.status !== 'uploading') {
+ expect(data).toBeCalled();
+ done();
+ }
+ },
+ },
+ slots: {
+ default: () => ,
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ wrapper.findComponent('ajaxUploader').vm.onChange({
+ target: {
+ files: [{ file: 'foo.png' }],
+ },
+ });
+ }, 0);
+ });
+
+ xit('upload promise return file in beforeUpload', done => {
+ const data = jest.fn();
+ const props = {
+ action: 'http://upload.com',
+ beforeUpload: file =>
+ new Promise(resolve =>
+ setTimeout(() => {
+ const result = file;
+ result.name = 'test.png';
+ resolve(result);
+ }, 100),
+ ),
+ data,
+ onChange: ({ file }) => {
+ if (file.status !== 'uploading') {
+ expect(data).toBeCalled();
+ expect(file.name).toEqual('test.png');
+ done();
+ }
+ },
+ slots: {
+ default: () => ,
+ },
+ sync: false,
+ };
+
+ const wrapper = mount(Upload, props);
+
+ setTimeout(() => {
+ wrapper.find({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [{ file: 'foo.png' }],
+ },
+ });
+ }, 0);
+ });
+
+ xit('should not stop upload when return value of beforeUpload is false', done => {
+ const data = jest.fn();
+ const props = {
+ action: 'http://upload.com',
+ beforeUpload: () => false,
+ data,
+ onChange: ({ file }) => {
+ expect(file instanceof File).toBe(true);
+ expect(data).not.toBeCalled();
+ done();
+ },
+ slots: {
+ default: () => ,
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.find({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ }, 0);
+ });
+
+ xit('should increase percent automaticly when call autoUpdateProgress in IE', done => {
+ let uploadInstance;
+ let lastPercent = -1;
+ const props = {
+ props: {
+ action: 'http://upload.com',
+ },
+ listeners: {
+ change: ({ file }) => {
+ if (file.percent === 0 && file.status === 'uploading') {
+ // manually call it
+ uploadInstance.autoUpdateProgress(0, file);
+ }
+ if (file.status === 'uploading') {
+ expect(file.percent).toBeGreaterThan(lastPercent);
+ lastPercent = file.percent;
+ }
+ if (file.status === 'done' || file.status === 'error') {
+ done();
+ }
+ },
+ },
+ slots: {
+ default: '',
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.find({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ uploadInstance = wrapper.vm;
+ }, 0);
+ });
+ xit('should not stop upload when return value of beforeUpload is not false', done => {
+ const data = jest.fn();
+ const props = {
+ props: {
+ action: 'http://upload.com',
+ beforeUpload() {},
+ data,
+ },
+ listeners: {
+ change: () => {
+ expect(data).toBeCalled();
+ done();
+ },
+ },
+ slots: {
+ default: '',
+ },
+ sync: false,
+ };
+
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.find({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ }, 0);
+ });
+
+ describe('util', () => {
+ // https://github.com/react-component/upload/issues/36
+ it('should T() return true', () => {
+ const res = T();
+ expect(res).toBe(true);
+ });
+ it('should be able to copy file instance', () => {
+ const file = new File([], 'aaa.zip');
+ const copiedFile = fileToObject(file);
+ ['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => {
+ expect(key in copiedFile).toBe(true);
+ });
+ });
+ it('should be able to progress from 0.1 ', () => {
+ // 0.1 -> 0.98
+ const getPercent = genPercentAdd();
+ let curPercent = 0;
+ curPercent = getPercent(curPercent);
+ expect(curPercent).toBe(0.1);
+ });
+
+ it('should be able to progress to 0.98 ', () => {
+ // 0.1 -> 0.98
+ const getPercent = genPercentAdd();
+ let curPercent = 0;
+ for (let i = 0; i < 500; i += 1) {
+ curPercent = getPercent(curPercent);
+ }
+ expect(parseFloat(curPercent.toFixed(2))).toBe(0.98);
+ });
+
+ it('should be able to get fileItem', () => {
+ const file = { uid: '-1', name: 'item.jpg' };
+ const fileList = [
+ {
+ uid: '-1',
+ name: 'item.jpg',
+ },
+ ];
+ const targetItem = getFileItem(file, fileList);
+ expect(targetItem).toBe(fileList[0]);
+ });
+
+ it('should be able to remove fileItem', () => {
+ const file = { uid: '-1', name: 'item.jpg' };
+ const fileList = [
+ {
+ uid: '-1',
+ name: 'item.jpg',
+ },
+ {
+ uid: '-2',
+ name: 'item2.jpg',
+ },
+ ];
+ const targetItem = removeFileItem(file, fileList);
+ expect(targetItem).toEqual(fileList.slice(1));
+ });
+
+ it('should not be able to remove fileItem', () => {
+ const file = { uid: '-3', name: 'item.jpg' };
+ const fileList = [
+ {
+ uid: '-1',
+ name: 'item.jpg',
+ },
+ {
+ uid: '-2',
+ name: 'item2.jpg',
+ },
+ ];
+ const targetItem = removeFileItem(file, fileList);
+ expect(targetItem).toBe(null);
+ });
+ });
+
+ it('should support linkProps as object', () => {
+ const fileList = [
+ {
+ uid: '-1',
+ name: 'foo.png',
+ status: 'done',
+ url: 'http://www.baidu.com/xxx.png',
+ linkProps: {
+ download: 'image',
+ rel: 'noopener',
+ },
+ },
+ ];
+ const props = {
+ props: {
+ fileList,
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const linkNode = wrapper.find('a.ant-upload-list-item-name');
+ expect(linkNode.props().download).toBe('image');
+ expect(linkNode.props().rel).toBe('noopener');
+ }, 0);
+ });
+
+ it('should support linkProps as json stringify', () => {
+ const linkPropsString = JSON.stringify({
+ download: 'image',
+ rel: 'noopener',
+ });
+ const fileList = [
+ {
+ uid: '-1',
+ name: 'foo.png',
+ status: 'done',
+ url: 'http://www.baidu.com/xxx.png',
+ linkProps: linkPropsString,
+ },
+ ];
+ const props = {
+ props: {
+ fileList,
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const linkNode = wrapper.find('a.ant-upload-list-item-name');
+ expect(linkNode.props().download).toBe('image');
+ expect(linkNode.props().rel).toBe('noopener');
+ }, 0);
+ });
+});
diff --git a/components/upload copy/__tests__/uploadlist.test.js b/components/upload copy/__tests__/uploadlist.test.js
new file mode 100644
index 000000000..6fe88fca9
--- /dev/null
+++ b/components/upload copy/__tests__/uploadlist.test.js
@@ -0,0 +1,433 @@
+import { mount } from '@vue/test-utils';
+import * as Vue from 'vue';
+import Upload from '..';
+import { errorRequest, successRequest } from './requests';
+import PropsTypes from '../../_util/vue-types';
+import { uploadListProps } from '../interface';
+import { sleep } from '../../../tests/utils';
+import { h } from 'vue';
+
+uploadListProps.items = PropsTypes.any;
+
+const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));
+const fileList = [
+ {
+ uid: -1,
+ name: 'xxx.png',
+ status: 'done',
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
+ },
+ {
+ uid: -2,
+ name: 'yyy.png',
+ status: 'done',
+ url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
+ thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ },
+];
+
+describe('Upload List', () => {
+ // jsdom not support `createObjectURL` yet. Let's handle this.
+ const originCreateObjectURL = window.URL.createObjectURL;
+ window.URL.createObjectURL = jest.fn(() => '');
+ const originHTMLCanvasElementGetContext = window.HTMLCanvasElement.prototype.getContext;
+ window.HTMLCanvasElement.prototype.getContext = jest.fn(() => '');
+ // https://github.com/ant-design/ant-design/issues/4653
+ afterAll(() => {
+ window.URL.createObjectURL = originCreateObjectURL;
+ window.HTMLCanvasElement.prototype.getContext = originHTMLCanvasElementGetContext;
+ });
+ it('should use file.thumbUrl for
in priority', done => {
+ const props = {
+ props: {
+ defaultFileList: fileList,
+ listType: 'picture',
+ action: '',
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ Vue.nextTick(() => {
+ fileList.forEach((file, i) => {
+ const linkNode = wrapper.findAll('.ant-upload-list-item-thumbnail')[i];
+ const imgNode = wrapper.findAll('.ant-upload-list-item-thumbnail img')[i];
+ expect(linkNode.attributes().href).toBe(file.url);
+ expect(imgNode.attributes().src).toBe(file.thumbUrl);
+ });
+ done();
+ });
+ });
+
+ // https://github.com/ant-design/ant-design/issues/7269
+ it('should remove correct item when uid is 0', done => {
+ const list = [
+ {
+ uid: 0,
+ name: 'xxx.png',
+ status: 'done',
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
+ },
+ {
+ uid: 1,
+ name: 'xxx.png',
+ status: 'done',
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
+ },
+ ];
+ const props = {
+ props: {
+ defaultFileList: list,
+ action: '',
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(async () => {
+ expect(wrapper.findAll('.ant-upload-list-item').length).toBe(2);
+ wrapper.findAll('.ant-upload-list-item')[0].find('.anticon-delete').trigger('click');
+ await delay(400);
+ // wrapper.update();
+ expect(wrapper.findAll('.ant-upload-list-item').length).toBe(1);
+ done();
+ }, 0);
+ });
+
+ xit('should be uploading when upload a file', done => {
+ const props = {
+ props: {
+ action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
+ customRequest: successRequest,
+ onChange: ({ file }) => {
+ if (file.status === 'uploading') {
+ expect(wrapper.html()).toMatchSnapshot();
+ done();
+ }
+ if (file.status === 'done') {
+ expect(wrapper.html()).toMatchSnapshot();
+ done();
+ }
+ },
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ }, 0);
+ });
+
+ xit('handle error', done => {
+ const props = {
+ props: {
+ action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
+ customRequest: errorRequest,
+ },
+ listeners: {
+ change: ({ file }) => {
+ if (file.status !== 'uploading') {
+ expect(wrapper.html()).toMatchSnapshot();
+ done();
+ }
+ },
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ }, 0);
+ });
+
+ xit('does concat filelist when beforeUpload returns false', done => {
+ const handleChange = jest.fn();
+ const props = {
+ props: {
+ action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
+ listType: 'picture',
+ defaultFileList: fileList,
+ beforeUpload: () => false,
+ onChange: handleChange,
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+
+ setTimeout(() => {
+ const mockFile = new File(['foo'], 'foo.png', {
+ type: 'image/png',
+ });
+ wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({
+ target: {
+ files: [mockFile],
+ },
+ });
+ Vue.nextTick(() => {
+ expect(wrapper.vm.sFileList.length).toBe(fileList.length + 1);
+ expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
+ done();
+ });
+ }, 0);
+ });
+
+ // https://github.com/ant-design/ant-design/issues/7762
+ // it('work with form validation', (done) => {
+ // let errors
+ // const TestForm = {
+ // methods: {
+ // handleSubmit () {
+ // const { validateFields } = this.form
+ // validateFields((err) => {
+ // errors = err
+ // })
+ // },
+ // },
+ // render () {
+ // const { getFieldDecorator } = this.form
+
+ // return (
+ //
+ // {getFieldDecorator('file', {
+ // valuePropname: 'fileList',
+ // getValueFromEvent: e => e.fileList,
+ // rules: [
+ // {
+ // required: true,
+ // validator: (rule, value, callback) => {
+ // if (!value || value.length === 0) {
+ // callback('file required')
+ // } else {
+ // callback()
+ // }
+ // },
+ // },
+ // ],
+ // })(
+ // false}
+ // >
+ //
+ //
+ // )}
+ //
+ //
+ // )
+ // },
+ // }
+
+ // const App = Form.create()(TestForm)
+ // console.dir(App)
+ // const wrapper = mount(() => {
+ // return
+ // })
+ // setTimeout(async () => {
+ // wrapper.find(Form).trigger('submit')
+ // expect(errors.file.errors).toEqual([{ message: 'file required', field: 'file' }])
+
+ // const mockFile = new File(['foo'], 'foo.png', {
+ // type: 'image/png',
+ // })
+ // wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({
+ // target: {
+ // files: [mockFile],
+ // },
+ // })
+ // wrapper.find(Form).trigger('submit')
+ // expect(errors).toBeNull()
+ // done()
+ // }, 0)
+ // })
+
+ it('should support onPreview', async () => {
+ const handlePreview = jest.fn();
+ const props = {
+ props: {
+ defaultFileList: fileList,
+ listType: 'picture-card',
+ action: '',
+ onPreview: handlePreview,
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ await sleep(500);
+ wrapper.findAll('.anticon-eye')[0].trigger('click');
+ expect(handlePreview).toBeCalledWith(fileList[0]);
+ wrapper.findAll('.anticon-eye')[1].trigger('click');
+ expect(handlePreview).toBeCalledWith(fileList[1]);
+ });
+
+ it('should support onRemove', done => {
+ const handleRemove = jest.fn();
+ const handleChange = jest.fn();
+ const props = {
+ props: {
+ defaultFileList: fileList,
+ listType: 'picture-card',
+ action: '',
+ remove: handleRemove,
+ onChange: handleChange,
+ },
+
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ jest.setTimeout(300000);
+ setTimeout(async () => {
+ wrapper.findAll('.anticon-delete')[0].trigger('click');
+ expect(handleRemove).toBeCalledWith(fileList[0]);
+ wrapper.findAll('.anticon-delete')[1].trigger('click');
+ expect(handleRemove).toBeCalledWith(fileList[1]);
+ await delay(0);
+ expect(handleChange.mock.calls.length).toBe(2);
+ done();
+ }, 0);
+ });
+
+ xit('should generate thumbUrl from file', done => {
+ const handlePreview = jest.fn();
+ const newFileList = [...fileList];
+ const newFile = { ...fileList[0], uid: -3, originFileObj: new File([], 'xxx.png') };
+ delete newFile.thumbUrl;
+ newFileList.push(newFile);
+ const props = {
+ props: {
+ defaultFileList: newFileList,
+ listType: 'picture-card',
+ action: '',
+ onPreview: handlePreview,
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ setTimeout(async () => {
+ const newFile = { ...fileList[2], uid: -4, originFileObj: new File([], 'xxx.png') };
+ newFileList.push(newFile);
+ wrapper.setProps({
+ defaultFileList: [...newFileList],
+ });
+ await delay(200);
+ expect(wrapper.vm.sFileList[2].thumbUrl).not.toBe(undefined);
+ done();
+ }, 1000);
+ });
+
+ it('should non-image format file preview', done => {
+ const list = [
+ {
+ name: 'not-image',
+ status: 'done',
+ uid: -3,
+ url: 'https://cdn.xxx.com/aaa.zip',
+ thumbUrl: 'data:application/zip;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
+ originFileObj: new File([], 'aaa.zip'),
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -4,
+ url: 'https://cdn.xxx.com/aaa',
+ },
+ {
+ name: 'not-image',
+ status: 'done',
+ uid: -5,
+ url: 'https://cdn.xxx.com/aaa.xx',
+ },
+ {
+ name: 'not-image',
+ status: 'done',
+ uid: -6,
+ url: 'https://cdn.xxx.com/aaa.png/xx.xx',
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -7,
+ url: 'https://cdn.xxx.com/xx.xx/aaa.png',
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -8,
+ url: 'https://cdn.xxx.com/xx.xx/aaa.png',
+ thumbUrl: 'data:image/png;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk',
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -9,
+ url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=123',
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -10,
+ url: 'https://cdn.xxx.com/xx.xx/aaa.png#anchor',
+ },
+ {
+ name: 'image',
+ status: 'done',
+ uid: -11,
+ url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=some.query.with.dot',
+ },
+ ];
+ const props = {
+ props: {
+ defaultFileList: list,
+ listType: 'picture',
+ action: '',
+ },
+ slots: {
+ default: () => h('button', 'upload'),
+ },
+ sync: false,
+ };
+ const wrapper = mount(Upload, props);
+ Vue.nextTick(() => {
+ expect(wrapper.html()).toMatchSnapshot();
+ done();
+ });
+ });
+});
diff --git a/components/upload copy/demo/avatar.vue b/components/upload copy/demo/avatar.vue
new file mode 100644
index 000000000..cd68f1a22
--- /dev/null
+++ b/components/upload copy/demo/avatar.vue
@@ -0,0 +1,116 @@
+
+---
+order: 1
+title:
+ zh-CN: 用户头像
+ en-US: Avatar
+---
+
+## zh-CN
+
+点击上传用户头像,并使用 `beforeUpload` 限制用户上传的图片格式和大小。
+
+> `beforeUpload` 的返回值可以是一个 Promise 以支持异步处理,如服务端校验等:[示例](http://react-component.github.io/upload/examples/beforeUpload.html)。
+
+## en-US
+
+Click to upload user's avatar, and validate size and format of picture with `beforeUpload`.
+
+> The return value of function `beforeUpload` can be a Promise to check asynchronously. [demo](http://react-component.github.io/upload/examples/beforeUpload.html)
+
+
+
+
+
+
+
+
+
+
diff --git a/components/upload copy/demo/basic.vue b/components/upload copy/demo/basic.vue
new file mode 100644
index 000000000..3c9862438
--- /dev/null
+++ b/components/upload copy/demo/basic.vue
@@ -0,0 +1,65 @@
+
+---
+order: 0
+title:
+ zh-CN: 点击上传
+ en-US: Upload by clicking
+---
+
+## zh-CN
+
+经典款式,用户点击按钮弹出文件选择框。
+
+## en-US
+
+Classic mode. File selection dialog pops up when upload button is clicked.
+
+
+
+
+
+
+ Click to Upload
+
+
+
+
diff --git a/components/upload copy/demo/defaultFileList.vue b/components/upload copy/demo/defaultFileList.vue
new file mode 100644
index 000000000..0cb1fab90
--- /dev/null
+++ b/components/upload copy/demo/defaultFileList.vue
@@ -0,0 +1,70 @@
+
+---
+order: 2
+title:
+ zh-CN: 已上传的文件列表
+ en-US: Default Files
+---
+
+## zh-CN
+
+使用 `defaultFileList` 设置已上传的内容。
+
+## en-US
+
+Use `defaultFileList` for uploaded files when page init.
+
+
+
+
+
+
+ Upload
+
+
+
+
diff --git a/components/upload copy/demo/directory.vue b/components/upload copy/demo/directory.vue
new file mode 100644
index 000000000..feaacb078
--- /dev/null
+++ b/components/upload copy/demo/directory.vue
@@ -0,0 +1,35 @@
+
+---
+order: 8
+title:
+ zh-CN: 文件夹上传
+ en-US: Upload directory
+---
+
+## zh-CN
+
+支持上传一个文件夹里的所有文件。
+
+## en-US
+
+You can select and upload a whole directory.
+
+
+
+
+
+
+ Upload Directory
+
+
+
+
diff --git a/components/upload copy/demo/drag.vue b/components/upload copy/demo/drag.vue
new file mode 100644
index 000000000..6bf30241a
--- /dev/null
+++ b/components/upload copy/demo/drag.vue
@@ -0,0 +1,68 @@
+
+---
+order: 5
+title:
+ zh-CN: 拖拽上传
+ en-US: Drag and Drop
+---
+
+## zh-CN
+
+把文件拖入指定区域,完成上传,同样支持点击上传。
+
+设置 `multiple` 后,在 `IE10+` 可以一次上传多个文件。
+
+## en-US
+
+You can drag files to a specific area, to upload. Alternatively, you can also upload by selecting.
+
+We can upload serveral files at once in modern browsers by giving the input the `multiple` attribute.
+
+
+
+
+
+
+
+ Click or drag file to this area to upload
+
+ Support for a single or bulk upload. Strictly prohibit from uploading company data or other
+ band files
+
+
+
+
diff --git a/components/upload copy/demo/fileList.vue b/components/upload copy/demo/fileList.vue
new file mode 100644
index 000000000..963343540
--- /dev/null
+++ b/components/upload copy/demo/fileList.vue
@@ -0,0 +1,81 @@
+
+---
+order: 4
+title:
+ zh-CN: 完全控制的上传列表
+ en-US: Complete control over file list
+---
+
+## zh-CN
+
+使用 `fileList` 对列表进行完全控制,可以实现各种自定义功能,以下演示二种情况:
+
+1. 上传列表数量的限制。
+
+2. 读取远程路径并显示链接。
+
+## en-US
+
+You can gain full control over filelist by configuring `fileList`. You can accomplish all kinds of customed functions. The following shows two circumstances:
+
+1. limit the number of uploaded files.
+
+2. read from response and show file link.
+
+
+
+
+
+
+ Upload
+
+
+
+
diff --git a/components/upload copy/demo/index.vue b/components/upload copy/demo/index.vue
new file mode 100644
index 000000000..b1ae4e92e
--- /dev/null
+++ b/components/upload copy/demo/index.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/upload copy/demo/picture-card.vue b/components/upload copy/demo/picture-card.vue
new file mode 100644
index 000000000..34fdd976d
--- /dev/null
+++ b/components/upload copy/demo/picture-card.vue
@@ -0,0 +1,126 @@
+
+---
+order: 3
+title:
+ zh-CN: 照片墙
+ en-US: Pictures Wall
+---
+
+## zh-CN
+
+用户可以上传图片并在列表中显示缩略图。当上传照片数到达限制后,上传按钮消失。
+
+## en-US
+
+After users upload picture, the thumbnail will be shown in list. The upload button will disappear when count meets limitation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/upload copy/demo/picture-style.vue b/components/upload copy/demo/picture-style.vue
new file mode 100644
index 000000000..9ad5b92a9
--- /dev/null
+++ b/components/upload copy/demo/picture-style.vue
@@ -0,0 +1,108 @@
+
+---
+order: 6
+title:
+ zh-CN: 图片列表样式
+ en-US: Pictures with list style
+---
+
+## zh-CN
+
+上传文件为图片,可展示本地缩略图。`IE8/9` 不支持浏览器本地缩略图展示([Ref](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL)),可以写 `thumbUrl` 属性来代替。
+
+## en-US
+
+If uploaded file is a picture, the thumbnail can be shown. `IE8/9` do not support local thumbnail show. Please use `thumbUrl` instead.
+
+
+
+
+
+
+
+ upload
+
+
+
+
+
+
+
+ upload
+
+
+
+
+
+
+
diff --git a/components/upload copy/demo/preview-file.vue b/components/upload copy/demo/preview-file.vue
new file mode 100644
index 000000000..9f2ceb2fb
--- /dev/null
+++ b/components/upload copy/demo/preview-file.vue
@@ -0,0 +1,58 @@
+
+---
+order: 9
+title:
+ zh-CN: 自定义预览
+ en-US: Customize preview file
+---
+
+## zh-CN
+
+自定义本地预览,用于处理非图片格式文件(例如视频文件)。
+
+## en-US
+
+Customize local preview. Can handle with non-image format files such as video.
+
+
+
+
+
+
diff --git a/components/upload copy/demo/transform-file.vue b/components/upload copy/demo/transform-file.vue
new file mode 100644
index 000000000..857a6b89a
--- /dev/null
+++ b/components/upload copy/demo/transform-file.vue
@@ -0,0 +1,66 @@
+
+---
+order: 10
+title:
+ zh-CN: 上传前转换文件
+ en-US: Transform file before request
+---
+
+## zh-CN
+
+使用 `beforeUpload` 转换上传的文件(例如添加水印)。
+
+## en-US
+
+Use `beforeUpload` for transform file before request such as add a watermark.
+
+
+
+
+
+
diff --git a/components/upload copy/demo/upload-manually.vue b/components/upload copy/demo/upload-manually.vue
new file mode 100644
index 000000000..d84c67de8
--- /dev/null
+++ b/components/upload copy/demo/upload-manually.vue
@@ -0,0 +1,96 @@
+
+---
+order: 7
+title:
+ zh-CN: 手动上传
+ en-US: Upload manually
+---
+
+## zh-CN
+
+`beforeUpload` 返回 `false` 后,手动上传文件。
+
+## en-US
+
+Upload files manually after `beforeUpload` returns `false`.
+
+
+
+
+
+
+
+ Select File
+
+
+
+ {{ uploading ? 'Uploading' : 'Start Upload' }}
+
+
+
+
diff --git a/components/upload copy/index.en-US.md b/components/upload copy/index.en-US.md
new file mode 100644
index 000000000..84500cf84
--- /dev/null
+++ b/components/upload copy/index.en-US.md
@@ -0,0 +1,80 @@
+---
+category: Components
+type: Data Entry
+title: Upload
+cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
+---
+
+Upload file by selecting or dragging.
+
+## When To Use
+
+Uploading is the process of publishing information (web pages, text, pictures, video, etc.) to a remote server via a web page or upload tool.
+
+- When you need to upload one or more files.
+- When you need to show the process of uploading.
+- When you need to upload files by dragging and dropping.
+
+## API
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| accept | File types that can be accepted. See [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | - | |
+| action | Uploading URL | string\|(file) => `Promise` | - | |
+| method | http method of upload request | string | `post` | 1.5.0 |
+| directory | support upload whole directory ([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false | |
+| beforeUpload | Hook function which will be executed before uploading. Uploading will be stopped with `false` or a rejected Promise returned. **Warning:this function is not supported in IE9**。 | (file, fileList) => `boolean | Promise` | - | |
+| customRequest | override for the default xhr behavior allowing for additional customization and ability to implement your own XMLHttpRequest | Function | - | |
+| data | Uploading params or function which can return uploading params. | object\|function(file) | - | |
+| disabled | disable upload button | boolean | false | |
+| fileList | List of files that have been uploaded (controlled). Here is a common issue [#2423](https://github.com/ant-design/ant-design/issues/2423) when using it | object\[] | - | |
+| headers | Set request headers, valid above IE10. | object | - | |
+| listType | Built-in stylesheets, support for three types: `text`, `picture` or `picture-card` | string | `text` | |
+| multiple | Whether to support selected multiple file. `IE10+` supported. You can select multiple files with CTRL holding down while multiple is set to be true | boolean | false | |
+| name | The name of uploading file | string | `file` | |
+| previewFile | Customize preview file logic | (file: File \| Blob) => Promise | - | 1.5.0 |
+| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon` and `showRemoveIcon` individually | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
+| supportServerRender | Need to be turned on while the server side is rendering. | boolean | false | |
+| withCredentials | ajax upload with cookie sent | boolean | false | |
+| openFileDialogOnClick | click open file dialog | boolean | true | |
+| remove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is `false` or a Promise which resolve(false) or reject. | Function(file): `boolean | Promise` | - | |
+| transformFile | Customize transform file before request | Function(file): `string | Blob | File | Promise` | - | 1.5.0 |
+
+### events
+
+| Events Name | Description | Arguments | Version |
+| --- | --- | --- | --- | --- |
+| change | A callback function, can be executed when uploading state is changing. See [change](#change) | Function | - | |
+| preview | A callback function, will be executed when file link or preview icon is clicked. | Function(file) | - | |
+| download | Click the method to download the file, pass the method to perform the method logic, do not pass the default jump to the new TAB. | Function(file): void | Jump to new TAB | 1.5.0 |
+| reject | A callback function, will be executed when drop files is not accept. | Function(fileList) | - | |
+
+### change
+
+> The function will be called when uploading is in progress, completed or failed
+
+When uploading state change, it returns:
+
+```jsx
+{
+ file: { /* ... */ },
+ fileList: [ /* ... */ ],
+ event: { /* ... */ },
+}
+```
+
+1. `file` File object for the current operation.
+
+ ```jsx
+ {
+ uid: 'uid', // unique identifier, negative is recommend, to prevent interference with internal generated id
+ name: 'xx.png', // file name
+ status: 'done', // options:uploading, done, error, removed
+ response: '{"status": "success"}', // response from server
+ linkProps: '{"download": "image"}', // additional html props of file link
+ xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header
+ }
+ ```
+
+2. `fileList` current list of files
+3. `event` response from server, including uploading progress, supported by advanced browsers.
diff --git a/components/upload copy/index.tsx b/components/upload copy/index.tsx
new file mode 100644
index 000000000..ba1b8b09a
--- /dev/null
+++ b/components/upload copy/index.tsx
@@ -0,0 +1,17 @@
+import type { App } from 'vue';
+import Upload from './Upload';
+import Dragger from './Dragger';
+
+export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
+
+/* istanbul ignore next */
+export const UploadDragger = Dragger;
+
+export default Object.assign(Upload, {
+ Dragger,
+ install(app: App) {
+ app.component(Upload.name, Upload);
+ app.component(Dragger.name, Dragger);
+ return app;
+ },
+});
diff --git a/components/upload copy/index.zh-CN.md b/components/upload copy/index.zh-CN.md
new file mode 100644
index 000000000..73db3ef2c
--- /dev/null
+++ b/components/upload copy/index.zh-CN.md
@@ -0,0 +1,81 @@
+---
+category: Components
+subtitle: 上传
+type: 数据录入
+title: Upload
+cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
+---
+
+文件选择上传和拖拽上传控件。
+
+## 何时使用
+
+上传是将信息(网页、文字、图片、视频等)通过网页或者上传工具发布到远程服务器上的过程。
+
+- 当需要上传一个或一些文件时。
+- 当需要展现上传的进度时。
+- 当需要使用拖拽交互时。
+
+## API
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| accept | 接受上传的文件类型, 详见 [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | 无 | |
+| action | 上传的地址 | string\|(file) => `Promise` | 无 | |
+| method | 上传请求的 http method | string | `post` | 1.5.0 |
+| directory | 支持上传文件夹([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false | |
+| beforeUpload | 上传文件之前的钩子,参数为上传的文件,若返回 `false` 则停止上传。支持返回一个 Promise 对象,Promise 对象 reject 时则停止上传,resolve 时开始上传( resolve 传入 `File` 或 `Blob` 对象则上传 resolve 传入对象)。**注意:IE9 不支持该方法**。 | (file, fileList) => `boolean | Promise` | 无 | |
+| customRequest | 通过覆盖默认的上传行为,可以自定义自己的上传实现 | Function | 无 | |
+| data | 上传所需参数或返回上传参数的方法 | object\|(file) => object | 无 | |
+| disabled | 是否禁用 | boolean | false | |
+| fileList | 已经上传的文件列表(受控) | object\[] | 无 | |
+| headers | 设置上传的请求头部,IE10 以上有效 | object | 无 | |
+| listType | 上传列表的内建样式,支持三种基本样式 `text`, `picture` 和 `picture-card` | string | `text` | |
+| multiple | 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件。 | boolean | false | |
+| name | 发到后台的文件参数名 | string | `file` | |
+| previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise | 无 | 1.5.0 |
+| showUploadList | 是否展示 uploadList, 可设为一个对象,用于单独设定 showPreviewIcon 和 showRemoveIcon | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
+| supportServerRender | 服务端渲染时需要打开这个 | boolean | false | |
+| withCredentials | 上传请求时是否携带 cookie | boolean | false | |
+| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | |
+| remove | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除。 | Function(file): `boolean | Promise` | 无 | |
+| transformFile | 在上传之前转换文件。支持返回一个 Promise 对象 | Function(file): `string | Blob | File | Promise` | 无 | 1.5.0 |
+
+### 事件
+
+| 事件名称 | 说明 | 回调参数 | 版本 |
+| --- | --- | --- | --- | --- |
+| change | 上传文件改变时的状态,详见 [change](#change) | Function | 无 | |
+| preview | 点击文件链接或预览图标时的回调 | Function(file) | 无 | |
+| download | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页。 | Function(file): void | 跳转新标签页 | 1.5.0 |
+| reject | 拖拽文件不符合 accept 类型时的回调 | Function(fileList) | 无 | |
+
+### change
+
+> 上传中、完成、失败都会调用这个函数。
+
+文件状态改变的回调,返回为:
+
+```jsx
+{
+ file: { /* ... */ },
+ fileList: [ /* ... */ ],
+ event: { /* ... */ },
+}
+```
+
+1. `file` 当前操作的文件对象。
+
+ ```jsx
+ {
+ uid: 'uid', // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突
+ name: 'xx.png', // 文件名
+ status: 'done', // 状态有:uploading done error removed
+ response: '{"status": "success"}', // 服务端响应内容
+ linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性
+ xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header
+ }
+ ```
+
+2. `fileList` 当前的文件列表。
+3. `event` 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。
diff --git a/components/upload copy/interface.tsx b/components/upload copy/interface.tsx
new file mode 100755
index 000000000..91e78958f
--- /dev/null
+++ b/components/upload copy/interface.tsx
@@ -0,0 +1,183 @@
+import type {
+ RcFile as OriRcFile,
+ UploadRequestOption as RcCustomRequestOptions,
+} from '../vc-upload/interface';
+import type { ProgressProps } from '../progress';
+import type { VueNode } from '../_util/type';
+import type { ExtractPropTypes, PropType } from 'vue';
+
+export interface RcFile extends OriRcFile {
+ readonly lastModifiedDate: Date;
+}
+
+export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
+
+export interface HttpRequestHeader {
+ [key: string]: string;
+}
+
+export interface UploadFile {
+ uid: string;
+ size?: number;
+ name: string;
+ fileName?: string;
+ lastModified?: number;
+ lastModifiedDate?: Date;
+ url?: string;
+ status?: UploadFileStatus;
+ percent?: number;
+ thumbUrl?: string;
+ originFileObj?: RcFile;
+ response?: T;
+ error?: any;
+ linkProps?: any;
+ type?: string;
+ xhr?: T;
+ preview?: string;
+}
+
+export interface InternalUploadFile extends UploadFile {
+ originFileObj: RcFile;
+}
+
+export interface UploadChangeParam {
+ // https://github.com/ant-design/ant-design/issues/14420
+ file: T;
+ fileList: UploadFile[];
+ event?: { percent: number };
+}
+
+// export interface ShowUploadListInterface {
+// showRemoveIcon?: boolean;
+// showPreviewIcon?: boolean;
+// showDownloadIcon?: boolean;
+// removeIcon?: VueNode | ((file: UploadFile) => VueNode);
+// downloadIcon?: VueNode | ((file: UploadFile) => VueNode);
+// previewIcon?: VueNode | ((file: UploadFile) => VueNode);
+// }
+
+export interface UploadLocale {
+ uploading?: string;
+ removeFile?: string;
+ downloadFile?: string;
+ uploadError?: string;
+ previewFile?: string;
+}
+
+export type UploadType = 'drag' | 'select';
+export type UploadListType = 'text' | 'picture' | 'picture-card';
+export type UploadListProgressProps = Omit;
+
+export type ItemRender = (opt: {
+ originNode: VueNode;
+ file: UploadFile;
+ fileList: Array>;
+ actions: {
+ download: () => void;
+ preview: () => void;
+ remove: () => void;
+ };
+}) => VueNode;
+
+type PreviewFileHandler = (file: File | Blob) => PromiseLike;
+type TransformFileHandler = (
+ file: RcFile,
+) => string | Blob | File | PromiseLike;
+type BeforeUploadValueType = void | boolean | string | Blob | File;
+
+function uploadProps() {
+ return {
+ capture: [Boolean, String] as PropType,
+ type: String as PropType,
+ name: String,
+ defaultFileList: Array as PropType>>,
+ fileList: Array as PropType>>,
+ action: [String, Function] as PropType<
+ string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike)
+ >,
+ directory: Boolean,
+ data: [Object, Function] as PropType<
+ | Record
+ | ((file: UploadFile) => Record | Promise>)
+ >,
+ method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>,
+ headers: Object as PropType,
+ showUploadList: Boolean,
+ multiple: Boolean,
+ accept: String,
+ beforeUpload: Function as PropType<
+ (file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise
+ >,
+ onChange: Function as PropType<(info: UploadChangeParam) => void>,
+ onDrop: Function as PropType<(event: DragEvent) => void>,
+ listType: String as PropType,
+ onPreview: Function as PropType<(file: UploadFile) => void>,
+ onDownload: Function as PropType<(file: UploadFile) => void>,
+ onRemove: Function as PropType<
+ (file: UploadFile) => void | boolean | Promise
+ >,
+ supportServerRender: Boolean,
+ disabled: Boolean,
+ prefixCls: String,
+ customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>,
+ withCredentials: Boolean,
+ openFileDialogOnClick: Boolean,
+ locale: Object as PropType,
+ id: String,
+ previewFile: Function as PropType,
+ /** @deprecated Please use `beforeUpload` directly */
+ transformFile: Function as PropType,
+ iconRender: Function as PropType<
+ (opt: { file: UploadFile; listType?: UploadListType }) => VueNode
+ >,
+ isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
+ progress: Object as PropType,
+ itemRender: Function as PropType>,
+ /** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
+ maxCount: Number,
+ height: [Number, String],
+
+ showRemoveIcon: Boolean,
+ showDownloadIcon: Boolean,
+ showPreviewIcon: Boolean,
+ removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ };
+}
+
+export type UploadProps = Partial>>;
+
+export interface UploadState {
+ fileList: UploadFile[];
+ dragState: string;
+}
+
+function uploadListProps() {
+ return {
+ listType: String as PropType,
+ onPreview: Function as PropType<(file: UploadFile) => void>,
+ onDownload: Function as PropType<(file: UploadFile) => void>,
+ onRemove: Function as PropType<(file: UploadFile) => void | boolean>,
+ items: Array as PropType>>,
+ progress: Object as PropType,
+ prefixCls: String as PropType,
+ showRemoveIcon: Boolean,
+ showDownloadIcon: Boolean,
+ showPreviewIcon: Boolean,
+ removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
+ locale: Object as PropType,
+ previewFile: Function as PropType,
+ iconRender: Function as PropType<
+ (opt: { file: UploadFile; listType?: UploadListType }) => VueNode
+ >,
+ isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
+ appendAction: Function as PropType<() => VueNode>,
+ itemRender: Function as PropType>,
+ };
+}
+
+export type UploadListProps = Partial>>;
+export { uploadProps, uploadListProps };
diff --git a/components/upload copy/style/index.less b/components/upload copy/style/index.less
new file mode 100644
index 000000000..49ba7f211
--- /dev/null
+++ b/components/upload copy/style/index.less
@@ -0,0 +1,589 @@
+@import '../../style/themes/index';
+@import '../../style/mixins/index';
+
+@upload-prefix-cls: ~'@{ant-prefix}-upload';
+@upload-item: ~'@{ant-prefix}-upload-list-item';
+@upload-picture-card-size: 104px;
+@upload-picture-card-border-style: @border-style-base;
+
+.@{upload-prefix-cls} {
+ .reset-component();
+
+ outline: 0;
+
+ p {
+ margin: 0;
+ }
+
+ &-btn {
+ display: block;
+ width: 100%;
+ outline: none;
+ }
+
+ input[type='file'] {
+ cursor: pointer;
+ }
+
+ &&-select {
+ display: inline-block;
+ }
+
+ &&-disabled {
+ cursor: not-allowed;
+ }
+
+ &&-select-picture-card {
+ width: @upload-picture-card-size;
+ height: @upload-picture-card-size;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ text-align: center;
+ vertical-align: top;
+ background-color: @background-color-light;
+ border: @border-width-base dashed @border-color-base;
+ border-radius: @border-radius-base;
+ cursor: pointer;
+ transition: border-color 0.3s;
+
+ > .@{upload-prefix-cls} {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ text-align: center;
+ }
+
+ &:hover {
+ border-color: @primary-color;
+ .@{upload-prefix-cls}-disabled& {
+ border-color: @border-color-base;
+ }
+ }
+ }
+
+ &&-drag {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ background: @background-color-light;
+ border: @border-width-base dashed @border-color-base;
+ border-radius: @border-radius-base;
+ cursor: pointer;
+ transition: border-color 0.3s;
+
+ .@{upload-prefix-cls} {
+ padding: @padding-md 0;
+ }
+
+ &.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
+ border-color: @primary-7;
+ }
+
+ &.@{upload-prefix-cls}-disabled {
+ cursor: not-allowed;
+ }
+
+ .@{upload-prefix-cls}-btn {
+ display: table;
+ height: 100%;
+ }
+
+ .@{upload-prefix-cls}-drag-container {
+ display: table-cell;
+ vertical-align: middle;
+ }
+
+ &:not(.@{upload-prefix-cls}-disabled):hover {
+ border-color: @primary-5;
+ }
+
+ p.@{upload-prefix-cls}-drag-icon {
+ .@{iconfont-css-prefix} {
+ color: @primary-5;
+ font-size: 48px;
+ }
+
+ margin-bottom: 20px;
+ }
+ p.@{upload-prefix-cls}-text {
+ margin: 0 0 4px;
+ color: @heading-color;
+ font-size: @font-size-lg;
+ }
+ p.@{upload-prefix-cls}-hint {
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ }
+
+ .@{iconfont-css-prefix}-plus {
+ color: @disabled-color;
+ font-size: 30px;
+ transition: all 0.3s;
+
+ &:hover {
+ color: @text-color-secondary;
+ }
+ }
+ &:hover .@{iconfont-css-prefix}-plus {
+ color: @text-color-secondary;
+ }
+ }
+
+ &-picture-card-wrapper {
+ .clearfix();
+
+ display: inline-block;
+ width: 100%;
+ }
+}
+
+.@{upload-prefix-cls}-list {
+ .reset-component();
+ .clearfix();
+ line-height: @line-height-base;
+
+ // ============================ Item ============================
+ &-item {
+ position: relative;
+ height: @line-height-base * @font-size-base;
+ margin-top: @margin-xs;
+ font-size: @font-size-base;
+
+ &-name {
+ display: inline-block;
+ width: 100%;
+ padding-left: @font-size-base + 8px;
+ overflow: hidden;
+ line-height: @line-height-base;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &-card-actions {
+ position: absolute;
+ right: 0;
+
+ &-btn {
+ opacity: 0;
+ }
+ &-btn.@{ant-prefix}-btn-sm {
+ height: 20px;
+ line-height: 1;
+ }
+
+ &.picture {
+ top: 22px;
+ line-height: 0;
+ }
+
+ &-btn:focus,
+ &.picture &-btn {
+ opacity: 1;
+ }
+
+ .@{iconfont-css-prefix} {
+ color: @upload-actions-color;
+ }
+ }
+
+ &-info {
+ height: 100%;
+ padding: 0 4px;
+ transition: background-color 0.3s;
+
+ > span {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
+
+ .@{iconfont-css-prefix}-loading,
+ .@{upload-prefix-cls}-text-icon {
+ .@{iconfont-css-prefix} {
+ position: absolute;
+ top: (@font-size-base / 2) - 2px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ }
+ }
+ }
+
+ .@{iconfont-css-prefix}-close {
+ position: absolute;
+ top: 6px;
+ right: 4px;
+ color: @text-color-secondary;
+ font-size: 10px;
+ line-height: 0;
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.3s;
+
+ &:hover {
+ color: @text-color;
+ }
+ }
+
+ &:hover &-info {
+ background-color: @item-hover-bg;
+ }
+
+ &:hover .@{iconfont-css-prefix}-close {
+ opacity: 1;
+ }
+
+ &:hover &-card-actions-btn {
+ opacity: 1;
+ }
+
+ &-error,
+ &-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
+ &-error &-name {
+ color: @error-color;
+ }
+
+ &-error &-card-actions {
+ .@{iconfont-css-prefix} {
+ color: @error-color;
+ }
+
+ &-btn {
+ opacity: 1;
+ }
+ }
+
+ &-progress {
+ position: absolute;
+ bottom: -12px;
+ width: 100%;
+ padding-left: @font-size-base + 12px;
+ font-size: @font-size-base;
+ line-height: 0;
+ }
+ }
+
+ // =================== Picture & Picture Card ===================
+ &-picture,
+ &-picture-card {
+ .@{upload-item} {
+ position: relative;
+ height: 66px;
+ padding: @padding-xs;
+ border: @border-width-base @upload-picture-card-border-style @border-color-base;
+ border-radius: @border-radius-base;
+
+ &:hover {
+ background: transparent;
+ }
+
+ &-error {
+ border-color: @error-color;
+ }
+ }
+
+ .@{upload-item}-info {
+ padding: 0;
+ }
+
+ .@{upload-item}:hover .@{upload-item}-info {
+ background: transparent;
+ }
+
+ .@{upload-item}-uploading {
+ border-style: dashed;
+ }
+
+ .@{upload-item}-thumbnail {
+ width: 48px;
+ height: 48px;
+ line-height: 60px;
+ text-align: center;
+ opacity: 0.8;
+
+ .@{iconfont-css-prefix} {
+ font-size: 26px;
+ }
+ }
+
+ // Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160
+ .@{upload-item}-error .@{upload-item}-thumbnail {
+ .@{iconfont-css-prefix} {
+ svg path {
+ &[fill='#e6f7ff'] {
+ fill: @error-color-deprecated-bg;
+ }
+
+ &[fill='#1890ff'] {
+ fill: @error-color;
+ }
+ }
+ }
+ }
+
+ .@{upload-item}-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ font-size: 26px;
+ transform: translate(-50%, -50%);
+
+ .@{iconfont-css-prefix} {
+ font-size: 26px;
+ }
+ }
+
+ .@{upload-item}-image {
+ max-width: 100%;
+ }
+
+ .@{upload-item}-thumbnail img {
+ display: block;
+ width: 48px;
+ height: 48px;
+ overflow: hidden;
+ }
+
+ .@{upload-item}-name {
+ display: inline-block;
+ box-sizing: border-box;
+ max-width: 100%;
+ margin: 0 0 0 8px;
+ padding-right: 8px;
+ padding-left: 48px;
+ overflow: hidden;
+ line-height: 44px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ transition: all 0.3s;
+ }
+
+ .@{upload-item}-uploading .@{upload-item}-name {
+ margin-bottom: 12px;
+ }
+
+ .@{upload-item}-progress {
+ bottom: 14px;
+ width: ~'calc(100% - 24px)';
+ margin-top: 0;
+ padding-left: 56px;
+ }
+
+ .@{iconfont-css-prefix}-close {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ line-height: 1;
+ opacity: 1;
+ }
+ }
+
+ // ======================== Picture Card ========================
+ &-picture-card {
+ &-container {
+ display: inline-block;
+ width: @upload-picture-card-size;
+ height: @upload-picture-card-size;
+ margin: 0 @margin-xs @margin-xs 0;
+ vertical-align: top;
+ }
+
+ &.@{upload-prefix-cls}-list::after {
+ display: none;
+ }
+
+ .@{upload-item} {
+ height: 100%;
+ margin: 0;
+ }
+
+ .@{upload-item}-info {
+ position: relative;
+ height: 100%;
+ overflow: hidden;
+
+ &::before {
+ position: absolute;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ background-color: fade(@black, 50%);
+ opacity: 0;
+ transition: all 0.3s;
+ content: ' ';
+ }
+ }
+
+ .@{upload-item}:hover .@{upload-item}-info::before {
+ opacity: 1;
+ }
+
+ .@{upload-item}-actions {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 10;
+ white-space: nowrap;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ transition: all 0.3s;
+
+ .@{iconfont-css-prefix}-eye,
+ .@{iconfont-css-prefix}-download,
+ .@{iconfont-css-prefix}-delete {
+ z-index: 10;
+ width: 16px;
+ margin: 0 4px;
+ color: @text-color-dark;
+ font-size: 16px;
+ cursor: pointer;
+ transition: all 0.3s;
+
+ &:hover {
+ color: @text-color-inverse;
+ }
+ }
+ }
+
+ .@{upload-item}-info:hover + .@{upload-item}-actions,
+ .@{upload-item}-actions:hover {
+ opacity: 1;
+ }
+
+ .@{upload-item}-thumbnail,
+ .@{upload-item}-thumbnail img {
+ position: static;
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ .@{upload-item}-name {
+ display: none;
+ margin: 8px 0 0;
+ padding: 0;
+ line-height: @line-height-base;
+ text-align: center;
+ }
+
+ .@{upload-item}-file + .@{upload-item}-name {
+ position: absolute;
+ bottom: 10px;
+ display: block;
+ }
+
+ .@{upload-item}-uploading {
+ &.@{upload-item} {
+ background-color: @background-color-light;
+ }
+
+ .@{upload-item}-info {
+ height: auto;
+
+ &::before,
+ .@{iconfont-css-prefix}-eye,
+ .@{iconfont-css-prefix}-delete {
+ display: none;
+ }
+ }
+ }
+
+ .@{upload-item}-progress {
+ bottom: 32px;
+ width: calc(100% - 14px);
+ padding-left: 0;
+ }
+ }
+
+ // ======================= Picture & Text =======================
+ &-text,
+ &-picture {
+ &-container {
+ transition: opacity @animation-duration-slow, height @animation-duration-slow;
+
+ &::before {
+ display: table;
+ width: 0;
+ height: 0;
+ content: '';
+ }
+
+ // Don't know why span here, just stretch it
+ .@{upload-prefix-cls}-span {
+ display: block;
+ flex: auto;
+ }
+ }
+
+ // text & picture no need this additional element.
+ // But it used for picture-card, let's keep it.
+ .@{upload-prefix-cls}-span {
+ display: flex;
+ align-items: center;
+
+ > * {
+ flex: none;
+ }
+ }
+
+ .@{upload-item}-name {
+ flex: auto;
+ margin: 0;
+ padding: 0 @padding-xs;
+ }
+
+ .@{upload-item}-card-actions {
+ position: static;
+ }
+ }
+
+ // ============================ Text ============================
+ &-text {
+ .@{upload-prefix-cls}-text-icon {
+ .@{iconfont-css-prefix} {
+ position: static;
+ }
+ }
+ }
+
+ // =========================== Motion ===========================
+ .@{upload-prefix-cls}-animate-inline-appear,
+ .@{upload-prefix-cls}-animate-inline-enter,
+ .@{upload-prefix-cls}-animate-inline-leave {
+ animation-duration: @animation-duration-slow;
+ animation-fill-mode: @ease-in-out-circ;
+ }
+
+ .@{upload-prefix-cls}-animate-inline-appear,
+ .@{upload-prefix-cls}-animate-inline-enter {
+ animation-name: uploadAnimateInlineIn;
+ }
+
+ .@{upload-prefix-cls}-animate-inline-leave {
+ animation-name: uploadAnimateInlineOut;
+ }
+}
+
+@keyframes uploadAnimateInlineIn {
+ from {
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+ opacity: 0;
+ }
+}
+
+@keyframes uploadAnimateInlineOut {
+ to {
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+ opacity: 0;
+ }
+}
+
+@import './rtl';
diff --git a/components/upload/style/index.ts b/components/upload copy/style/index.tsx
similarity index 82%
rename from components/upload/style/index.ts
rename to components/upload copy/style/index.tsx
index b8fd70a86..582def0cc 100644
--- a/components/upload/style/index.ts
+++ b/components/upload copy/style/index.tsx
@@ -2,5 +2,6 @@ import '../../style/index.less';
import './index.less';
// style dependencies
+import '../../button/style';
import '../../progress/style';
import '../../tooltip/style';
diff --git a/components/upload copy/style/rtl.less b/components/upload copy/style/rtl.less
new file mode 100644
index 000000000..0dd9836ef
--- /dev/null
+++ b/components/upload copy/style/rtl.less
@@ -0,0 +1,179 @@
+@import '../../style/themes/index';
+@import '../../style/mixins/index';
+
+@upload-prefix-cls: ~'@{ant-prefix}-upload';
+@upload-item: ~'@{ant-prefix}-upload-list-item';
+
+.@{upload-prefix-cls} {
+ &-rtl {
+ direction: rtl;
+ }
+
+ &&-select-picture-card {
+ .@{upload-prefix-cls}-rtl& {
+ margin-right: auto;
+ margin-left: 8px;
+ }
+ }
+}
+
+.@{upload-prefix-cls}-list {
+ &-rtl {
+ direction: rtl;
+ }
+
+ &-item-list-type-text {
+ &:hover {
+ .@{upload-prefix-cls}-list-item-name-icon-count-1 {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: 22px;
+ padding-left: 14px;
+ }
+ }
+ .@{upload-prefix-cls}-list-item-name-icon-count-2 {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: 22px;
+ padding-left: 28px;
+ }
+ }
+ }
+ }
+
+ &-item {
+ &-name {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: @font-size-base + 8px;
+ padding-left: 0;
+ }
+ }
+
+ &-name-icon-count-1 {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-left: 14px;
+ }
+ }
+
+ &-card-actions {
+ .@{upload-prefix-cls}-list-rtl & {
+ right: auto;
+ left: 0;
+ }
+ .@{iconfont-css-prefix} {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: 0;
+ padding-left: 5px;
+ }
+ }
+ }
+
+ &-info {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding: 0 4px 0 12px;
+ }
+ }
+
+ .@{iconfont-css-prefix}-close {
+ .@{upload-prefix-cls}-list-rtl & {
+ right: auto;
+ left: 4px;
+ }
+ }
+
+ &-error &-card-actions {
+ .@{iconfont-css-prefix} {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: 0;
+ padding-left: 5px;
+ }
+ }
+ }
+
+ &-progress {
+ .@{upload-prefix-cls}-list-rtl & {
+ padding-right: @font-size-base + 12px;
+ padding-left: 0;
+ }
+ }
+ }
+
+ &-picture,
+ &-picture-card {
+ .@{upload-item}-info {
+ padding: 0;
+ }
+
+ .@{upload-item}-thumbnail {
+ .@{upload-prefix-cls}-list-rtl& {
+ right: 8px;
+ left: auto;
+ }
+ }
+
+ .@{upload-item}-icon {
+ .@{upload-prefix-cls}-list-rtl& {
+ right: 50%;
+ left: auto;
+ transform: translate(50%, -50%);
+ }
+ }
+
+ .@{upload-item}-name {
+ .@{upload-prefix-cls}-list-rtl& {
+ margin: 0 8px 0 0;
+ padding-right: 48px;
+ padding-left: 8px;
+ }
+ }
+
+ .@{upload-item}-name-icon-count-1 {
+ .@{upload-prefix-cls}-list-rtl& {
+ padding-right: 48px;
+ padding-left: 18px;
+ }
+ }
+
+ .@{upload-item}-name-icon-count-2 {
+ .@{upload-prefix-cls}-list-rtl& {
+ padding-right: 48px;
+ padding-left: 36px;
+ }
+ }
+
+ .@{upload-item}-progress {
+ .@{upload-prefix-cls}-list-rtl& {
+ padding-right: 0;
+ padding-left: 0;
+ }
+ }
+
+ .@{iconfont-css-prefix}-close {
+ .@{upload-prefix-cls}-list-rtl& {
+ right: auto;
+ left: 8px;
+ }
+ }
+ }
+
+ &-picture-card {
+ &-container {
+ .@{upload-prefix-cls}-list-rtl & {
+ margin: 0 0 @margin-xs @margin-xs;
+ }
+ }
+
+ .@{upload-item}-actions {
+ .@{upload-prefix-cls}-list-rtl& {
+ right: 50%;
+ left: auto;
+ transform: translate(50%, -50%);
+ }
+ }
+
+ .@{upload-item}-file + .@{upload-item}-name {
+ .@{upload-prefix-cls}-list-rtl& {
+ margin: 8px 0 0;
+ padding: 0;
+ }
+ }
+ }
+}
diff --git a/components/upload copy/utils.tsx b/components/upload copy/utils.tsx
new file mode 100644
index 000000000..3f605723a
--- /dev/null
+++ b/components/upload copy/utils.tsx
@@ -0,0 +1,115 @@
+import type { RcFile, UploadFile, InternalUploadFile } from './interface';
+
+export function file2Obj(file: RcFile): InternalUploadFile {
+ return {
+ ...file,
+ lastModified: file.lastModified,
+ lastModifiedDate: file.lastModifiedDate,
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ uid: file.uid,
+ percent: 0,
+ originFileObj: file,
+ };
+}
+
+/** Upload fileList. Replace file if exist or just push into it. */
+export function updateFileList(file: UploadFile, fileList: UploadFile[]) {
+ const nextFileList = [...fileList];
+ const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === file.uid);
+ if (fileIndex === -1) {
+ nextFileList.push(file);
+ } else {
+ nextFileList[fileIndex] = file;
+ }
+ return nextFileList;
+}
+
+export function getFileItem(file: RcFile, fileList: UploadFile[]) {
+ const matchKey = file.uid !== undefined ? 'uid' : 'name';
+ return fileList.filter(item => item[matchKey] === file[matchKey])[0];
+}
+
+export function removeFileItem(file: UploadFile, fileList: UploadFile[]) {
+ const matchKey = file.uid !== undefined ? 'uid' : 'name';
+ const removed = fileList.filter(item => item[matchKey] !== file[matchKey]);
+ if (removed.length === fileList.length) {
+ return null;
+ }
+ return removed;
+}
+
+// ==================== Default Image Preview ====================
+const extname = (url = '') => {
+ const temp = url.split('/');
+ const filename = temp[temp.length - 1];
+ const filenameWithoutSuffix = filename.split(/#|\?/)[0];
+ return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0];
+};
+
+const isImageFileType = (type: string): boolean => type.indexOf('image/') === 0;
+
+export const isImageUrl = (file: UploadFile): boolean => {
+ if (file.type && !file.thumbUrl) {
+ return isImageFileType(file.type);
+ }
+ const url: string = (file.thumbUrl || file.url || '') as string;
+ const extension = extname(url);
+ if (
+ /^data:image\//.test(url) ||
+ /(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico)$/i.test(extension)
+ ) {
+ return true;
+ }
+ if (/^data:/.test(url)) {
+ // other file types of base64
+ return false;
+ }
+ if (extension) {
+ // other file types which have extension
+ return false;
+ }
+ return true;
+};
+
+const MEASURE_SIZE = 200;
+export function previewImage(file: File | Blob): Promise {
+ return new Promise(resolve => {
+ if (!file.type || !isImageFileType(file.type)) {
+ resolve('');
+ return;
+ }
+
+ const canvas = document.createElement('canvas');
+ canvas.width = MEASURE_SIZE;
+ canvas.height = MEASURE_SIZE;
+ canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
+ document.body.appendChild(canvas);
+ const ctx = canvas.getContext('2d');
+ const img = new Image();
+ img.onload = () => {
+ const { width, height } = img;
+
+ let drawWidth = MEASURE_SIZE;
+ let drawHeight = MEASURE_SIZE;
+ let offsetX = 0;
+ let offsetY = 0;
+
+ if (width > height) {
+ drawHeight = height * (MEASURE_SIZE / width);
+ offsetY = -(drawHeight - drawWidth) / 2;
+ } else {
+ drawWidth = width * (MEASURE_SIZE / height);
+ offsetX = -(drawWidth - drawHeight) / 2;
+ }
+
+ ctx!.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
+ const dataURL = canvas.toDataURL();
+ document.body.removeChild(canvas);
+
+ resolve(dataURL);
+ };
+ img.src = window.URL.createObjectURL(file);
+ });
+}