refactor(components): refactor image-viewer (#3810)

* refactor(components): refactor image-viewer

* fix: remove unused

* refactor: improve type
This commit is contained in:
三咲智子
2021-10-18 16:24:52 +08:00
committed by GitHub
parent ffe6d251c6
commit 12bf28e8c8
7 changed files with 120 additions and 102 deletions

View File

@@ -2,7 +2,7 @@ import { nextTick } from 'vue'
import { IMAGE_SUCCESS } from '@element-plus/test-utils/mock'
import makeMount from '@element-plus/test-utils/make-mount'
import ImageViewer from '../src/index.vue'
import ImageViewer from '../src/image-viewer.vue'
const mount = makeMount(ImageViewer, {
props: {

View File

@@ -1,13 +1,8 @@
import ImageViewer from './src/index.vue'
import { withInstall } from '@element-plus/utils/with-install'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
import ImageViewer from './src/image-viewer.vue'
ImageViewer.install = (app: App): void => {
app.component(ImageViewer.name, ImageViewer)
}
export const ElImageViewer = withInstall(ImageViewer)
export default ElImageViewer
const _ImageViewer = ImageViewer as SFCWithInstall<typeof ImageViewer>
export default _ImageViewer
export const ElImageViewer = _ImageViewer
export * from './src/image-viewer'

View File

@@ -0,0 +1,32 @@
import { buildProps, definePropType, mutable } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
export const imageViewerProps = buildProps({
urlList: {
type: definePropType<string[]>(Array),
default: () => mutable([] as const),
},
zIndex: {
type: Number,
default: 2000,
},
initialIndex: {
type: Number,
default: 0,
},
infinite: {
type: Boolean,
default: true,
},
hideOnClickModal: {
type: Boolean,
default: false,
},
} as const)
export type ImageViewerProps = ExtractPropTypes<typeof imageViewerProps>
export const imageViewerEmits = {
close: () => true,
switch: (index: number) => typeof index === 'number',
}
export type ImageViewerEmits = typeof imageViewerEmits

View File

@@ -9,11 +9,13 @@
<div
class="el-image-viewer__mask"
@click.self="hideOnClickModal && hide()"
></div>
/>
<!-- CLOSE -->
<span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
<i class="el-icon-close"></i>
<i class="el-icon-close" />
</span>
<!-- ARROW -->
<template v-if="!isSingle">
<span
@@ -21,32 +23,32 @@
:class="{ 'is-disabled': !infinite && isFirst }"
@click="prev"
>
<i class="el-icon-arrow-left"></i>
<i class="el-icon-arrow-left" />
</span>
<span
class="el-image-viewer__btn el-image-viewer__next"
:class="{ 'is-disabled': !infinite && isLast }"
@click="next"
>
<i class="el-icon-arrow-right"></i>
<i class="el-icon-arrow-right" />
</span>
</template>
<!-- ACTIONS -->
<div class="el-image-viewer__btn el-image-viewer__actions">
<div class="el-image-viewer__actions__inner">
<i class="el-icon-zoom-out" @click="handleActions('zoomOut')"></i>
<i class="el-icon-zoom-in" @click="handleActions('zoomIn')"></i>
<i class="el-image-viewer__actions__divider"></i>
<i :class="mode.icon" @click="toggleMode"></i>
<i class="el-image-viewer__actions__divider"></i>
<i class="el-icon-zoom-out" @click="handleActions('zoomOut')" />
<i class="el-icon-zoom-in" @click="handleActions('zoomIn')" />
<i class="el-image-viewer__actions__divider" />
<i :class="mode.icon" @click="toggleMode" />
<i class="el-image-viewer__actions__divider" />
<i
class="el-icon-refresh-left"
@click="handleActions('anticlocelise')"
></i>
/>
<i
class="el-icon-refresh-right"
@click="handleActions('clocelise')"
></i>
/>
</div>
</div>
<!-- CANVAS -->
@@ -70,13 +72,22 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted, watch, nextTick } from 'vue'
import {
defineComponent,
computed,
ref,
onMounted,
watch,
nextTick,
effectScope,
} from 'vue'
import { useEventListener } from '@vueuse/core'
import { useLocaleInject } from '@element-plus/hooks'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'
import { rafThrottle, isFirefox } from '@element-plus/utils/util'
import { imageViewerProps, imageViewerEmits } from './image-viewer'
import type { PropType, CSSProperties } from 'vue'
import type { CSSProperties } from 'vue'
const Mode = {
CONTAIN: {
@@ -90,8 +101,6 @@ const Mode = {
}
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
const CLOSE_EVENT = 'close'
const SWITCH_EVENT = 'switch'
export type ImageViewerAction =
| 'zoomIn'
| 'zoomOut'
@@ -100,42 +109,19 @@ export type ImageViewerAction =
export default defineComponent({
name: 'ElImageViewer',
props: {
urlList: {
type: Array as PropType<string[]>,
default: () => [],
},
zIndex: {
type: Number,
default: 2000,
},
initialIndex: {
type: Number,
default: 0,
},
infinite: {
type: Boolean,
default: true,
},
hideOnClickModal: {
type: Boolean,
default: false,
},
},
emits: [CLOSE_EVENT, SWITCH_EVENT],
props: imageViewerProps,
emits: imageViewerEmits,
setup(props, { emit }) {
const { t } = useLocaleInject()
const wrapper = ref<HTMLDivElement>()
const img = ref<HTMLImageElement>()
let _keyDownHandler = null
let _mouseWheelHandler = null
let _dragHandler = null
const scopeEventListener = effectScope()
const loading = ref(true)
const index = ref(props.initialIndex)
const wrapper = ref(null)
const img = ref(null)
const mode = ref(Mode.CONTAIN)
const transform = ref({
scale: 1,
@@ -164,12 +150,12 @@ export default defineComponent({
const imgStyle = computed(() => {
const { scale, deg, offsetX, offsetY, enableTransition } = transform.value
const style = {
const style: CSSProperties = {
transform: `scale(${scale}) rotate(${deg}deg)`,
transition: enableTransition ? 'transform .3s' : '',
marginLeft: `${offsetX}px`,
marginTop: `${offsetY}px`,
} as CSSProperties
}
if (mode.value.name === Mode.CONTAIN.name) {
style.maxWidth = style.maxHeight = '100%'
}
@@ -177,12 +163,12 @@ export default defineComponent({
})
function hide() {
deviceSupportUninstall()
emit(CLOSE_EVENT)
unregisterEventListener()
emit('close')
}
function deviceSupportInstall() {
_keyDownHandler = rafThrottle((e: KeyboardEvent) => {
function registerEventListener() {
const keydownHandler = rafThrottle((e: KeyboardEvent) => {
switch (e.code) {
// ESC
case EVENT_CODE.esc:
@@ -210,43 +196,44 @@ export default defineComponent({
break
}
})
_mouseWheelHandler = rafThrottle((e) => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail
if (delta > 0) {
handleActions('zoomIn', {
zoomRate: 0.015,
enableTransition: false,
})
} else {
handleActions('zoomOut', {
zoomRate: 0.015,
enableTransition: false,
})
const mousewheelHandler = rafThrottle(
(e: WheelEvent | any /* TODO: wheelDelta is deprecated */) => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail
if (delta > 0) {
handleActions('zoomIn', {
zoomRate: 0.015,
enableTransition: false,
})
} else {
handleActions('zoomOut', {
zoomRate: 0.015,
enableTransition: false,
})
}
}
)
scopeEventListener.run(() => {
useEventListener(document, 'keydown', keydownHandler)
useEventListener(document, mousewheelEventName, mousewheelHandler)
})
on(document, 'keydown', _keyDownHandler)
on(document, mousewheelEventName, _mouseWheelHandler)
}
function deviceSupportUninstall() {
off(document, 'keydown', _keyDownHandler)
off(document, mousewheelEventName, _mouseWheelHandler)
_keyDownHandler = null
_mouseWheelHandler = null
function unregisterEventListener() {
scopeEventListener.stop()
}
function handleImgLoad() {
loading.value = false
}
function handleImgError(e) {
function handleImgError(e: Event) {
loading.value = false
e.target.alt = t('el.image.error')
;(e.target as HTMLImageElement).alt = t('el.image.error')
}
function handleMouseDown(e: MouseEvent) {
if (loading.value || e.button !== 0) return
if (loading.value || e.button !== 0 || !wrapper.value) return
const { offsetX, offsetY } = transform.value
const startX = e.pageX
@@ -257,17 +244,21 @@ export default defineComponent({
const divTop = wrapper.value.clientTop
const divBottom = wrapper.value.clientTop + wrapper.value.clientHeight
_dragHandler = rafThrottle((ev) => {
const dragHandler = rafThrottle((ev: MouseEvent) => {
transform.value = {
...transform.value,
offsetX: offsetX + ev.pageX - startX,
offsetY: offsetY + ev.pageY - startY,
}
})
on(document, 'mousemove', _dragHandler)
on(document, 'mouseup', (e: MouseEvent) => {
const mouseX = e.pageX
const mouseY = e.pageY
const removeMousemove = useEventListener(
document,
'mousemove',
dragHandler
)
useEventListener(document, 'mouseup', (evt) => {
const mouseX = evt.pageX
const mouseY = evt.pageY
if (
mouseX < divLeft ||
mouseX > divRight ||
@@ -276,7 +267,7 @@ export default defineComponent({
) {
reset()
}
off(document, 'mousemove', _dragHandler)
removeMousemove()
})
e.preventDefault()
@@ -350,7 +341,7 @@ export default defineComponent({
watch(currentImg, () => {
nextTick(() => {
const $img = img.value
if (!$img.complete) {
if (!$img?.complete) {
loading.value = true
}
})
@@ -358,11 +349,11 @@ export default defineComponent({
watch(index, (val) => {
reset()
emit(SWITCH_EVENT, val)
emit('switch', val)
})
onMounted(() => {
deviceSupportInstall()
registerEventListener()
// add tabindex then wrapper can be focusable via Javascript
// focus wrapper so arrow key can't cause inner scroll behavior underneath
wrapper.value?.focus?.()

View File

@@ -1,4 +1,5 @@
import { buildProps, definePropType, mutable } from '@element-plus/utils/props'
import { isNumber } from '@element-plus/utils/util'
import type { ExtractPropTypes } from 'vue'
@@ -40,7 +41,7 @@ export type ImageProps = ExtractPropTypes<typeof imageProps>
export const imageEmits = {
error: (evt: Event) => evt instanceof Event,
switch: (val: boolean) => typeof val === 'boolean',
switch: (val: number) => isNumber(val),
close: () => true,
}
export type ImageEmits = typeof imageEmits

View File

@@ -215,7 +215,7 @@ export default defineComponent({
emit('close')
}
function switchViewer(val: boolean) {
function switchViewer(val: number) {
emit('switch', val)
}

View File

@@ -17,7 +17,7 @@ import isServer from './isServer'
import { debugWarn } from './error'
import type { ComponentPublicInstance, CSSProperties, Ref } from 'vue'
import type { AnyFunction, TimeoutHandle, Nullable } from './types'
import type { TimeoutHandle, Nullable } from './types'
export const SCOPE = 'Util'
@@ -148,18 +148,17 @@ export const isBool = (val: unknown): val is boolean => typeof val === 'boolean'
export const isNumber = (val: unknown): val is number => typeof val === 'number'
export const isHTMLElement = (val: unknown) => toRawType(val).startsWith('HTML')
export function rafThrottle<T extends AnyFunction<any>>(
fn: T
): AnyFunction<void> {
export function rafThrottle<T extends (...args: any) => any>(fn: T): T {
let locked = false
return function (...args: any[]) {
return function (this: ThisParameterType<T>, ...args: any[]) {
if (locked) return
locked = true
window.requestAnimationFrame(() => {
fn.apply(this, args)
Reflect.apply(fn, this, args)
locked = false
})
}
} as T
}
export const clearTimer = (timer: Ref<TimeoutHandle>) => {