【每日编程】- 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>

结果展示

代码参考

相关推荐
上海运维Q先生18 分钟前
面试题整理14----kube-proxy有什么作用
运维·面试·kubernetes
叫我菜菜就好23 分钟前
【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理
前端·网络·flutter
NoneCoder28 分钟前
CSS系列(26)-- 动画性能优化详解
前端·css·性能优化
滚雪球~29 分钟前
@vue/cli启动异常:ENOENT: no such file or directory, scandir
前端·javascript·vue.js
GDAL39 分钟前
vue3入门教程:ref函数
前端·vue.js·elementui
GISer_Jing1 小时前
Vue3状态管理——Pinia
前端·javascript·vue.js
好开心331 小时前
axios的使用
开发语言·前端·javascript·前端框架·html
Domain-zhuo1 小时前
Git常用命令
前端·git·gitee·github·gitea·gitcode
开发者每周简报1 小时前
求职市场变化
人工智能·面试·职场和发展
菜根Sec2 小时前
XSS跨站脚本攻击漏洞练习
前端·xss