前端前端都会去了解的让前端飞

vue3+TypeScript div 拖动 通用逻辑,附源码

2023-11-12  本文已影响0人  阿巳交不起水电费

前言

相比github,现在其实更喜欢在博客上记录代码,图文并茂,方便后面使用的时候快速想起来,毕竟写的时候要考虑到小白也能看懂。

回到主题,这种在有定位的盒子内【如:position: relative;】可以拖动其内部盒子【position: absolute;】移动到其他位置的需求其实比较常见,很多时候之前的拖动逻辑换个地方就表现异常了!一点也不复用,搞得每次都要分析一遍哪里减去哪里,哪里的dom获取有问题才正常!这里写个vue3 ts 的通用逻辑,防止以后再写,相同的逻辑写一次就行了嘛,直接一步到位!

image.png

支持

1.可选择是否开启边界条件,也就是限定在“有定位父级”范围内!参数openBoundary设置为true即可。
2.可自行处理拖动,传入 moveingCallback 参数即可,注意这是函数,参数为:


image.png

3.拖拽结束回调函数:moveEndCallback。
4.拖拽盒子在布局上允许有其他子节点。
5.页面有滚动条不影响拖动。
6.父节点【有定位的父级】和子节点【 position: absolute;】不是直接父子关系也不影响,当然一般不会出现这样的场景。
7.未考虑缩放场景-缩放因子自己结合代码加,只要搞清楚每步获取到的值是真实值还是缩放值就行了,代码有注释,改起来也简单,这里没加是因为懒得改,毕竟这需求不常见。。。

gif 效果演示如下:

GIF 2023-11-13 14-56-45.gif

代码如下:

divDrag.ts

/**
 vue3 div 拖动 通用逻辑
 author:yangfeng
 date:20231110
 */
import { ref, onMounted, onUnmounted } from 'vue'

/**
 * 判断指定dom节点是否是具有定位属性的节点 - 即:position为 absolute | relative | fixed
 * @param _node
 * @returns {boolean}
 */
function judgeIsLocateNode(_node: HTMLElement) {
  let cssStyle = window.getComputedStyle(_node, null)
  return cssStyle.position !== 'static' // 不是默认的就是有定位的
}

/**
 * 获取指定节点的有定位的父节点
 * @param ele 子节点
 * @param flag 父节点类或id选择器或者元素节点名称,eg: 类:.app | id: #app | 元素节点名称 body 或者 flag直接是dom对象
 * @returns {HTMLElement | null}
 */
function findLocateParentNode(ele: HTMLElement) {
  if (!ele) return null;
  let parent: HTMLElement | null = ele.parentNode as HTMLElement;

  let locateParentNode: HTMLElement | null = null; // 有定位父节点
  while (parent && parent.nodeName !== "BODY" && parent.nodeName !== "HTML") {
    if (judgeIsLocateNode(parent)) {
      // 是定位节点
      locateParentNode = parent;
      break;
    }
    parent = parent.parentNode as HTMLElement;
  }

  // 默认是body
  if (!locateParentNode) {
    locateParentNode = document.getElementsByTagName("body")[0];
  }

  return locateParentNode;
}

/**
 * div 拖动 通用逻辑
 * @param {
  moveingCallback, // 当前正在移动回调函数 非必填 - 有此参数则外部自行处理更改定位的逻辑,不传则拖动时更改dragBoxRef的left,top值
  moveEndCallback // 移动结束回调函数 非必填
 * } param0 
 * @returns 
 */
