2.3 Elements – useDraggable
https://vueuse.org/core/useDraggable/
作用
让指定html
元素变成可以拖动的。
官方示例
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'
const el = ref<HTMLElement | null>(null)
// style 是一个响应式的计算属性,包含left和top的值
const { x, y, style } = useDraggable(el, {
initialValue: { x: 40, y: 40 },
})
</script>
<template>
<div ref="el" :style="style" style="position: fixed">
Drag me! I am at {{x}}, {{y}}
</div>
</template>
被移动的元素应该是可以修改位置的,比如上面例子中style="position: fixed"
。
- 无渲染组件的用法
<UseDraggable :initialValue="{ x: 10, y: 10 }" v-slot="{ x, y }">
Drag me! I am at {{x}}, {{y}}
</UseDraggable>
可以将额外的属性storageKey
和storageType
传递给组件,来启用元素位置的持久化。这样再次打开窗口,元素还是上次移动到的位置。
<UseDraggable storage-key="vueuse-draggable" storage-type="session">
Refresh the page and I am still in the same position!
</UseDraggable>
源码分析
源码地址:https://github.com/vueuse/vueuse/blob/main/packages/core/useDraggable/index.ts
1、定义初始变量
// 拖拽事件的监听目标,默认为全局window对象。超出这个目标,拖拽事件不可监控。
const draggingElement = options.draggingElement ?? defaultWindow
// 可以触发移动的区域,默认是目标元素
const draggingHandle = options.handle ?? target
// 初始位置,默认0,0
const position = ref<Position>(resolveUnref(options.initialValue) ?? { x: 0, y: 0 })
// 初始位置和当前位置的差值
const pressedDelta = ref<Position>()
2、注册监听事件
// 以默认值来解释
// 监听target的pointerdown事件,触发start事件,在捕获阶段处理。
useEventListener(draggingHandle, 'pointerdown', start, true)
// 监听window的pointermove事件,触发move事件,在捕获阶段处理。
useEventListener(draggingElement, 'pointermove', move, true)
useEventListener(draggingElement, 'pointerup', end, true)
⚠️:pointerdown、pointermove、pointerup
这几个变量是为了同时适用于web
和h5
,相当于在web
触发mouse
事件,在h5
触发touch
事件。但是h5
需要根据浏览器版本来看,低版本可能不兼容,需要改成touchmove
等变量。
https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event
3、分别看三个方法都做了什么
3.1、start
事件,主要功能如下:
- 判断是否需要出发start事件
- 记录鼠标点和
target
左上角的距离pressedDelta
const start = (e: PointerEvent) => {
// 不是['mouse', 'touch', 'pen']事件,不处理
if (!filterEvent(e))
return
// exact表示精确:如果点击的元素和target不是同一个元素,不处理,比如子元素
if (resolveUnref(options.exact) && e.target !== resolveUnref(target))
return
// getBoundingClientRect:获取target相对于视口的位置
const rect = resolveUnref(target)!.getBoundingClientRect()
// 获取鼠标点和target的距离
const pos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}
if (options.onStart?.(pos, e) === false)
return
// 记下这个距离,以便在move和end函数中使用
pressedDelta.value = pos
// 处理一些默认事件
handleEvent(e)
}
const handleEvent = (e: PointerEvent) => {
if (resolveUnref(options.preventDefault))
e.preventDefault()
if (resolveUnref(options.stopPropagation))
e.stopPropagation()
}
3.2、move
事件
随着鼠标移动,onMove事件触发,不断计算 position.value
,计算的方式是鼠标位置和pressedDelta
计算差值。
从图上来看,需要算的是蓝线的长度,它的值就等于绿线的长度剪去黄线的长度。
const move = (e: PointerEvent) => {
if (!filterEvent(e))
return
if (!pressedDelta.value)
return
// 主要就是这一步
position.value = {
x: e.clientX - pressedDelta.value.x,
y: e.clientY - pressedDelta.value.y,
}
options.onMove?.(position.value, e)
handleEvent(e)
}
/**
* 通过position这个变量,响应式修改style的值,最后返回的style是一个计算属性
*/
return {
style: computed(() => `left:${position.value.x}px;top:${position.value.y}px;`),
}
3.3、end
事件,主要功能是清除pressedDelta
的值
const end = (e: PointerEvent) => {
if (!filterEvent(e))
return
if (!pressedDelta.value)
return
// 清除pressedDelta的值,下次点击的时候,重新记录新的
pressedDelta.value = undefined
options.onEnd?.(position.value, e)
handleEvent(e)
}
从代码来看,这个函数不处理滚动,只监听视口内的拖动事件。