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

前言

相比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,转载请注明出处,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 课程目标: 学会使用CSS选择器熟记CSS样式和外观属性熟练掌握CSS各种选择器熟练掌握CSS各种选择器熟练掌握C...
    兰为鹏阅读 524评论 0 1
  • # CSS样式规则overflow 使用HTML时,需要遵从一定的规范。CSS亦如此,要想熟练地使用CSS对网页进...
    低调迷人的反派角色阅读 998评论 0 1
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,400评论 1 45
  • 课程目标: 学会使用CSS选择器熟记CSS样式和外观属性熟练掌握CSS各种选择器熟练掌握CSS各种选择器熟练掌握C...
    sunny688阅读 388评论 0 1
  • 盒子模型(CSS重点) 其实,CSS就三个大模块: 盒子模型 、 浮动 、 定位,其余的都是细节。要求这三部分,...
    xlystar阅读 1,519评论 0 1