詳解如何在Vue2中實(shí)現(xiàn)useDraggable
前言
最近接到個(gè)需求:要使 Modal 組件可以被拖拽。接到需求后立馬想到使用 mousedown
mousemove
mouseup
等事件及定位去實(shí)現(xiàn),于是一頓操作后實(shí)現(xiàn)了一個(gè) useMovable
hook。但總覺得不夠完美,有以下問題:
- 被拖拽元素必須是定位元素,否則無法拖拽
- 有一個(gè)極難復(fù)現(xiàn)的bug,在開發(fā)環(huán)境甚至沒有復(fù)現(xiàn)過,生產(chǎn)環(huán)境也極少復(fù)現(xiàn),因此一直未找到問題所在。
于是,就想到去看下一些開源組件庫是如何實(shí)現(xiàn)拖拽的,最終在 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 } /** * 使目標(biāo)元素可以被拖動(dòng)的 hook * @param targetRef 目標(biāo)元素,即被拖動(dòng)的元素 * @param dragRef 可執(zhí)行拖動(dòng)的元素 */ 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) => { // 獲取移動(dòng)偏移量,同時(shí)保證在視口范圍內(nèi) 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,這里我做了點(diǎn)小改動(dòng) 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
實(shí)現(xiàn)的,這就解決了之前提到過的元素必須是定位元素的問題。
同時(shí)為了保證元素拖拽時(shí)不被拖到視口之外,這里通過視口的寬高、元素的寬高、元素的位置等來計(jì)算出元素的 translate 的最大和最小值。
// 保證在視口范圍內(nèi)主要是以下代碼 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
兩個(gè)參數(shù),分別表示被拖拽的元素和可以執(zhí)行拖拽的元素,這樣可以將兩個(gè)元素區(qū)分開了(當(dāng)然,是一個(gè)元素也完全沒有問題),便于實(shí)現(xiàn)如:在彈窗 header 部分按下鼠標(biāo)可以拖拽整個(gè)彈窗,而在彈窗 body / footer 部分按下則無法進(jìn)行拖拽的功能。
最后值得一提的是:draggable
參數(shù)的類型是 ComputedRef
,這樣的好處就是可以監(jiān)聽 draggable
來動(dòng)態(tài)的綁定和解綁拖拽函數(shù)。
當(dāng)元素本身就具有 transform: translate 值時(shí)的處理方法
這樣似乎很完美,但測試過程中我發(fā)現(xiàn)一個(gè)問題:當(dāng)被拖拽元素本身就具有 transform: translate 值就會(huì)出現(xiàn)bug;原因是 transform
變量在初次拖拽時(shí)兩個(gè)屬性的值都是 0,而在保證元素必須在視口中的計(jì)算代碼中使用到了 transform
變量,而當(dāng)元素本身就具有 transform: translate 值時(shí)該計(jì)算就不再準(zhǔn)確。
解決這個(gè)問題的方法就是拿到元素初始的 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
發(fā)現(xiàn)是一個(gè)字符串,類似這樣: matrix(1, 0, 0, 1, 10, 10)
,最后兩個(gè)數(shù)字代表 translateX 和 translateY 的值,但問題是如何取出來呢?
首先想到的是通過正則匹配取出再 parseFloat,但這樣顯然比較麻煩。于是我去搜索了一番,找到了 DOMMatrix
,但它的兼容性較差,又經(jīng)過一番搜索找到了 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 } }
只要把這個(gè)函數(shù)放在 onMousedown
函數(shù)體最上面調(diào)用一下就解決了這個(gè)問題。
結(jié)語
當(dāng)遇到問題時(shí)不妨多借鑒別人的代碼,尤其是第三方開源組件庫的源碼,也許你會(huì)有意想不到的收獲,思路一下子就打開了。但在借鑒別人代碼的同時(shí)你也得深入理解這段代碼,否則只是抄過來的話,需要新加需求時(shí)你可能就束手無策了。
到此這篇關(guān)于詳解如何在Vue2中實(shí)現(xiàn)useDraggable的文章就介紹到這了,更多相關(guān)Vue2實(shí)現(xiàn)useDraggable內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解關(guān)于element el-button使用$attrs的一個(gè)注意要點(diǎn)
這篇文章主要介紹了詳解關(guān)于element el-button使用$attrs的一個(gè)注意要點(diǎn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11vue.js 實(shí)現(xiàn)v-model與{{}}指令方法
這篇文章主要介紹了vue.js 實(shí)現(xiàn)v-model與{{}}指令方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-10-10nuxt框架中對vuex進(jìn)行模塊化設(shè)置的實(shí)現(xiàn)方法
這篇文章主要介紹了nuxt框架中對vuex進(jìn)行模塊化設(shè)置的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09vue調(diào)用微信JSDK 掃一掃,相冊等需要注意的事項(xiàng)
這篇文章主要介紹了vue調(diào)用微信JSDK 掃一掃,相冊等需要注意的事項(xiàng),幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2021-01-01vue+Vue Router多級(jí)側(cè)導(dǎo)航切換路由(頁面)的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue+Vue Router多級(jí)側(cè)導(dǎo)航切換路由(頁面)的實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-12-12elementUI select組件默認(rèn)選中效果實(shí)現(xiàn)的方法
這篇文章主要介紹了elementUI select組件默認(rèn)選中效果實(shí)現(xiàn)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03uniapp Vue3中如何解決web/H5網(wǎng)頁瀏覽器跨域的問題
存在跨域問題的原因是因?yàn)闉g覽器的同源策略,也就是說前端無法直接發(fā)起跨域請求,同源策略是一個(gè)基礎(chǔ)的安全策略,但是這也會(huì)給uniapp/Vue開發(fā)者在部署時(shí)帶來一定的麻煩,這篇文章主要介紹了在uniapp Vue3版本中如何解決web/H5網(wǎng)頁瀏覽器跨域的問題,需要的朋友可以參考下2024-06-06Vue3通過JSON渲染ElementPlus表單的流程步驟
這篇文章主要介紹了Vue3通過JSON渲染ElementPlus表單的流程步驟,文中通過代碼示例和圖文給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-10-10