From 3b8c9d922f8fdaf9e9de66a3ffa2c672b4694252 Mon Sep 17 00:00:00 2001 From: Liuzj <530604689@qq.com> Date: Wed, 2 Mar 2022 15:15:34 +0800 Subject: [PATCH] refactor(comp: image): rebuild imageViewer (#772) --- .../image/__tests__/imageViewer.spec.ts | 88 +++--- packages/components/image/src/ImageViewer.tsx | 135 ++++++++- .../src/component/ImageViewerContent.tsx | 286 ------------------ .../image/src/composables/useOpr.ts | 167 ++++++++++ .../components/image/src/contents/OprIcon.tsx | 36 +++ packages/components/image/src/types.ts | 17 +- 6 files changed, 378 insertions(+), 351 deletions(-) delete mode 100644 packages/components/image/src/component/ImageViewerContent.tsx create mode 100644 packages/components/image/src/composables/useOpr.ts create mode 100644 packages/components/image/src/contents/OprIcon.tsx diff --git a/packages/components/image/__tests__/imageViewer.spec.ts b/packages/components/image/__tests__/imageViewer.spec.ts index 71e987750..70f3c7127 100644 --- a/packages/components/image/__tests__/imageViewer.spec.ts +++ b/packages/components/image/__tests__/imageViewer.spec.ts @@ -1,22 +1,24 @@ import type { ImageViewerProps } from '../src/types' import type { MountingOptions } from '@vue/test-utils' -import { flushPromises, mount } from '@vue/test-utils' +import { DOMWrapper, flushPromises, mount } from '@vue/test-utils' -import { renderWork, wait } from '@tests' +import { isElementVisible, renderWork, wait } from '@tests' import ImageViewer from '../src/ImageViewer' -import ImageViewerContent from '../src/component/ImageViewerContent' describe('ImageViewer', () => { + let bodyWrapper: DOMWrapper + beforeEach(() => { - const el = document.createElement('div') - el.className = 'ix-image-viewer-container' - document.body.appendChild(el) + bodyWrapper = new DOMWrapper(document.body, {}) as DOMWrapper }) afterEach(() => { - ;(document.querySelector('.ix-image-viewer-container') as HTMLElement).innerHTML = '' + const container = document.querySelector('.ix-image-viewer-container') + if (container) { + container.innerHTML = '' + } }) const ImageViewerMount = (options: MountingOptions> = {}) => { @@ -38,21 +40,21 @@ describe('ImageViewer', () => { const wrapper = ImageViewerMount({ props: { visible: false, 'onUpdate:visible': onUpdateVisible } }) await flushPromises() - expect(wrapper.findComponent(ImageViewerContent).exists()).toBe(false) + expect(isElementVisible(document.querySelector('.ix-image-viewer'))).toBe(false) await wrapper.setProps({ visible: true }) - expect(wrapper.findComponent(ImageViewerContent).exists()).toBe(true) + expect(isElementVisible(document.querySelector('.ix-image-viewer'))).toBe(true) - await wrapper.findComponent(ImageViewerContent).find('img').trigger('click') + await bodyWrapper.find('.ix-image-viewer img').trigger('click') expect(onUpdateVisible).toBeCalledWith(false) - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-close').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-close').trigger('click') expect(onUpdateVisible).toBeCalledWith(false) - await wrapper.findComponent(ImageViewerContent).trigger('keydown.esc') + await bodyWrapper.find('.ix-image-viewer').trigger('keydown.esc') expect(onUpdateVisible).toBeCalledWith(false) }) @@ -65,28 +67,28 @@ describe('ImageViewer', () => { }) await flushPromises() - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('src')).toBe(images[0]) + expect(bodyWrapper.find('.ix-image-viewer img').attributes('src')).toBe(images[0]) await wrapper.setProps({ activeIndex: 1 }) - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('src')).toBe(images[1]) + expect(bodyWrapper.find('.ix-image-viewer img').attributes('src')).toBe(images[1]) - await wrapper.findComponent(ImageViewerContent).trigger('keydown', { code: 'ArrowRight' }) + await bodyWrapper.find('.ix-image-viewer').trigger('keydown', { code: 'ArrowRight' }) await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(2) - await wrapper.findComponent(ImageViewerContent).trigger('keydown', { code: 'ArrowLeft' }) + await bodyWrapper.find('.ix-image-viewer').trigger('keydown', { code: 'ArrowLeft' }) await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(0) - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-left').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-left').trigger('click') await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(0) - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-right').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-right').trigger('click') await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(2) @@ -99,55 +101,55 @@ describe('ImageViewer', () => { }) await flushPromises() - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('src')).toBe(imagesOld[0]) + expect(bodyWrapper.find('.ix-image-viewer img').attributes('src')).toBe(imagesOld[0]) const imageNew = ['/2.png'] await wrapper.setProps({ images: imageNew }) - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('src')).toBe(imageNew[0]) + expect(bodyWrapper.find('.ix-image-viewer img').attributes('src')).toBe(imageNew[0]) }) test('zoom work', async () => { const wrapper = ImageViewerMount({ props: { visible: true } }) await flushPromises() - await wrapper.findComponent(ImageViewerContent).trigger('mousewheel', { wheelDelta: 10 }) + await bodyWrapper.find('.ix-image-viewer').trigger('mousewheel', { wheelDelta: 10 }) await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1.2)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1.2)') - await wrapper.findComponent(ImageViewerContent).trigger('mousewheel', { wheelDelta: -10 }) + await bodyWrapper.find('.ix-image-viewer').trigger('mousewheel', { wheelDelta: -10 }) await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1)') - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-zoom-in').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-zoom-in').trigger('click') await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1.2)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1.2)') - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-zoom-out').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-zoom-out').trigger('click') await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1)') await wrapper.setProps({ zoom: [0.8, 0.9] }) - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(0.9)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(0.9)') - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-zoom-in').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-zoom-in').trigger('click') await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(0.9)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(0.9)') await wrapper.setProps({ zoom: [1.1, 1.2] }) - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1.1)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1.1)') - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-zoom-out').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-zoom-out').trigger('click') await wait(20) // debounce - expect(wrapper.findComponent(ImageViewerContent).find('img').attributes('style')).toMatch('scale(1.1)') + expect(bodyWrapper.find('.ix-image-viewer img').attributes('style')).toMatch('scale(1.1)') }) test('loop work', async () => { @@ -158,24 +160,24 @@ describe('ImageViewer', () => { }) await flushPromises() - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-left').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-left').trigger('click') await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(2) await wrapper.setProps({ activeIndex: 2 }) - await wrapper.findComponent(ImageViewerContent).find('.ix-icon-right').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-icon-right').trigger('click') await wait(20) // debounce expect(onUpdateActiveIndex).toBeCalledWith(0) await wrapper.setProps({ loop: false }) - expect(wrapper.findComponent(ImageViewerContent).find('.ix-icon-right').classes().toString()).toMatch('disabled') + expect(bodyWrapper.find('.ix-image-viewer .ix-icon-right').classes().toString()).toMatch('disabled') await wrapper.setProps({ activeIndex: 0 }) - expect(wrapper.findComponent(ImageViewerContent).find('.ix-icon-left').classes().toString()).toMatch('disabled') + expect(bodyWrapper.find('.ix-image-viewer .ix-icon-left').classes().toString()).toMatch('disabled') }) test('maskClosable work', async () => { @@ -183,13 +185,13 @@ describe('ImageViewer', () => { const wrapper = ImageViewerMount({ props: { visible: true, 'onUpdate:visible': onUpdateVisible }, }) - await wrapper.findComponent(ImageViewerContent).find('img').trigger('click') + await bodyWrapper.find('.ix-image-viewer img').trigger('click') expect(onUpdateVisible).toBeCalledWith(false) onUpdateVisible.mockRestore() await wrapper.setProps({ maskClosable: false }) - await wrapper.findComponent(ImageViewerContent).find('.ix-image-viewer-preview').trigger('click') + await bodyWrapper.find('.ix-image-viewer .ix-image-viewer-preview').trigger('click') expect(onUpdateVisible).not.toBeCalled() }) @@ -198,12 +200,10 @@ describe('ImageViewer', () => { const wrapper = ImageViewerMount({ props: { visible: true } }) await flushPromises() - expect((document.querySelector('.ix-image-viewer-container .ix-image-viewer') as HTMLElement).innerHTML).not.toBe( - '', - ) + expect((document.querySelector('.ix-image-viewer-container .ix-image-viewer') as Element).innerHTML).not.toBe('') await wrapper.setProps({ target: 'image-viewer-container' }) - expect((document.querySelector('.image-viewer-container .ix-image-viewer') as HTMLElement).innerHTML).not.toBe('') + expect((document.querySelector('.image-viewer-container .ix-image-viewer') as Element).innerHTML).not.toBe('') }) }) diff --git a/packages/components/image/src/ImageViewer.tsx b/packages/components/image/src/ImageViewer.tsx index 5c5df5d36..fdb330ac1 100644 --- a/packages/components/image/src/ImageViewer.tsx +++ b/packages/components/image/src/ImageViewer.tsx @@ -5,39 +5,142 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ImageViewerProps } from './types' -import type { ImageViewerConfig } from '@idux/components/config' -import type { ComputedRef } from 'vue' +import type { Ref } from 'vue' -import { Transition, computed, defineComponent } from 'vue' +import { Transition, computed, defineComponent, onBeforeUnmount, onMounted, watch } from 'vue' +import { isFirefox } from '@idux/cdk/platform' import { CdkPortal } from '@idux/cdk/portal' import { useControlledProp } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' -import ImageViewerContent from './component/ImageViewerContent' +import { useImgStyleOpr, useImgSwitch, useOprList } from './composables/useOpr' +import OprIcon from './contents/OprIcon' import { imageViewerProps } from './types' +const mousewheelEventName = isFirefox ? 'DOMMouseScroll' : 'mousewheel' + +type ScaleType = 'in' | 'out' +type GoType = 'previous' | 'next' + export default defineComponent({ name: 'IxImageViewer', props: imageViewerProps, setup(props) { const common = useGlobalConfig('common') const config = useGlobalConfig('imageViewer') - const [visible] = useControlledProp(props, 'visible', false) const mergedPrefixCls = computed(() => `${common.prefixCls}-image-viewer`) - const target = useTarget(props, config, mergedPrefixCls) - - return () => ( - - - {visible.value && } - - + + const [visible, setVisible] = useControlledProp(props, 'visible', false) + + const zoom = computed(() => props.zoom ?? config.zoom) + const loop = computed(() => props.loop ?? config.loop) + const maskClosable = computed(() => props.maskClosable ?? config.maskClosable) + const target = computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-container`) + + const { transformStyle, scaleDisabled, rotateHandle, scaleHandle, resetTransform } = useImgStyleOpr(zoom) + const { activeIndex, switchDisabled, switchVisible, goHandle } = useImgSwitch(props, loop) + + const onClickLayer = () => maskClosable.value && setVisible(false) + + const oprList = useOprList( + goHandle, + rotateHandle, + scaleHandle, + setVisible, + scaleDisabled, + switchDisabled, + switchVisible, ) + + const { onWheelScroll, onKeydown } = getImageEvent(visible, setVisible, scaleHandle, goHandle) + + onMounted(() => { + window.addEventListener(mousewheelEventName, onWheelScroll, { passive: false, capture: false }) + window.addEventListener('keydown', onKeydown, false) + }) + + onBeforeUnmount(() => { + window.removeEventListener(mousewheelEventName, onWheelScroll) + window.removeEventListener('keydown', onKeydown) + }) + + watch([visible, activeIndex], ([visible$$]) => { + visible$$ && resetTransform() + }) + + return () => { + const prefixCls = mergedPrefixCls.value + const curImgSrc = props.images[activeIndex.value] + + return ( + + + {visible.value && ( +
+
+ {oprList.value + .filter(item => item.visible) + .map(item => { + const oprIconProps = { + ...item, + prefixCls, + } + return + })} +
+
+ +
+
+ )} +
+
+ ) + } }, }) -function useTarget(props: ImageViewerProps, config: ImageViewerConfig, mergedPrefixCls: ComputedRef) { - return computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-container`) +function getImageEvent( + visible: Ref, + setVisible: (visible: boolean) => void, + scaleHandle: (direction: ScaleType, step?: number) => void, + goHandle: (direction: GoType) => void, +) { + const onWheelScroll = (e: WheelEvent | Event) => { + if (!visible.value) { + return + } + const event = e as WheelEvent & { wheelDelta?: number } + event.preventDefault() + const delta = event.wheelDelta ?? -event.detail + if (delta > 0) { + scaleHandle('in', 0.2) + } else { + scaleHandle('out', 0.2) + } + } + + const keyHandle: Record void> = { + ArrowUp: () => scaleHandle('in', 0.2), + ArrowDown: () => scaleHandle('out', 0.2), + ArrowLeft: () => goHandle('previous'), + ArrowRight: () => goHandle('next'), + Escape: () => setVisible(false), + } + + const onKeydown = (e: KeyboardEvent) => { + if (!visible.value) { + return + } + e.preventDefault() + if (e.code in keyHandle) { + keyHandle[e.code]() + } + } + + return { + onWheelScroll, + onKeydown, + } } diff --git a/packages/components/image/src/component/ImageViewerContent.tsx b/packages/components/image/src/component/ImageViewerContent.tsx deleted file mode 100644 index fbce58fc2..000000000 --- a/packages/components/image/src/component/ImageViewerContent.tsx +++ /dev/null @@ -1,286 +0,0 @@ -/** - * @license - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE - */ - -import type { ImageViewerContentProps } from '../types' -import type { ImageViewerConfig } from '@idux/components/config' -import type { ComputedRef, Ref } from 'vue' - -import { computed, defineComponent, normalizeClass, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue' - -import { debounce } from 'lodash-es' - -import { isFirefox } from '@idux/cdk/platform' -import { useControlledProp } from '@idux/cdk/utils' -import { useGlobalConfig } from '@idux/components/config' -import { IxIcon } from '@idux/components/icon' - -import { imageViewerContentProps } from '../types' - -const mousewheelEventName = isFirefox ? 'DOMMouseScroll' : 'mousewheel' -const debounceTime = 10 - -type ScaleType = 'in' | 'out' -type RotateType = 'left' | 'right' -type GoType = 'previous' | 'next' - -interface OprType { - icon: string - key: string - opr: () => void - visible: boolean - disabled?: boolean -} - -export default defineComponent({ - name: 'IxImageViewerContent', - props: imageViewerContentProps, - setup(props) { - const config = useGlobalConfig('imageViewer') - const zoom = useZoomRange(props, config) - const maskClosable = useMaskClosable(props, config) - const [visible, setVisible] = useControlledProp(props, 'visible', false) - const { calcTransform, scaleDisabled, rotateHandle, scaleHandle, resetTransform } = useStyleOpr(zoom) - const { activeIndex, switchDisabled, switchVisible, goHandle } = useSwitch(props, config) - const oprList = useOprList( - { - goNext: () => goHandle('next'), - goPrevious: () => goHandle('previous'), - rotateLeft: () => rotateHandle('left'), - rotateRight: () => rotateHandle('right'), - zoomOut: () => scaleHandle('out'), - zoomIn: () => scaleHandle('in'), - close: () => setVisible(false), - }, - scaleDisabled, - switchDisabled, - switchVisible, - ) - const { onWheelScroll, onKeydown } = getImageEvent(visible, { setVisible, scaleHandle, goHandle }) - const onClickLayer = () => maskClosable.value && setVisible(false) - - onMounted(() => { - window.addEventListener(mousewheelEventName, onWheelScroll, { passive: false, capture: false }) - window.addEventListener('keydown', onKeydown, false) - }) - - onBeforeUnmount(() => { - window.removeEventListener(mousewheelEventName, onWheelScroll) - window.removeEventListener('keydown', onKeydown) - }) - - watch([visible, activeIndex], ([visible$$]) => { - visible$$ && resetTransform() - }) - - return () => ( -
- {renderOprNode(props, oprList)} - {renderPreviewImg(props, calcTransform, activeIndex, onClickLayer)} -
- ) - }, -}) - -function renderOprNode(props: ImageViewerContentProps, oprList: ComputedRef) { - return ( -
- {oprList.value - .filter(item => item.visible) - .map(item => { - const iconClasses = computed(() => - normalizeClass([ - `${props.mergedPrefixCls}-opr-item`, - { [`${props.mergedPrefixCls}-opr-item-disabled`]: item.disabled }, - ]), - ) - return - })} -
- ) -} - -function renderPreviewImg( - props: ImageViewerContentProps, - calcTransform: ComputedRef>, - activeIndex: ComputedRef, - onClickLayer: () => void, -) { - const curImgSrc = (props.images ?? [])[activeIndex.value] - return ( -
- -
- ) -} - -function useOprList( - { goNext, goPrevious, rotateLeft, rotateRight, zoomOut, zoomIn, close }: Record void>, - scaleDisabled: ComputedRef>, - switchDisabled: ComputedRef>, - switchVisible: ComputedRef, -): ComputedRef { - return computed(() => [ - { - key: 'goPrevious', - icon: 'left', - opr: goPrevious, - disabled: switchDisabled.value.previous, - visible: switchVisible.value, - }, - { key: 'goNext', icon: 'right', opr: goNext, disabled: switchDisabled.value.next, visible: switchVisible.value }, - { key: 'rotateLeft', icon: 'rotate-left', opr: rotateLeft, visible: true }, - { key: 'rotateRight', icon: 'rotate-right', opr: rotateRight, visible: true }, - { key: 'zoomOut', icon: 'zoom-out', opr: zoomOut, disabled: scaleDisabled.value.out, visible: true }, - { key: 'zoomIn', icon: 'zoom-in', opr: zoomIn, disabled: scaleDisabled.value.in, visible: true }, - { key: 'close', icon: 'close', opr: close, visible: true }, - ]) -} - -function useSwitch(props: ImageViewerContentProps, config: ImageViewerConfig) { - const [activeIndex, setIndex] = useControlledProp(props, 'activeIndex', 0) - const loop = computed(() => props.loop ?? config.loop) - const switchDisabled = computed(() => ({ - previous: !loop.value && activeIndex.value === 0, - next: !loop.value && activeIndex.value === props.images.length - 1, - })) - const switchVisible = computed(() => props.images.length > 1) - const goHandle = debounce((direction: GoType = 'next') => { - if (direction === 'next') { - if (switchDisabled.value.next) { - return - } - setIndex(activeIndex.value >= props.images.length - 1 ? 0 : activeIndex.value + 1) - return - } - if (switchDisabled.value.previous) { - return - } - setIndex(activeIndex.value <= 0 ? props.images.length - 1 : activeIndex.value - 1) - }, debounceTime) - - return { - activeIndex, - switchDisabled, - switchVisible, - goHandle, - } -} - -function useStyleOpr(zoom: ComputedRef) { - const initScale = computed(() => getInitScale(zoom.value)) - const initRotate = 0 - const scale = ref(1) - const rotate = ref(initRotate) - const rotateFactor = { - left: -1, - right: 1, - } as const - const scaleFactor = { - in: 1, - out: -1, - } - - watchEffect(() => (scale.value = initScale.value)) - - const scaleDisabled = computed(() => ({ - in: scale.value >= zoom.value[1], - out: scale.value <= zoom.value[0], - })) - const calcTransform = computed(() => ({ transform: `scale(${scale.value}) rotate(${rotate.value}deg)` })) - - const rotateHandle = debounce((direction: RotateType = 'left', rotateStep = 90) => { - rotate.value = rotate.value + rotateStep * rotateFactor[direction] - }, debounceTime) - - const scaleHandle = debounce((direction: ScaleType, scaleStep = 0.2) => { - if (scaleDisabled.value[direction]) { - return - } - scale.value = scale.value + scaleStep * scaleFactor[direction] - }, debounceTime) - - const resetTransform = () => { - scale.value = initScale.value - rotate.value = initRotate - } - - return { - calcTransform, - scaleDisabled, - rotateHandle, - scaleHandle, - resetTransform, - } -} - -function useZoomRange(props: ImageViewerContentProps, config: ImageViewerConfig) { - return computed(() => props.zoom ?? config.zoom) -} - -function useMaskClosable(props: ImageViewerContentProps, config: ImageViewerConfig) { - return computed(() => props.maskClosable ?? config.maskClosable) -} - -function getImageEvent( - visible: Ref, - { - setVisible, - scaleHandle, - goHandle, - }: { - setVisible: (visible: boolean) => void - scaleHandle: (direction: ScaleType, step?: number) => void - goHandle: (direction: GoType) => void - }, -) { - const scroll = (e: WheelEvent | Event) => { - if (!visible.value) { - return - } - const event = e as WheelEvent & { wheelDelta?: number } - event.preventDefault() - const delta = event.wheelDelta ?? -event.detail - if (delta > 0) { - scaleHandle('in', 0.2) - } else { - scaleHandle('out', 0.2) - } - } - - const keyHandle: Record void> = { - ArrowUp: () => scaleHandle('in', 0.2), - ArrowDown: () => scaleHandle('out', 0.2), - ArrowLeft: () => goHandle('previous'), - ArrowRight: () => goHandle('next'), - Escape: () => setVisible(false), - } - const keyDown = (e: KeyboardEvent) => { - if (!visible.value) { - return - } - e.preventDefault() - if (e.code in keyHandle) { - keyHandle[e.code]() - } - } - - return { - onWheelScroll: scroll, - onKeydown: keyDown, - } -} - -function getInitScale(zoom: number[]) { - const defaultScale = 1 - if (zoom[0] > defaultScale) { - return zoom[0] - } - if (zoom[1] < defaultScale) { - return zoom[1] - } - return defaultScale -} diff --git a/packages/components/image/src/composables/useOpr.ts b/packages/components/image/src/composables/useOpr.ts new file mode 100644 index 000000000..3db07eda9 --- /dev/null +++ b/packages/components/image/src/composables/useOpr.ts @@ -0,0 +1,167 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { GoType, ImageViewerProps, OprType, RotateType, ScaleType } from '../types' +import type { DebouncedFunc } from 'lodash-es' +import type { ComputedRef } from 'vue' + +import { computed, ref, watchEffect } from 'vue' + +import { debounce } from 'lodash-es' + +import { useControlledProp } from '@idux/cdk/utils' + +const debounceTime = 10 +const defaultScale = 1 +const defaultRotate = 0 +const defaultScaleStep = 0.2 +const defaultRotateStep = 90 + +interface ImgStyleOprContext { + transformStyle: ComputedRef<{ + transform: string + }> + scaleDisabled: ComputedRef<{ + in: boolean + out: boolean + }> + rotateHandle: DebouncedFunc<(direction?: RotateType, rotateStep?: number) => void> + scaleHandle: DebouncedFunc<(direction: ScaleType, scaleStep?: number) => void> + resetTransform: () => void +} + +interface ImgSwitchContext { + activeIndex: ComputedRef + switchDisabled: ComputedRef<{ + previous: boolean + next: boolean + }> + switchVisible: ComputedRef + goHandle: DebouncedFunc<(direction?: GoType) => void> +} + +export function useOprList( + goHandle: DebouncedFunc<(direction?: GoType) => void>, + rotateHandle: DebouncedFunc<(direction?: RotateType, rotateStep?: number) => void>, + scaleHandle: DebouncedFunc<(direction: ScaleType, scaleStep?: number) => void>, + setVisible: (value: boolean) => void, + scaleDisabled: ComputedRef>, + switchDisabled: ComputedRef>, + switchVisible: ComputedRef, +): ComputedRef { + const goNext = () => goHandle('next') + const goPrevious = () => goHandle('previous') + const rotateLeft = () => rotateHandle('left') + const rotateRight = () => rotateHandle('right') + const zoomOut = () => scaleHandle('out') + const zoomIn = () => scaleHandle('in') + const close = () => setVisible(false) + + return computed(() => [ + { + key: 'goPrevious', + icon: 'left', + opr: goPrevious, + disabled: switchDisabled.value.previous, + visible: switchVisible.value, + }, + { key: 'goNext', icon: 'right', opr: goNext, disabled: switchDisabled.value.next, visible: switchVisible.value }, + { key: 'rotateLeft', icon: 'rotate-left', opr: rotateLeft, visible: true }, + { key: 'rotateRight', icon: 'rotate-right', opr: rotateRight, visible: true }, + { key: 'zoomOut', icon: 'zoom-out', opr: zoomOut, disabled: scaleDisabled.value.out, visible: true }, + { key: 'zoomIn', icon: 'zoom-in', opr: zoomIn, disabled: scaleDisabled.value.in, visible: true }, + { key: 'close', icon: 'close', opr: close, visible: true }, + ]) +} + +export function useImgStyleOpr(zoom: ComputedRef): ImgStyleOprContext { + const scale = ref(defaultScale) + const rotate = ref(defaultRotate) + const rotateFactor = { + left: -1, + right: 1, + } as const + const scaleFactor = { + in: 1, + out: -1, + } as const + + const initScale = computed(() => { + const [zoomOut, zoomIn] = zoom.value + if (zoomOut > defaultScale) { + return zoomOut + } + if (zoomIn < defaultScale) { + return zoomIn + } + return defaultScale + }) + + watchEffect(() => (scale.value = initScale.value)) + + const scaleDisabled = computed(() => { + const [zoomOut, zoomIn] = zoom.value + + return { + in: scale.value >= zoomIn, + out: scale.value <= zoomOut, + } + }) + + const transformStyle = computed(() => ({ transform: `scale(${scale.value}) rotate(${rotate.value}deg)` })) + + const rotateHandle = debounce((direction: RotateType = 'left', rotateStep: number = defaultRotateStep) => { + rotate.value = rotate.value + rotateStep * rotateFactor[direction] + }, debounceTime) + + const scaleHandle = debounce((direction: ScaleType, scaleStep: number = defaultScaleStep) => { + if (scaleDisabled.value[direction]) { + return + } + scale.value = scale.value + scaleStep * scaleFactor[direction] + }, debounceTime) + + const resetTransform = () => { + scale.value = initScale.value + rotate.value = defaultRotate + } + + return { + transformStyle, + scaleDisabled, + rotateHandle, + scaleHandle, + resetTransform, + } +} + +export function useImgSwitch(props: ImageViewerProps, loop: ComputedRef): ImgSwitchContext { + const [activeIndex, setIndex] = useControlledProp(props, 'activeIndex', 0) + + const switchDisabled = computed(() => ({ + previous: !loop.value && activeIndex.value === 0, + next: !loop.value && activeIndex.value === props.images.length - 1, + })) + const switchVisible = computed(() => props.images.length > 1) + const goHandle = debounce((direction: GoType = 'next') => { + if (direction === 'next') { + if (switchDisabled.value.next || switchDisabled.value.previous) { + return + } + setIndex(activeIndex.value >= props.images.length - 1 ? 0 : activeIndex.value + 1) + return + } + setIndex(activeIndex.value <= 0 ? props.images.length - 1 : activeIndex.value - 1) + }, debounceTime) + + return { + activeIndex, + switchDisabled, + switchVisible, + goHandle, + } +} diff --git a/packages/components/image/src/contents/OprIcon.tsx b/packages/components/image/src/contents/OprIcon.tsx new file mode 100644 index 000000000..a73a69f35 --- /dev/null +++ b/packages/components/image/src/contents/OprIcon.tsx @@ -0,0 +1,36 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { FunctionalComponent } from 'vue' + +import { computed, normalizeClass } from 'vue' + +import { IxIcon } from '@idux/components/icon' +import { useKey } from '@idux/components/utils' + +interface OprIconProps { + disabled: boolean + icon: string + prefixCls: string + opr: () => void +} + +const OprIcon: FunctionalComponent = props => { + const { disabled, icon, prefixCls, opr } = props + + const key = useKey() + const classes = computed(() => + normalizeClass({ + [`${prefixCls}-opr-item`]: true, + [`${prefixCls}-opr-item-disabled`]: disabled, + }), + ) + + return +} + +export default OprIcon diff --git a/packages/components/image/src/types.ts b/packages/components/image/src/types.ts index fbcec0981..a475358f4 100644 --- a/packages/components/image/src/types.ts +++ b/packages/components/image/src/types.ts @@ -21,11 +21,12 @@ const zoomValidator = { export const imageViewerProps = { visible: IxPropTypes.bool, activeIndex: IxPropTypes.number, - images: IxPropTypes.array().isRequired, + images: IxPropTypes.array().def([]), zoom: IxPropTypes.custom(zoomValidator.validator, zoomValidator.msg), loop: IxPropTypes.bool, target: ɵPortalTargetDef, maskClosable: IxPropTypes.bool, + 'onUpdate:visible': IxPropTypes.emit<(visible: boolean) => void>(), 'onUpdate:activeIndex': IxPropTypes.emit<(curIndex: number) => void>(), } @@ -51,8 +52,14 @@ export type ImageViewerComponent = DefineComponent< export type ImageViewerInstance = InstanceType> // private -export const imageViewerContentProps = { - mergedPrefixCls: IxPropTypes.string.isRequired, - ...imageViewerProps, +export interface OprType { + icon: string + key: string + opr: () => void + visible: boolean + disabled?: boolean } -export type ImageViewerContentProps = IxInnerPropTypes + +export type ScaleType = 'in' | 'out' +export type RotateType = 'left' | 'right' +export type GoType = 'previous' | 'next'