mirror of
https://github.com/element-plus/element-plus.git
synced 2024-12-27 03:01:14 +08:00
364 lines
9.3 KiB
Vue
364 lines
9.3 KiB
Vue
<template>
|
|
<transition name="viewer-fade">
|
|
<div
|
|
ref="wrapper"
|
|
:tabindex="-1"
|
|
class="el-image-viewer__wrapper"
|
|
:style="{ zIndex }"
|
|
>
|
|
<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>
|
|
</span>
|
|
<!-- ARROW -->
|
|
<template v-if="!isSingle">
|
|
<span
|
|
class="el-image-viewer__btn el-image-viewer__prev"
|
|
:class="{ 'is-disabled': !infinite && isFirst }"
|
|
@click="prev"
|
|
>
|
|
<i class="el-icon-arrow-left"></i>
|
|
</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>
|
|
</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-refresh-left" @click="handleActions('anticlocelise')"></i>
|
|
<i class="el-icon-refresh-right" @click="handleActions('clocelise')"></i>
|
|
</div>
|
|
</div>
|
|
<!-- CANVAS -->
|
|
<div class="el-image-viewer__canvas">
|
|
<img
|
|
v-for="(url, i) in urlList"
|
|
v-show="i === index"
|
|
ref="img"
|
|
:key="url"
|
|
:src="url"
|
|
:style="imgStyle"
|
|
class="el-image-viewer__img"
|
|
@load="handleImgLoad"
|
|
@error="handleImgError"
|
|
@mousedown="handleMouseDown"
|
|
>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</template>
|
|
|
|
<script lang='ts'>
|
|
|
|
import { defineComponent, computed, ref, onMounted, watch, nextTick } from 'vue'
|
|
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 type { PropType } from 'vue'
|
|
|
|
const Mode = {
|
|
CONTAIN: {
|
|
name: 'contain',
|
|
icon: 'el-icon-full-screen',
|
|
},
|
|
ORIGINAL: {
|
|
name: 'original',
|
|
icon: 'el-icon-c-scale-to-original',
|
|
},
|
|
}
|
|
|
|
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
|
|
const CLOSE_EVENT = 'close'
|
|
const SWITCH_EVENT = 'switch'
|
|
export type ImageViewerAction = 'zoomIn' | 'zoomOut' | 'clocelise' | 'anticlocelise'
|
|
|
|
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],
|
|
|
|
setup(props, { emit }) {
|
|
const { t } = useLocaleInject()
|
|
|
|
let _keyDownHandler = null
|
|
let _mouseWheelHandler = null
|
|
let _dragHandler = null
|
|
|
|
const loading = ref(true)
|
|
const index = ref(props.initialIndex)
|
|
const wrapper = ref(null)
|
|
const img = ref(null)
|
|
const mode = ref(Mode.CONTAIN)
|
|
let transform = ref({
|
|
scale: 1,
|
|
deg: 0,
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
enableTransition: false,
|
|
})
|
|
|
|
const isSingle = computed(() => {
|
|
const { urlList } = props
|
|
return urlList.length <= 1
|
|
})
|
|
|
|
const isFirst = computed(() => {
|
|
return index.value === 0
|
|
})
|
|
|
|
const isLast = computed(() => {
|
|
return index.value === props.urlList.length - 1
|
|
})
|
|
|
|
const currentImg = computed(() => {
|
|
return props.urlList[index.value]
|
|
})
|
|
|
|
const imgStyle = computed(() => {
|
|
const { scale, deg, offsetX, offsetY, enableTransition } = transform.value
|
|
const style = {
|
|
transform: `scale(${scale}) rotate(${deg}deg)`,
|
|
transition: enableTransition ? 'transform .3s' : '',
|
|
marginLeft: `${offsetX}px`,
|
|
marginTop: `${offsetY}px`,
|
|
} as CSSStyleDeclaration
|
|
if (mode.value.name === Mode.CONTAIN.name) {
|
|
style.maxWidth = style.maxHeight = '100%'
|
|
}
|
|
return style
|
|
})
|
|
|
|
function hide() {
|
|
deviceSupportUninstall()
|
|
emit(CLOSE_EVENT)
|
|
}
|
|
|
|
function deviceSupportInstall() {
|
|
_keyDownHandler = rafThrottle((e: KeyboardEvent) => {
|
|
switch (e.code) {
|
|
// ESC
|
|
case EVENT_CODE.esc:
|
|
hide()
|
|
break
|
|
// SPACE
|
|
case EVENT_CODE.space:
|
|
toggleMode()
|
|
break
|
|
// LEFT_ARROW
|
|
case EVENT_CODE.left:
|
|
prev()
|
|
break
|
|
// UP_ARROW
|
|
case EVENT_CODE.up:
|
|
handleActions('zoomIn')
|
|
break
|
|
// RIGHT_ARROW
|
|
case EVENT_CODE.right:
|
|
next()
|
|
break
|
|
// DOWN_ARROW
|
|
case EVENT_CODE.down:
|
|
handleActions('zoomOut')
|
|
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,
|
|
})
|
|
}
|
|
})
|
|
on(document, 'keydown', _keyDownHandler)
|
|
on(document, mousewheelEventName, _mouseWheelHandler)
|
|
}
|
|
|
|
function deviceSupportUninstall() {
|
|
off(document, 'keydown', _keyDownHandler)
|
|
off(document, mousewheelEventName, _mouseWheelHandler)
|
|
_keyDownHandler = null
|
|
_mouseWheelHandler = null
|
|
}
|
|
|
|
function handleImgLoad() {
|
|
loading.value = false
|
|
}
|
|
|
|
function handleImgError(e) {
|
|
loading.value = false
|
|
e.target.alt = t('el.image.error')
|
|
}
|
|
|
|
function handleMouseDown(e: MouseEvent) {
|
|
if (loading.value || e.button !== 0) return
|
|
|
|
const { offsetX, offsetY } = transform.value
|
|
const startX = e.pageX
|
|
const startY = e.pageY
|
|
_dragHandler = rafThrottle(ev => {
|
|
transform.value = {
|
|
...transform.value,
|
|
offsetX: offsetX + ev.pageX - startX,
|
|
offsetY: offsetY + ev.pageY - startY,
|
|
}
|
|
})
|
|
on(document, 'mousemove', _dragHandler)
|
|
on(document, 'mouseup', () => {
|
|
off(document, 'mousemove', _dragHandler)
|
|
})
|
|
|
|
e.preventDefault()
|
|
}
|
|
|
|
function reset() {
|
|
transform.value = {
|
|
scale: 1,
|
|
deg: 0,
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
enableTransition: false,
|
|
}
|
|
}
|
|
|
|
function toggleMode() {
|
|
if (loading.value) return
|
|
|
|
const modeNames = Object.keys(Mode)
|
|
const modeValues = Object.values(Mode)
|
|
const currentMode = mode.value.name
|
|
const index = modeValues.findIndex(i => i.name === currentMode)
|
|
const nextIndex = (index + 1) % modeNames.length
|
|
mode.value = Mode[modeNames[nextIndex]]
|
|
reset()
|
|
}
|
|
|
|
function prev() {
|
|
if (isFirst.value && !props.infinite) return
|
|
const len = props.urlList.length
|
|
index.value = (index.value - 1 + len) % len
|
|
}
|
|
|
|
function next() {
|
|
if (isLast.value && !props.infinite) return
|
|
const len = props.urlList.length
|
|
index.value = (index.value + 1) % len
|
|
}
|
|
|
|
function handleActions(action: ImageViewerAction, options = {}) {
|
|
if (loading.value) return
|
|
const { zoomRate, rotateDeg, enableTransition } = {
|
|
zoomRate: 0.2,
|
|
rotateDeg: 90,
|
|
enableTransition: true,
|
|
...options,
|
|
}
|
|
switch (action) {
|
|
case 'zoomOut':
|
|
if (transform.value.scale > 0.2) {
|
|
transform.value.scale = parseFloat((transform.value.scale - zoomRate).toFixed(3))
|
|
}
|
|
break
|
|
case 'zoomIn':
|
|
transform.value.scale = parseFloat((transform.value.scale + zoomRate).toFixed(3))
|
|
break
|
|
case 'clocelise':
|
|
transform.value.deg += rotateDeg
|
|
break
|
|
case 'anticlocelise':
|
|
transform.value.deg -= rotateDeg
|
|
break
|
|
}
|
|
transform.value.enableTransition = enableTransition
|
|
}
|
|
|
|
watch(currentImg, () => {
|
|
nextTick(() => {
|
|
const $img = img.value
|
|
if (!$img.complete) {
|
|
loading.value = true
|
|
}
|
|
})
|
|
})
|
|
|
|
watch(index, val => {
|
|
reset()
|
|
emit(SWITCH_EVENT, val)
|
|
})
|
|
|
|
onMounted(() => {
|
|
deviceSupportInstall()
|
|
// add tabindex then wrapper can be focusable via Javascript
|
|
// focus wrapper so arrow key can't cause inner scroll behavior underneath
|
|
wrapper.value?.focus?.()
|
|
})
|
|
|
|
return {
|
|
index,
|
|
wrapper,
|
|
img,
|
|
isSingle,
|
|
isFirst,
|
|
isLast,
|
|
currentImg,
|
|
imgStyle,
|
|
mode,
|
|
handleActions,
|
|
prev,
|
|
next,
|
|
hide,
|
|
toggleMode,
|
|
handleImgLoad,
|
|
handleImgError,
|
|
handleMouseDown,
|
|
}
|
|
},
|
|
})
|
|
</script>
|