mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): refactor image-viewer (#3810)
* refactor(components): refactor image-viewer * fix: remove unused * refactor: improve type
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
|
||||
32
packages/components/image-viewer/src/image-viewer.ts
Normal file
32
packages/components/image-viewer/src/image-viewer.ts
Normal 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
|
||||
@@ -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?.()
|
||||
@@ -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
|
||||
|
||||
@@ -215,7 +215,7 @@ export default defineComponent({
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function switchViewer(val: boolean) {
|
||||
function switchViewer(val: number) {
|
||||
emit('switch', val)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
Reference in New Issue
Block a user