ant-design-vue/components/vc-image/src/Image.tsx

307 lines
8.8 KiB
Vue

import type { 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<CSSProperties>, default: undefined as CSSProperties },
rootClassName: String,
prefixCls: String,
previewPrefixCls: String,
width: [Number, String],
height: [Number, String],
previewMask: {
type: [Boolean, Function] as PropType<false | (() => any)>,
default: undefined,
},
placeholder: PropTypes.any,
fallback: String,
preview: {
type: [Boolean, Object] as PropType<boolean | ImagePreviewType>,
default: true as boolean | ImagePreviewType,
},
onClick: {
type: Function as PropType<MouseEventHandler>,
},
onError: {
type: Function as PropType<HTMLImageElement['onerror']>,
},
});
export type ImageProps = Partial<ReturnType<typeof imageProps>>;
export type ImageStatus = 'normal' | 'error' | 'loading';
export const mergeDefaultValue = <T extends object>(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<ImagePreviewType>(() => {
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<ImageStatus>(isCustomPlaceholder.value ? 'loading' : 'normal');
watch(
() => props.src,
() => {
status.value = isCustomPlaceholder.value ? 'loading' : 'normal';
},
);
const mousePosition = ref<null | { x: number; y: number }>(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<HTMLImageElement>(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,
width,
height,
crossorigin,
decoding,
alt,
sizes,
srcset,
usemap,
class: cls,
style,
} = { ...props, ...attrs } as any;
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 (
<>
<div
class={wrappperClass}
onClick={
canPreview.value
? onPreview
: e => {
emit('click', e);
}
}
style={{
width: toSizePx(width),
height: toSizePx(height),
...wrapperStyle,
}}
>
<img
{...imgCommonProps}
{...(isError.value && fallback
? {
src: fallback,
}
: { onLoad, onError, src: imgSrc })}
ref={img}
/>
{status.value === 'loading' && (
<div aria-hidden="true" class={`${prefixCls}-placeholder`}>
{placeholder || (slots.placeholder && slots.placeholder())}
</div>
)}
{/* Preview Click Mask */}
{slots.previewMask && canPreview.value && (
<div class={[`${prefixCls}-mask`, maskClassName]}>{slots.previewMask()}</div>
)}
</div>
{!isPreviewGroup.value && canPreview.value && (
<Preview
{...dialogProps}
aria-hidden={!isShowPreview.value}
visible={isShowPreview.value}
prefixCls={previewPrefixCls.value}
onClose={onPreviewClose}
mousePosition={mousePosition.value}
src={mergedSrc}
alt={alt}
getContainer={getPreviewContainer.value}
icons={icons}
rootClassName={rootClassName}
/>
)}
</>
);
};
},
});
ImageInternal.PreviewGroup = PreviewGroup;
export default ImageInternal as typeof ImageInternal & {
readonly PreviewGroup: typeof PreviewGroup;
};