Skip to content

Commit

Permalink
refactor(comp: image): rebuild imageViewer (#772)
Browse files Browse the repository at this point in the history
  • Loading branch information
liuzaijiang authored Mar 2, 2022
1 parent 8680ef0 commit 3b8c9d9
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 351 deletions.
88 changes: 44 additions & 44 deletions packages/components/image/__tests__/imageViewer.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Element>

beforeEach(() => {
const el = document.createElement('div')
el.className = 'ix-image-viewer-container'
document.body.appendChild(el)
bodyWrapper = new DOMWrapper(document.body, {}) as DOMWrapper<Element>
})

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<Partial<ImageViewerProps>> = {}) => {
Expand All @@ -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)
})
Expand All @@ -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)
Expand All @@ -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 () => {
Expand All @@ -158,38 +160,38 @@ 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 () => {
const onUpdateVisible = jest.fn()
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()
})
Expand All @@ -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('')
})
})
135 changes: 119 additions & 16 deletions packages/components/image/src/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => (
<CdkPortal target={target.value} load={visible.value}>
<Transition name={`${common.prefixCls}-zoom-big-fast`} appear>
{visible.value && <ImageViewerContent mergedPrefixCls={mergedPrefixCls.value} {...props} />}
</Transition>
</CdkPortal>

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 (
<CdkPortal target={target.value} load={visible.value}>
<Transition name={`${common.prefixCls}-zoom-big-fast`} appear>
{visible.value && (
<div class={prefixCls}>
<div class={`${prefixCls}-opr`}>
{oprList.value
.filter(item => item.visible)
.map(item => {
const oprIconProps = {
...item,
prefixCls,
}
return <OprIcon {...oprIconProps} />
})}
</div>
<div class={`${prefixCls}-preview`} onClick={onClickLayer}>
<img class={`${prefixCls}-preview-img`} src={curImgSrc} style={transformStyle.value} />
</div>
</div>
)}
</Transition>
</CdkPortal>
)
}
},
})

function useTarget(props: ImageViewerProps, config: ImageViewerConfig, mergedPrefixCls: ComputedRef<string>) {
return computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-container`)
function getImageEvent(
visible: Ref<boolean>,
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<string, () => 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,
}
}
Loading

0 comments on commit 3b8c9d9

Please sign in to comment.