详解如何在Vue2中实现useDraggable

 更新时间:2023年12月06日 09:52:12   作者:何期骤雨降青霄  
这篇文章主要为大家详细介绍了Vue2中实现useDraggable的相关知识,文中的示例代码简洁易懂,对我们深入了解vue有一定的帮助,需要的小伙伴可以参考下

前言

最近接到个需求:要使 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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解关于element el-button使用$attrs的一个注意要点

    详解关于element el-button使用$attrs的一个注意要点

    这篇文章主要介绍了详解关于element el-button使用$attrs的一个注意要点,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • vue.js 实现v-model与{{}}指令方法

    vue.js 实现v-model与{{}}指令方法

    这篇文章主要介绍了vue.js 实现v-model与{{}}指令方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-10-10
  • nuxt框架中对vuex进行模块化设置的实现方法

    nuxt框架中对vuex进行模块化设置的实现方法

    这篇文章主要介绍了nuxt框架中对vuex进行模块化设置的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • vue调用微信JSDK 扫一扫,相册等需要注意的事项

    vue调用微信JSDK 扫一扫,相册等需要注意的事项

    这篇文章主要介绍了vue调用微信JSDK 扫一扫,相册等需要注意的事项,帮助大家更好的理解和使用vue框架,感兴趣的朋友可以了解下
    2021-01-01
  • vue+Vue Router多级侧导航切换路由(页面)的实现代码

    vue+Vue Router多级侧导航切换路由(页面)的实现代码

    这篇文章主要介绍了vue+Vue Router多级侧导航切换路由(页面)的实现代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • 脚手架vue-cli工程webpack的基本用法详解

    脚手架vue-cli工程webpack的基本用法详解

    这篇文章主要介绍了vue-cli工程webpack的基本用法,非常不错,具有一定的参考借鉴价值 ,需要的朋友可以参考下
    2018-09-09
  • elementUI select组件默认选中效果实现的方法

    elementUI select组件默认选中效果实现的方法

    这篇文章主要介绍了elementUI select组件默认选中效果实现的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • uniapp Vue3中如何解决web/H5网页浏览器跨域的问题

    uniapp Vue3中如何解决web/H5网页浏览器跨域的问题

    存在跨域问题的原因是因为浏览器的同源策略,也就是说前端无法直接发起跨域请求,同源策略是一个基础的安全策略,但是这也会给uniapp/Vue开发者在部署时带来一定的麻烦,这篇文章主要介绍了在uniapp Vue3版本中如何解决web/H5网页浏览器跨域的问题,需要的朋友可以参考下
    2024-06-06
  • Vue3通过JSON渲染ElementPlus表单的流程步骤

    Vue3通过JSON渲染ElementPlus表单的流程步骤

    这篇文章主要介绍了Vue3通过JSON渲染ElementPlus表单的流程步骤,文中通过代码示例和图文给大家讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-10-10
  • vue  自定义组件实现通讯录功能

    vue 自定义组件实现通讯录功能

    本文通过实例代码给介绍了vue使用自定义组件实现通讯录功能,需要的朋友可以参考下
    2018-09-09

最新评论