Browse Source

feat(image): add new features (#5479)

* feat(image): add new features

* fix: lint error

* update test

* update code

* update docs

* perf: reset currentIndex after close

* update code

* update code

* update code

* update code

* update code

* add rootClassName props

* fix lint
pull/5502/head
bqy_fe 3 years ago committed by GitHub
parent
commit
bd87079e12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      components/_util/EventInterface.ts
  2. 19
      components/image/PreviewGroup.tsx
  3. 56
      components/image/__tests__/__snapshots__/demo.test.js.snap
  4. 5
      components/image/demo/index.vue
  5. 2
      components/image/demo/preview-group-visible.vue
  6. 31
      components/image/demo/preview-src.vue
  7. 5
      components/image/index.en-US.md
  8. 45
      components/image/index.tsx
  9. 5
      components/image/index.zh-CN.md
  10. 3
      components/vc-dialog/Dialog.tsx
  11. 3
      components/vc-dialog/IDialogPropTypes.ts
  12. 96
      components/vc-image/src/Image.tsx
  13. 107
      components/vc-image/src/Preview.tsx
  14. 135
      components/vc-image/src/PreviewGroup.tsx

1
components/_util/EventInterface.ts

@ -4,6 +4,7 @@ export type KeyboardEventHandler = (e: KeyboardEvent) => void;
export type CompositionEventHandler = (e: CompositionEvent) => void;
export type ClipboardEventHandler = (e: ClipboardEvent) => void;
export type ChangeEventHandler = (e: ChangeEvent) => void;
export type WheelEventHandler = (e: WheelEvent) => void;
export type ChangeEvent = Event & {
target: {
value?: string | undefined;

19
components/image/PreviewGroup.tsx

@ -2,6 +2,24 @@ import PreviewGroup from '../vc-image/src/PreviewGroup';
import { computed, defineComponent } from 'vue';
import useConfigInject from '../_util/hooks/useConfigInject';
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';
export const icons = {
rotateLeft: <RotateLeftOutlined />,
rotateRight: <RotateRightOutlined />,
zoomIn: <ZoomInOutlined />,
zoomOut: <ZoomOutOutlined />,
close: <CloseOutlined />,
left: <LeftOutlined />,
right: <RightOutlined />,
};
const InternalPreviewGroup = defineComponent({
name: 'AImagePreviewGroup',
inheritAttrs: false,
@ -13,6 +31,7 @@ const InternalPreviewGroup = defineComponent({
return (
<PreviewGroup
{...{ ...attrs, ...props }}
icons={icons}
previewPrefixCls={prefixCls.value}
v-slots={slots}
></PreviewGroup>

56
components/image/__tests__/__snapshots__/demo.test.js.snap

@ -3,8 +3,11 @@
exports[`renders ./components/image/demo/basic.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
`;
exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] = `
@ -13,7 +16,9 @@ exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] =
</button>
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" style="display: none;" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
</div>
@ -22,8 +27,11 @@ exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] =
exports[`renders ./components/image/demo/fallback.vue correctly 1`] = `
<div class="ant-image" style="width: 200px; height: 200px;"><img class="ant-image-img" src="https://www.antdv.com/#error">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
`;
exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
@ -37,7 +45,9 @@ exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
</div>
<!---->
</div>
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
</div>
@ -52,35 +62,61 @@ exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
exports[`renders ./components/image/demo/preview-group.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/vue.png">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/logo.png">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<!---->
`;
exports[`renders ./components/image/demo/preview-group-visible.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<div style="display: none;">
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/cV16ZqzMjW/photo-1473091540282-9b846e7965e3.webp">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/x43I27A55%26/photo-1438109491414-7198515b166b.webp">
<!---->
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
<!---->
</div>
`;
exports[`renders ./components/image/demo/preview-src.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/logo.png">
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
`;

5
components/image/demo/index.vue

@ -4,8 +4,9 @@
<fallback />
<placeholder />
<preview-group />
<controlled-preview />
<previewGroupVisibleVue />
<previewSrc />
<controlled-preview />
</demo-sort>
</template>
@ -13,6 +14,7 @@
import Basic from './basic.vue';
import Fallback from './fallback.vue';
import Placeholder from './placeholder.vue';
import previewSrc from './preview-src.vue';
import PreviewGroup from './preview-group.vue';
import ControlledPreview from './controlled-preview.vue';
import previewGroupVisibleVue from './preview-group-visible.vue';
@ -26,6 +28,7 @@ export default defineComponent({
components: {
Basic,
Fallback,
previewSrc,
Placeholder,
PreviewGroup,
ControlledPreview,

2
components/image/demo/preview-group-visible.vue

@ -18,7 +18,7 @@ Preview a collection from one image.
<template>
<a-image
:preview="{ visible }"
:preview="{ visible: false }"
:width="200"
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
@click="visible = true"

31
components/image/demo/preview-src.vue

@ -0,0 +1,31 @@
<docs>
---
order: 4
title:
zh-CN: 自定义预览图片
en-US: Custom preview image
---
## zh-CN
可以设置不同的预览图片
## en-US
You can set different preview image.
</docs>
<template>
<a-image
:width="200"
src="https://aliyuncdn.antdv.com/logo.png"
:preview="{
src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
}"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>

5
components/image/index.en-US.md

@ -22,7 +22,9 @@ Previewable image.
| placeholder | Load placeholder, use default placeholder when set `true` | boolean \| slot | - | 2.0.0 |
| preview | preview config, disabled when `false` | boolean \| [previewType](#previewType) | true | 2.0.0 |
| src | Image path | string | - | 2.0.0 |
| previewMask | custom mask | slot | - | 3.2.0 |
| width | Image width | string \| number | - | 2.0.0 |
| onError | Load failed callback | (event: Event) => void | - | 3.2.0 |
### previewType
@ -31,6 +33,9 @@ Previewable image.
visible?: boolean;
onVisibleChange?: (visible, prevVisible) => void;
getContainer?: string | HTMLElement | (() => HTMLElement);
src?: string;
maskClassName?: string;
current?: number;
}
```

45
components/image/index.tsx

@ -1,9 +1,12 @@
import type { App, ExtractPropTypes, ImgHTMLAttributes, Plugin } from 'vue';
import { defineComponent } from 'vue';
import { defineComponent, computed } from 'vue';
import ImageInternal from '../vc-image';
import { imageProps } from '../vc-image/src/Image';
import defaultLocale from '../locale/en_US';
import useConfigInject from '../_util/hooks/useConfigInject';
import PreviewGroup from './PreviewGroup';
import PreviewGroup, { icons } from './PreviewGroup';
import EyeOutlined from '@ant-design/icons-vue/EyeOutlined';
import { getTransitionName } from '../_util/transition';
export type ImageProps = Partial<
ExtractPropTypes<ReturnType<typeof imageProps>> &
@ -14,12 +17,46 @@ const Image = defineComponent<ImageProps>({
inheritAttrs: false,
props: imageProps() as any,
setup(props, { slots, attrs }) {
const { prefixCls } = useConfigInject('image', props);
const { prefixCls, rootPrefixCls, configProvider } = useConfigInject('image', props);
const mergedPreview = computed(() => {
const { preview } = props;
if (preview === false) {
return preview;
}
const _preview = typeof preview === 'object' ? preview : {};
return {
icons,
..._preview,
transitionName: getTransitionName(rootPrefixCls.value, 'zoom', _preview.transitionName),
maskTransitionName: getTransitionName(
rootPrefixCls.value,
'fade',
_preview.maskTransitionName,
),
};
});
return () => {
const imageLocale = configProvider.locale?.Image || defaultLocale.Image;
return (
<ImageInternal
{...{ ...attrs, ...props, prefixCls: prefixCls.value }}
v-slots={slots}
preview={mergedPreview.value}
v-slots={{
...slots,
previewMask:
slots.previewMask ??
(() => (
<div class={`${prefixCls.value}-mask-info`}>
<EyeOutlined />
{imageLocale?.preview}
</div>
)),
}}
></ImageInternal>
);
};

5
components/image/index.zh-CN.md

@ -23,7 +23,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
| placeholder | 加载占位, 为 `true` 时使用默认占位 | boolean \| slot | - | 2.0.0 |
| preview | 预览参数,为 `false` 时禁用 | boolean \| [previewType](#previewType) | true | 2.0.0 |
| src | 图片地址 | string | - | 2.0.0 |
| previewMask | 自定义 mask | slot | - | 3.2.0 |
| width | 图像宽度 | string \| number | - | 2.0.0 |
| onError | 加载错误回调 | (event: Event) => void | - | 3.2.0 |
### previewType
@ -32,6 +34,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
visible?: boolean;
onVisibleChange?: (visible, prevVisible) => void;
getContainer: string | HTMLElement | (() => HTMLElement);
src?: string;
maskClassName?: string;
current?: number;
}
```

3
components/vc-dialog/Dialog.tsx

@ -143,6 +143,7 @@ export default defineComponent({
maskAnimation,
zIndex,
wrapClassName,
rootClassName,
wrapStyle,
closable,
maskProps,
@ -154,7 +155,7 @@ export default defineComponent({
} = props;
const { style, class: className } = attrs;
return (
<div class={`${prefixCls}-root`} {...pickAttrs(props, { data: true })}>
<div class={[`${prefixCls}-root`, rootClassName]} {...pickAttrs(props, { data: true })}>
<Mask
prefixCls={prefixCls}
visible={mask && visible}

3
components/vc-dialog/IDialogPropTypes.ts

@ -1,7 +1,7 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
function dialogPropTypes() {
export function dialogPropTypes() {
return {
keyboard: { type: Boolean, default: undefined },
mask: { type: Boolean, default: undefined },
@ -25,6 +25,7 @@ function dialogPropTypes() {
maskStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
prefixCls: String,
wrapClassName: String,
rootClassName: String,
width: [String, Number],
height: [String, Number],
zIndex: Number,

96
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<ImgHTMLAttributes, 'placeholder' | 'onClick'> {
// Original
@ -27,11 +35,14 @@ export interface ImagePropsType extends Omit<ImgHTMLAttributes, 'placeholder' |
placeholder?: boolean;
fallback?: string;
preview?: boolean | ImagePreviewType;
onClick?: MouseEventHandler;
onError?: HTMLImageElement['onerror'];
}
export const imageProps = () => ({
src: String,
wrapperClassName: String,
wrapperStyle: { type: Object as PropType<CSSProperties>, 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<boolean | ImagePreviewType>,
default: true as boolean | ImagePreviewType,
},
onClick: {
type: Function as PropType<MouseEventHandler>,
},
onError: {
type: Function as PropType<HTMLImageElement['onerror']>,
},
});
type ImageStatus = 'normal' | 'error' | 'loading';
export type ImageProps = Partial<ReturnType<typeof imageProps>>;
export type ImageStatus = 'normal' | 'error' | 'loading';
const mergeDefaultValue = <T extends object>(obj: T, defaultValues: object): T => {
export const mergeDefaultValue = <T extends object>(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<ImagePreviewType>(() => {
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<ImageStatus>(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 (
<>
<div
class={wrappperClass}
onClick={
preview && !isError.value
canPreview.value
? onPreview
: e => {
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({
</div>
)}
{/* Preview Click Mask */}
{previewMask && canPreview.value && (
<div class={`${prefixCls}-mask`}>{previewMask}</div>
{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}
@ -262,6 +298,8 @@ const ImageInternal = defineComponent({
src={mergedSrc}
alt={alt}
getContainer={getPreviewContainer.value}
icons={icons}
rootClassName={rootClassName}
/>
)}
</>

107
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<typeof IDialogPropTypes, 'onClose'> {
export interface PreviewProps extends Omit<IDialogChildProps, 'onClose' | 'mask'> {
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<PreviewProps['icons']>,
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 (
<Dialog
{...attrs}
@ -247,11 +281,12 @@ const Preview = defineComponent({
maskTransitionName="fade"
closable={false}
keyboard
prefixCls={props.prefixCls}
prefixCls={prefixCls}
onClose={onClose}
afterClose={onAfterClose}
visible={props.visible}
visible={visible}
wrapClassName={wrapClassName}
rootClassName={rootClassName}
getContainer={props.getContainer}
>
<ul class={`${props.prefixCls}-operations`}>
@ -263,7 +298,7 @@ const Preview = defineComponent({
onClick={onClick}
key={type}
>
<IconType class={iconClassName} />
{cloneVNode(IconType, { class: iconClassName })}
</li>
))}
</ul>
@ -291,7 +326,7 @@ const Preview = defineComponent({
})}
onClick={onSwitchLeft}
>
<LeftOutlined />
{left}
</div>
)}
{showLeftOrRightSwitches.value && (
@ -302,7 +337,7 @@ const Preview = defineComponent({
})}
onClick={onSwitchRight}
>
<RightOutlined />
{right}
</div>
)}
</Dialog>

135
components/vc-image/src/PreviewGroup.tsx

@ -1,19 +1,39 @@
import type { Ref } from 'vue';
import { ref, provide, defineComponent, inject, reactive } from 'vue';
import type { PropType, Ref, ComputedRef } from 'vue';
import { ref, provide, defineComponent, inject, watch, reactive, computed, watchEffect } from 'vue';
import { type ImagePreviewType, mergeDefaultValue } from './Image';
import Preview from './Preview';
import type { PreviewProps } from './Preview';
export interface PreviewGroupPreview
extends Omit<ImagePreviewType, 'icons' | 'mask' | 'maskClassName'> {
/**
* If Preview the show img index
* @default 0
*/
current?: number;
}
export interface GroupConsumerProps {
previewPrefixCls?: string;
icons?: PreviewProps['icons'];
preview?: boolean | PreviewGroupPreview;
}
interface PreviewUrl {
url: string;
canPreview: boolean;
}
export interface GroupConsumerValue extends GroupConsumerProps {
isPreviewGroup?: Ref<boolean | undefined>;
previewUrls: Record<number, string>;
setPreviewUrls: (previewUrls: Record<number, string>) => void;
previewUrls: ComputedRef<Map<number, string>>;
setPreviewUrls: (id: number, url: string, canPreview?: boolean) => void;
current: Ref<number>;
setCurrent: (current: number) => void;
setShowPreview: (isShowPreview: boolean) => void;
setMousePosition: (mousePosition: null | { x: number; y: number }) => void;
registerImage: (id: number, url: string) => () => void;
registerImage: (id: number, url: string, canPreview?: boolean) => () => void;
rootClassName?: string;
}
const previewGroupContext = Symbol('previewGroupContext');
export const context = {
@ -23,13 +43,14 @@ export const context = {
inject: () => {
return inject<GroupConsumerValue>(previewGroupContext, {
isPreviewGroup: ref(false),
previewUrls: reactive({}),
previewUrls: computed(() => new Map()),
setPreviewUrls: () => {},
current: ref(null),
setCurrent: () => {},
setShowPreview: () => {},
setMousePosition: () => {},
registerImage: null,
rootClassName: '',
});
},
};
@ -37,14 +58,56 @@ export const context = {
const Group = defineComponent({
name: 'PreviewGroup',
inheritAttrs: false,
props: { previewPrefixCls: String },
props: {
previewPrefixCls: String,
preview: {
type: [Boolean, Object] as PropType<boolean | ImagePreviewType>,
default: true as boolean | ImagePreviewType,
},
icons: {
type: Object as PropType<PreviewProps['icons']>,
default: () => ({}),
},
},
setup(props, { slots }) {
const previewUrls = reactive<Record<number, string>>({});
const preview = computed<PreviewGroupPreview>(() => {
const defaultValues = {
visible: undefined,
onVisibleChange: () => {},
getContainer: undefined,
current: 0,
};
return typeof props.preview === 'object'
? mergeDefaultValue(props.preview, defaultValues)
: defaultValues;
});
const previewUrls = reactive<Map<number, PreviewUrl>>(new Map());
const current = ref<number>();
const isShowPreview = ref<boolean>(false);
const previewVisible = computed(() => preview.value.visible);
const onPreviewVisibleChange = computed(() => preview.value.onVisibleChange);
const getPreviewContainer = computed(() => preview.value.getContainer);
const isShowPreview = ref(!!previewVisible.value);
const mousePosition = ref<{ x: number; y: number }>(null);
const setPreviewUrls = (val: Record<number, string>) => {
Object.assign(previewUrls, val);
const isControlled = computed(() => previewVisible.value !== undefined);
const previewUrlsKeys = computed(() => Array.from(previewUrls.keys()));
const currentControlledKey = computed(() => previewUrlsKeys.value[preview.value.current]);
const canPreviewUrls = computed(
() =>
new Map<number, string>(
Array.from(previewUrls)
.filter(([, { canPreview }]) => !!canPreview)
.map(([id, { url }]) => [id, url]),
),
);
const setPreviewUrls = (id: number, url: string, canPreview = true) => {
previewUrls.set(id, {
url,
canPreview,
});
};
const setCurrent = (val: number) => {
current.value = val;
@ -55,21 +118,54 @@ const Group = defineComponent({
const setShowPreview = (val: boolean) => {
isShowPreview.value = val;
};
const registerImage = (id: number, url: string) => {
previewUrls[id] = url;
return () => {
delete previewUrls[id];
const registerImage = (id: number, url: string, canPreview = true) => {
const unRegister = () => {
previewUrls.delete(id);
};
previewUrls.set(id, {
url,
canPreview,
});
return unRegister;
};
const onPreviewClose = (e: any) => {
e?.stopPropagation();
isShowPreview.value = false;
mousePosition.value = null;
};
watch(previewVisible, () => {
isShowPreview.value = !!previewVisible.value;
});
watch(isShowPreview, (val, preVal) => {
onPreviewVisibleChange.value(val, preVal);
});
watch(
currentControlledKey,
val => {
setCurrent(val);
},
{
immediate: true,
flush: 'post',
},
);
watchEffect(
() => {
if (!isShowPreview.value && isControlled.value) {
setCurrent(currentControlledKey.value);
}
},
{
flush: 'post',
},
);
context.provide({
isPreviewGroup: ref(true),
previewUrls,
previewUrls: canPreviewUrls,
setPreviewUrls,
current,
setCurrent,
@ -77,17 +173,22 @@ const Group = defineComponent({
setMousePosition,
registerImage,
});
return () => {
const { ...dialogProps } = preview.value;
return (
<>
{slots.default && slots.default()}
<Preview
{...dialogProps}
ria-hidden={!isShowPreview.value}
visible={isShowPreview.value}
prefixCls={props.previewPrefixCls}
onClose={onPreviewClose}
mousePosition={mousePosition.value}
src={previewUrls[current.value]}
src={canPreviewUrls.value.get(current.value)}
icons={props.icons}
getContainer={getPreviewContainer.value}
/>
</>
);

Loading…
Cancel
Save