import type { ImgHTMLAttributes, CSSProperties, PropType } from 'vue'; import { ref, watch, defineComponent, computed, onMounted, onUnmounted } from 'vue'; import isNumber from 'lodash-es/isNumber'; import cn from '../../_util/classNames'; import PropTypes from '../../_util/vue-types'; import { getOffset } from '../../vc-util/Dom/css'; import useMergedState from '../../_util/hooks/useMergedState'; import Preview from './Preview'; import type { MouseEventHandler } from '../../_util/EventInterface'; import PreviewGroup, { context } from './PreviewGroup'; import type { IDialogChildProps } from '../../vc-dialog/IDialogPropTypes'; export type GetContainer = string | HTMLElement | (() => HTMLElement); import type { PreviewProps } from './Preview'; export type ImagePreviewType = Omit< IDialogChildProps, 'mask' | 'visible' | 'closable' | 'prefixCls' | 'onClose' | 'afterClose' | 'wrapClassName' > & { src?: string; visible?: boolean; onVisibleChange?: (value: boolean, prevValue: boolean) => void; getContainer?: GetContainer | false; maskClassName?: string; icons?: PreviewProps['icons']; }; export const imageProps = () => ({ src: String, wrapperClassName: String, wrapperStyle: { type: Object as PropType, default: undefined as CSSProperties }, rootClassName: String, prefixCls: String, previewPrefixCls: String, previewMask: { type: [Boolean, Function] as PropType any)>, default: undefined, }, placeholder: PropTypes.any, fallback: String, preview: { type: [Boolean, Object] as PropType, default: true as boolean | ImagePreviewType, }, onClick: { type: Function as PropType, }, onError: { type: Function as PropType, }, }); export type ImageProps = Partial>; export type ImageStatus = 'normal' | 'error' | 'loading'; export const mergeDefaultValue = (obj: T, defaultValues: object): T => { const res = { ...obj }; Object.keys(defaultValues).forEach(key => { if (obj[key] === undefined) { res[key] = defaultValues[key]; } }); return res; }; let uuid = 0; const ImageInternal = defineComponent({ compatConfig: { MODE: 3 }, name: 'VcImage', inheritAttrs: false, props: imageProps(), emits: ['click', 'error'], setup(props, { attrs, slots, emit }) { const prefixCls = computed(() => props.prefixCls); const previewPrefixCls = computed(() => `${prefixCls.value}-preview`); const preview = computed(() => { const defaultValues = { visible: undefined, onVisibleChange: () => {}, getContainer: undefined, }; return typeof props.preview === 'object' ? mergeDefaultValue(props.preview, defaultValues) : defaultValues; }); const src = computed(() => preview.value.src ?? props.src); const isCustomPlaceholder = computed( () => (props.placeholder && props.placeholder !== true) || slots.placeholder, ); const previewVisible = computed(() => preview.value.visible); const getPreviewContainer = computed(() => preview.value.getContainer); const isControlled = computed(() => previewVisible.value !== undefined); const onPreviewVisibleChange = (val, preval) => { preview.value.onVisibleChange?.(val, preval); }; const [isShowPreview, setShowPreview] = useMergedState(!!previewVisible.value, { value: previewVisible, onChange: onPreviewVisibleChange, }); const status = ref(isCustomPlaceholder.value ? 'loading' : 'normal'); watch( () => props.src, () => { status.value = isCustomPlaceholder.value ? 'loading' : 'normal'; }, ); const mousePosition = ref(null); const isError = computed(() => status.value === 'error'); const groupContext = context.inject(); const { isPreviewGroup, setCurrent, setShowPreview: setGroupShowPreview, setMousePosition: setGroupMousePosition, registerImage, } = groupContext; const currentId = ref(uuid++); const canPreview = computed(() => props.preview && !isError.value); const onLoad = () => { status.value = 'normal'; }; const onError = (e: Event) => { status.value = 'error'; emit('error', e); }; const onPreview: MouseEventHandler = e => { if (!isControlled.value) { const { left, top } = getOffset(e.target); if (isPreviewGroup.value) { setCurrent(currentId.value); setGroupMousePosition({ x: left, y: top, }); } else { mousePosition.value = { x: left, y: top, }; } } if (isPreviewGroup.value) { setGroupShowPreview(true); } else { setShowPreview(true); } emit('click', e); }; const onPreviewClose = () => { setShowPreview(false); if (!isControlled.value) { mousePosition.value = null; } }; const img = ref(null); watch( () => img, () => { if (status.value !== 'loading') return; if (img.value.complete && (img.value.naturalWidth || img.value.naturalHeight)) { onLoad(); } }, ); let unRegister = () => {}; onMounted(() => { watch( [src, canPreview], () => { unRegister(); if (!isPreviewGroup.value) { return () => {}; } unRegister = registerImage(currentId.value, src.value, canPreview.value); if (!canPreview.value) { unRegister(); } }, { flush: 'post', immediate: true }, ); }); onUnmounted(() => { unRegister(); }); const toSizePx = (l: number | string) => { if (isNumber(l)) return l + 'px'; return l; }; return () => { const { prefixCls, wrapperClassName, fallback, src: imgSrc, placeholder, wrapperStyle, rootClassName, } = props; const { width, height, crossorigin, decoding, alt, sizes, srcset, usemap, class: cls, style, } = attrs as ImgHTMLAttributes; const { icons, maskClassName, ...dialogProps } = preview.value; const wrappperClass = cn(prefixCls, wrapperClassName, rootClassName, { [`${prefixCls}-error`]: isError.value, }); const mergedSrc = isError.value && fallback ? fallback : src.value; const imgCommonProps = { crossorigin, decoding, alt, sizes, srcset, usemap, width, height, class: cn( `${prefixCls}-img`, { [`${prefixCls}-img-placeholder`]: placeholder === true, }, cls, ), style: { height: toSizePx(height), ...(style as CSSProperties), }, }; return ( <>
{ emit('click', e); } } style={{ width: toSizePx(width), height: toSizePx(height), ...wrapperStyle, }} > {status.value === 'loading' && ( )} {/* Preview Click Mask */} {slots.previewMask && canPreview.value && (
{slots.previewMask()}
)}
{!isPreviewGroup.value && canPreview.value && ( )} ); }; }, }); ImageInternal.PreviewGroup = PreviewGroup; export default ImageInternal as typeof ImageInternal & { readonly PreviewGroup: typeof PreviewGroup; };