You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ant-design-vue/components/upload/UploadList/ListItem.tsx

306 lines
9.6 KiB

import {
computed,
defineComponent,
onBeforeUnmount,
onMounted,
shallowRef,
watch,
Transition,
} from 'vue';
import type { ExtractPropTypes, 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 '../../config-provider/hooks/useConfigInject';
import { getTransitionProps } from '../../_util/transition';
import { booleanType, stringType, functionType, arrayType, objectType } from '../../_util/type';
export const listItemProps = () => {
return {
prefixCls: String,
locale: objectType<UploadLocale>(undefined as UploadLocale),
file: objectType<UploadFile>(),
items: arrayType<UploadFile[]>(),
listType: stringType<UploadListType>(),
isImgUrl: functionType<(file: UploadFile) => boolean>(),
showRemoveIcon: booleanType(),
showDownloadIcon: booleanType(),
showPreviewIcon: booleanType(),
removeIcon: functionType<(opt: { file: UploadFile }) => VueNode>(),
downloadIcon: functionType<(opt: { file: UploadFile }) => VueNode>(),
previewIcon: functionType<(opt: { file: UploadFile }) => VueNode>(),
iconRender: functionType<(opt: { file: UploadFile }) => VueNode>(),
actionIconRender:
functionType<
(opt: {
customIcon: VueNode;
callback: () => void;
prefixCls: string;
title?: string | undefined;
}) => VueNode
>(),
itemRender: functionType<ItemRender>(),
onPreview: functionType<(file: UploadFile, e: Event) => void>(),
onClose: functionType<(file: UploadFile) => void>(),
onDownload: functionType<(file: UploadFile) => void>(),
progress: objectType<UploadListProgressProps>(),
};
};
export type ListItemProps = Partial<ExtractPropTypes<ReturnType<typeof listItemProps>>>;
export default defineComponent({
compatConfig: { MODE: 3 },
name: 'ListItem',
inheritAttrs: false,
props: listItemProps(),
setup(props, { slots, attrs }) {
const showProgress = shallowRef(false);
const progressRafRef = shallowRef();
onMounted(() => {
progressRafRef.value = setTimeout(() => {
showProgress.value = true;
}, 300);
});
onBeforeUnmount(() => {
clearTimeout(progressRafRef.value);
});
const mergedStatus = shallowRef(props.file?.status);
watch(
() => props.file?.status,
status => {
if (status !== 'removed') {
mergedStatus.value = status;
}
},
);
const { rootPrefixCls } = useConfigInject('upload', props);
const transitionProps = computed(() => getTransitionProps(`${rootPrefixCls.value}-fade`));
return () => {
const {
prefixCls,
locale,
listType,
file,
items,
progress: progressProps,
iconRender = slots.iconRender,
actionIconRender = slots.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 iconNode = iconRender({ file });
let icon = <div class={`${prefixCls}-text-icon`}>{iconNode}</div>;
if (listType === 'picture' || listType === 'picture-card') {
if (mergedStatus.value === 'uploading' || (!file.thumbUrl && !file.url)) {
const uploadingClassName = {
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: mergedStatus.value !== 'uploading',
};
icon = <div class={uploadingClassName}>{iconNode}</div>;
} else {
const thumbnail = isImgUrl?.(file) ? (
<img
src={file.thumbUrl || file.url}
alt={file.name}
class={`${prefixCls}-list-item-image`}
crossorigin={file.crossOrigin}
/>
) : (
iconNode
);
const aClassName = {
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file),
};
icon = (
<a
class={aClassName}
onClick={e => onPreview(file, e)}
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
>
{thumbnail}
</a>
);
}
}
const infoUploadingClass = {
[`${prefixCls}-list-item`]: true,
[`${prefixCls}-list-item-${mergedStatus.value}`]: true,
};
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
const removeIcon = showRemoveIcon
? actionIconRender({
customIcon: customRemoveIcon ? customRemoveIcon({ file }) : <DeleteOutlined />,
callback: () => onClose(file),
prefixCls,
title: locale.removeFile,
})
: null;
const downloadIcon =
showDownloadIcon && mergedStatus.value === 'done'
? actionIconRender({
customIcon: customDownloadIcon ? customDownloadIcon({ file }) : <DownloadOutlined />,
callback: () => onDownload(file),
prefixCls,
title: locale.downloadFile,
})
: null;
const downloadOrDelete = listType !== 'picture-card' && (
<span
key="download-delete"
class={[
`${prefixCls}-list-item-actions`,
{
picture: listType === 'picture',
},
]}
>
{downloadIcon}
{removeIcon}
</span>
);
const listItemNameClass = `${prefixCls}-list-item-name`;
const fileName = file.url
? [
<a
key="view"
target="_blank"
rel="noopener noreferrer"
class={listItemNameClass}
title={file.name}
{...linkProps}
href={file.url}
onClick={e => onPreview(file, e)}
>
{file.name}
</a>,
downloadOrDelete,
]
: [
<span
key="view"
class={listItemNameClass}
onClick={e => onPreview(file, e)}
title={file.name}
>
{file.name}
</span>,
downloadOrDelete,
];
const previewStyle: CSSProperties = {
pointerEvents: 'none',
opacity: 0.5,
};
const previewIcon = showPreviewIcon ? (
<a
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
style={file.url || file.thumbUrl ? undefined : previewStyle}
onClick={e => onPreview(file, e)}
title={locale.previewFile}
>
{customPreviewIcon ? customPreviewIcon({ file }) : <EyeOutlined />}
</a>
) : null;
const pictureCardActions = listType === 'picture-card' &&
mergedStatus.value !== 'uploading' && (
<span class={`${prefixCls}-list-item-actions`}>
{previewIcon}
{mergedStatus.value === 'done' && downloadIcon}
{removeIcon}
</span>
);
const dom = (
<div class={infoUploadingClass}>
{icon}
{fileName}
{pictureCardActions}
{showProgress.value && (
<Transition {...transitionProps.value}>
<div
v-show={mergedStatus.value === 'uploading'}
class={`${prefixCls}-list-item-progress`}
>
{'percent' in file ? (
<Progress
{...(progressProps as UploadListProgressProps)}
type="line"
percent={file.percent}
/>
) : null}
</div>
</Transition>
)}
</div>
);
const listContainerNameClass = {
[`${prefixCls}-list-item-container`]: true,
[`${className}`]: !!className,
};
const message =
file.response && typeof file.response === 'string'
? file.response
: file.error?.statusText || file.error?.message || locale.uploadError;
const item =
mergedStatus.value === 'error' ? (
<Tooltip title={message} getPopupContainer={node => node.parentNode as HTMLElement}>
{dom}
</Tooltip>
) : (
dom
);
return (
<div class={listContainerNameClass} style={style as CSSProperties}>
{itemRender
? itemRender({
originNode: item,
file,
fileList: items,
actions: {
download: onDownload.bind(null, file),
preview: onPreview.bind(null, file),
remove: onClose.bind(null, file),
},
})
: item}
</div>
);
};
},
});