+
, default: undefined as CSSProperties },
prefixCls: String,
wrapClassName: String,
+ rootClassName: String,
width: [String, Number],
height: [String, Number],
zIndex: Number,
diff --git a/components/vc-image/src/Image.tsx b/components/vc-image/src/Image.tsx
index 79ee7b984..abd7c721f 100644
--- a/components/vc-image/src/Image.tsx
+++ b/components/vc-image/src/Image.tsx
@@ -1,21 +1,29 @@
import type { ImgHTMLAttributes, CSSProperties, PropType } from 'vue';
-import { ref, watch, defineComponent, computed, onMounted } 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 type { MouseEventHandler } from './Preview';
+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);
-export interface ImagePreviewType {
+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 interface ImagePropsType extends Omit {
// Original
@@ -27,11 +35,14 @@ export interface ImagePropsType extends Omit ({
src: String,
wrapperClassName: String,
wrapperStyle: { type: Object as PropType, default: undefined as CSSProperties },
+ rootClassName: String,
prefixCls: String,
previewPrefixCls: String,
placeholder: PropTypes.any,
@@ -40,10 +51,17 @@ export const imageProps = () => ({
type: [Boolean, Object] as PropType,
default: true as boolean | ImagePreviewType,
},
+ onClick: {
+ type: Function as PropType,
+ },
+ onError: {
+ type: Function as PropType,
+ },
});
-type ImageStatus = 'normal' | 'error' | 'loading';
+export type ImageProps = Partial>;
+export type ImageStatus = 'normal' | 'error' | 'loading';
-const mergeDefaultValue = (obj: T, defaultValues: object): T => {
+export const mergeDefaultValue = (obj: T, defaultValues: object): T => {
const res = { ...obj };
Object.keys(defaultValues).forEach(key => {
if (obj[key] === undefined) {
@@ -57,11 +75,11 @@ const ImageInternal = defineComponent({
name: 'Image',
inheritAttrs: false,
props: imageProps(),
- emits: ['click'],
+ emits: ['click', 'error'],
setup(props, { attrs, slots, emit }) {
const prefixCls = computed(() => props.prefixCls);
const previewPrefixCls = computed(() => `${prefixCls.value}-preview`);
- const preview = computed(() => {
+ const preview = computed(() => {
const defaultValues = {
visible: undefined,
onVisibleChange: () => {},
@@ -75,16 +93,21 @@ const ImageInternal = defineComponent({
() => (props.placeholder && props.placeholder !== true) || slots.placeholder,
);
const previewVisible = computed(() => preview.value.visible);
- const onPreviewVisibleChange = computed(() => preview.value.onVisibleChange);
const getPreviewContainer = computed(() => preview.value.getContainer);
-
const isControlled = computed(() => previewVisible.value !== undefined);
- const isShowPreview = ref(!!previewVisible.value);
- watch(previewVisible, () => {
- isShowPreview.value = !!previewVisible.value;
+
+ const onPreviewVisibleChange = (val, preval) => {
+ preview.value.onVisibleChange?.(val, preval);
+ };
+ const [isShowPreview, setShowPreview] = useMergedState(!!previewVisible.value, {
+ value: previewVisible,
+ onChange: onPreviewVisibleChange,
+ });
+ watch(previewVisible, val => {
+ setShowPreview(Boolean(val));
});
watch(isShowPreview, (val, preVal) => {
- onPreviewVisibleChange.value(val, preVal);
+ onPreviewVisibleChange(val, preVal);
});
const status = ref(isCustomPlaceholder.value ? 'loading' : 'normal');
watch(
@@ -108,13 +131,15 @@ const ImageInternal = defineComponent({
const onLoad = () => {
status.value = 'normal';
};
- const onError = () => {
+ 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({
@@ -131,13 +156,13 @@ const ImageInternal = defineComponent({
if (isPreviewGroup.value) {
setGroupShowPreview(true);
} else {
- isShowPreview.value = true;
+ setShowPreview(true);
}
emit('click', e);
};
const onPreviewClose = () => {
- isShowPreview.value = false;
+ setShowPreview(false);
if (!isControlled.value) {
mousePosition.value = null;
}
@@ -163,7 +188,7 @@ const ImageInternal = defineComponent({
return () => {};
}
- unRegister = registerImage(currentId.value, props.src);
+ unRegister = registerImage(currentId.value, props.src, canPreview.value);
if (!canPreview.value) {
unRegister();
@@ -172,13 +197,21 @@ const ImageInternal = defineComponent({
{ 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, preview, placeholder, wrapperStyle } =
- props;
+ const {
+ prefixCls,
+ wrapperClassName,
+ fallback,
+ src: imgSrc,
+ placeholder,
+ wrapperStyle,
+ rootClassName,
+ } = props;
const {
width,
height,
@@ -191,11 +224,12 @@ const ImageInternal = defineComponent({
class: cls,
style,
} = attrs as ImgHTMLAttributes;
- const wrappperClass = cn(prefixCls, wrapperClassName, {
+ const { icons, maskClassName, src: previewSrc, ...dialogProps } = preview.value;
+
+ const wrappperClass = cn(prefixCls, wrapperClassName, rootClassName, {
[`${prefixCls}-error`]: isError.value,
});
- const mergedSrc = isError.value && fallback ? fallback : src;
- const previewMask = slots.previewMask && slots.previewMask();
+ const mergedSrc = isError.value && fallback ? fallback : previewSrc ?? imgSrc;
const imgCommonProps = {
crossorigin,
decoding,
@@ -215,12 +249,13 @@ const ImageInternal = defineComponent({
...(style as CSSProperties),
},
};
+
return (
<>
{
emit('click', e);
@@ -238,7 +273,7 @@ const ImageInternal = defineComponent({
? {
src: fallback,
}
- : { onLoad, onError, src })}
+ : { onLoad, onError, src: imgSrc })}
ref={img}
/>
@@ -248,12 +283,13 @@ const ImageInternal = defineComponent({
)}
{/* Preview Click Mask */}
- {previewMask && canPreview.value && (
- {previewMask}
+ {slots.previewMask && canPreview.value && (
+ {slots.previewMask()}
)}
{!isPreviewGroup.value && canPreview.value && (
)}
>
diff --git a/components/vc-image/src/Preview.tsx b/components/vc-image/src/Preview.tsx
index 55e9f4b1d..aada154b1 100644
--- a/components/vc-image/src/Preview.tsx
+++ b/components/vc-image/src/Preview.tsx
@@ -1,47 +1,65 @@
-import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
-import RotateLeftOutlined from '@ant-design/icons-vue/RotateLeftOutlined';
-import RotateRightOutlined from '@ant-design/icons-vue/RotateRightOutlined';
-import ZoomInOutlined from '@ant-design/icons-vue/ZoomInOutlined';
-import ZoomOutOutlined from '@ant-design/icons-vue/ZoomOutOutlined';
-import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
-import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
-import RightOutlined from '@ant-design/icons-vue/RightOutlined';
+import {
+ computed,
+ defineComponent,
+ onMounted,
+ onUnmounted,
+ reactive,
+ ref,
+ watch,
+ cloneVNode,
+} from 'vue';
+import type { VNode, PropType } from 'vue';
import classnames from '../../_util/classNames';
import Dialog from '../../vc-dialog';
-import getIDialogPropTypes from '../../vc-dialog/IDialogPropTypes';
+import { type IDialogChildProps, dialogPropTypes } from '../../vc-dialog/IDialogPropTypes';
import { getOffset } from '../../vc-util/Dom/css';
import addEventListener from '../../vc-util/Dom/addEventListener';
import { warning } from '../../vc-util/warning';
import useFrameSetState from './hooks/useFrameSetState';
import getFixScaleEleTransPosition from './getFixScaleEleTransPosition';
+import type { MouseEventHandler, WheelEventHandler } from '../../_util/EventInterface';
import { context } from './PreviewGroup';
-const IDialogPropTypes = getIDialogPropTypes();
-export type MouseEventHandler = (payload: MouseEvent) => void;
-
-export interface PreviewProps extends Omit
{
+export interface PreviewProps extends Omit {
onClose?: (e: Element) => void;
src?: string;
alt?: string;
+ rootClassName?: string;
+ icons?: {
+ rotateLeft?: VNode;
+ rotateRight?: VNode;
+ zoomIn?: VNode;
+ zoomOut?: VNode;
+ close?: VNode;
+ left?: VNode;
+ right?: VNode;
+ };
}
const initialPosition = {
x: 0,
y: 0,
};
-const PreviewType = {
+export const previewProps = {
+ ...dialogPropTypes(),
src: String,
alt: String,
- ...IDialogPropTypes,
+ rootClassName: String,
+ icons: {
+ type: Object as PropType,
+ default: () => ({} as PreviewProps['icons']),
+ },
};
const Preview = defineComponent({
name: 'Preview',
inheritAttrs: false,
- props: PreviewType,
+ props: previewProps,
emits: ['close', 'afterClose'],
setup(props, { emit, attrs }) {
+ const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right } = reactive(props.icons);
+
const scale = ref(1);
const rotate = ref(0);
const [position, setPosition] = useFrameSetState<{
@@ -65,22 +83,22 @@ const Preview = defineComponent({
const isMoving = ref(false);
const groupContext = context.inject();
const { previewUrls, current, isPreviewGroup, setCurrent } = groupContext;
- const previewGroupCount = computed(() => Object.keys(previewUrls).length);
- const previewUrlsKeys = computed(() => Object.keys(previewUrls));
- const currentPreviewIndex = computed(() =>
- previewUrlsKeys.value.indexOf(String(current.value)),
- );
- const combinationSrc = computed(() =>
- isPreviewGroup.value ? previewUrls[current.value] : props.src,
- );
+ const previewGroupCount = computed(() => previewUrls.value.size);
+ const previewUrlsKeys = computed(() => Array.from(previewUrls.value.keys()));
+ const currentPreviewIndex = computed(() => previewUrlsKeys.value.indexOf(current.value));
+ const combinationSrc = computed(() => {
+ return isPreviewGroup.value ? previewUrls.value.get(current.value) : props.src;
+ });
const showLeftOrRightSwitches = computed(
() => isPreviewGroup.value && previewGroupCount.value > 1,
);
+ const lastWheelZoomDirection = ref({ wheelDirection: 0 });
const onAfterClose = () => {
scale.value = 1;
rotate.value = 0;
setPosition(initialPosition);
+ emit('afterClose');
};
const onZoomIn = () => {
@@ -106,7 +124,7 @@ const Preview = defineComponent({
// Without this mask close will abnormal
event.stopPropagation();
if (currentPreviewIndex.value > 0) {
- setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value - 1)]);
+ setCurrent(previewUrlsKeys.value[currentPreviewIndex.value - 1]);
}
};
@@ -115,7 +133,7 @@ const Preview = defineComponent({
// Without this mask close will abnormal
event.stopPropagation();
if (currentPreviewIndex.value < previewGroupCount.value - 1) {
- setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value + 1)]);
+ setCurrent(previewUrlsKeys.value[currentPreviewIndex.value + 1]);
}
};
@@ -126,28 +144,28 @@ const Preview = defineComponent({
const iconClassName = `${props.prefixCls}-operations-icon`;
const tools = [
{
- icon: CloseOutlined,
+ icon: close,
onClick: onClose,
type: 'close',
},
{
- icon: ZoomInOutlined,
+ icon: zoomIn,
onClick: onZoomIn,
type: 'zoomIn',
},
{
- icon: ZoomOutOutlined,
+ icon: zoomOut,
onClick: onZoomOut,
type: 'zoomOut',
disabled: computed(() => scale.value === 1),
},
{
- icon: RotateRightOutlined,
+ icon: rotateRight,
onClick: onRotateRight,
type: 'rotateRight',
},
{
- icon: RotateLeftOutlined,
+ icon: rotateLeft,
onClick: onRotateLeft,
type: 'rotateLeft',
},
@@ -175,6 +193,8 @@ const Preview = defineComponent({
};
const onMouseDown: MouseEventHandler = event => {
+ // Only allow main button
+ if (event.button !== 0) return;
event.preventDefault();
// Without this mask close will abnormal
event.stopPropagation();
@@ -193,6 +213,14 @@ const Preview = defineComponent({
});
}
};
+
+ const onWheelMove: WheelEventHandler = event => {
+ if (!props.visible) return;
+ event.preventDefault();
+ const wheelDirection = event.deltaY;
+ lastWheelZoomDirection.value = { wheelDirection };
+ };
+
let removeListeners = () => {};
onMounted(() => {
watch(
@@ -204,6 +232,9 @@ const Preview = defineComponent({
const onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false);
const onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false);
+ const onScrollWheelListener = addEventListener(window, 'wheel', onWheelMove, {
+ passive: false,
+ });
try {
// Resolve if in iframe lost event
@@ -225,6 +256,7 @@ const Preview = defineComponent({
removeListeners = () => {
onMouseUpListener.remove();
onMouseMoveListener.remove();
+ onScrollWheelListener.remove();
/* istanbul ignore next */
if (onTopMouseUpListener) onTopMouseUpListener.remove();
@@ -240,6 +272,8 @@ const Preview = defineComponent({
});
return () => {
+ const { visible, prefixCls, rootClassName } = props;
+
return (
)}
{showLeftOrRightSwitches.value && (
@@ -302,7 +337,7 @@ const Preview = defineComponent({
})}
onClick={onSwitchRight}
>
-