【每日编程】- JS实现元素拖拽

实现DOM元素拖拽应该考虑以下几个问题

  1. 监听的鼠标事件
  2. 拖拽定位父元素的兼容
  3. 拖拽超出父元素范围的处理
  4. 鼠标始终定位在被拖拽元素的中心点 - 合理优化

几个【距离】属性

在正式开始写逻辑前,首先要对几个属性要有所了解

  • 获取元素的宽/高

    • offsetWidth/offsetHeight - 获取元素的宽度/高度(包括边框和内边距)
    • clientWidth/clientHeigh - 获取元素的宽度/高度(不包括边框)
    • clientLeft/clientTop - 获取左/上边框大小
  • 鼠标位置

    • clientX/clientY - 鼠标相对于浏览器窗口左上角为坐标原点,X/Y轴的距离
  • getBoundingClientRect()

    • 返回值是一个 [DOMRect]对象,是包含整个元素的最小矩形(包括 paddingborder-width)。该对象使用 lefttoprightbottomxywidthheight 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 widthheight 以外的属性是相对于视图窗口的左上角来计算

几个【鼠标事件】监听

  • mousedown

    • 鼠标【按下】事件
    • 在这个事件中进行:
      1. 去获取子元素的DOMRect
      2. 开启拖拽标识
  • mouseup

    • 鼠标【松开】事件
    • 在这个事件中进行:
      1. 关闭拖拽标识
      2. 收尾工作
  • mousemove

    • 鼠标【移动】事件
    • 在这个事件中进行:
      1. 改变拖拽元素的定位
      2. 处理拖拽元素超出范围问题

"拖拽对象" - dragDomObj

为了方便属性的调用,创建并暴露一个"拖拽对象" - dragDomObj。这个对象存储公共属性、入口方法。

js 复制代码
 const dragObjCom = {
    dragObj: null, // 被拖拽元素
    parentObj: null, // 被拖拽元素的父元素
    dragFlag: false, // 拖拽开始的标识
    topDistance: 0, // 被拖拽元素的top距离
    leftDistance: 0, // 被拖拽元素的left距离
    rectOfDragObj: null, // 获取被拖拽DOM的视口边缘距离 top right bottom left

    /*
    DOM元素拖拽函数封装
    @param { object } dragObj 被拖拽元素
    @param { object } parentObj 被拖拽元素的父元素
    */
    dragDomFunc(dragObj, parentObj = document.body) {
        this.dragObj = dragObj
        this.parentObj = parentObj

        // 监听鼠标按下事件
        document.addEventListener('mousedown', event => this.mouseDownforDrag(event))
        // 监听鼠标松开事件
        document.addEventListener('mouseup', () => this.mouseUpForDrag())
        // 监听鼠标移动事件
        document.addEventListener('mousemove', event => this.mouseMoveForDrag(event))
    }
 }

在这里之所以要传入参数parentObj 被拖拽元素的父元素,是为了兼容父元素是document.body自定义父元素两种情况

mouseDownforDrag 的实现

js 复制代码
// 鼠标按下方法
mouseDownforDrag(event) {
    // event不是被拖拽元素时,不作处理
    if (event.target !== this.dragObj) return
    
    this.dragFlag = true; // 开始拖拽
    if (this.parentObj === document.body) {
        this.rectOfDragObj = this.dragObj.getBoundingClientRect()
    } else {
        // 当自定义传入父元素时,要自主构建一个DOMRect对象
        this.rectOfDragObj = this.handleGetBoundingClientRect()
    }
},

handleGetBoundingClientRect 的实现

js 复制代码
// getBoundingClientRect方法改写 - 当父元素不是document.body时
handleGetBoundingClientRect() {
    // 获取传入父元素的DOMRect
    const parentRectObj = this.parentObj.getBoundingClientRect();
    
    // 创建top/right/bototm/left属性
    const top = parentRectObj.top
    const right = parentRectObj.right
    const bottom = parentRectObj.bottom
    const left = parentRectObj.left 

    const boundingClientRectObj = {
        width: this.dragObj.offsetWidth,
        height: this.dragObj.offsetHeight,
        top,
        right,
        bottom,
        left,
    }
    return boundingClientRectObj
},