type funType = (()=>void) | undefined;
type moveingCallbackType = ((e:MouseEvent, arg:{left:number;top:number;})=>void) | undefined
export default function useDivDrag({
  moveingCallback, // 当前正在移动回调函数 非必填
  moveEndCallback, // 移动结束回调函数 非必填
  openBoundary // 是否开启边界条件【将拖拽盒子限制在定位父节点范围内】 - 注意:如果拖拽盒子有margin 偏移或者translate 偏移,会导致看起来不准确
}:{
  moveingCallback?: moveingCallbackType;
  moveEndCallback?: funType;
  openBoundary?: boolean;
}={}) {
  const dragBoxRef = ref() // 需要拖动的盒子
  const isMoving = ref(false) // 当前是否正在移动

  const tools = {
    isFunction(fn: any) {
      return fn && typeof fn === 'function'
    },
    getToContainerXY(e: MouseEvent) {
      return {
        x: e.x || e.pageX,
        y: e.y || e.pageY,
      }
    },
    // 添加移除鼠标事件
    addRemoveMouseEvent(callback: Function) {
      // 鼠标移动
      let moveHandle = (moveE: MouseEvent) => {
        callback && callback(moveE)
      }

      // 移除鼠标事件
      let clearMouseEvent = () => {
        window.removeEventListener('mousemove', moveHandle)
        window.removeEventListener('mouseup', clearMouseEvent)
        changeMoveing(false)
        // 移动结束
        if (tools.isFunction(moveEndCallback)){
          moveEndCallback && moveEndCallback()
        }
      }
      window.addEventListener('mousemove', moveHandle, false)
      window.addEventListener('mouseup', clearMouseEvent, false)
    },
  }

  const changeMoveing = (bool = false) => {
    isMoving.value = bool
  }

  // 鼠标事件监听
  const mouseDownEventListenerHandle = (e: MouseEvent) => {
    e?.stopPropagation && e.stopPropagation()
    e?.preventDefault && e.preventDefault()

    changeMoveing(true)

    // 1.获取拖拽盒子有定位的父节点距离浏览器的距离
    let LocateParentNode = findLocateParentNode(dragBoxRef.value)
    let canvasBoxLeft = 0
    let canvvasBoxTop = 0
    if (LocateParentNode) {
      let info = LocateParentNode.getBoundingClientRect()
      canvasBoxLeft = info.left
      canvvasBoxTop = info.top
    }

    // 2.被拖拽盒子距离有定位父节点左、上的距离信息
    let boxLeft = dragBoxRef.value.offsetLeft
    let boxTop = dragBoxRef.value.offsetTop

    // 3.鼠标在被拖拽盒子中按下的位置距离信息【距离浏览器】
    let { x: mouseLeft, y: mouseTop } = tools.getToContainerXY(e)

    // 4.计算出鼠标按下点距离拖拽盒子左侧、顶部的距离 保证后续拖拽时鼠标位置相对拖拽盒子不变
    // 若发现拖动有偏移考虑是否是边框引起的
    let toBox_X = mouseLeft - boxLeft - canvasBoxLeft // 鼠标距离盒子左侧距离 鼠标距离浏览器左侧距离 - 拖拽盒子距离有定位父节点左侧距离 - 有定位父节点距离左侧距离浏览器左侧距离
    let toBox_Y = mouseTop - boxTop - canvvasBoxTop // 鼠标距离盒子顶部距离

    tools.addRemoveMouseEvent((moveE: MouseEvent) => {
      let { x, y } = tools.getToContainerXY(moveE) // 鼠标点击位置,距离画布边界的距离

      let left = x - toBox_X - canvasBoxLeft
      let top = y - toBox_Y - canvvasBoxTop

      let dragDom = dragBoxRef.value

      // 拖拽边界 拖拽盒子不允许超出定位父节点
      if(openBoundary){
        try {
          let minX = 0;
          let minY = 0
          let maxX = LocateParentNode!.clientWidth - dragDom.offsetWidth;
          let maxY = LocateParentNode!.clientHeight - dragDom.offsetHeight;
          left<minX && (left = minX)
          top<minY && (top = minY)
          left>maxX && maxX>0 && (left = maxX)
          top>maxY && maxY>0 && (top = maxY)
        } catch (error) {}
      }

      if (tools.isFunction(moveingCallback)) {
        // 有回调函数,交给外部处理
        moveingCallback && moveingCallback(
          moveE, // 鼠标event
          {
            left, // 拖动盒子现在的left 像素
            top, // 拖动盒子现在的top 像素
          },
        )
      } else {
        // 没有回调函数,直接更改

        dragDom.style.left = left + 'px'
        dragDom.style.top = top + 'px'
      }
    })
  }

  onMounted(() => {
    if (!dragBoxRef.value) return console.error('dragBoxRef 未绑定到需要移动的 dom 上!')
    dragBoxRef.value.addEventListener('mousedown', mouseDownEventListenerHandle, false)
  })
  onUnmounted(() => {
    dragBoxRef.value &&
      dragBoxRef.value.removeEventListener('mousedown', mouseDownEventListenerHandle)
  })

  return {
    dragBoxRef, // 需要拖动的盒子 ref
    isMoving, // 当前是否正在移动
  }
}

