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
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;
|
|
|