Skip to content

Commit

Permalink
feat(ui/image-preview): add new component image-preview prototype
Browse files Browse the repository at this point in the history
affects: @varlet/ui
  • Loading branch information
haoziqaq committed Jun 23, 2021
1 parent 0c58686 commit 25a8907
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/varlet-ui/src/dialog/Dialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<var-popup
class="var-dialog__popup-radius"
var-dialog-cover
:show="popupShow"
:overlay="overlay"
:overlay-class="overlayClass"
Expand Down
2 changes: 1 addition & 1 deletion packages/varlet-ui/src/dialog/dialog.less
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
border-radius: 3px;
background: @dialog-background;

&__popup-radius {
&__popup-radius[var-dialog-cover] {
border-radius: @dialog-border-radius;
}

Expand Down
259 changes: 259 additions & 0 deletions packages/varlet-ui/src/image-preview/ImagePreview.vue
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 packages/varlet-ui/src/image-preview/__tests__/index.spec.js
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.
44 changes: 44 additions & 0 deletions packages/varlet-ui/src/image-preview/example/index.vue
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>
33 changes: 33 additions & 0 deletions packages/varlet-ui/src/image-preview/imagePreview.less
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;
}
}
Loading

0 comments on commit 25a8907

Please sign in to comment.