测试demo如下:

<!-- 盒子拖拽测试demo -->
<template>
  <div class="wrap">

    <!-- demo1 -->
    <p>基础demo <span class="red-span" v-show="isMoving_demo1">正在移动...</span></p>
    <div class="demoBox">
      <div :class="{ 'dragBox': true, 'move': isMoving_demo1 }" ref="dragBoxRef_demo1">
        移动盒子
      </div>
    </div>

    <!-- demo2 -->
    <p>拖动区域限定在边界范围内 <span class="red-span" v-show="isMoving_demo2">正在移动...</span></p>
    <div class="demoBox">
      <div :class="{ 'dragBox': true, 'move': isMoving_demo2 }" ref="dragBoxRef_demo2">
        移动盒子
      </div>
    </div>

    <!-- demo3 -->
    <p>使用 moveingCallback 自行处理拖动 <span class="red-span" v-show="isMoving_demo3">正在移动...</span></p>
    <div class="demoBox">
      <div :class="{ 'dragBox': true, 'move': isMoving_demo3 }" ref="dragBoxRef_demo3">
        移动盒子
      </div>
    </div>

  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import useDivDrag from './divDrag'

// demo1
const {
  dragBoxRef: dragBoxRef_demo1, // 需要拖动的盒子 ref
  isMoving: isMoving_demo1, // 当前是否正在移动
} = useDivDrag()

// demo2
const {
  dragBoxRef: dragBoxRef_demo2, // 需要拖动的盒子 ref
  isMoving: isMoving_demo2, // 当前是否正在移动
} = useDivDrag({
  openBoundary: true // 开启边界条件【将拖拽盒子限制在定位父节点范围内】
})

// demo3
const {
  dragBoxRef: dragBoxRef_demo3, // 需要拖动的盒子 ref
  isMoving: isMoving_demo3, // 当前是否正在移动
} = useDivDrag({
  // openBoundary: true, // 开启边界条件【将拖拽盒子限制在定位父节点范围内】
  moveEndCallback: () => {
    console.log('拖动结束!')
  },
  moveingCallback: (e, { left, top }) => {
    dragBoxRef_demo3.value.style.left = left + 'px'
    dragBoxRef_demo3.value.style.top = top + 'px'
  }
})

</script>

<style scoped lang="scss">
p {
  font-weight: bold;
}

.red-span {
  margin-left: 10px;
  color: red;
  text-shadow: 4px 4px 10px #000000;
  font-weight: normal;
}

.demoBox {
  width: 500px;
  height: 300px;
  margin: 20px auto;
  border: 1px solid #000000;
  box-sizing: border-box;
  position: relative;
}

.dragBox {
  width: 80px;
  height: 60px;
  border: 1px solid #dddddd;
  box-sizing: border-box;
  position: absolute;
  left: 0;
  top: 0;
  cursor: move;
  display: flex;
  justify-content: center;
  align-items: center;

  &.move {
    box-shadow: rgb(1, 10, 21) 0px 0px 16px;
    z-index: 9;
  }
}
</style>

效果为,也就是上面的gif:


image.png

可以自行决定是否开启边界限定!

image.png

需要注意的是:在鼠标按下,或者拖拽过程中动态更改鼠标状态,比如从cursor:default改为了cursor: move,可能不会生效,可以使用蒙层的方式替代,比如覆盖一层透明div,鼠标按下的时候隐藏蒙层达到切换鼠标cursor的目的,这里只是提供一种思路,具体大家可以自行尝试。

本文原创,若对你有帮助,请点个赞吧,若能打赏不胜感激,谢谢支持!
本文地址:https://www.jianshu.com/p/f05be231b1fd,转载请注明出处,谢谢。

上一篇下一篇

猜你喜欢

热点阅读