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.
 
 

297 lines
8.7 KiB

import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
import EyeOutlined from '@ant-design/icons/EyeOutlined';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import * as React from 'react';
import { ConfigContext } from '../../config-provider';
import Progress from '../../progress';
import Tooltip from '../../tooltip';
import type {
ItemRender,
UploadFile,
UploadListProgressProps,
UploadListType,
UploadLocale,
} from '../interface';
export interface ListItemProps {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
locale: UploadLocale;
file: UploadFile;
items: UploadFile[];
listType?: UploadListType;
isImgUrl?: (file: UploadFile) => boolean;
showRemoveIcon?: boolean;
showDownloadIcon?: boolean;
showPreviewIcon?: boolean;
removeIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
downloadIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
previewIcon?: React.ReactNode | ((file: UploadFile) => React.ReactNode);
iconRender: (file: UploadFile) => React.ReactNode;
actionIconRender: (
customIcon: React.ReactNode,
callback: () => void,
prefixCls: string,
title?: string,
) => React.ReactNode;
itemRender?: ItemRender;
onPreview: (file: UploadFile, e: React.SyntheticEvent<HTMLElement>) => void;
onClose: (file: UploadFile) => void;
onDownload: (file: UploadFile) => void;
progress?: UploadListProgressProps;
}
const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
(
{
prefixCls,
className,
style,
locale,
listType,
file,
items,
progress: progressProps,
iconRender,
actionIconRender,
itemRender,
isImgUrl,
showPreviewIcon,
showRemoveIcon,
showDownloadIcon,
previewIcon: customPreviewIcon,
removeIcon: customRemoveIcon,
downloadIcon: customDownloadIcon,
onPreview,
onDownload,
onClose,
},
ref,
) => {
// Status: which will ignore `removed` status
const { status } = file;
const [mergedStatus, setMergedStatus] = React.useState(status);
React.useEffect(() => {
if (status !== 'removed') {
setMergedStatus(status);
}
}, [status]);
// Delay to show the progress bar
const [showProgress, setShowProgress] = React.useState(false);
React.useEffect(() => {
const timer = setTimeout(() => {
setShowProgress(true);
}, 300);
return () => {
clearTimeout(timer);
};
}, []);
const iconNode = iconRender(file);
let icon = <div className={`${prefixCls}-icon`}>{iconNode}</div>;
if (listType === 'picture' || listType === 'picture-card' || listType === 'picture-circle') {
if (mergedStatus === 'uploading' || (!file.thumbUrl && !file.url)) {
const uploadingClassName = classNames({
[`${prefixCls}-list-item-thumbnail`]: true,
[`${prefixCls}-list-item-file`]: mergedStatus !== 'uploading',
});
icon = <div className={uploadingClassName}>{iconNode}</div>;
} else {
const thumbnail = isImgUrl?.(file) ? (
<img
src={file.thumbUrl || file.url}
alt={file.name}
className={`${prefixCls}-list-item-image`}
crossOrigin={file.crossOrigin}
/>
) : (
iconNode
);
const aClassName = classNames(`${prefixCls}-list-item-thumbnail`, {
[`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file),
});
icon = (
<a
className={aClassName}
onClick={(e) => onPreview(file, e)}
href={file.url || file.thumbUrl}
target="_blank"
rel="noopener noreferrer"
>
{thumbnail}
</a>
);
}
}
const listItemClassName = classNames(
`${prefixCls}-list-item`,
`${prefixCls}-list-item-${mergedStatus}`,
);
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
const removeIcon = showRemoveIcon
? actionIconRender(
(typeof customRemoveIcon === 'function' ? customRemoveIcon(file) : customRemoveIcon) || (
<DeleteOutlined />
),
() => onClose(file),
prefixCls,
locale.removeFile,
)
: null;
const downloadIcon =
showDownloadIcon && mergedStatus === 'done'
? actionIconRender(
(typeof customDownloadIcon === 'function'
? customDownloadIcon(file)
: customDownloadIcon) || <DownloadOutlined />,
() => onDownload(file),
prefixCls,
locale.downloadFile,
)
: null;
const downloadOrDelete = listType !== 'picture-card' && listType !== 'picture-circle' && (
<span
key="download-delete"
className={classNames(`${prefixCls}-list-item-actions`, {
picture: listType === 'picture',
})}
>
{downloadIcon}
{removeIcon}
</span>
);
const listItemNameClass = classNames(`${prefixCls}-list-item-name`);
const fileName = file.url
? [
<a
key="view"
target="_blank"
rel="noopener noreferrer"
className={listItemNameClass}
title={file.name}
{...linkProps}
href={file.url}
onClick={(e) => onPreview(file, e)}
>
{file.name}
</a>,
downloadOrDelete,
]
: [
<span
key="view"
className={listItemNameClass}
onClick={(e) => onPreview(file, e)}
title={file.name}
>
{file.name}
</span>,
downloadOrDelete,
];
const previewStyle: React.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}
>
{typeof customPreviewIcon === 'function'
? customPreviewIcon(file)
: customPreviewIcon || <EyeOutlined />}
</a>
) : null;
const pictureCardActions = (listType === 'picture-card' || listType === 'picture-circle') &&
mergedStatus !== 'uploading' && (
<span className={`${prefixCls}-list-item-actions`}>
{previewIcon}
{mergedStatus === 'done' && downloadIcon}
{removeIcon}
</span>
);
const { getPrefixCls } = React.useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
const dom = (
<div className={listItemClassName}>
{icon}
{fileName}
{pictureCardActions}
{showProgress && (
<CSSMotion
motionName={`${rootPrefixCls}-fade`}
visible={mergedStatus === 'uploading'}
motionDeadline={2000}
>
{({ className: motionClassName }) => {
// show loading icon if upload progress listener is disabled
const loadingProgress =
'percent' in file ? (
<Progress
{...progressProps}
type="line"
percent={file.percent}
aria-label={file['aria-label']}
aria-labelledby={file['aria-labelledby']}
/>
) : null;
return (
<div className={classNames(`${prefixCls}-list-item-progress`, motionClassName)}>
{loadingProgress}
</div>
);
}}
</CSSMotion>
)}
</div>
);
const message =
file.response && typeof file.response === 'string'
? file.response
: file.error?.statusText || file.error?.message || locale.uploadError;
const item =
mergedStatus === 'error' ? (
<Tooltip title={message} getPopupContainer={(node) => node.parentNode as HTMLElement}>
{dom}
</Tooltip>
) : (
dom
);
return (
<div
className={classNames(`${prefixCls}-list-item-container`, className)}
style={style}
ref={ref}
>
{itemRender
? itemRender(item, file, items, {
download: onDownload.bind(null, file),
preview: onPreview.bind(null, file),
remove: onClose.bind(null, file),
})
: item}
</div>
);
},
);
export default ListItem;