mouseUpForDrag 的实现

js 复制代码
// 鼠标按下方法
mouseUpForDrag() {
    this.dragFlag = false; // 拖拽停止
    
    // 做元素定位左、上是否溢出父元素的范围
    if (this.dragObj.style.top.split('px')[0] < 0) this.dragObj.style.top = '0px';
    if (this.dragObj.style.left.split('px')[0] < 0) this.dragObj.style.left = '0px';
},

mouseMoveForDrag 的实现

js 复制代码
// 鼠标移动方法
mouseMoveForDrag(e) {
    // 非拖拽状态下直接返回
    if (!this.dragFlag) return
    
    // clientX,clientY分别代表鼠标距离屏幕可视区左上角的距离
    /*
      减去被拖拽元素的宽高的1/2
      【目的】在拖动过程中鼠标始终停留在元素的中心位置
      【引发的问题】
        1. 停止拖动后,若 -top <= -rectOfDragObj.height/2, -left <= -rectOfDragObj.width/2 也识别超出可视区外,所以在监听鼠标松开时做判断
        2. 拖动过程,距离右、下距离分别为 rectOfDragObj.width/2、rectOfDragObj.height/2 时,被拖拽元素迅速定位到可视区域的宽高最大值,需要在,移出右、上的判断中减去这部分的值 
    */
    leftDistance = e.clientX - this.rectOfDragObj.width / 2
    topDistance = e.clientY - this.rectOfDragObj.height / 2

    // 处理鼠标移出屏幕可视区的判断
    let clientWidth = this.parentObj.clientWidth; // 获取父元素的可视宽度
    let clientHeight = this.parentObj.clientHeight; // 获取父元素的可视高度

    //【下面的判断都加了 this.rectOfDragObj.width(height)/2的判断,是因为上面逻辑把鼠标的位置放到拖拽元素的中心位置】
    // 移出左边区域
    if (e.clientX - this.rectOfDragObj.width / 2 <= 0) { leftDistance = 0;}
    // 移出上边区域
    if (e.clientY - this.rectOfDragObj.height / 2 <= 0) { topDistance = 0; }
    // 移出右边区域
    if (e.clientX + this.rectOfDragObj.width - this.rectOfDragObj.width / 2 > clientWidth) {
      leftDistance = clientWidth - this.rectOfDragObj.width;
    }
    // 移出下边区域
    if (e.clientY + this.rectOfDragObj.height - this.rectOfDragObj.height / 2 > clientHeight) {
      topDistance = clientHeight - this.rectOfDragObj.height;
    }

    // 改变元素定位
    this.dragObj.style.top = `${topDistance}px`
    this.dragObj.style.left = `${leftDistance}px`
}

html

html 复制代码
<div id="drag_parent">
    <div id="drag_dom"></div>
</div>

<style>
    body {
      height: 100vh;
    }
    #drag_parent {
      position: relative;
      width:300px;
      height: 200px;
      background-color: beige;
    }
    #drag_dom {
      position: absolute;
      top: 0;
      left: 0;
      width: 100px;
      height: 80px;
      background-color:chocolate;
    }
</style>

结果展示

代码参考

相关推荐
转转技术团队12 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding13 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
gnip27 分钟前
SSE技术介绍
前端·javascript
掘金安东尼35 分钟前
蔚来 600 亿研发成本,信还是不信。。
面试·程序员·github
yinke小琪42 分钟前
JavaScript DOM节点操作(增删改)常用方法
前端·javascript
枣把儿1 小时前
Vercel 收购 NuxtLabs!Nuxt UI Pro 即将免费!
前端·vue.js·nuxt.js
望获linux1 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
爱编程的喵1 小时前
从XMLHttpRequest到Fetch:前端异步请求的演进之路
前端·javascript
喜欢吃豆1 小时前
深入企业内部的MCP知识(三):FastMCP工具转换(Tool Transformation)全解析:从适配到增强的工具进化指南
java·前端·人工智能·大模型·github·mcp
豆苗学前端1 小时前
手把手实现支持百万级数据量、高可用和可扩展性的穿梭框组件
前端·javascript·面试