-
-
Notifications
You must be signed in to change notification settings - Fork 629
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui/image-preview): add new component image-preview prototype
affects: @varlet/ui
- Loading branch information
Showing
17 changed files
with
477 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
<template> | ||
<var-popup | ||
class="var-image-preview__popup" | ||
var-image-preview-cover | ||
transition="var-fade" | ||
:show="popupShow" | ||
:close-on-click-overlay="false" | ||
:lock-scroll="lockScroll" | ||
:teleport="teleport" | ||
@open="onOpen" | ||
@close="onClose" | ||
@closed="onClosed" | ||
@opened="onOpened" | ||
@route-change="onRouteChange" | ||
> | ||
<var-swipe | ||
class="var-image-preview__swipe" | ||
var-image-preview-cover | ||
:touchable="canSwipe" | ||
:initial-index="initialIndex" | ||
:loop="loop" | ||
@change="onChange" | ||
v-bind="$attrs" | ||
> | ||
<template #default> | ||
<var-swipe-item | ||
class="var-image-preview__swipe-item" | ||
var-image-preview-cover | ||
v-for="image in images" | ||
:key="image" | ||
> | ||
<div | ||
class="var-image-preview__zoom-container" | ||
:style="{ | ||
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`, | ||
transitionTimingFunction, | ||
transitionDuration, | ||
}" | ||
@touchstart="handleTouchstart" | ||
@touchmove="handleTouchmove" | ||
@touchend="handleTouchend" | ||
> | ||
<img class="var-image-preview__image" :src="image" :alt="image" /> | ||
</div> | ||
</var-swipe-item> | ||
</template> | ||
|
||
<template #indicator="{ index, length }"> | ||
<slot name="indicator" :index="index" :length="length"> | ||
<div class="var-image-preview__indicators" v-show="indicator">{{ index + 1 }} / {{ length }}</div> | ||
</slot> | ||
</template> | ||
</var-swipe> | ||
</var-popup> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import Swipe from '../swipe' | ||
import SwipeItem from '../swipe-item' | ||
import Popup from '../popup' | ||
import { defineComponent, ref, computed, Ref, ComputedRef, watch } from 'vue' | ||
import { props } from './props' | ||
import { toNumber } from '../utils/shared' | ||
type VarTouch = { | ||
clientX: number | ||
clientY: number | ||
timestamp: number | ||
target: HTMLElement | ||
} | ||
const DISTANCE_OFFSET = 4 | ||
const EVENT_DELAY = 200 | ||
export default defineComponent({ | ||
name: 'VarImagePreview', | ||
components: { | ||
[Swipe.name]: Swipe, | ||
[SwipeItem.name]: SwipeItem, | ||
[Popup.name]: Popup, | ||
}, | ||
inheritAttrs: false, | ||
props, | ||
setup(props) { | ||
const popupShow: Ref<boolean> = ref(false) | ||
const swipe: Ref<typeof Swipe | null> = ref(null) | ||
const initialIndex: ComputedRef<number> = computed(() => { | ||
const { images, current } = props | ||
const index = images.findIndex((image: string) => image === current) | ||
return index >= 0 ? index : 0 | ||
}) | ||
const scale: Ref<number> = ref(1) | ||
const translateX: Ref<number> = ref(0) | ||
const translateY: Ref<number> = ref(0) | ||
const transitionTimingFunction: Ref<string | null> = ref(null) | ||
const transitionDuration: Ref<string | null> = ref(null) | ||
const canSwipe: Ref<boolean> = ref(true) | ||
let startTouch: VarTouch | null = null | ||
let prevTouch: VarTouch | null = null | ||
let checker: number | null = null | ||
const getDistance = (touch: VarTouch, target: VarTouch): number => { | ||
const { clientX: touchX, clientY: touchY } = touch | ||
const { clientX: targetX, clientY: targetY } = target | ||
return Math.abs(Math.sqrt((targetX - touchX) ** 2 + (targetY - touchY) ** 2)) | ||
} | ||
const createVarTouch = (touches: Touch, target: HTMLElement): VarTouch => ({ | ||
clientX: touches.clientX, | ||
clientY: touches.clientY, | ||
timestamp: Date.now(), | ||
target, | ||
}) | ||
const zoomIn = () => { | ||
scale.value = toNumber(props.zoom) | ||
canSwipe.value = false | ||
prevTouch = null | ||
window.setTimeout(() => { | ||
transitionTimingFunction.value = 'linear' | ||
transitionDuration.value = '0s' | ||
}, 200) | ||
} | ||
const zoomOut = () => { | ||
scale.value = 1 | ||
translateX.value = 0 | ||
translateY.value = 0 | ||
canSwipe.value = true | ||
prevTouch = null | ||
transitionTimingFunction.value = null | ||
transitionDuration.value = null | ||
} | ||
const isDoubleTouch = (currentTouch: VarTouch) => { | ||
if (!prevTouch) { | ||
return false | ||
} | ||
return ( | ||
getDistance(prevTouch, currentTouch) <= DISTANCE_OFFSET && | ||
currentTouch.timestamp - prevTouch.timestamp < EVENT_DELAY && | ||
prevTouch.target === currentTouch.target | ||
) | ||
} | ||
const isTapTouch = (target: HTMLElement) => { | ||
if (!startTouch || !prevTouch) { | ||
return false | ||
} | ||
return getDistance(startTouch, prevTouch) <= DISTANCE_OFFSET && target === startTouch.target | ||
} | ||
const handleTouchend = (event: Event) => { | ||
checker && window.clearTimeout(checker) | ||
checker = window.setTimeout(() => { | ||
if (isTapTouch(event.target as HTMLElement)) { | ||
if (scale.value > 1) { | ||
zoomOut() | ||
setTimeout(() => props['onUpdate:show']?.(false), 200) | ||
return | ||
} | ||
props['onUpdate:show']?.(false) | ||
} | ||
startTouch = null | ||
}, EVENT_DELAY) | ||
} | ||
const handleTouchstart = (event: TouchEvent) => { | ||
const { touches } = event | ||
const currentTouch: VarTouch = createVarTouch(touches[0], event.currentTarget as HTMLElement) | ||
startTouch = currentTouch | ||
if (isDoubleTouch(currentTouch)) { | ||
scale.value > 1 ? zoomOut() : zoomIn() | ||
return | ||
} | ||
prevTouch = currentTouch | ||
} | ||
const handleTouchmove = (event: TouchEvent) => { | ||
if (!prevTouch) { | ||
return | ||
} | ||
const target = event.currentTarget as HTMLElement | ||
const { touches } = event | ||
const currentTouch: VarTouch = createVarTouch(touches[0], target) | ||
if (scale.value !== 1) { | ||
const moveX = currentTouch.clientX - prevTouch.clientX | ||
const moveY = currentTouch.clientY - prevTouch.clientY | ||
const zoom = toNumber(props.zoom) | ||
const { offsetWidth: zoomContainerOffsetWidth, offsetHeight: zoomContainerOffsetHeight } = target | ||
const { offsetWidth, offsetHeight } = target.querySelector('.var-image-preview__image') as HTMLElement | ||
const limitX = Math.abs(offsetWidth * zoom - zoomContainerOffsetWidth) / (2 * zoom) | ||
const limitY = Math.abs(zoomContainerOffsetHeight - offsetHeight * zoom) / (2 * zoom) | ||
if (translateX.value + moveX >= limitX) { | ||
translateX.value = limitX | ||
} else if (translateX.value + moveX <= -limitX) { | ||
translateX.value = -limitX | ||
} else { | ||
translateX.value += moveX | ||
} | ||
if (translateY.value + moveY >= limitY) { | ||
translateY.value = limitY | ||
} else if (translateY.value + moveY <= -limitY) { | ||
translateY.value = -limitY | ||
} else { | ||
translateY.value += moveY | ||
} | ||
} | ||
prevTouch = currentTouch | ||
} | ||
watch( | ||
() => props.current, | ||
() => swipe.value?.resize() | ||
) | ||
watch( | ||
() => props.show, | ||
(newValue) => { | ||
popupShow.value = newValue | ||
}, | ||
{ immediate: true } | ||
) | ||
return { | ||
initialIndex, | ||
popupShow, | ||
scale, | ||
translateX, | ||
translateY, | ||
canSwipe, | ||
transitionTimingFunction, | ||
transitionDuration, | ||
handleTouchstart, | ||
handleTouchmove, | ||
handleTouchend, | ||
} | ||
}, | ||
}) | ||
</script> | ||
|
||
<style lang="less"> | ||
@import '../swipe/swipe'; | ||
@import '../swipe-item/swipeItem'; | ||
@import '../popup/popup'; | ||
@import './imagePreview'; | ||
</style> |
14 changes: 14 additions & 0 deletions
14
packages/varlet-ui/src/image-preview/__tests__/index.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import example from '../example' | ||
import ImagePreview from '..' | ||
import { mount } from '@vue/test-utils' | ||
import { createApp } from 'vue' | ||
|
||
test('test imagePreview example', () => { | ||
const wrapper = mount(example) | ||
expect(wrapper.html()).toMatchSnapshot() | ||
}) | ||
|
||
test('test imagePreview plugin', () => { | ||
const app = createApp({}).use(ImagePreview) | ||
expect(app.component(ImagePreview.name)).toBeTruthy() | ||
}) |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<template> | ||
<app-type>命令调用</app-type> | ||
<var-button type="primary" @click="preview">预览</var-button> | ||
|
||
<app-type>组件调用</app-type> | ||
<var-button type="warning" @click="show = true">预览</var-button> | ||
<var-image-preview :images="images" v-model:show="show" /> | ||
</template> | ||
|
||
<script> | ||
import ImagePreview from '..' | ||
import Button from '../../button' | ||
import AppType from '@varlet/cli/site/mobile/components/AppType' | ||
import { defineComponent, ref } from 'vue' | ||
export default defineComponent({ | ||
name: 'ImagePreviewExample', | ||
components: { | ||
[ImagePreview.Component.name]: ImagePreview.Component, | ||
[Button.name]: Button, | ||
[AppType.name]: AppType, | ||
}, | ||
setup() { | ||
const images = [ | ||
'https://varlet.gitee.io/varlet-ui/cat.jpg', | ||
'https://varlet.gitee.io/varlet-ui/cat2.jpg', | ||
'https://img01.yzcdn.cn/vant/apple-4.jpg', | ||
] | ||
return { | ||
preview() { | ||
ImagePreview(images) | ||
}, | ||
show: ref(false), | ||
images: ref(images), | ||
} | ||
}, | ||
}) | ||
</script> | ||
|
||
<style> | ||
.var-image-preview__image { | ||
pointer-events: none; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
@image-preview-swipe-indicators-text-color: #ddd; | ||
|
||
.var-image-preview { | ||
&__popup[var-image-preview-cover] { | ||
background: transparent; | ||
} | ||
|
||
&__swipe[var-image-preview-cover] { | ||
width: 100vw; | ||
height: 100vh; | ||
} | ||
|
||
&__swipe-item[var-image-preview-cover] { | ||
overflow: hidden; | ||
} | ||
|
||
&__indicators { | ||
color: @image-preview-swipe-indicators-text-color; | ||
} | ||
|
||
&__image { | ||
width: 100vw; | ||
} | ||
|
||
&__zoom-container { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
width: 100vw; | ||
height: 100vh; | ||
transition: transform 0.2s; | ||
} | ||
} |
Oops, something went wrong.