feat: image preview gestures (#668)

This commit is contained in:
Joaquín Sánchez 2023-01-01 20:59:31 +01:00 committed by GitHub
parent 9e8ee0da41
commit bd72ecd0e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 206 additions and 1 deletions

View file

@ -1,10 +1,27 @@
<script setup lang="ts">
import { useImageGesture } from '~/composables/gestures'
const emit = defineEmits(['close'])
const img = ref()
const current = computed(() => mediaPreviewList.value[mediaPreviewIndex.value])
const hasNext = computed(() => mediaPreviewIndex.value < mediaPreviewList.value.length - 1)
const hasPrev = computed(() => mediaPreviewIndex.value > 0)
useImageGesture(img, {
hasNext,
hasPrev,
onNext() {
if (hasNext.value)
mediaPreviewIndex.value++
},
onPrev() {
if (hasPrev.value)
mediaPreviewIndex.value--
},
})
const keys = useMagicKeys()
whenever(keys.arrowLeft, prev)
@ -45,7 +62,10 @@ function onClick(e: MouseEvent) {
<div i-ri:arrow-left-s-line text-white />
</button>
<img
:src="current.url || current.previewUrl" :alt="current.description || ''" max-h-full max-w-full ma
ref="img"
:src="current.url || current.previewUrl"
:alt="current.description || ''"
max-h-full max-w-full ma
>
<div absolute top-0 w-full flex justify-between>

View file

@ -0,0 +1,77 @@
import type { PermissiveMotionProperties } from '@vueuse/motion'
import type { Handlers } from '@vueuse/gesture'
import { useMotionProperties, useSpring } from '@vueuse/motion'
import { useGesture } from '@vueuse/gesture'
import type { MaybeRef } from '@vueuse/core'
export interface CarouselOptions {
hasNext: MaybeRef<boolean>
hasPrev: MaybeRef<boolean>
onPrev: () => void
onNext: () => void
}
export const useImageGesture = (
domTarget: MaybeRef<HTMLElement>,
carouselOptions?: CarouselOptions,
) => {
const { motionProperties } = useMotionProperties(domTarget, {
cursor: 'grab',
scale: 1,
x: 0,
y: 0,
})
const { set } = useSpring(motionProperties as Partial<PermissiveMotionProperties>)
// @ts-expect-error we need to fix types: just suppress it for now
const handlers: Handlers = {
onPinch({ offset: [d] }) {
set({ scale: 1 + d / 200 })
},
onDragStart() {
set({ cursor: 'grabbing' })
},
onDrag({ movement: [x, y], pinching }) {
if (!pinching)
set({ x, y, cursor: 'grabbing' })
},
onDragEnd({ vxvy: [vx], pinching }) {
if (pinching)
return
set({ cursor: 'grab' })
if (carouselOptions) {
const isSwipe = Math.abs(vx) > 0.25
if (isSwipe) {
if (vx > 0 && unref(carouselOptions.hasPrev))
carouselOptions.onPrev()
else if (vx < 0 && unref(carouselOptions.hasNext))
carouselOptions.onNext()
}
}
set({ x: 0, y: 0 })
},
onMove({ movement: [x, y], dragging, pinching }) {
if (dragging && !pinching)
set({ x, y })
},
onWheel({ event, dragging, pinching }) {
if (!dragging && !pinching && event.altKey) {
event.preventDefault()
// @ts-expect-error why is ts complaining here (motionProperties.scale)?
set({ scale: motionProperties.scale + event.deltaY * 0.001 })
}
},
onDblclick() {
set({ scale: 1 })
},
onTouchstart(event) {
if (event.touches === 2)
set({ scale: 1 })
},
}
useGesture(handlers, { domTarget })
}

View file

@ -37,7 +37,9 @@
"@tiptap/suggestion": "2.0.0-beta.204",
"@tiptap/vue-3": "2.0.0-beta.204",
"@vueuse/core": "^9.9.0",
"@vueuse/gesture": "2.0.0-beta.1",
"@vueuse/integrations": "^9.9.0",
"@vueuse/motion": "2.0.0-beta.12",
"blurhash": "^2.0.4",
"browser-fs-access": "^0.31.1",
"floating-vue": "2.0.0-beta.20",

View file

@ -33,7 +33,9 @@ specifiers:
'@vitejs/plugin-vue': ^3.2.0
'@vue-macros/nuxt': ^0.2.2
'@vueuse/core': ^9.9.0
'@vueuse/gesture': 2.0.0-beta.1
'@vueuse/integrations': ^9.9.0
'@vueuse/motion': 2.0.0-beta.12
'@vueuse/nuxt': ^9.9.0
blurhash: ^2.0.4
browser-fs-access: ^0.31.1
@ -94,7 +96,9 @@ dependencies:
'@tiptap/suggestion': 2.0.0-beta.204
'@tiptap/vue-3': 2.0.0-beta.204
'@vueuse/core': 9.9.0
'@vueuse/gesture': 2.0.0-beta.1
'@vueuse/integrations': 9.9.0_ha7ivgav6uqpoo2b5thfugqwjq
'@vueuse/motion': 2.0.0-beta.12
blurhash: 2.0.4
browser-fs-access: 0.31.1
floating-vue: 2.0.0-beta.20
@ -2708,6 +2712,10 @@ packages:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: true
/@types/web-bluetooth/0.0.14:
resolution: {integrity: sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==}
dev: false
/@types/web-bluetooth/0.0.16:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
@ -3498,6 +3506,23 @@ packages:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
dev: true
/@vueuse/core/8.9.4:
resolution: {integrity: sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
'@types/web-bluetooth': 0.0.14
'@vueuse/metadata': 8.9.4
'@vueuse/shared': 8.9.4
vue-demi: 0.13.11
dev: false
/@vueuse/core/9.9.0:
resolution: {integrity: sha512-JdDb7TrE0imZnwBhMF4+0PCJqGD3AxzH8S2sfk54P0rqvklK+EAtAR/mPb1HwV/JPujQFQJhghQ190Yq03YpVw==}
dependencies:
@ -3509,6 +3534,21 @@ packages:
- '@vue/composition-api'
- vue
/@vueuse/gesture/2.0.0-beta.1:
resolution: {integrity: sha512-HTLibLy3bh6TjRnDAbMAvHSsEmrkRituMj2x+mHwmp1EnM8A8CDRTfNJEr8d/hIairnPPp5Va2KWYVmyP/zvkA==}
peerDependencies:
'@vue/composition-api': ^1.4.1
vue: ^2.0.0 || >=3.0.0-rc.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dependencies:
chokidar: 3.5.3
consola: 2.15.3
upath: 2.0.1
vue-demi: 0.13.11
dev: false
/@vueuse/head/1.0.19_vue@3.2.45:
resolution: {integrity: sha512-UB8Vij9fjLS+VIL8VnRxkkkX1dosQEgdfq7kyHNev5tMzAlUc1BwIRlSU5PtJv9+Zk46BhTNdh/Btp+dEinWFQ==}
peerDependencies:
@ -3570,9 +3610,30 @@ packages:
- vue
dev: false
/@vueuse/metadata/8.9.4:
resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==}
dev: false
/@vueuse/metadata/9.9.0:
resolution: {integrity: sha512-pgxsUJv/d7IjKpLeB6TthggEsaBwM3ffc5jPrr5TmxAm/fup0mGR5VTzrdA/PSx85tpb+CIvP92D+55qBNc8ag==}
/@vueuse/motion/2.0.0-beta.12:
resolution: {integrity: sha512-cAZqXexLX6xo+H1N1Mv+wBSSqG4wB+BdjIuHQ50jwlelXCDxSi8gj0K/9nDS+aUZtWh6YMwS6UGCKg58jMVglA==}
peerDependencies:
'@vue/composition-api': ^1.4.1
vue: ^2.0.0 || >=3.0.0-rc.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dependencies:
'@vueuse/core': 8.9.4
'@vueuse/shared': 8.9.4
framesync: 6.1.2
popmotion: 11.0.5
style-value-types: 5.1.2
vue-demi: 0.13.11
dev: false
/@vueuse/nuxt/9.9.0_nuxt@3.0.0:
resolution: {integrity: sha512-bhvHsy3vM38WWRhFgyjbDP/tfn0AMP7z1KeaWt5+ysxGHZ1EHMsY4lcFbpcFlQ6K9TKMcYvzzcBSzYxlWanMnQ==}
peerDependencies:
@ -3591,6 +3652,20 @@ packages:
- vue
dev: true
/@vueuse/shared/8.9.4:
resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
vue-demi: 0.13.11
dev: false
/@vueuse/shared/9.9.0:
resolution: {integrity: sha512-+D0XFwHG0T+uaIbCSlROBwm1wzs71B7n3KyDOxnvfEMMHDOzl09rYKwaE2AENmYwYPXfHPbSBRDD2gBVHbvTcg==}
dependencies:
@ -5763,6 +5838,12 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true
/framesync/6.1.2:
resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==}
dependencies:
tslib: 2.4.0
dev: false
/fresh/0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -6093,6 +6174,10 @@ packages:
tslib: 2.4.1
dev: false
/hey-listen/1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false
/hookable/5.4.2:
resolution: {integrity: sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg==}
dev: true
@ -7826,6 +7911,15 @@ packages:
engines: {node: '>=4'}
dev: true
/popmotion/11.0.5:
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
dependencies:
framesync: 6.1.2
hey-listen: 1.0.8
style-value-types: 5.1.2
tslib: 2.4.0
dev: false
/postcss-calc/8.2.4_postcss@8.4.19:
resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==}
peerDependencies:
@ -9032,6 +9126,13 @@ packages:
dependencies:
acorn: 8.8.1
/style-value-types/5.1.2:
resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==}
dependencies:
hey-listen: 1.0.8
tslib: 2.4.0
dev: false
/stylehacks/5.1.1_postcss@8.4.19:
resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==}
engines: {node: ^10 || ^12 || >=14.0}
@ -9692,6 +9793,11 @@ packages:
engines: {node: '>=4'}
dev: true
/upath/2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
engines: {node: '>=4'}
dev: false
/update-browserslist-db/1.0.10_browserslist@4.21.4:
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true