详解如何在Vue2中实现useDraggable
作者:何期骤雨降青霄
前言
最近接到个需求:要使 Modal 组件可以被拖拽。接到需求后立马想到使用 mousedown
mousemove
mouseup
等事件及定位去实现,于是一顿操作后实现了一个 useMovable
hook。但总觉得不够完美,有以下问题:
- 被拖拽元素必须是定位元素,否则无法拖拽
- 有一个极难复现的bug,在开发环境甚至没有复现过,生产环境也极少复现,因此一直未找到问题所在。
于是,就想到去看下一些开源组件库是如何实现拖拽的,最终在 element-plus
中找到了(虽然 element-plus
是基于 Vue3 的,但在 Vue2.7 中同样可以使用);那么我们就来看看它的源码。
useDraggable 源码解读
import { onBeforeUnmount, onMounted, watchEffect } from 'vue' import type { ComputedRef, Ref } from 'vue' function toCssValue (val?: number | string | null): string { if (val == null) return '' if (typeof val === 'number') return `${val}px` return val } /** * 使目标元素可以被拖动的 hook * @param targetRef 目标元素,即被拖动的元素 * @param dragRef 可执行拖动的元素 */ export function useDraggable ( targetRef: Ref<HTMLElement | null | undefined>, dragRef: Ref<HTMLElement | null | undefined>, draggable: ComputedRef<boolean>, ) { let transform = { offsetX: 0, offsetY: 0, } const onMousedown = (e: MouseEvent) => { const downX = e.clientX const downY = e.clientY const { offsetX, offsetY } = transform const targetRect = targetRef.value!.getBoundingClientRect() const targetLeft = targetRect.left const targetTop = targetRect.top const targetWidth = targetRect.width const targetHeight = targetRect.height const clientWidth = document.documentElement.clientWidth const clientHeight = document.documentElement.clientHeight const minLeft = -targetLeft + offsetX // translateX 最小值 const minTop = -targetTop + offsetY // translateY 最小值 const maxLeft = clientWidth - targetLeft - targetWidth + offsetX // translateX 最大值 const maxTop = clientHeight - targetTop - targetHeight + offsetY // translateY 最大值 const onMousemove = (e: MouseEvent) => { // 获取移动偏移量,同时保证在视口范围内 const moveX = Math.min( Math.max(offsetX + e.clientX - downX, minLeft), maxLeft, ) const moveY = Math.min( Math.max(offsetY + e.clientY - downY, minTop), maxTop, ) transform = { offsetX: moveX, offsetY: moveY, } // 源码中使用了 addUnit,这里我做了点小改动 targetRef.value!.style.transform = `translate(${toCssValue(moveX)}, ${toCssValue(moveY)})` } const onMouseup = () => { document.removeEventListener('mousemove', onMousemove) document.removeEventListener('mouseup', onMouseup) } document.addEventListener('mousemove', onMousemove) document.addEventListener('mouseup', onMouseup) } const onDraggable = () => { if (dragRef.value && targetRef.value) { dragRef.value.addEventListener('mousedown', onMousedown) } } const offDraggable = () => { if (dragRef.value && targetRef.value) { dragRef.value.removeEventListener('mousedown', onMousedown) } } onMounted(() => { watchEffect(() => { if (draggable.value) { onDraggable() } else { offDraggable() } }) }) onBeforeUnmount(() => { offDraggable() }) }
可以看到,这里的拖拽是通过 transform
实现的,这就解决了之前提到过的元素必须是定位元素的问题。
同时为了保证元素拖拽时不被拖到视口之外,这里通过视口的宽高、元素的宽高、元素的位置等来计算出元素的 translate 的最大和最小值。
// 保证在视口范围内主要是以下代码 const moveX = Math.min( Math.max(offsetX + e.clientX - downX, minLeft), maxLeft, ) const moveY = Math.min( Math.max(offsetY + e.clientY - downY, minTop), maxTop, )
另外,可以看到 useDraggable 接收了 targetRef
dragRef
两个参数,分别表示被拖拽的元素和可以执行拖拽的元素,这样可以将两个元素区分开了(当然,是一个元素也完全没有问题),便于实现如:在弹窗 header 部分按下鼠标可以拖拽整个弹窗,而在弹窗 body / footer 部分按下则无法进行拖拽的功能。
最后值得一提的是:draggable
参数的类型是 ComputedRef
,这样的好处就是可以监听 draggable
来动态的绑定和解绑拖拽函数。
当元素本身就具有 transform: translate 值时的处理方法
这样似乎很完美,但测试过程中我发现一个问题:当被拖拽元素本身就具有 transform: translate 值就会出现bug;原因是 transform
变量在初次拖拽时两个属性的值都是 0,而在保证元素必须在视口中的计算代码中使用到了 transform
变量,而当元素本身就具有 transform: translate 值时该计算就不再准确。
解决这个问题的方法就是拿到元素初始的 transform: translate 值赋给 transform
变量,于是我写下了如下代码:
function getComputedStylePropertyValue ( el: Element, property: string, ): string { const css = window.getComputedStyle(el, null) return css.getPropertyValue(property) } const cssTransform = getComputedStylePropertyValue(targetRef.value!, 'transform')
然后一打印 cssTransform
发现是一个字符串,类似这样: matrix(1, 0, 0, 1, 10, 10)
,最后两个数字代表 translateX 和 translateY 的值,但问题是如何取出来呢?
首先想到的是通过正则匹配取出再 parseFloat,但这样显然比较麻烦。于是我去搜索了一番,找到了 DOMMatrix
,但它的兼容性较差,又经过一番搜索找到了 WebKitCSSMatrix
,于是就有以下代码:
const setTransformInitialValue = () => { const Matrix = DOMMatrix || WebKitCSSMatrix const cssTransform = getComputedStylePropertyValue(targetRef.value!, 'transform') const matrix = new Matrix(cssTransform) transform = { offsetX: matrix.e || 0, // matrix.e 代表 translateX offsetY: matrix.f || 0, // matrix.f 代表 translateY } }
只要把这个函数放在 onMousedown
函数体最上面调用一下就解决了这个问题。
结语
当遇到问题时不妨多借鉴别人的代码,尤其是第三方开源组件库的源码,也许你会有意想不到的收获,思路一下子就打开了。但在借鉴别人代码的同时你也得深入理解这段代码,否则只是抄过来的话,需要新加需求时你可能就束手无策了。
到此这篇关于详解如何在Vue2中实现useDraggable的文章就介绍到这了,更多相关Vue2实现useDraggable内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!