diff --git a/components/style/themes/default.less b/components/style/themes/default.less index a036d2753..4276be61b 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -965,6 +965,10 @@ @typography-title-margin-top: 1.2em; @typography-title-margin-bottom: 0.5em; +// Upload +// --- +@upload-actions-color: @text-color-secondary; + // Image // --- @image-size-base: 48px; diff --git a/components/upload copy/Dragger.tsx b/components/upload copy/Dragger.tsx new file mode 100644 index 000000000..251912a4f --- /dev/null +++ b/components/upload copy/Dragger.tsx @@ -0,0 +1,22 @@ +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/Upload.tsx b/components/upload copy/Upload.tsx new file mode 100644 index 000000000..6bf14054e --- /dev/null +++ b/components/upload copy/Upload.tsx @@ -0,0 +1,342 @@ +import classNames from '../_util/classNames'; +import uniqBy from 'lodash-es/uniqBy'; +import findIndex from 'lodash-es/findIndex'; +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 { 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'; + +export default defineComponent({ + name: 'AUpload', + mixins: [BaseMixin], + inheritAttrs: false, + Dragger, + props: initDefaultProps(uploadProps, { + type: 'select', + multiple: false, + action: '', + data: {}, + accept: '', + beforeUpload: T, + showUploadList: true, + listType: 'text', // or pictrue + disabled: false, + supportServerRender: true, + }), + setup() { + 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); + } + }, + + onSuccess(response, file, xhr) { + this.clearProgressTimer(); + try { + if (typeof response === 'string') { + response = JSON.parse(response); + } + } catch (e) { + /* do nothing */ + } + const fileList = this.sFileList; + const targetItem = getFileItem(file, fileList); + // removed + if (!targetItem) { + return; + } + targetItem.status = 'done'; + targetItem.response = response; + targetItem.xhr = xhr; + this.handleChange({ + file: { ...targetItem }, + fileList, + }); + }, + onProgress(e, file) { + const fileList = this.sFileList; + const targetItem = getFileItem(file, fileList); + // removed + if (!targetItem) { + return; + } + 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); + // removed + if (!targetItem) { + return; + } + 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 => { + // Prevent removing file + if (ret === false) { + return; + } + + const removedFileList = removeFileItem(file, fileList); + + if (removedFileList) { + file.status = 'removed'; // eslint-disable-line + + if (this.upload) { + this.upload.abort(file); + } + + this.handleChange({ + file, + fileList: 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, + }); + + // 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} +
+ ); + + if (listType === 'picture-card') { + return ( + + {uploadList} + {uploadButton} + + ); + } + return ( + + {uploadButton} + {uploadList} + + ); + }, +}); diff --git a/components/upload copy/UploadList/ListItem.tsx b/components/upload copy/UploadList/ListItem.tsx new file mode 100644 index 000000000..63bd7b33a --- /dev/null +++ b/components/upload copy/UploadList/ListItem.tsx @@ -0,0 +1,287 @@ +import { computed, defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'; +import type { ExtractPropTypes, PropType, CSSProperties } from 'vue'; +import EyeOutlined from '@ant-design/icons-vue/EyeOutlined'; +import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined'; +import DownloadOutlined from '@ant-design/icons-vue/DownloadOutlined'; +import Tooltip from '../../tooltip'; +import Progress from '../../progress'; + +import type { + ItemRender, + UploadFile, + UploadListProgressProps, + UploadListType, + UploadLocale, +} from '../interface'; +import type { VueNode } from '../../_util/type'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import Transition, { getTransitionProps } from '../../_util/transition'; +export const listItemProps = () => { + return { + prefixCls: String, + locale: { type: Object as PropType, default: undefined as UploadLocale }, + file: Object as PropType, + items: Array as PropType, + listType: String as PropType, + isImgUrl: Function as PropType<(file: UploadFile) => boolean>, + + 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>, + + iconRender: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + actionIconRender: Function as PropType< + (opt: { + customIcon: VueNode; + callback: () => void; + prefixCls: string; + title?: string | undefined; + }) => VueNode + >, + itemRender: Function as PropType, + onPreview: Function as PropType<(file: UploadFile, e: Event) => void>, + onClose: Function as PropType<(file: UploadFile) => void>, + onDownload: Function as PropType<(file: UploadFile) => void>, + progress: Object as PropType, + }; +}; + +export type ListItemProps = Partial>>; + +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) ? ( + {file.name} + ) : ( + 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 + ); + }; + + // ============================== 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`] = ` +
+
Upload
+
+`; + +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`] = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
image.png + +
+ + +
+
+
+
+
Upload
+
+ +
+`; + +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`] = ` +
foo.png
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Upload List handle error 2`] = ` +
foo.png
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +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. + + + + 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. + + + + 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. + + + + 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. + + + + 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. + + + + 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. + + + + + + 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`. + + + + 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); + }); +}