diff --git a/components/components.ts b/components/components.ts index 8db7c37e4..64374607b 100644 --- a/components/components.ts +++ b/components/components.ts @@ -224,7 +224,7 @@ export { TypographyTitle, } from './typography'; -export type { UploadProps, UploadListProps, UploadChangeParam } from './upload'; +export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './upload'; export { default as Upload, UploadDragger } from './upload'; diff --git a/components/locale-provider/index.tsx b/components/locale-provider/index.tsx index 8ab541de6..44f864ac6 100644 --- a/components/locale-provider/index.tsx +++ b/components/locale-provider/index.tsx @@ -9,6 +9,7 @@ import type { TransferLocale } from '../transfer'; import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker'; import type { PaginationLocale } from '../pagination/Pagination'; import type { TableLocale } from '../table/interface'; +import type { UploadLocale } from '../upload/interface'; interface TransferLocaleForEmpty { description: string; @@ -18,7 +19,6 @@ export interface Locale { Pagination?: PaginationLocale; Table?: TableLocale; Popconfirm?: Record; - Upload?: Record; Form?: { optional?: string; defaultValidateMessages: ValidateMessages; @@ -32,6 +32,7 @@ export interface Locale { Modal?: ModalLocale; Transfer?: Partial; Select?: Record; + Upload?: UploadLocale; Empty?: TransferLocaleForEmpty; global?: Record; PageHeader?: { back: string }; diff --git a/components/upload copy/Dragger.tsx b/components/upload copy/Dragger.tsx deleted file mode 100644 index 251912a4f..000000000 --- a/components/upload copy/Dragger.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineComponent } from 'vue'; -import Upload from './Upload'; -import { uploadProps } from './interface'; - -export default defineComponent({ - name: 'AUploadDragger', - inheritAttrs: false, - props: uploadProps(), - setup(props, { slots, attrs }) { - return () => { - const { height, ...restProps } = props; - const { style, ...restAttrs } = attrs; - const draggerProps = { - ...restProps, - ...restAttrs, - type: 'drag', - style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height }, - } as any; - return ; - }; - }, -}); diff --git a/components/upload copy/UploadList/index.tsx b/components/upload copy/UploadList/index.tsx deleted file mode 100644 index 89955c212..000000000 --- a/components/upload copy/UploadList/index.tsx +++ /dev/null @@ -1,261 +0,0 @@ -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 - ); - }; - - // ============================== 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/index.tsx b/components/upload copy/index.tsx deleted file mode 100644 index ba1b8b09a..000000000 --- a/components/upload copy/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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/interface.tsx b/components/upload copy/interface.tsx deleted file mode 100755 index 91e78958f..000000000 --- a/components/upload copy/interface.tsx +++ /dev/null @@ -1,183 +0,0 @@ -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 old/Dragger.tsx b/components/upload old/Dragger.tsx new file mode 100644 index 000000000..d3d985eb1 --- /dev/null +++ b/components/upload old/Dragger.tsx @@ -0,0 +1,22 @@ +import { defineComponent } from 'vue'; +import { getOptionProps, getSlot } from '../_util/props-util'; +import Upload from './Upload'; +import { uploadProps } from './interface'; + +export default defineComponent({ + name: 'AUploadDragger', + inheritAttrs: false, + props: uploadProps, + render() { + const props = getOptionProps(this); + const { height, ...restProps } = props; + const { style, ...restAttrs } = this.$attrs; + const draggerProps = { + ...restProps, + ...restAttrs, + type: 'drag', + style: { ...(style as any), height }, + } as any; + return {getSlot(this)}; + }, +}); diff --git a/components/upload copy/Upload.tsx b/components/upload old/Upload.tsx similarity index 100% rename from components/upload copy/Upload.tsx rename to components/upload old/Upload.tsx diff --git a/components/upload/UploadList.tsx b/components/upload old/UploadList.tsx similarity index 100% rename from components/upload/UploadList.tsx rename to components/upload old/UploadList.tsx diff --git a/components/upload copy/__tests__/__snapshots__/demo.test.js.snap b/components/upload old/__tests__/__snapshots__/demo.test.js.snap similarity index 100% rename from components/upload copy/__tests__/__snapshots__/demo.test.js.snap rename to components/upload old/__tests__/__snapshots__/demo.test.js.snap diff --git a/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload old/__tests__/__snapshots__/uploadlist.test.js.snap similarity index 100% rename from components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap rename to components/upload old/__tests__/__snapshots__/uploadlist.test.js.snap diff --git a/components/upload copy/__tests__/demo.test.js b/components/upload old/__tests__/demo.test.js similarity index 100% rename from components/upload copy/__tests__/demo.test.js rename to components/upload old/__tests__/demo.test.js diff --git a/components/upload copy/__tests__/mock.js b/components/upload old/__tests__/mock.js similarity index 100% rename from components/upload copy/__tests__/mock.js rename to components/upload old/__tests__/mock.js diff --git a/components/upload copy/__tests__/requests.js b/components/upload old/__tests__/requests.js similarity index 100% rename from components/upload copy/__tests__/requests.js rename to components/upload old/__tests__/requests.js diff --git a/components/upload copy/__tests__/upload.test.js b/components/upload old/__tests__/upload.test.js similarity index 100% rename from components/upload copy/__tests__/upload.test.js rename to components/upload old/__tests__/upload.test.js diff --git a/components/upload copy/__tests__/uploadlist.test.js b/components/upload old/__tests__/uploadlist.test.js similarity index 100% rename from components/upload copy/__tests__/uploadlist.test.js rename to components/upload old/__tests__/uploadlist.test.js diff --git a/components/upload copy/demo/avatar.vue b/components/upload old/demo/avatar.vue similarity index 100% rename from components/upload copy/demo/avatar.vue rename to components/upload old/demo/avatar.vue diff --git a/components/upload copy/demo/basic.vue b/components/upload old/demo/basic.vue similarity index 100% rename from components/upload copy/demo/basic.vue rename to components/upload old/demo/basic.vue diff --git a/components/upload copy/demo/defaultFileList.vue b/components/upload old/demo/defaultFileList.vue similarity index 100% rename from components/upload copy/demo/defaultFileList.vue rename to components/upload old/demo/defaultFileList.vue diff --git a/components/upload copy/demo/directory.vue b/components/upload old/demo/directory.vue similarity index 100% rename from components/upload copy/demo/directory.vue rename to components/upload old/demo/directory.vue diff --git a/components/upload copy/demo/drag.vue b/components/upload old/demo/drag.vue similarity index 100% rename from components/upload copy/demo/drag.vue rename to components/upload old/demo/drag.vue diff --git a/components/upload copy/demo/fileList.vue b/components/upload old/demo/fileList.vue similarity index 100% rename from components/upload copy/demo/fileList.vue rename to components/upload old/demo/fileList.vue diff --git a/components/upload copy/demo/index.vue b/components/upload old/demo/index.vue similarity index 100% rename from components/upload copy/demo/index.vue rename to components/upload old/demo/index.vue diff --git a/components/upload copy/demo/picture-card.vue b/components/upload old/demo/picture-card.vue similarity index 100% rename from components/upload copy/demo/picture-card.vue rename to components/upload old/demo/picture-card.vue diff --git a/components/upload copy/demo/picture-style.vue b/components/upload old/demo/picture-style.vue similarity index 100% rename from components/upload copy/demo/picture-style.vue rename to components/upload old/demo/picture-style.vue diff --git a/components/upload copy/demo/preview-file.vue b/components/upload old/demo/preview-file.vue similarity index 100% rename from components/upload copy/demo/preview-file.vue rename to components/upload old/demo/preview-file.vue diff --git a/components/upload copy/demo/transform-file.vue b/components/upload old/demo/transform-file.vue similarity index 100% rename from components/upload copy/demo/transform-file.vue rename to components/upload old/demo/transform-file.vue diff --git a/components/upload copy/demo/upload-manually.vue b/components/upload old/demo/upload-manually.vue similarity index 100% rename from components/upload copy/demo/upload-manually.vue rename to components/upload old/demo/upload-manually.vue diff --git a/components/upload copy/index.en-US.md b/components/upload old/index.en-US.md similarity index 100% rename from components/upload copy/index.en-US.md rename to components/upload old/index.en-US.md diff --git a/components/upload old/index.tsx b/components/upload old/index.tsx new file mode 100644 index 000000000..a29ac4d3c --- /dev/null +++ b/components/upload old/index.tsx @@ -0,0 +1,21 @@ +import type { App, Plugin } from 'vue'; +import Upload from './Upload'; +import Dragger from './Dragger'; + +export type { UploadProps, UploadListProps, UploadChangeParam } from './interface'; + +Upload.Dragger = Dragger; + +/* istanbul ignore next */ +Upload.install = function (app: App) { + app.component(Upload.name, Upload); + app.component(Dragger.name, Dragger); + return app; +}; + +export const UploadDragger = Dragger; + +export default Upload as typeof Upload & + Plugin & { + readonly Dragger: typeof Dragger; + }; diff --git a/components/upload copy/index.zh-CN.md b/components/upload old/index.zh-CN.md similarity index 100% rename from components/upload copy/index.zh-CN.md rename to components/upload old/index.zh-CN.md diff --git a/components/upload old/interface.tsx b/components/upload old/interface.tsx new file mode 100755 index 000000000..d95add183 --- /dev/null +++ b/components/upload old/interface.tsx @@ -0,0 +1,116 @@ +import type { ExtractPropTypes, PropType } from 'vue'; +import { tuple } from '../_util/type'; +import PropsTypes from '../_util/vue-types'; + +export const UploadFileStatus = PropsTypes.oneOf( + tuple('error', 'success', 'done', 'uploading', 'removed'), +); + +export interface HttpRequestHeader { + [key: string]: string; +} + +export interface VcFile extends File { + uid: string; + readonly lastModifiedDate: Date; + readonly webkitRelativePath: string; +} + +export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; +export interface UploadFile { + uid: string; + size?: number; + name: string; + fileName?: string; + lastModified?: number; + lastModifiedDate?: Date; + url?: string; + status?: UploadFileStatus; + percent?: number; + thumbUrl?: string; + originFileObj?: any; + response?: T; + error?: any; + linkProps?: any; + type?: string; + xhr?: T; + preview?: string; +} + +export interface UploadChangeParam { + file: T; + fileList: UploadFile[]; + event?: { percent: number }; +} + +export const ShowUploadListInterface = PropsTypes.shape({ + showRemoveIcon: PropsTypes.looseBool, + showPreviewIcon: PropsTypes.looseBool, +}).loose; + +export interface UploadLocale { + uploading?: string; + removeFile?: string; + downloadFile?: string; + uploadError?: string; + previewFile?: string; +} + +export const uploadProps = { + type: PropsTypes.oneOf(tuple('drag', 'select')), + name: PropsTypes.string, + defaultFileList: { type: Array as PropType }, + fileList: { type: Array as PropType }, + action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]), + directory: PropsTypes.looseBool, + data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]), + method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')), + headers: PropsTypes.object, + showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]), + multiple: PropsTypes.looseBool, + accept: PropsTypes.string, + beforeUpload: PropsTypes.func, + listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), + // className: PropsTypes.string, + remove: PropsTypes.func, + supportServerRender: PropsTypes.looseBool, + // style: PropsTypes.object, + disabled: PropsTypes.looseBool, + prefixCls: PropsTypes.string, + customRequest: PropsTypes.func, + withCredentials: PropsTypes.looseBool, + openFileDialogOnClick: PropsTypes.looseBool, + locale: { type: Object as PropType }, + height: PropsTypes.number, + id: PropsTypes.string, + previewFile: PropsTypes.func, + transformFile: PropsTypes.func, + onChange: { type: Function as PropType<(info: UploadChangeParam) => void> }, + onPreview: { type: Function as PropType<(file: UploadFile) => void> }, + onRemove: { + type: Function as PropType<(file: UploadFile) => void | boolean | Promise>, + }, + onDownload: { type: Function as PropType<(file: UploadFile) => void> }, + 'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> }, +}; + +export type UploadProps = Partial>; +export const uploadListProps = { + listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), + // items: PropsTypes.arrayOf(UploadFile), + items: { type: Array as PropType }, + progressAttr: PropsTypes.object, + prefixCls: PropsTypes.string, + showRemoveIcon: PropsTypes.looseBool, + showDownloadIcon: PropsTypes.looseBool, + showPreviewIcon: PropsTypes.looseBool, + locale: { type: Object as PropType }, + previewFile: PropsTypes.func, + onPreview: { type: Function as PropType<(file: UploadFile) => void> }, + onRemove: { + type: Function as PropType<(file: UploadFile) => void | boolean>, + }, + onDownload: { type: Function as PropType<(file: UploadFile) => void> }, +}; + +export type UploadListProps = Partial>; diff --git a/components/upload copy/style/index.less b/components/upload old/style/index.less similarity index 71% rename from components/upload copy/style/index.less rename to components/upload old/style/index.less index 49ba7f211..17f4154eb 100644 --- a/components/upload copy/style/index.less +++ b/components/upload old/style/index.less @@ -34,6 +34,8 @@ } &&-select-picture-card { + display: table; + float: left; width: @upload-picture-card-size; height: @upload-picture-card-size; margin-right: 8px; @@ -44,21 +46,19 @@ border: @border-width-base dashed @border-color-base; border-radius: @border-radius-base; cursor: pointer; - transition: border-color 0.3s; + transition: border-color 0.3s ease; > .@{upload-prefix-cls} { - display: flex; - align-items: center; - justify-content: center; + display: table-cell; + width: 100%; height: 100%; + padding: 8px; text-align: center; + vertical-align: middle; } &:hover { border-color: @primary-color; - .@{upload-prefix-cls}-disabled& { - border-color: @border-color-base; - } } } @@ -74,7 +74,7 @@ transition: border-color 0.3s; .@{upload-prefix-cls} { - padding: @padding-md 0; + padding: 16px 0; } &.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) { @@ -116,12 +116,10 @@ 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; } @@ -142,55 +140,52 @@ .@{upload-prefix-cls}-list { .reset-component(); .clearfix(); - line-height: @line-height-base; - - // ============================ Item ============================ + &-item-list-type-text { + &:hover { + .@{upload-prefix-cls}-list-item-name-icon-count-1 { + padding-right: 14px; + } + .@{upload-prefix-cls}-list-item-name-icon-count-2 { + padding-right: 28px; + } + } + } &-item { position: relative; - height: @line-height-base * @font-size-base; - margin-top: @margin-xs; + height: 22px; + margin-top: 8px; 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; } + &-name-icon-count-1 { + padding-right: 14px; + } + &-card-actions { position: absolute; right: 0; - - &-btn { - opacity: 0; - } - &-btn.@{ant-prefix}-btn-sm { - height: 20px; - line-height: 1; - } - + opacity: 0; &.picture { - top: 22px; - line-height: 0; - } - - &-btn:focus, - &.picture &-btn { + top: 25px; + line-height: 1; opacity: 1; } - - .@{iconfont-css-prefix} { - color: @upload-actions-color; + .anticon { + padding-right: 6px; + color: rgba(0, 0, 0, 0.45); } } &-info { height: 100%; - padding: 0 4px; + padding: 0 12px 0 4px; transition: background-color 0.3s; > span { @@ -200,27 +195,25 @@ } .@{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}-paper-clip { + position: absolute; + top: (@font-size-base / 2) - 2px; + color: @text-color-secondary; + font-size: @font-size-base; } } .@{iconfont-css-prefix}-close { + .iconfont-size-under-12px(10px); + 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; } @@ -234,24 +227,21 @@ opacity: 1; } - &:hover &-card-actions-btn { + &:hover &-card-actions { opacity: 1; } &-error, - &-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix}, + &-error .@{iconfont-css-prefix}-paper-clip, &-error &-name { color: @error-color; } &-error &-card-actions { - .@{iconfont-css-prefix} { + .anticon { color: @error-color; } - - &-btn { - opacity: 1; - } + opacity: 1; } &-progress { @@ -264,20 +254,17 @@ } } - // =================== Picture & Picture Card =================== &-picture, &-picture-card { .@{upload-item} { position: relative; height: 66px; - padding: @padding-xs; + padding: 8px; 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; } @@ -296,30 +283,15 @@ } .@{upload-item}-thumbnail { + position: absolute; + top: 8px; + left: 8px; width: 48px; height: 48px; - line-height: 60px; + font-size: 26px; + line-height: 54px; 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 { @@ -328,10 +300,6 @@ left: 50%; font-size: 26px; transform: translate(-50%, -50%); - - .@{iconfont-css-prefix} { - font-size: 26px; - } } .@{upload-item}-image { @@ -359,8 +327,16 @@ transition: all 0.3s; } + .@{upload-item}-name-icon-count-1 { + padding-right: 18px; + } + + .@{upload-item}-name-icon-count-2 { + padding-right: 36px; + } + .@{upload-item}-uploading .@{upload-item}-name { - margin-bottom: 12px; + line-height: 28px; } .@{upload-item}-progress { @@ -379,23 +355,21 @@ } } - // ======================== 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; } - + &-container { + float: left; + width: @upload-picture-card-size; + height: @upload-picture-card-size; + margin: 0 8px 8px 0; + } .@{upload-item} { - height: 100%; - margin: 0; + float: left; + width: @upload-picture-card-size; + height: @upload-picture-card-size; + margin: 0 8px 8px 0; } .@{upload-item}-info { @@ -439,7 +413,6 @@ font-size: 16px; cursor: pointer; transition: all 0.3s; - &:hover { color: @text-color-inverse; } @@ -457,7 +430,7 @@ display: block; width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } .@{upload-item}-name { @@ -468,7 +441,7 @@ text-align: center; } - .@{upload-item}-file + .@{upload-item}-name { + .anticon-picture + .@{upload-item}-name { position: absolute; bottom: 10px; display: block; @@ -481,82 +454,46 @@ .@{upload-item}-info { height: auto; - &::before, - .@{iconfont-css-prefix}-eye, + .@{iconfont-css-prefix}-eye-o, .@{iconfont-css-prefix}-delete { display: none; } } + + &-text { + margin-top: 18px; + color: @text-color-secondary; + } } .@{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; - } + .@{upload-prefix-cls}-success-icon { + color: @success-color; + font-weight: bold; } - // ============================ Text ============================ - &-text { - .@{upload-prefix-cls}-text-icon { - .@{iconfont-css-prefix} { - position: static; - } - } - } - - // =========================== Motion =========================== - .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-enter, + .@{upload-prefix-cls}-animate-leave, .@{upload-prefix-cls}-animate-inline-enter, .@{upload-prefix-cls}-animate-inline-leave { - animation-duration: @animation-duration-slow; + animation-duration: 0.3s; animation-fill-mode: @ease-in-out-circ; } - .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-enter { + animation-name: uploadAnimateIn; + } + + .@{upload-prefix-cls}-animate-leave { + animation-name: uploadAnimateOut; + } + .@{upload-prefix-cls}-animate-inline-enter { animation-name: uploadAnimateInlineIn; } @@ -566,6 +503,24 @@ } } +@keyframes uploadAnimateIn { + from { + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + +@keyframes uploadAnimateOut { + to { + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + @keyframes uploadAnimateInlineIn { from { width: 0; @@ -585,5 +540,3 @@ opacity: 0; } } - -@import './rtl'; diff --git a/components/upload/utils.jsx b/components/upload old/utils.jsx similarity index 100% rename from components/upload/utils.jsx rename to components/upload old/utils.jsx diff --git a/components/upload/Dragger.tsx b/components/upload/Dragger.tsx index d3d985eb1..251912a4f 100644 --- a/components/upload/Dragger.tsx +++ b/components/upload/Dragger.tsx @@ -1,22 +1,22 @@ import { defineComponent } from 'vue'; -import { getOptionProps, getSlot } from '../_util/props-util'; import Upload from './Upload'; import { uploadProps } from './interface'; export default defineComponent({ name: 'AUploadDragger', inheritAttrs: false, - props: uploadProps, - render() { - const props = getOptionProps(this); - const { height, ...restProps } = props; - const { style, ...restAttrs } = this.$attrs; - const draggerProps = { - ...restProps, - ...restAttrs, - type: 'drag', - style: { ...(style as any), height }, - } as any; - return {getSlot(this)}; + props: uploadProps(), + setup(props, { slots, attrs }) { + return () => { + const { height, ...restProps } = props; + const { style, ...restAttrs } = attrs; + const draggerProps = { + ...restProps, + ...restAttrs, + type: 'drag', + style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height }, + } as any; + return ; + }; }, }); diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index 6bf14054e..3ba5d4aa8 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -1,86 +1,191 @@ -import classNames from '../_util/classNames'; -import uniqBy from 'lodash-es/uniqBy'; -import findIndex from 'lodash-es/findIndex'; +import type { UploadProps as RcUploadProps } from '../vc-upload'; import VcUpload from '../vc-upload'; -import BaseMixin from '../_util/BaseMixin'; -import { getOptionProps, hasProp, getSlot } from '../_util/props-util'; -import initDefaultProps from '../_util/props-util/initDefaultProps'; -import LocaleReceiver from '../locale-provider/LocaleReceiver'; -import defaultLocale from '../locale-provider/default'; -import { defaultConfigProvider } from '../config-provider'; -import Dragger from './Dragger'; import UploadList from './UploadList'; -import type { UploadFile } from './interface'; +import type { + UploadType, + UploadListType, + RcFile, + UploadFile, + UploadChangeParam, + ShowUploadListInterface, +} from './interface'; import { uploadProps } from './interface'; -import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils'; -import { defineComponent, inject } from 'vue'; -import { getDataAndAriaProps } from '../_util/util'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils'; +import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; +import defaultLocale from '../locale/default'; +import { computed, defineComponent, onMounted, ref, toRef } from 'vue'; +import { flattenChildren, initDefaultProps } from '../_util/props-util'; +import useMergedState from '../_util/hooks/useMergedState'; +import devWarning from '../vc-util/devWarning'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import type { VueNode } from '../_util/type'; +import classNames from '../_util/classNames'; +import { useInjectFormItemContext } from '../form'; + +export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`; export default defineComponent({ name: 'AUpload', - mixins: [BaseMixin], inheritAttrs: false, - Dragger, - props: initDefaultProps(uploadProps, { - type: 'select', + props: initDefaultProps(uploadProps(), { + type: 'select' as UploadType, multiple: false, action: '', data: {}, accept: '', - beforeUpload: T, showUploadList: true, - listType: 'text', // or pictrue + listType: 'text' as UploadListType, // or picture disabled: false, supportServerRender: true, }), - setup() { + setup(props, { slots, attrs, expose }) { const formItemContext = useInjectFormItemContext(); - return { - upload: null, - progressTimer: null, - configProvider: inject('configProvider', defaultConfigProvider), - formItemContext, - }; - }, - // recentUploadStatus: boolean | PromiseLike; - data() { - return { - sFileList: this.fileList || this.defaultFileList || [], - dragState: 'drop', - }; - }, - watch: { - fileList(val) { - this.sFileList = val || []; - }, - }, - beforeUnmount() { - this.clearProgressTimer(); - }, - methods: { - onStart(file) { - const targetItem = fileToObject(file); - targetItem.status = 'uploading'; - const nextFileList = this.sFileList.concat(); - const fileIndex = findIndex(nextFileList, ({ uid }) => uid === targetItem.uid); - if (fileIndex === -1) { - nextFileList.push(targetItem); - } else { - nextFileList[fileIndex] = targetItem; - } - this.handleChange({ - file: targetItem, - fileList: nextFileList, - }); - // fix ie progress - if (!window.File || (typeof process === 'object' && process.env.TEST_IE)) { - this.autoUpdateProgress(0, targetItem); - } - }, + const [mergedFileList, setMergedFileList] = useMergedState(props.defaultFileList || [], { + value: toRef(props, 'fileList'), + postState: list => { + const timestamp = Date.now(); + return (list ?? []).map((file, index) => { + if (!file.uid && !Object.isFrozen(file)) { + file.uid = `__AUTO__${timestamp}_${index}__`; + } + return file; + }); + }, + }); + const dragState = ref('drop'); - onSuccess(response, file, xhr) { - this.clearProgressTimer(); + const upload = ref(); + onMounted(() => { + devWarning( + 'fileList' in props || !('value' in props), + 'Upload', + '`value` is not a valid prop, do you mean `fileList`?', + ); + + devWarning( + !('transformFile' in props), + 'Upload', + '`transformFile` is deprecated. Please use `beforeUpload` directly.', + ); + }); + + const onInternalChange = ( + file: UploadFile, + changedFileList: UploadFile[], + event?: { percent: number }, + ) => { + let cloneList = [...changedFileList]; + + // Cut to match count + if (props.maxCount === 1) { + cloneList = cloneList.slice(-1); + } else if (props.maxCount) { + cloneList = cloneList.slice(0, props.maxCount); + } + + setMergedFileList(cloneList); + + const changeInfo: UploadChangeParam = { + file: file as UploadFile, + fileList: cloneList, + }; + + if (event) { + changeInfo.event = event; + } + props['onUpdate:fileList']?.(changeInfo.fileList); + props.onChange?.(changeInfo); + formItemContext.onFieldChange(); + }; + + const mergedBeforeUpload = async (file: RcFile, fileListArgs: RcFile[]) => { + const { beforeUpload, transformFile } = props; + + let parsedFile: File | Blob | string = file; + if (beforeUpload) { + const result = await beforeUpload(file, fileListArgs); + + if (result === false) { + return false; + } + + // Hack for LIST_IGNORE, we add additional info to remove from the list + delete (file as any)[LIST_IGNORE]; + if ((result as any) === LIST_IGNORE) { + Object.defineProperty(file, LIST_IGNORE, { + value: true, + configurable: true, + }); + return false; + } + + if (typeof result === 'object' && result) { + parsedFile = result as File; + } + } + + if (transformFile) { + parsedFile = await transformFile(parsedFile as any); + } + + return parsedFile as RcFile; + }; + + const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => { + // Skip file which marked as `LIST_IGNORE`, these file will not add to file list + const filteredFileInfoList = batchFileInfoList.filter( + info => !(info.file as any)[LIST_IGNORE], + ); + + // Nothing to do since no file need upload + if (!filteredFileInfoList.length) { + return; + } + + const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as RcFile)); + + // Concat new files with prev files + let newFileList = [...mergedFileList.value]; + + objectFileList.forEach(fileObj => { + // Replace file if exist + newFileList = updateFileList(fileObj, newFileList); + }); + + objectFileList.forEach((fileObj, index) => { + // Repeat trigger `onChange` event for compatible + let triggerFileObj: UploadFile = fileObj; + + if (!filteredFileInfoList[index].parsedFile) { + // `beforeUpload` return false + const { originFileObj } = fileObj; + let clone; + + try { + clone = new File([originFileObj], originFileObj.name, { + type: originFileObj.type, + }) as any as UploadFile; + } catch (e) { + clone = new Blob([originFileObj], { + type: originFileObj.type, + }) as any as UploadFile; + clone.name = originFileObj.name; + clone.lastModifiedDate = new Date(); + clone.lastModified = new Date().getTime(); + } + + clone.uid = fileObj.uid; + triggerFileObj = clone; + } else { + // Inject `uploading` status + fileObj.status = 'uploading'; + } + + onInternalChange(triggerFileObj, newFileList); + }); + }; + + const onSuccess = (response: any, file: RcFile, xhr: any) => { try { if (typeof response === 'string') { response = JSON.parse(response); @@ -88,255 +193,231 @@ export default defineComponent({ } catch (e) { /* do nothing */ } - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); targetItem.status = 'done'; + targetItem.percent = 100; targetItem.response = response; targetItem.xhr = xhr; - this.handleChange({ - file: { ...targetItem }, - fileList, - }); - }, - onProgress(e, file) { - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList); + }; + + const onProgress = (e: { percent: number }, file: RcFile) => { // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); + targetItem.status = 'uploading'; targetItem.percent = e.percent; - this.handleChange({ - event: e, - file: { ...targetItem }, - fileList: this.sFileList, - }); - }, - onError(error, response, file) { - this.clearProgressTimer(); - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList, e); + }; + + const onError = (error: Error, response: any, file: RcFile) => { // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); targetItem.error = error; targetItem.response = response; targetItem.status = 'error'; - this.handleChange({ - file: { ...targetItem }, - fileList, - }); - }, - onReject(fileList) { - this.$emit('reject', fileList); - }, - handleRemove(file) { - const { remove: onRemove } = this; - const { sFileList: fileList } = this.$data; - Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => { + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList); + }; + + const handleRemove = (file: UploadFile) => { + let currentFile: UploadFile; + Promise.resolve( + typeof props.onRemove === 'function' ? props.onRemove(file) : props.onRemove, + ).then(ret => { // Prevent removing file if (ret === false) { return; } - const removedFileList = removeFileItem(file, fileList); + const removedFileList = removeFileItem(file, mergedFileList.value); if (removedFileList) { - file.status = 'removed'; // eslint-disable-line - - if (this.upload) { - this.upload.abort(file); - } - - this.handleChange({ - file, - fileList: removedFileList, + currentFile = { ...file, status: 'removed' }; + mergedFileList.value?.forEach(item => { + const matchKey = currentFile.uid !== undefined ? 'uid' : 'name'; + if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) { + item.status = 'removed'; + } }); + upload.value?.abort(currentFile); + + onInternalChange(currentFile, removedFileList); } }); - }, - handleManualRemove(file) { - if (this.$refs.uploadRef) { - (this.$refs.uploadRef as any).abort(file); - } - this.handleRemove(file); - }, - handleChange(info) { - if (!hasProp(this, 'fileList')) { - this.setState({ sFileList: info.fileList }); - } - this.$emit('update:fileList', info.fileList); - this.$emit('change', info); - this.formItemContext.onFieldChange(); - }, - onFileDrop(e) { - this.setState({ - dragState: e.type, - }); - }, - reBeforeUpload(file, fileList) { - const { beforeUpload } = this.$props; - const { sFileList: stateFileList } = this.$data; - if (!beforeUpload) { - return true; - } - const result = beforeUpload(file, fileList); - if (result === false) { - this.handleChange({ - file, - fileList: uniqBy( - stateFileList.concat(fileList.map(fileToObject)), - (item: UploadFile) => item.uid, - ), - }); - return false; - } - if (result && result.then) { - return result; - } - return true; - }, - clearProgressTimer() { - clearInterval(this.progressTimer); - }, - autoUpdateProgress(_, file) { - const getPercent = genPercentAdd(); - let curPercent = 0; - this.clearProgressTimer(); - this.progressTimer = setInterval(() => { - curPercent = getPercent(curPercent); - this.onProgress( - { - percent: curPercent * 100, - }, - file, - ); - }, 200); - }, - renderUploadList(locale) { - const { - showUploadList = {}, - listType, - previewFile, - disabled, - locale: propLocale, - } = getOptionProps(this); - const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList; - const { sFileList: fileList } = this.$data; - const { onDownload, onPreview } = this.$props; - const uploadListProps = { - listType, - items: fileList, - previewFile, - showRemoveIcon: !disabled && showRemoveIcon, - showPreviewIcon, - showDownloadIcon, - locale: { ...locale, ...propLocale }, - onRemove: this.handleManualRemove, - onDownload, - onPreview, - }; - return ; - }, - }, - render() { - const { - prefixCls: customizePrefixCls, - showUploadList, - listType, - type, - disabled, - } = getOptionProps(this); - const { sFileList: fileList, dragState } = this.$data; - const { class: className, style } = this.$attrs; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('upload', customizePrefixCls); - - const vcUploadProps = { - ...this.$props, - id: this.$props.id ?? this.formItemContext.id.value, - prefixCls, - beforeUpload: this.reBeforeUpload, - onStart: this.onStart, - onError: this.onError, - onProgress: this.onProgress, - onSuccess: this.onSuccess, - onReject: this.onReject, - ref: 'uploadRef', }; - const uploadList = showUploadList ? ( - - ) : null; - - const children = getSlot(this); - - if (type === 'drag') { - const dragCls = classNames(prefixCls, { - [`${prefixCls}-drag`]: true, - [`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'), - [`${prefixCls}-drag-hover`]: dragState === 'dragover', - [`${prefixCls}-disabled`]: disabled, - }); - return ( - -
- -
{children}
-
-
- {uploadList} -
- ); - } - - const uploadButtonCls = classNames(prefixCls, { - [`${prefixCls}-select`]: true, - [`${prefixCls}-select-${listType}`]: true, - [`${prefixCls}-disabled`]: disabled, + const onFileDrop = (e: DragEvent) => { + dragState.value = e.type; + if (e.type === 'drop') { + props.onDrop?.(e); + } + }; + expose({ + onBatchStart, + onSuccess, + onProgress, + onError, + fileList: mergedFileList, + upload, }); - // Remove id to avoid open by label when trigger is hidden - // https://github.com/ant-design/ant-design/issues/14298 - if (!children.length || disabled) { - delete vcUploadProps.id; - } - - const uploadButton = ( -
- {children} -
+ const { prefixCls, direction } = useConfigInject('upload', props); + const [locale] = useLocaleReceiver( + 'Upload', + defaultLocale.Upload, + computed(() => props.locale), ); + const renderUploadList = (button?: VueNode) => { + const { + removeIcon, + previewIcon, + downloadIcon, + previewFile, + onPreview, + onDownload, + disabled, + isImageUrl, + progress, + itemRender, + iconRender, + showUploadList, + } = props; + const { showDownloadIcon, showPreviewIcon, showRemoveIcon } = + typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList; + return showUploadList ? ( + button }} + /> + ) : ( + button + ); + }; + return () => { + const { listType, disabled, type } = props; + const rcUploadProps = { + onBatchStart, + onError, + onProgress, + onSuccess, + ...(props as RcUploadProps), + id: props.id ?? formItemContext.id.value, + prefixCls: prefixCls.value, + beforeUpload: mergedBeforeUpload, + onChange: undefined, + }; - if (listType === 'picture-card') { + // Remove id to avoid open by label when trigger is hidden + // !children: https://github.com/ant-design/ant-design/issues/14298 + // disabled: https://github.com/ant-design/ant-design/issues/16478 + // https://github.com/ant-design/ant-design/issues/24197 + if (!slots.default || disabled) { + delete rcUploadProps.id; + } + if (type === 'drag') { + const dragCls = classNames( + prefixCls.value, + { + [`${prefixCls.value}-drag`]: true, + [`${prefixCls.value}-drag-uploading`]: mergedFileList.value.some( + file => file.status === 'uploading', + ), + [`${prefixCls.value}-drag-hover`]: dragState.value === 'dragover', + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }, + attrs.class, + ); + return ( + +
+ +
{slots.default?.()}
+
+
+ {renderUploadList()} +
+ ); + } + + const uploadButtonCls = classNames(prefixCls.value, { + [`${prefixCls.value}-select`]: true, + [`${prefixCls.value}-select-${listType}`]: true, + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }); + const children = flattenChildren(slots.default?.()); + const uploadButton = ( +
+ +
+ ); + + if (listType === 'picture-card') { + return ( + + {renderUploadList(uploadButton)} + + ); + } return ( - - {uploadList} + {uploadButton} + {renderUploadList()} ); - } - return ( - - {uploadButton} - {uploadList} - - ); + }; }, }); diff --git a/components/upload copy/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx similarity index 97% rename from components/upload copy/UploadList/ListItem.tsx rename to components/upload/UploadList/ListItem.tsx index 63bd7b33a..4e51a8184 100644 --- a/components/upload copy/UploadList/ListItem.tsx +++ b/components/upload/UploadList/ListItem.tsx @@ -25,9 +25,9 @@ export const listItemProps = () => { listType: String as PropType, isImgUrl: Function as PropType<(file: UploadFile) => boolean>, - showRemoveIcon: Boolean, - showDownloadIcon: Boolean, - showPreviewIcon: Boolean, + showRemoveIcon: { type: Boolean, default: undefined }, + showDownloadIcon: { type: Boolean, default: undefined }, + showPreviewIcon: { type: Boolean, default: undefined }, removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, @@ -76,8 +76,8 @@ export default defineComponent({ file, items, progress: progressProps, - iconRender, - actionIconRender, + iconRender = slots.iconRender, + actionIconRender = slots.actionIconRender, itemRender = slots.itemRender, isImgUrl, showPreviewIcon, diff --git a/components/upload/UploadList/index.tsx b/components/upload/UploadList/index.tsx new file mode 100644 index 000000000..206636c53 --- /dev/null +++ b/components/upload/UploadList/index.tsx @@ -0,0 +1,206 @@ +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import PaperClipOutlined from '@ant-design/icons-vue/PaperClipOutlined'; +import PictureTwoTone from '@ant-design/icons-vue/PictureTwoTone'; +import FileTwoTone from '@ant-design/icons-vue/FileTwoTone'; +import type { UploadListType, InternalUploadFile, UploadFile } from '../interface'; +import { uploadListProps } from '../interface'; +import { previewImage, isImageUrl } from '../utils'; +import type { ButtonProps } from '../../button'; +import Button from '../../button'; +import ListItem from './ListItem'; +import type { HTMLAttributes } from 'vue'; +import { computed, defineComponent, getCurrentInstance, onMounted, ref, watchEffect } from 'vue'; +import { initDefaultProps, isValidElement } from '../../_util/props-util'; +import type { VueNode } from '../../_util/type'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import { getTransitionGroupProps, TransitionGroup } from '../../_util/transition'; + +const HackSlot = (_, { slots }) => { + return slots.default?.()[0]; +}; +export default defineComponent({ + name: 'AUploadList', + props: initDefaultProps(uploadListProps(), { + listType: 'text' as UploadListType, // or picture + progress: { + strokeWidth: 2, + showInfo: false, + }, + showRemoveIcon: true, + showDownloadIcon: false, + showPreviewIcon: true, + previewFile: previewImage, + isImageUrl, + items: [], + }), + setup(props, { slots, expose }) { + const motionAppear = ref(false); + const instance = getCurrentInstance(); + onMounted(() => { + motionAppear.value == true; + }); + watchEffect(() => { + if (props.listType !== 'picture' && props.listType !== 'picture-card') { + return; + } + (props.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 (props.previewFile) { + props.previewFile(file.originFileObj as File).then((previewDataUrl: string) => { + // Need append '' to avoid dead loop + file.thumbUrl = previewDataUrl || ''; + instance.update(); + }); + } + }); + }); + + // ============================= Events ============================= + const onInternalPreview = (file: UploadFile, e?: Event) => { + if (!props.onPreview) { + return; + } + e?.preventDefault(); + return props.onPreview(file); + }; + + const onInternalDownload = (file: UploadFile) => { + if (typeof props.onDownload === 'function') { + props.onDownload(file); + } else if (file.url) { + window.open(file.url); + } + }; + + const onInternalClose = (file: UploadFile) => { + props.onRemove?.(file); + }; + + const internalIconRender = ({ file }: { file: UploadFile }) => { + const iconRender = props.iconRender || slots.iconRender; + if (iconRender) { + return iconRender({ file, listType: props.listType }); + } + const isLoading = file.status === 'uploading'; + const fileIcon = + props.isImageUrl && props.isImageUrl(file) ? : ; + let icon: VueNode = isLoading ? : ; + if (props.listType === 'picture') { + icon = isLoading ? : fileIcon; + } else if (props.listType === 'picture-card') { + icon = isLoading ? props.locale.uploading : fileIcon; + } + return icon; + }; + + const actionIconRender = (opt: { + customIcon: VueNode; + callback: () => void; + prefixCls: string; + title?: string; + }) => { + const { customIcon, callback, prefixCls, title } = opt; + const btnProps: ButtonProps & HTMLAttributes = { + type: 'text', + size: 'small', + title, + onClick: () => { + callback(); + }, + class: `${prefixCls}-list-item-card-actions-btn`, + }; + if (isValidElement(customIcon)) { + return + ); + }; + + expose({ + handlePreview: onInternalPreview, + handleDownload: onInternalDownload, + }); + + const { prefixCls, direction } = useConfigInject('upload', props); + + const listClassNames = computed(() => ({ + [`${prefixCls.value}-list`]: true, + [`${prefixCls.value}-list-${props.listType}`]: true, + [`${prefixCls.value}-list-rtl`]: direction.value === 'rtl', + })); + const transitionGroupProps = computed(() => ({ + ...getTransitionGroupProps( + `${prefixCls.value}-${props.listType === 'picture-card' ? 'animate-inline' : 'animate'}`, + ), + class: listClassNames.value, + appear: motionAppear.value, + })); + return () => { + const { + listType, + locale, + isImageUrl: isImgUrl, + items = [], + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + removeIcon, + previewIcon, + downloadIcon, + progress, + appendAction = slots.appendAction, + itemRender, + } = props; + const appendActionDom = appendAction?.()[0]; + return ( + + {items.map(file => { + const { uid: key } = file; + return ( + + ); + })} + {isValidElement(appendActionDom) ? ( + {appendActionDom} + ) : null} + + ); + }; + }, +}); diff --git a/components/upload/demo/basic.vue b/components/upload/demo/basic.vue index 3c9862438..c2fb1f5e0 100644 --- a/components/upload/demo/basic.vue +++ b/components/upload/demo/basic.vue @@ -19,7 +19,6 @@ Classic mode. File selection dialog pops up when upload button is clicked.
-
Upload
+
Upload
- + example @@ -36,7 +36,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt