From 5913cf9c5c2e98de3dcb7ae46465242161f8ff46 Mon Sep 17 00:00:00 2001 From: Zou Jian Date: Fri, 18 Dec 2020 18:02:51 +0800 Subject: [PATCH] feat: add image (#3235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🆕 image组件 - 添加样式 * feat: 🆕 增加 AImage 组件 * feat: 🆕 参考 ant-design 增加 相关 Image 组件的less定义 * feat: 🆕 Image placeholder 的修改 * fix: 🐞 去除 onPreviewClose 相关 * test: 🧪 image width/height 以及相关测试 * fix: 🐞 fix lint no-setup-props-destructure * test: 🧪 image test snap file * refactor: 💡 去掉多加了的文件,重构文件以使逻辑清晰 * feat: 🆕 rc-image 相关内容 列入 vc-image 文件夹中 * feat: 🆕 antd 4.9.1 增加 image-preview-group * feat: 🆕 add ImagePropsType * feat: udpate image components * feat: update image * feat: update image Co-authored-by: tanjinzhou <415800467@qq.com> --- antdv-demo | 2 +- components/image/PreviewGroup.tsx | 25 ++ .../__tests__/__snapshots__/demo.test.js.snap | 0 components/image/__tests__/demo.test.js | 3 + components/image/__tests__/index.test.js | 32 ++ components/image/index.tsx | 35 ++ components/image/style/index.less | 141 ++++++++ components/image/style/index.ts | 2 + components/index.ts | 4 +- components/style.ts | 1 + components/style/mixins/box.less | 7 + components/style/mixins/index.less | 2 + components/style/mixins/modal-mask.less | 31 ++ components/style/themes/default.less | 11 + components/vc-image/assets/index.less | 313 +++++++++++++++++ components/vc-image/index.ts | 4 + components/vc-image/src/Image.tsx | 291 ++++++++++++++++ components/vc-image/src/Preview.tsx | 314 ++++++++++++++++++ components/vc-image/src/PreviewGroup.tsx | 97 ++++++ .../src/getFixScaleEleTransPosition.ts | 61 ++++ .../vc-image/src/hooks/useFrameSetState.ts | 31 ++ components/vc-util/Dom/css.ts | 115 +++++++ tests/__snapshots__/index.test.js.snap | 1 + 23 files changed, 1521 insertions(+), 2 deletions(-) create mode 100644 components/image/PreviewGroup.tsx create mode 100644 components/image/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/image/__tests__/demo.test.js create mode 100644 components/image/__tests__/index.test.js create mode 100644 components/image/index.tsx create mode 100644 components/image/style/index.less create mode 100644 components/image/style/index.ts create mode 100644 components/style/mixins/box.less create mode 100644 components/style/mixins/modal-mask.less create mode 100644 components/vc-image/assets/index.less create mode 100644 components/vc-image/index.ts create mode 100644 components/vc-image/src/Image.tsx create mode 100644 components/vc-image/src/Preview.tsx create mode 100644 components/vc-image/src/PreviewGroup.tsx create mode 100644 components/vc-image/src/getFixScaleEleTransPosition.ts create mode 100644 components/vc-image/src/hooks/useFrameSetState.ts create mode 100644 components/vc-util/Dom/css.ts diff --git a/antdv-demo b/antdv-demo index ab88ac19d..db458a227 160000 --- a/antdv-demo +++ b/antdv-demo @@ -1 +1 @@ -Subproject commit ab88ac19de8ce0d3c8a559ad7bcd5fc04532c184 +Subproject commit db458a2276cd9156a7824f4e876de5702efd9ff7 diff --git a/components/image/PreviewGroup.tsx b/components/image/PreviewGroup.tsx new file mode 100644 index 000000000..cb6af56d3 --- /dev/null +++ b/components/image/PreviewGroup.tsx @@ -0,0 +1,25 @@ +import PreviewGroup from '../vc-image/src/PreviewGroup'; +import { defineComponent, inject } from 'vue'; +import { defaultConfigProvider } from '../config-provider'; +import PropTypes from '../_util/vue-types'; + +const InternalPreviewGroup = defineComponent({ + name: 'AImagePreviewGroup', + inheritAttrs: false, + props: { previewPrefixCls: PropTypes.string }, + setup(props, { attrs, slots }) { + const configProvider = inject('configProvider', defaultConfigProvider); + return () => { + const { getPrefixCls } = configProvider; + const prefixCls = getPrefixCls('image-preview', props.previewPrefixCls); + return ( + + ); + }; + }, +}); +export default InternalPreviewGroup; diff --git a/components/image/__tests__/__snapshots__/demo.test.js.snap b/components/image/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..e69de29bb diff --git a/components/image/__tests__/demo.test.js b/components/image/__tests__/demo.test.js new file mode 100644 index 000000000..639111d52 --- /dev/null +++ b/components/image/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('image'); diff --git a/components/image/__tests__/index.test.js b/components/image/__tests__/index.test.js new file mode 100644 index 000000000..cc176de39 --- /dev/null +++ b/components/image/__tests__/index.test.js @@ -0,0 +1,32 @@ +import Image from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import { mount } from '@vue/test-utils'; +describe('Image', () => { + mountTest(Image); + it('image size', () => { + const wrapper = mount({ + render() { + return ( + + ); + }, + }); + expect(wrapper.find('.ant-image').element.style.width).toBe('200px'); + }); + it('image size number', () => { + const wrapper = mount({ + render() { + return ( + + ); + }, + }); + expect(wrapper.find('.ant-image').element.style.width).toBe('200px'); + }); +}); diff --git a/components/image/index.tsx b/components/image/index.tsx new file mode 100644 index 000000000..b9110c8a8 --- /dev/null +++ b/components/image/index.tsx @@ -0,0 +1,35 @@ +import { App, defineComponent, inject, Plugin } from 'vue'; +import { defaultConfigProvider } from '../config-provider'; +import ImageInternal from '../vc-image'; +import { ImageProps, ImagePropsType } from '../vc-image/src/Image'; + +import PreviewGroup from './PreviewGroup'; +const Image = defineComponent({ + name: 'AImage', + inheritAttrs: false, + props: ImageProps, + setup(props, ctx) { + const { slots, attrs } = ctx; + const configProvider = inject('configProvider', defaultConfigProvider); + return () => { + const { getPrefixCls } = configProvider; + const prefixCls = getPrefixCls('image', props.prefixCls); + return ; + }; + }, +}); + +export { ImageProps, ImagePropsType }; + +Image.PreviewGroup = PreviewGroup; + +Image.install = function(app: App) { + app.component(Image.name, Image); + app.component(Image.PreviewGroup.name, Image.PreviewGroup); + return app; +}; + +export default Image as typeof Image & + Plugin & { + readonly PreviewGroup: typeof PreviewGroup; + }; diff --git a/components/image/style/index.less b/components/image/style/index.less new file mode 100644 index 000000000..0cc1a13c9 --- /dev/null +++ b/components/image/style/index.less @@ -0,0 +1,141 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@image-prefix-cls: ~'@{ant-prefix}-image'; +@image-preview-prefix-cls: ~'@{image-prefix-cls}-preview'; + +.@{image-prefix-cls} { + position: relative; + display: inline-block; + &-img { + width: 100%; + height: auto; + &-placeholder { + background-color: @image-bg; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTQuNSAyLjVoLTEzQS41LjUgMCAwIDAgMSAzdjEwYS41LjUgMCAwIDAgLjUuNWgxM2EuNS41IDAgMCAwIC41LS41VjNhLjUuNSAwIDAgMC0uNS0uNXpNNS4yODEgNC43NWExIDEgMCAwIDEgMCAyIDEgMSAwIDAgMSAwLTJ6bTguMDMgNi44M2EuMTI3LjEyNyAwIDAgMS0uMDgxLjAzSDIuNzY5YS4xMjUuMTI1IDAgMCAxLS4wOTYtLjIwN2wyLjY2MS0zLjE1NmEuMTI2LjEyNiAwIDAgMSAuMTc3LS4wMTZsLjAxNi4wMTZMNy4wOCAxMC4wOWwyLjQ3LTIuOTNhLjEyNi4xMjYgMCAwIDEgLjE3Ny0uMDE2bC4wMTUuMDE2IDMuNTg4IDQuMjQ0YS4xMjcuMTI3IDAgMCAxLS4wMi4xNzV6IiBmaWxsPSIjOEM4QzhDIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=); + background-repeat: no-repeat; + background-position: center center; + background-size: 30%; + } + } + + &-placeholder { + .box; + } + + &-preview { + .modal-mask; + + height: 100%; + text-align: center; + + &-body { + .box; + overflow: hidden; + } + + &-img { + max-width: 100%; + max-height: 100%; + vertical-align: middle; + transform: scale3d(1, 1, 1); + cursor: grab; + transition: transform 0.3s @ease-out 0s; + user-select: none; + pointer-events: auto; + &-wrapper { + .box; + transition: transform 0.3s @ease-out 0s; + &::before { + display: inline-block; + width: 1px; + height: 50%; + margin-right: -1px; + content: ''; + } + } + } + + &-moving { + .@{image-prefix-cls}-preview-img { + cursor: grabbing; + &-wrapper { + transition-duration: 0s; + } + } + } + + &-wrap { + z-index: @zindex-image; + } + + &-operations { + .reset-component; + position: absolute; + top: 0; + right: 0; + z-index: 1; + display: flex; + flex-direction: row-reverse; + align-items: center; + width: 100%; + color: @image-preview-operation-color; + list-style: none; + background: fade(@modal-mask-bg, 10%); + pointer-events: auto; + + &-operation { + margin-left: @control-padding-horizontal; + padding: @control-padding-horizontal; + cursor: pointer; + &-disabled { + color: @image-preview-operation-disabled-color; + pointer-events: none; + } + &:last-of-type { + margin-left: 0; + } + } + &-icon { + font-size: @image-preview-operation-size; + } + } + + &-switch-left, + &-switch-right { + position: absolute; + top: 50%; + right: 10px; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + margin-top: -22px; + color: @image-preview-operation-color; + background: fade(@modal-mask-bg, 10%); + border-radius: 50%; + cursor: pointer; + pointer-events: auto; + &-disabled { + color: @image-preview-operation-disabled-color; + cursor: not-allowed; + > .anticon { + cursor: not-allowed; + } + } + > .anticon { + font-size: 18px; + } + } + + &-switch-left { + left: 10px; + } + + &-switch-right { + right: 10px; + } + } +} diff --git a/components/image/style/index.ts b/components/image/style/index.ts new file mode 100644 index 000000000..3a3ab0de5 --- /dev/null +++ b/components/image/style/index.ts @@ -0,0 +1,2 @@ +import '../../style/index.less'; +import './index.less'; diff --git a/components/index.ts b/components/index.ts index f6692751e..aeae715ff 100644 --- a/components/index.ts +++ b/components/index.ts @@ -133,7 +133,7 @@ import { default as Drawer } from './drawer'; import { default as Skeleton } from './skeleton'; import { default as Comment } from './comment'; - +import { default as Image } from './image'; // import { default as ColorPicker } from './color-picker'; import { default as ConfigProvider } from './config-provider'; @@ -209,6 +209,7 @@ const components = [ Descriptions, PageHeader, Space, + Image, ]; const install = function(app: App) { @@ -296,6 +297,7 @@ export { Descriptions, PageHeader, Space, + Image, }; export default { diff --git a/components/style.ts b/components/style.ts index a99f1e085..aa8db9c86 100644 --- a/components/style.ts +++ b/components/style.ts @@ -60,4 +60,5 @@ import './descriptions/style'; import './page-header/style'; import './form/style'; import './space/style'; +import './image/style'; // import './color-picker/style'; diff --git a/components/style/mixins/box.less b/components/style/mixins/box.less new file mode 100644 index 000000000..4bd3ffad7 --- /dev/null +++ b/components/style/mixins/box.less @@ -0,0 +1,7 @@ +.box(@position: absolute) { + position: @position; + top: 0; + right: 0; + bottom: 0; + left: 0; +} diff --git a/components/style/mixins/index.less b/components/style/mixins/index.less index 16f9211f5..802d774c9 100644 --- a/components/style/mixins/index.less +++ b/components/style/mixins/index.less @@ -8,3 +8,5 @@ @import 'reset'; @import 'operation-unit'; @import 'typography'; +@import 'box'; +@import 'modal-mask'; diff --git a/components/style/mixins/modal-mask.less b/components/style/mixins/modal-mask.less new file mode 100644 index 000000000..581cf3113 --- /dev/null +++ b/components/style/mixins/modal-mask.less @@ -0,0 +1,31 @@ +@import 'box'; + +.modal-mask() { + pointer-events: none; + + &.zoom-enter, + &.zoom-appear { + transform: none; // reset scale avoid mousePosition bug + opacity: 0; + animation-duration: @animation-duration-slow; + user-select: none; // https://github.com/ant-design/ant-design/issues/11777 + } + + &-mask { + .box(fixed); + z-index: @zindex-modal-mask; + height: 100%; + background-color: @modal-mask-bg; + + &-hidden { + display: none; + } + } + + &-wrap { + .box(fixed); + overflow: auto; + outline: 0; + -webkit-overflow-scrolling: touch; + } +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 487c45252..e9c3d5133 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -288,6 +288,7 @@ @zindex-dropdown: 1050; @zindex-picker: 1050; @zindex-tooltip: 1060; +@zindex-image: 1080; // Animation @animation-duration-slow: 0.3s; // Modal @@ -720,3 +721,13 @@ @typography-title-font-weight: 600; @typography-title-margin-top: 1.2em; @typography-title-margin-bottom: 0.5em; + +// Image +// --- +@image-size-base: 48px; +@image-font-size-base: 24px; +@image-bg: #f5f5f5; +@image-color: #fff; +@image-preview-operation-size: 18px; +@image-preview-operation-color: @text-color-dark; +@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 45%); diff --git a/components/vc-image/assets/index.less b/components/vc-image/assets/index.less new file mode 100644 index 000000000..baaf5b626 --- /dev/null +++ b/components/vc-image/assets/index.less @@ -0,0 +1,313 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@prefixCls: ~'@{ant-prefix}-image'; +@zindex-preview-mask: 1000; +@preview-mask-bg: fade(#000, 40%); +@text-color: #bbb; +@text-color-disabled: darken(@text-color, 30%); +@background-color: #f3f3f3; + +.reset() { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.box() { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.@{prefixCls} { + display: inline-block; + position: relative; + &-img { + width: 100%; + height: auto; + &-placeholder { + background-color: @background-color; + background-repeat: no-repeat; + background-position: center center; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjhweCIgaGVpZ2h0PSIyMnB4IiB2aWV3Qm94PSIwIDAgMjggMjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDU1LjIgKDc4MTgxKSAtIGh0dHBzOi8vc2tldGNoYXBwLmNvbSAtLT4KICAgIDx0aXRsZT5pbWFnZS1maWxs5aSH5Lu9PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9Iuafpeeci+WbvueJh+S8mOWMljQuMCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IuWKoOi9veWbvueJhyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTU3Mi4wMDAwMDAsIC01MDYuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJpbWFnZS1maWxs5aSH5Lu9IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1NzAuMDAwMDAwLCA1MDEuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjMDAwMDAwIiBvcGFjaXR5PSIwIiB4PSIwIiB5PSIwIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjwvcmVjdD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOSw1IEwzLDUgQzIuNDQ2ODc1LDUgMiw1LjQ0Njg3NSAyLDYgTDIsMjYgQzIsMjYuNTUzMTI1IDIuNDQ2ODc1LDI3IDMsMjcgTDI5LDI3IEMyOS41NTMxMjUsMjcgMzAsMjYuNTUzMTI1IDMwLDI2IEwzMCw2IEMzMCw1LjQ0Njg3NSAyOS41NTMxMjUsNSAyOSw1IFogTTEwLjU2MjUsOS41IEMxMS42NjU2MjUsOS41IDEyLjU2MjUsMTAuMzk2ODc1IDEyLjU2MjUsMTEuNSBDMTIuNTYyNSwxMi42MDMxMjUgMTEuNjY1NjI1LDEzLjUgMTAuNTYyNSwxMy41IEM5LjQ1OTM3NSwxMy41IDguNTYyNSwxMi42MDMxMjUgOC41NjI1LDExLjUgQzguNTYyNSwxMC4zOTY4NzUgOS40NTkzNzUsOS41IDEwLjU2MjUsOS41IFogTTI2LjYyMTg3NSwyMy4xNTkzNzUgQzI2LjU3ODEyNSwyMy4xOTY4NzUgMjYuNTE4NzUsMjMuMjE4NzUgMjYuNDU5Mzc1LDIzLjIxODc1IEw1LjUzNzUsMjMuMjE4NzUgQzUuNCwyMy4yMTg3NSA1LjI4NzUsMjMuMTA2MjUgNS4yODc1LDIyLjk2ODc1IEM1LjI4NzUsMjIuOTA5Mzc1IDUuMzA5Mzc1LDIyLjg1MzEyNSA1LjM0Njg3NSwyMi44MDYyNSBMMTAuNjY4NzUsMTYuNDkzNzUgQzEwLjc1NjI1LDE2LjM4NzUgMTAuOTE1NjI1LDE2LjM3NSAxMS4wMjE4NzUsMTYuNDYyNSBDMTEuMDMxMjUsMTYuNDcxODc1IDExLjA0Mzc1LDE2LjQ4MTI1IDExLjA1MzEyNSwxNi40OTM3NSBMMTQuMTU5Mzc1LDIwLjE4MTI1IEwxOS4xLDE0LjMyMTg3NSBDMTkuMTg3NSwxNC4yMTU2MjUgMTkuMzQ2ODc1LDE0LjIwMzEyNSAxOS40NTMxMjUsMTQuMjkwNjI1IEMxOS40NjI1LDE0LjMgMTkuNDc1LDE0LjMwOTM3NSAxOS40ODQzNzUsMTQuMzIxODc1IEwyNi42NTkzNzUsMjIuODA5Mzc1IEMyNi43NDA2MjUsMjIuOTEyNSAyNi43MjgxMjUsMjMuMDcxODc1IDI2LjYyMTg3NSwyMy4xNTkzNzUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRThFOEU4Ij48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==); + } + } + + &-placeholder { + .box; + } + + &-preview { + text-align: center; + height: 100%; + pointer-events: none; + + &-body { + .box; + overflow: hidden; + } + + &.zoom-enter, + &.zoom-appear { + transform: none; + opacity: 0; + animation-duration: 0.3s; + } + + &-mask { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-preview-mask; + height: 100%; + background-color: @preview-mask-bg; + filter: ~'alpha(opacity=50)'; + + &-hidden { + display: none; + } + } + + &-img { + cursor: grab; + transform: scale3d(1, 1, 1); + transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; + user-select: none; + vertical-align: middle; + max-width: 100%; + max-height: 100%; + pointer-events: auto; + &-wrapper { + .box; + transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; + &::before { + content: ''; + display: inline-block; + height: 50%; + width: 1px; + margin-right: -1px; + } + } + } + + &-moving { + .@{prefixCls}-preview-img { + cursor: grabbing; + &-wrapper { + transition-duration: 0s; + } + } + } + + &-wrap { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-preview-mask; + overflow: auto; + outline: 0; + -webkit-overflow-scrolling: touch; + } + + &-operations { + .reset; + pointer-events: auto; + list-style: none; + position: absolute; + display: flex; + top: 0; + right: 0; + width: 100%; + align-items: center; + flex-direction: row-reverse; + z-index: 1; + color: @text-color; + background: fade(@preview-mask-bg, 45%); + + &-operation { + padding: 10px; + cursor: pointer; + margin-left: 10px; + &-disabled { + pointer-events: none; + color: @text-color-disabled; + } + &:last-of-type { + margin-left: 0; + } + } + &-icon { + font-size: 18px; + } + } + + &-switch-left { + position: absolute; + left: 10px; + top: 50%; + width: 44px; + height: 44px; + margin-top: -22px; + background: fade(@text-color, 45%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + pointer-events: auto; + color: @text-color; + &-disabled { + background: fade(@text-color, 30%); + color: @text-color-disabled; + cursor: not-allowed; + > .anticon { + cursor: not-allowed; + } + } + > .anticon { + font-size: 24px; + } + } + + &-switch-right { + position: absolute; + right: 10px; + top: 50%; + width: 44px; + height: 44px; + margin-top: -22px; + background: fade(@text-color, 45%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + pointer-events: auto; + color: @text-color; + &-disabled { + background: fade(@text-color, 20%); + color: @text-color-disabled; + cursor: not-allowed; + > .anticon { + cursor: not-allowed; + } + } + > .anticon { + font-size: 24px; + } + } + } +} + +.fade-enter, +.fade-appear { + animation-duration: 0.2s; + animation-fill-mode: both; + animation-play-state: paused; + opacity: 0; + animation-timing-function: linear; +} +.fade-leave { + animation-duration: 0.2s; + animation-fill-mode: both; + animation-play-state: paused; + animation-timing-function: linear; +} +.fade-enter.fade-enter-active, +.fade-appear.fade-appear-active { + animation-name: rcImageFadeIn; + animation-play-state: running; +} +.fade-leave.fade-leave-active { + animation-name: rcImageFadeOut; + animation-play-state: running; + pointer-events: none; +} + +@keyframes rcImageFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes rcImageFadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.zoom-enter, +.zoom-appear { + -webkit-animation-duration: 0.2s; + animation-duration: 0.2s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation-play-state: paused; + animation-play-state: paused; + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; + -webkit-animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); +} +.zoom-leave { + -webkit-animation-duration: 0.2s; + animation-duration: 0.2s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation-play-state: paused; + animation-play-state: paused; + -webkit-animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); + animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); +} +.zoom-enter.zoom-enter-active, +.zoom-appear.zoom-appear-active { + -webkit-animation-name: rcImageZoomIn; + animation-name: rcImageZoomIn; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.zoom-leave.zoom-leave-active { + -webkit-animation-name: rcImageZoomOut; + animation-name: rcImageZoomOut; + -webkit-animation-play-state: running; + animation-play-state: running; + pointer-events: none; +} + +@keyframes rcImageZoomIn { + 0% { + -webkit-transform: scale(0.2); + transform: scale(0.2); + opacity: 0; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + } +} +@keyframes rcImageZoomOut { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + 100% { + -webkit-transform: scale(0.2); + transform: scale(0.2); + opacity: 0; + } +} diff --git a/components/vc-image/index.ts b/components/vc-image/index.ts new file mode 100644 index 000000000..c8f3c5f39 --- /dev/null +++ b/components/vc-image/index.ts @@ -0,0 +1,4 @@ +// based on rc-image 4.3.2 +import Image from './src/Image'; +export * from './src/Image'; +export default Image; diff --git a/components/vc-image/src/Image.tsx b/components/vc-image/src/Image.tsx new file mode 100644 index 000000000..dd4ce4334 --- /dev/null +++ b/components/vc-image/src/Image.tsx @@ -0,0 +1,291 @@ +import { + ImgHTMLAttributes, + CSSProperties, + ref, + watch, + defineComponent, + computed, + onMounted, +} from 'vue'; +import { isNumber } from 'lodash-es'; + +import BaseMixin from '../../_util/BaseMixin'; +import cn from '../../_util/classNames'; +import PropTypes from '../../_util/vue-types'; +import { getOffset } from '../../vc-util/Dom/css'; + +import Preview, { MouseEventHandler } from './Preview'; + +import PreviewGroup, { context } from './PreviewGroup'; + +export type GetContainer = string | HTMLElement | (() => HTMLElement); +export interface ImagePreviewType { + visible?: boolean; + onVisibleChange?: (value: boolean, prevValue: boolean) => void; + getContainer?: GetContainer | false; +} + +export interface ImagePropsType extends Omit { + // Original + src?: string; + wrapperClassName?: string; + wrapperStyle?: CSSProperties; + prefixCls?: string; + previewPrefixCls?: string; + placeholder?: boolean; + fallback?: string; + preview?: boolean | ImagePreviewType; +} +export const ImageProps = { + src: PropTypes.string, + wrapperClassName: PropTypes.string, + wrapperStyle: PropTypes.style, + prefixCls: PropTypes.string, + previewPrefixCls: PropTypes.string, + placeholder: PropTypes.VNodeChild, + fallback: PropTypes.string, + preview: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.shape({})]).def( + true, + ), +}; +type ImageStatus = 'normal' | 'error' | 'loading'; + +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({ + name: 'Image', + mixins: [BaseMixin], + inheritAttrs: false, + props: ImageProps, + emits: ['click'], + 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 isCustomPlaceholder = computed( + () => (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; + }); + watch(isShowPreview, (val, preVal) => { + onPreviewVisibleChange.value(val, preVal); + }); + 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 = () => { + status.value = 'error'; + }; + + 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 { + isShowPreview.value = true; + } + emit('click', e); + }; + + const onPreviewClose = () => { + isShowPreview.value = 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( + [() => props.src, canPreview], + () => { + unRegister(); + if (!isPreviewGroup.value) { + return () => {}; + } + + unRegister = registerImage(currentId.value, props.src); + + if (!canPreview.value) { + unRegister(); + } + }, + { flush: 'post', immediate: true }, + ); + }); + const toSizePx = (l: number | string) => { + if (isNumber(l)) return l + 'px'; + return l; + }; + return () => { + const { + prefixCls, + wrapperClassName, + fallback, + src, + preview, + placeholder, + wrapperStyle, + } = props; + const { + width, + height, + crossorigin, + decoding, + alt, + sizes, + srcset, + usemap, + class: cls, + style, + } = attrs as ImgHTMLAttributes; + const wrappperClass = cn(prefixCls, wrapperClassName, { + [`${prefixCls}-error`]: isError.value, + }); + const mergedSrc = isError.value && fallback ? fallback : src; + const previewMask = slots.previewMask && slots.previewMask(); + const imgCommonProps = { + crossorigin, + decoding, + alt, + sizes, + srcset, + usemap, + class: cn( + `${prefixCls}-img`, + { + [`${prefixCls}-img-placeholder`]: placeholder === true, + }, + cls, + ), + style: { + height, + ...(style as CSSProperties), + }, + }; + return ( + <> +
{ + emit('click', e); + } + } + style={{ + width: toSizePx(width), + height: toSizePx(height), + ...wrapperStyle, + }} + > + + + {status.value === 'loading' && ( + + )} + {/* Preview Click Mask */} + {previewMask && canPreview.value && ( +
{previewMask}
+ )} +
+ {!isPreviewGroup.value && canPreview.value && ( + + )} + + ); + }; + }, +}); +ImageInternal.PreviewGroup = PreviewGroup; + +export default ImageInternal as typeof ImageInternal & { + readonly PreviewGroup: typeof PreviewGroup; +}; diff --git a/components/vc-image/src/Preview.tsx b/components/vc-image/src/Preview.tsx new file mode 100644 index 000000000..5996d555c --- /dev/null +++ b/components/vc-image/src/Preview.tsx @@ -0,0 +1,314 @@ +import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'; +import { + RotateLeftOutlined, + RotateRightOutlined, + ZoomInOutlined, + ZoomOutOutlined, + CloseOutlined, + LeftOutlined, + RightOutlined, +} from '@ant-design/icons-vue'; + +import classnames from '../../_util/classNames'; +import PropTypes from '../../_util/vue-types'; +import Dialog from '../../vc-dialog'; +import getIDialogPropTypes 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 { context } from './PreviewGroup'; + +const IDialogPropTypes = getIDialogPropTypes(); +export type MouseEventHandler = (payload: MouseEvent) => void; + +export interface PreviewProps extends Omit { + onClose?: (e: Element) => void; + src?: string; + alt?: string; +} + +const initialPosition = { + x: 0, + y: 0, +}; +const PreviewType = { + src: PropTypes.string, + alt: PropTypes.string, + ...IDialogPropTypes, +}; +const Preview = defineComponent({ + name: 'Preview', + inheritAttrs: false, + props: PreviewType, + emits: ['close', 'afterClose'], + setup(props, { emit, attrs }) { + const scale = ref(1); + const rotate = ref(0); + const [position, setPosition] = useFrameSetState<{ + x: number; + y: number; + }>(initialPosition); + + const onClose = () => emit('close'); + const imgRef = ref(); + const originPositionRef = reactive<{ + originX: number; + originY: number; + deltaX: number; + deltaY: number; + }>({ + originX: 0, + originY: 0, + deltaX: 0, + deltaY: 0, + }); + 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 showLeftOrRightSwitches = computed( + () => isPreviewGroup.value && previewGroupCount.value > 1, + ); + + const onAfterClose = () => { + scale.value = 1; + rotate.value = 0; + setPosition(initialPosition); + }; + + const onZoomIn = () => { + scale.value++; + setPosition(initialPosition); + }; + const onZoomOut = () => { + if (scale.value > 1) { + scale.value--; + } + setPosition(initialPosition); + }; + + const onRotateRight = () => { + rotate.value += 90; + }; + + const onRotateLeft = () => { + rotate.value -= 90; + }; + const onSwitchLeft: MouseEventHandler = event => { + event.preventDefault(); + // Without this mask close will abnormal + event.stopPropagation(); + if (currentPreviewIndex.value > 0) { + setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value - 1)]); + } + }; + + const onSwitchRight: MouseEventHandler = event => { + event.preventDefault(); + // Without this mask close will abnormal + event.stopPropagation(); + if (currentPreviewIndex.value < previewGroupCount.value - 1) { + setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value + 1)]); + } + }; + + const wrapClassName = classnames({ + [`${props.prefixCls}-moving`]: isMoving.value, + }); + const toolClassName = `${props.prefixCls}-operations-operation`; + const iconClassName = `${props.prefixCls}-operations-icon`; + const tools = [ + { + icon: CloseOutlined, + onClick: onClose, + type: 'close', + }, + { + icon: ZoomInOutlined, + onClick: onZoomIn, + type: 'zoomIn', + }, + { + icon: ZoomOutOutlined, + onClick: onZoomOut, + type: 'zoomOut', + disabled: computed(() => scale.value === 1), + }, + { + icon: RotateRightOutlined, + onClick: onRotateRight, + type: 'rotateRight', + }, + { + icon: RotateLeftOutlined, + onClick: onRotateLeft, + type: 'rotateLeft', + }, + ]; + + const onMouseUp: MouseEventHandler = () => { + if (props.visible && isMoving.value) { + const width = imgRef.value.offsetWidth * scale.value; + const height = imgRef.value.offsetHeight * scale.value; + const { left, top } = getOffset(imgRef.value); + const isRotate = rotate.value % 180 !== 0; + + isMoving.value = false; + + const fixState = getFixScaleEleTransPosition( + isRotate ? height : width, + isRotate ? width : height, + left, + top, + ); + if (fixState) { + setPosition({ ...fixState }); + } + } + }; + + const onMouseDown: MouseEventHandler = event => { + event.preventDefault(); + // Without this mask close will abnormal + event.stopPropagation(); + originPositionRef.deltaX = event.pageX - position.x; + originPositionRef.deltaY = event.pageY - position.y; + originPositionRef.originX = position.x; + originPositionRef.originY = position.y; + isMoving.value = true; + }; + + const onMouseMove: MouseEventHandler = event => { + if (props.visible && isMoving.value) { + setPosition({ + x: event.pageX - originPositionRef.deltaX, + y: event.pageY - originPositionRef.deltaY, + }); + } + }; + let removeListeners = () => {}; + onMounted(() => { + watch( + [() => props.visible, isMoving], + () => { + removeListeners(); + let onTopMouseUpListener: { remove: any }; + let onTopMouseMoveListener: { remove: any }; + + const onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false); + const onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false); + + try { + // Resolve if in iframe lost event + /* istanbul ignore next */ + if (window.top !== window.self) { + onTopMouseUpListener = addEventListener(window.top, 'mouseup', onMouseUp, false); + onTopMouseMoveListener = addEventListener( + window.top, + 'mousemove', + onMouseMove, + false, + ); + } + } catch (error) { + /* istanbul ignore next */ + warning(false, `[vc-image] ${error}`); + } + + removeListeners = () => { + onMouseUpListener.remove(); + onMouseMoveListener.remove(); + + /* istanbul ignore next */ + if (onTopMouseUpListener) onTopMouseUpListener.remove(); + /* istanbul ignore next */ + if (onTopMouseMoveListener) onTopMouseMoveListener.remove(); + }; + }, + { flush: 'post', immediate: true }, + ); + }); + onUnmounted(() => { + removeListeners(); + }); + + return () => ( + +
    + {tools.map(({ icon: IconType, onClick, type, disabled }) => ( +
  • + +
  • + ))} +
+
+ {props.alt} +
+ {showLeftOrRightSwitches.value && ( +
+ +
+ )} + {showLeftOrRightSwitches.value && ( +
= previewGroupCount.value - 1, + })} + onClick={onSwitchRight} + > + +
+ )} +
+ ); + }, +}); + +export default Preview; diff --git a/components/vc-image/src/PreviewGroup.tsx b/components/vc-image/src/PreviewGroup.tsx new file mode 100644 index 000000000..229ec8988 --- /dev/null +++ b/components/vc-image/src/PreviewGroup.tsx @@ -0,0 +1,97 @@ +import { ref, provide, defineComponent, inject, Ref, reactive } from 'vue'; +import Preview from './Preview'; + +export interface GroupConsumerProps { + previewPrefixCls?: string; +} +export interface GroupConsumerValue extends GroupConsumerProps { + isPreviewGroup?: Ref; + previewUrls: Record; + setPreviewUrls: (previewUrls: Record) => void; + current: Ref; + setCurrent: (current: number) => void; + setShowPreview: (isShowPreview: boolean) => void; + setMousePosition: (mousePosition: null | { x: number; y: number }) => void; + registerImage: (id: number, url: string) => () => void; +} +const previewGroupContext = Symbol('previewGroupContext'); +export const context = { + provide: (val: GroupConsumerValue) => { + provide(previewGroupContext, val); + }, + inject: () => { + return inject(previewGroupContext, { + isPreviewGroup: ref(false), + previewUrls: reactive({}), + setPreviewUrls: () => {}, + current: ref(null), + setCurrent: () => {}, + setShowPreview: () => {}, + setMousePosition: () => {}, + registerImage: null, + }); + }, +}; + +const Group = defineComponent({ + name: 'PreviewGroup', + inheritAttrs: false, + props: { previewPrefixCls: String }, + setup(props, { slots }) { + const previewUrls = reactive>({}); + const current = ref(); + const isShowPreview = ref(false); + const mousePosition = ref<{ x: number; y: number }>(null); + const setPreviewUrls = (val: Record) => { + Object.assign(previewUrls, val); + }; + const setCurrent = (val: number) => { + current.value = val; + }; + const setMousePosition = (val: null | { x: number; y: number }) => { + mousePosition.value = val; + }; + const setShowPreview = (val: boolean) => { + isShowPreview.value = val; + }; + const registerImage = (id: number, url: string) => { + previewUrls[id] = url; + + return () => { + delete previewUrls[id]; + }; + }; + const onPreviewClose = (e: any) => { + e?.stopPropagation(); + isShowPreview.value = false; + mousePosition.value = null; + }; + context.provide({ + isPreviewGroup: ref(true), + previewUrls, + setPreviewUrls, + current, + setCurrent, + setShowPreview, + setMousePosition, + registerImage, + }); + return () => { + return ( + <> + {slots.default && slots.default()} + + + ); + }; + }, +}); + +export default Group; diff --git a/components/vc-image/src/getFixScaleEleTransPosition.ts b/components/vc-image/src/getFixScaleEleTransPosition.ts new file mode 100644 index 000000000..5ba8e2cb1 --- /dev/null +++ b/components/vc-image/src/getFixScaleEleTransPosition.ts @@ -0,0 +1,61 @@ +import { getClientSize } from '../../vc-util/Dom/css'; + +function fixPoint(key: 'x' | 'y', start: number, width: number, clientWidth: number) { + const startAddWidth = start + width; + const offsetStart = (width - clientWidth) / 2; + + if (width > clientWidth) { + if (start > 0) { + return { + [key]: offsetStart, + }; + } + if (start < 0 && startAddWidth < clientWidth) { + return { + [key]: -offsetStart, + }; + } + } else if (start < 0 || startAddWidth > clientWidth) { + return { + [key]: start < 0 ? offsetStart : -offsetStart, + }; + } + return {}; +} + +/** + * Fix positon x,y point when + * + * Ele width && height < client + * - Back origin + * + * - Ele width | height > clientWidth | clientHeight + * - left | top > 0 -> Back 0 + * - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight + * + * Regardless of other + */ +export default function getFixScaleEleTransPosition( + width: number, + height: number, + left: number, + top: number, +): null | { x: number; y: number } { + const { width: clientWidth, height: clientHeight } = getClientSize(); + + let fixPos = null; + + if (width <= clientWidth && height <= clientHeight) { + fixPos = { + x: 0, + y: 0, + }; + } else if (width > clientWidth || height > clientHeight) { + fixPos = { + ...fixPoint('x', left, width, clientWidth), + ...fixPoint('y', top, height, clientHeight), + }; + } + + return fixPos; +} diff --git a/components/vc-image/src/hooks/useFrameSetState.ts b/components/vc-image/src/hooks/useFrameSetState.ts new file mode 100644 index 000000000..3ba34a125 --- /dev/null +++ b/components/vc-image/src/hooks/useFrameSetState.ts @@ -0,0 +1,31 @@ +import raf from '../../../_util/raf'; +import { onMounted, reactive, ref } from 'vue'; + +type SetActionType = Partial | ((state: T) => Partial); +export default function useFrameSetState( + initial: T, +): [Record, (newState: SetActionType) => void] { + const frame = ref(null); + const state = reactive({ ...initial }); + const queue = ref[]>([]); + + const setFrameState = (newState: SetActionType) => { + if (frame.value === null) { + queue.value = []; + frame.value = raf(() => { + let memoState: any; + queue.value.forEach((queueState: object) => { + memoState = { ...memoState, ...queueState }; + }); + Object.assign(state, memoState); + frame.value = null; + }); + } + + queue.value.push(newState as any); + }; + onMounted(() => { + frame.value && raf.cancel(frame.value); + }); + return [state, setFrameState]; +} diff --git a/components/vc-util/Dom/css.ts b/components/vc-util/Dom/css.ts new file mode 100644 index 000000000..99de2383b --- /dev/null +++ b/components/vc-util/Dom/css.ts @@ -0,0 +1,115 @@ +const PIXEL_PATTERN = /margin|padding|width|height|max|min|offset/; + +const removePixel = { + left: true, + top: true, +}; +const floatMap = { + cssFloat: 1, + styleFloat: 1, + float: 1, +}; + +function getComputedStyle(node: HTMLElement) { + return node.nodeType === 1 ? node.ownerDocument.defaultView.getComputedStyle(node, null) : {}; +} + +function getStyleValue(node: HTMLElement, type: string, value: string) { + type = type.toLowerCase(); + if (value === 'auto') { + if (type === 'height') { + return node.offsetHeight; + } + if (type === 'width') { + return node.offsetWidth; + } + } + if (!(type in removePixel)) { + removePixel[type] = PIXEL_PATTERN.test(type); + } + return removePixel[type] ? parseFloat(value) || 0 : value; +} + +export function get(node: HTMLElement, name: any) { + const length = arguments.length; + const style = getComputedStyle(node); + + name = floatMap[name] ? ('cssFloat' in node.style ? 'cssFloat' : 'styleFloat') : name; + + return length === 1 ? style : getStyleValue(node, name, style[name] || node.style[name]); +} + +export function set(node: HTMLElement, name: any, value: string | number) { + const length = arguments.length; + name = floatMap[name] ? ('cssFloat' in node.style ? 'cssFloat' : 'styleFloat') : name; + if (length === 3) { + if (typeof value === 'number' && PIXEL_PATTERN.test(name)) { + value = `${value}px`; + } + node.style[name as string] = value; // Number + return value; + } + for (const x in name) { + if (name.hasOwnProperty(x)) { + set(node, x, name[x]); + } + } + return getComputedStyle(node); +} + +export function getOuterWidth(el: HTMLElement) { + if (el === document.body) { + return document.documentElement.clientWidth; + } + return el.offsetWidth; +} + +export function getOuterHeight(el: HTMLElement) { + if (el === document.body) { + return window.innerHeight || document.documentElement.clientHeight; + } + return el.offsetHeight; +} + +export function getDocSize() { + const width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth); + const height = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); + + return { + width, + height, + }; +} + +export function getClientSize() { + const width = document.documentElement.clientWidth; + const height = window.innerHeight || document.documentElement.clientHeight; + return { + width, + height, + }; +} + +export function getScroll() { + return { + scrollLeft: Math.max(document.documentElement.scrollLeft, document.body.scrollLeft), + scrollTop: Math.max(document.documentElement.scrollTop, document.body.scrollTop), + }; +} + +export function getOffset(node: any) { + const box = node.getBoundingClientRect(); + const docElem = document.documentElement; + + // < ie8 不支持 win.pageXOffset, 则使用 docElem.scrollLeft + return { + left: + box.left + + (window.pageXOffset || docElem.scrollLeft) - + (docElem.clientLeft || document.body.clientLeft || 0), + top: + box.top + + (window.pageYOffset || docElem.scrollTop) - + (docElem.clientTop || document.body.clientTop || 0), + }; +} diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index 2ef1ea962..02b2d798f 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -60,6 +60,7 @@ Array [ "Drawer", "Skeleton", "Comment", + "Image", "ConfigProvider", "Empty", "Result",