原生js实现元素拖动

3,151 阅读7分钟

前言

最近工作中有个需要拖动元素的需求,而且只能通过原生来实现,需求完成之后,我把这个拖动的逻辑抽离出来。

拖动模型示例

拖动模型.gif

思路

主要思路

计算到鼠标移动的距离(从0开始增加),然后把移动的距离设置到元素的top,left属性(或transform: translate(x,y))上。

具体思路

  1. 监听目标元素上的鼠标按下事件mousedown

    1. 标记鼠标按下状态isMousedown=true
    2. 记录开始x,y轴的起始位置startXstartY
    3. 将x,y轴的移动距离清零
  2. 监听document上鼠标移动事件mousemove,记录鼠标移动的距离

    1. 计算移动距离,移动距离 = 鼠标当前x,y坐标 - 鼠标起始x,y坐标

      1. 例如moveInsX = pageX - startXmoveInsY = pageY - startY
  3. 监听document上鼠标松开事件mouseup,标记鼠标松开状态isMousedown=false

    1. 标记鼠标松开状态isMousedown=false
    2. 将开始x,y轴的起始位置清零startX = 0startY = 0
  4. 上面的步骤可以获取到鼠标移动的x,y的距离moveInsXmoveInsY(从0开始增加)

  5. 将鼠标移动的moveInsXmoveInsY加到目标元素的top,left属性(或transform: translate(x,y))上

  6. 提供一个销毁方法,移除所有监听事件

代码设计

计算鼠标的移动距离(从0开始增加)

/**
 * 拖动模型
 * */
class DragMoveModel {
  startX = 0 // 按下的鼠标x值
  startY = 0 // 按下的鼠标y值
  moveInsX = 0 // 移动的x的值(从0开始累加)
  moveInsY = 0 // 移动的y的值(从0开始累加)
  isMousedown = false // 是否按下鼠标
​
  constructor() {
    this._initEvent()
  }
​
  // 鼠标移动事件
  _mousemoveHandler = (e) => {
    if (this.isMousedown) {
      // 往左
      if (e.pageX < this.startX) {
        this.moveInsX = e.pageX - this.startX
      }
      // 往右
      if (e.pageX > this.startX) {
        this.moveInsX = e.pageX - this.startX
      }
      // 往上
      if (e.pageY < this.startY) {
        this.moveInsY = e.pageY - this.startY
      }
      // 往下
      if (e.pageY > this.startY) {
        this.moveInsY = e.pageY - this.startY
      }
      console.log('moveInsX', this.moveInsX, 'moveInsY', this.moveInsY)
    }
  }
​
  // 鼠标按下事件
  _mousedownHandler = (e) => {
    this.startX = e.pageX // 记录鼠标起始位置x
    this.startY = e.pageY // 记录鼠标起始位置y
    this.moveInsX = 0 // 将x轴移动距离清零
    this.moveInsY = 0 // 将y轴移动距离清零
    this.isMousedown = true // 标记鼠标按下状态
  }
​
  // 鼠标松开事件
  _mouseupHandler = (e) => {
    this.isMousedown = false // 标记鼠标松开状态
    this.startX = 0 // 将x轴鼠标起始位置清零
    this.startY = 0 // 将y轴鼠标起始位置清零
  }
​
  // 初始化监听事件
  _initEvent() {
    document.addEventListener('mousemove', this._mousemoveHandler)
    document.addEventListener('mousedown', this._mousedownHandler)
    document.addEventListener('mouseup', this._mouseupHandler)
  }
}
​
const moveModel = new DragMoveModel()

先看效果: move.gif

主要逻辑:

  1. 监听document上的鼠标按下事件mousedown

    1. 标记鼠标按下状态isMousedown=true
    2. 记录开始x,y轴的起始位置startXstartY
    3. 将x,y轴的移动距离清零
  2. 监听document上鼠标移动事件mousemove,记录鼠标移动的距离

    1. 计算移动距离,移动距离 = 鼠标当前x,y坐标 - 鼠标起始x,y坐标

      1. 例如moveInsX = pageX - startXmoveInsY = pageY - startY
  3. 监听document上鼠标松开事件mouseup,标记鼠标松开状态isMousedown=false

    1. 标记鼠标松开状态isMousedown=false
    2. 将开始x,y轴的起始位置清零startX = 0startY = 0

代码链接:dragmove-demo-step-1 现在我们已经获取到鼠标移动的x,y的距离 接下来就是把这个距离加到目标移动元素上

把移动距离加到目标元素

先创建个div元素

<div id="rect-1" class="rect">1</div><style>
  * {
    padding: 0;
    margin: 0;
  }
​
  .rect {
    width: 50px;
    height: 50px;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: default;
    user-select: none;
    position: relative;
  }
​
  #rect-1 {
    background-color: red;
  }
</style>

接下来给div元素实现可移动功能 继续编写DragMoveModel类

/**
 * 拖动模型
 * */
class DragMoveModel {
    // ...
  targetEl = null // 目标元素
​
  constructor(config = {}) {
    this._initConfig(config)
    this._initEvent()
  }
​
  // ...
​
  _initConfig(config) {
    this.targetEl = config.targetEl || document.body
  }
​
  // 初始化监听事件
  _initEvent() {
    document.addEventListener('mousemove', this._mousemoveHandler)
    this.targetEl && this.targetEl.addEventListener('mousedown', this._mousedownHandler)
    document.addEventListener('mouseup', this._mouseupHandler)
  }
}
​
const targetEl = document.getElementById('rect-1')
const moveModel = new DragMoveModel({targetEl})

这里的改动

  1. 增加了一个targetEl属性
  2. 在构造函数中增加一个config对象参数,用于初始化配置
  3. mousedown的监听事件绑定到targetEl
  4. 然后就可以实现按住目标元素,再移动鼠标计算移动距离

move (1).gif

代码连接:dragmove-demo-step-3 接下来,要把移动距离设置到目标元素上,实现移动。

实现元素移动

方式一:使用transform的方式

实现移动

这里以transform: translate的方式来实现 在mousemove事件中给目标元素加上transform: translate

// translate移动元素
_translateMoveEl() {
  if (this.targetEl) {
    this.targetEl.style.transform = `translate(${this.moveInsX}px, ${this.moveInsY}px)`
  }
}
​
// 鼠标移动事件
_mousemoveHandler = (e) => {
  if (this.isMousedown) {
    // ...
    // ...
    this._translateMoveEl()
  }
}

dragmove.gif

但是这里有个问题,第二次点击拖动的时候,目标元素会跑回原点 dragmove (1).gif 需要处理一下,先分析一下原因,主要是因为我们计算的是鼠标移动的距离(从0开始计算), 所以直接取到moveInsXmoveInsY设置到translate是有问题的。 那么我们需要做什么呢?需要做的是

  1. 鼠标按下(mousedown)的时候,需要先计算目标元素已有的translate的值targetElTxtargetElTy
  2. 再在鼠标移动(mousemove)的时候,设置translate的值为targetElTx + moveInsXtargetElTy + moveInsY

代码如下:

/**
 * 拖动模型
 * */
class DragMoveModel {
    // ...
  targetElTx = 0 // 目标元素的translate的x的值 新增这个属性
  targetElTy = 0 // 目标元素的translate的y的值 新增这个属性
​
  // ...
​
  // 工具函数:获取style的transform的属性值translate
  _getStyleTransformProp(transform = '', prop = 'translate') {
    transform = transform.replaceAll(', ', ',').trim()
    let strArr = transform.split(' ')
    let res = ''
    strArr.forEach(str => {
      if (str.includes(prop)) {
        res = str
      }
    })
    return res
  }
​
  // 工具函数:计算元素的translate的值
  _calcTargetTranlate = () => {
    if (this.targetEl) {
      let translate = this._getStyleTransformProp(this.targetEl.style.transform, 'translate')
      if (translate.includes('translate')) {
        let reg = /((.*))/g
        let res = reg.exec(translate)
        if (res) {
          translate = res[1].replaceAll(', ', ',')
        }
        let translateArr = translate.replace('(', '').replace(')', '').split(',')
        this.targetElTx = +translateArr[0].replace('px', '') || 0
        this.targetElTy = +translateArr[1].replace('px', '') || 0
      }
    }
  }
​
  // translate移动元素
  _translateMoveEl() {
    if (this.targetEl) {
      let tx = this.targetElTx + this.moveInsX // 重新计算x
      let ty = this.targetElTy + this.moveInsY // 重新计算y
      this.targetEl.style.transform = `translate(${tx}px, ${ty}px)`
    }
  }
​
​
  // 鼠标移动事件
  _mousemoveHandler = (e) => {
    if (this.isMousedown) {
      // ...
      this._translateMoveEl()
    }
  }
​
  // 鼠标按下事件
  _mousedownHandler = (e) => {
    // ...
​
    // 计算目标元素的translate的值
    this._calcTargetTranlate()
  }
}
​

这里主要的逻辑是

  1. 鼠标按下时调用_calcTargetTranlate()计算目标元素的旧的translate的值targetElTxtargetElTy

    1. 这里写了两个工具函数

      1. 工具函数_calcTargetTranlate():计算元素的translate的值
      2. 工具函数_getStyleTransformProp():获取style的transform的属性值translate
  2. _translateMoveEl()里重新计算translate的x和y值

    1. tx = targetElTx + moveInsX
    2. ty = targetElTy + moveInsY

然后看下效果: dragmove (2).gif

优化拖动速度

然后这里可以做个优化,开启translate的第3个属性,再给目标元素的style加上will-chage属性

  • transform: translate3d(x, y, z)
  • will-change: transform

CSS 属性 will-change 为 web 开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。 ——MDN

优化后的代码

this.targetEl.style.transform = `translate3d(${tx}px, ${ty}px, 0px)`
this.targetEl.style['will-change'] = 'transform'
优化设置transform属性

上面设置transform的时候我们都是直接赋值的,如果transform有其他属性的时候,例如scale,这样会丢掉原本的属性,所以需要处理这种情况。

// 工具函数:设置transform属性
_setTransformProp(transform = '', prop = '', value = '') {
  let reg = new RegExp(`${prop}((.*))`, 'g')
  if(transform.includes(prop)) {
    let propList = transform.replaceAll(', ', ',').trim().split(' ')
    let newPropList = propList.map(item => item.replaceAll(reg, `${prop}(${value})`))
    transform = newPropList.join(' ')
  } else {
    transform = `${prop}(${value}) ` + transform
  }
  return transform
}
​
// translate移动元素
_translateMoveEl() {
  if (this.targetEl) {
    // ...
​
    let transform = this.targetEl.style.transform
    transform = transform ? this._setTransformProp(transform, 'translate3d', `${tx}px, ${ty}px, 0px`) : `translate3d(${tx}px, ${ty}px, 0px)`
    this.targetEl.style.transform = transform
  }
}

新增工具函数_setTransformProp()用于设置transform属性,这样就可以保留原来的属性了

相关代码:dragmove-demo-step-4

限制移动边界

前面实现可移动了,但是元素可以移出浏览器窗口,因此我们可以加个限制边界的功能 代码如下:

/**
 * 拖动模型
 * */
class DragMoveModel {
    // ...
  initTargetElTop = 0 // 目标元素的初始top值
  initTargetElLeft = 0 // 目标元素的初始left值
  limitMoveBorder = false // 限制移动边界
​
  constructor(config = {}) {
    // ...
    this._initConfig(config)
    this._initTragetElInfo()
  }
​
  // 初始化配置
  _initConfig(config) {
    // ...
    this.limitMoveBorder = !!config.limitMoveBorder
  }
​
  // 初始化目标元素相关信息
  _initTragetElInfo() {
    if (this.targetEl) {
      const { top, left } = this.targetEl.getBoundingClientRect()
      this.initTargetElTop = top
      this.initTargetElLeft = left
    }
  }
​
  // translate移动元素
  _translateMoveEl() {
    if (this.targetEl) {
      let tx = this.targetElTx + this.moveInsX
      let ty = this.targetElTy + this.moveInsY
​
      // 工具函数:限制移动边界
      const limitBorder = () => {
        const { width, height } = this.targetEl.getBoundingClientRect()
        if (tx + width > window.innerWidth) { // 限制右边界
          tx = window.innerWidth - width - this.initTargetElLeft // 窗口宽度-元素宽度-元素初始时的左偏移距离
        }
        if (tx < -this.initTargetElLeft) { // 限制左边界
          tx = -this.initTargetElLeft
        }
        if (ty + height > window.innerHeight) { // 限制下边界
          ty = window.innerHeight - height - this.initTargetElTop
        }
        if (ty < -this.initTargetElTop) { // 限制上边界
          ty = -this.initTargetElTop
        }
      }
​
      if (this.limitMoveBorder) { // 限制移动边界
        limitBorder()
      }
​
      this.targetEl.style.transform = `translate3d(${tx}px, ${ty}px, 0px)` // 优化移动速度
      this.targetEl.style['will-change'] = 'transform' // 优化移动速度
    }
  }
​
    // ...
}
​
const targetEl = document.getElementById('rect-1')
const moveModel = new DragMoveModel({ targetEl: targetEl, limitMoveBorder: true })

这里的主要逻辑:

  1. 新增limitMoveBorder配置,用于开启移动边界限制

  2. 计算初始化目标元素相关信息,初始时top偏移量initTargetElTop和left偏移量initTargetElLeft

  3. _translateMoveEl()增加一个工具函数limitBorder(),限制四个边界

    1. tx + width + initTargetElLeft > window.innerWidth表示超出右边界,将tx赋值为window.innerWidth - width - initTargetElLeft
    2. tx < -initTargetElLeft表示超出左边界,将tx赋值为-initTargetElLeft
    3. ty + height + initTargetElTop > window.innerHeight表示超出下边界,将ty赋值为window.innerHeight - height - initTargetElTop
    4. ty < -initTargetElTop表示超出上边界,将ty赋值为-initTargetElTop
  4. 如果limitMoveBorder=true则执行工具函数limitBorder()

上面的逻辑挺饶的,需要慢慢理解

理解右边界限制

为了好理解,我调整一下目标元素的初始位置

#rect-1 {
  background-color: red;
  position: absolute;
  top: 100px;
  left: 100px;
}

image.png 接下来将元素移动到最右边 image.png 所以可以得出结论,tx + width + initTargetElLeft > window.innerWidth表示元素超出右边界,此时设置元素的宽度

// tx = 窗口宽度-元素宽度-元素初始时的左偏移距离
tx = window.innerWidth - width - this.initTargetElLeft

下边界处理方式同理

理解左边界限制

将元素移动到最左边 image.png 此时tx = -100pxinitTargetElLeft = 100px,所以可以得出结论,如果tx再小,将其设置为-initTargetElLeft

// 取初始时left的负值
tx = -this.initTargetElLeft

上边界计算同理

看效果 dragmove (3).gif

相关代码:dragmove-demo-step-5

方式二:top,left的方式

实现移动

改动代码如下:

<div id="rect-2" class="rect">2</div>
/**
 * 拖动模型
 * */
class DragMoveModel {
  // ...
  moveMode = 'transform' // transform为transform-translate方式移动,position为top,left方式移动
​
  // ...
​
  // 使用top,left的方式移动元素
  _topLeftMoveTargetEl = () => {
    let left = this.moveInsX + this.initTargetElLeft
    let top = this.moveInsY + this.initTargetElTop
​
    this.targetEl.style.left = left + 'px'
    this.targetEl.style.top = top + 'px'
  }
​
  // 鼠标移动事件
  _mousemoveHandler = (e) => {
    if (this.isMousedown) {
      // ...
      
      if(this.moveMode === 'position') {
        this._topLeftMoveTargetEl()
      }else {
        this._translateMoveEl()
      }
    }
  }
​
  // ...
}
​
// const targetEl = document.getElementById('rect-1')
// const moveModel = new DragMoveModel({ targetEl: targetEl, limitMoveBorder: true })const targetEl2 = document.getElementById('rect-2')
const moveModel2 = new DragMoveModel({ targetEl: targetEl2, moveMode: 'position', limitMoveBorder: true  })

这里的逻辑如下:

  1. 新增一个moveMode配置标记,transform为transform-translate方式移动,position为top,left方式移动

  2. 新增一个方法_topLeftMoveTargetEl(),计算新的top,left

    1. left = moveInsX + initTargetElLeft
    2. top = moveInsY + initTargetElTop
  3. 再把新的left,top设置到目标元素style上

  4. 在鼠标移动事件_mousemoveHandler()增加判断,调用哪个移动方式

dragmove (4).gif 这里同样会有第2次点击拖动时,元素回到原点的问题 dragmove (5).gif 解决方式是,鼠标按下时,重新计算一下目标元素的信息

// 鼠标按下事件
_mousedownHandler = (e) => {
  // ...
​
  if (this.moveMode === 'position') {
    this._initTragetElInfo()
  }
}

dragmove (6).gif

限制移动边界

主要改造_topLeftMoveTargetEl()方法

// 使用top,left的方式移动元素
_topLeftMoveTargetEl = () => {
  let left = this.moveInsX + this.initTargetElLeft
  let top = this.moveInsY + this.initTargetElTop
​
  // 工具函数:限制移动边界
  const limitBorder = () => {
    const { width, height } = this.targetEl.getBoundingClientRect()
​
    if (top < 0) {
      top = 0
    }
    if (top > (window.innerHeight - height)) {
      top = window.innerHeight - height
    }
    if (left < 0) {
      left = 0
    }
    if (left > (window.innerWidth - width)) {
      left = window.innerWidth - width
    }
  }
  if (this.limitMoveBorder) {
    limitBorder()
  }
  this.targetEl.style.left = left + 'px'
  this.targetEl.style.top = top + 'px'
}

dragmove (7).gif

兼容H5

把监听事件替换为touch相关的事件即可

// 初始化监听事件
_initEvent() {
  const moveEvent = this.h5 ? 'touchmove' : 'mousemove'
  const downEvent = this.h5 ?  'touchstart' : 'mousedown'
  const upEvent = this.h5 ? 'touchend' : 'mouseup'
  document.addEventListener(moveEvent, this._mousemoveHandler)
  this.targetEl && this.targetEl.addEventListener(downEvent, this._mousedownHandler)
  document.addEventListener(upEvent, this._mouseupHandler)
}

提供销毁方法

提供一个销毁方法,在元素使用完之后,移除监听事件

// 销毁方法
destroy() {
  this.targetEl && this.targetEl.removeEventListener('mousedown', this._mousedownHandler)
  this.document.removeEventListener('mousemove', this._mousemoveHandler)
  this.document.removeEventListener('mouseup', this._mouseupHandler)
}

其他用途

这个类可以获取到鼠标移动的距离,然后我们可以将这个移动距离暴露出去,可以实现其他用途 通过callback将获取到的移动距离暴露出去

/**
 * 拖动模型
 * */
class DragMoveModel {
  // ...
  callback = null // 回调函数,用于获取鼠标移动距离
​
  constructor(config = {}, callback = () => {}) {
    // ...
    this.callback = callback
  }
​
  // 鼠标移动事件
  _mousemoveHandler = (e) => {
    if (this.isMousedown) {
      // ...
      // 计算第三边的长度(勾股定理 a^2 + b^2 = c^2)
      let c = Math.round(Math.pow((this.moveInsX * this.moveInsX + this.moveInsY * this.moveInsY), 0.5))
      this.callback(this.moveInsX, this.moveInsY, c)
    }
  }
​
  // ...
}
​

image.png 计算到第三边的距离可以用于缩放

完整代码

链接:dragmove-demo

<!DOCTYPE html>
<html lang="en">
​
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      padding: 0;
      margin: 0;
    }
​
    .rect {
      width: 50px;
      height: 50px;
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: default;
      user-select: none;
      position: relative;
    }
​
    #rect-1 {
      background-color: red;
    }
​
    #rect-2 {
      background-color: black;
      position: absolute;
      top: 100px;
      left: 100px;
    }
  </style>
</head>
​
<body>
  <div id="rect-1" class="rect">1</div>
  <div id="rect-2" class="rect">2</div>
  <script>
    /**
     * 拖动模型
     * */
    class DragMoveModel {
      startX = 0 // 按下的鼠标x值
      startY = 0 // 按下的鼠标y值
      moveInsX = 0 // 移动的x的值(从0开始累加)
      moveInsY = 0 // 移动的y的值(从0开始累加)
      isMousedown = false // 是否按下鼠标
      targetEl = null // 目标元素
      targetElTx = 0 // 目标元素的translate的x的值
      targetElTy = 0 // 目标元素的translate的y的值
      initTargetElTop = 0 // 目标元素的初始top值
      initTargetElLeft = 0 // 目标元素的初始left值
      limitMoveBorder = false // 限制移动边界
      moveMode = 'transform' // transform为transform-translate方式移动,position为top,left方式移动
      callback = null // 回调函数,用于获取鼠标移动距离
      h5 = false // 是否用于h5
​
      constructor(config = {}, callback = () => {}) {
        this._initConfig(config)
        this._initEvent()
        this._initTragetElInfo()
        this.callback = callback
      }
​
      // 初始化配置
      _initConfig(config) {
        this.targetEl = config.targetEl || document.body
        this.limitMoveBorder = !!config.limitMoveBorder
        this.moveMode = config.moveMode || 'transform'
        this.h5 = !!config.h5
      }
​
      // 初始化目标元素相关信息
      _initTragetElInfo() {
        if (this.targetEl) {
          const { top, left } = this.targetEl.getBoundingClientRect()
          this.initTargetElTop = top
          this.initTargetElLeft = left
          this.targetEl.style['will-change'] = this.moveMode === 'transform' ? 'transform' : 'left, top'
        }
      }
​
      // 获取style的transform的属性值translate
      _getStyleTransformProp(transform = '', prop = 'scale') {
        transform = transform.replaceAll(', ', ',').trim()
        let strArr = transform.split(' ')
        let res = ''
        strArr.forEach(str => {
          if (str.includes(prop)) {
            res = str
          }
        })
        return res
      }
​
      // 计算元素的translate的值
      _calcTargetTranlate = () => {
        if (this.targetEl) {
          let translate = this._getStyleTransformProp(this.targetEl.style.transform, 'translate3d')
          if (translate.includes('translate3d')) {
            let reg = /((.*))/g
            let res = reg.exec(translate)
            if (res) {
              translate = res[1].replaceAll(', ', ',')
            }
            let translateArr = translate.replace('(', '').replace(')', '').split(',')
            this.targetElTx = +translateArr[0].replace('px', '') || 0
            this.targetElTy = +translateArr[1].replace('px', '') || 0
          }
        }
      }
​
      // 设置transform属性
      _setTransformProp(transform = '', prop = '', value = '') {
        let reg = new RegExp(`${prop}((.*))`, 'g')
        if(transform.includes(prop)) {
          let propList = transform.replaceAll(', ', ',').trim().split(' ')
          let newPropList = propList.map(item => item.replaceAll(reg, `${prop}(${value})`))
          transform = newPropList.join(' ')
        } else {
          transform = `${prop}(${value}) ` + transform
        }
        return transform
      }
​
      // translate移动元素
      _translateMoveEl() {
        if (this.targetEl) {
          let tx = this.targetElTx + this.moveInsX
          let ty = this.targetElTy + this.moveInsY
​
          // 工具函数:限制移动边界
          const limitBorder = () => {
            const { width, height } = this.targetEl.getBoundingClientRect()
            if (tx + width + this.initTargetElLeft > window.innerWidth) { // 限制右边界
              tx = window.innerWidth - width - this.initTargetElLeft // 窗口宽度-元素宽度-元素初始时的左偏移距离
            }
            if (tx < -this.initTargetElLeft) { // 限制左边界
              tx = -this.initTargetElLeft
            }
            if (ty + height + this.initTargetElTop > window.innerHeight) { // 限制下边界
              ty = window.innerHeight - height - this.initTargetElTop
            }
            if (ty < -this.initTargetElTop) { // 限制上边界
              ty = -this.initTargetElTop
            }
          }
​
          if (this.limitMoveBorder) {
            limitBorder()
          }
​
          let transform = this.targetEl.style.transform
          transform = transform ? this._setTransformProp(transform, 'translate3d', `${tx}px, ${ty}px, 0px`) : `translate3d(${tx}px, ${ty}px, 0px)`
          this.targetEl.style.transform = transform
        }
      }
​
      // 使用top,left的方式移动元素
      _topLeftMoveTargetEl = () => {
        let left = this.moveInsX + this.initTargetElLeft
        let top = this.moveInsY + this.initTargetElTop
​
        // 工具函数:限制移动边界
        const limitBorder = () => {
          const { width, height } = this.targetEl.getBoundingClientRect()
​
          if (top < 0) {
            top = 0
          }
          if (top > (window.innerHeight - height)) {
            top = window.innerHeight - height
          }
          if (left < 0) {
            left = 0
          }
          if (left > (window.innerWidth - width)) {
            left = window.innerWidth - width
          }
        }
        if (this.limitMoveBorder) {
          limitBorder()
        }
        this.targetEl.style.left = left + 'px'
        this.targetEl.style.top = top + 'px'
      }
​
      // 鼠标移动事件
      _mousemoveHandler = (e) => {
        const pageX = this.h5 ? e.changedTouches[0].pageX : e.pageX
        const pageY = this.h5 ? e.changedTouches[0].pageY : e.pageY
        if (this.isMousedown) {
          // 往左
          if (pageX < this.startX) {
            this.moveInsX = pageX - this.startX
          }
          // 往右
          if (pageX > this.startX) {
            this.moveInsX = pageX - this.startX
          }
          // 往上
          if (pageY < this.startY) {
            this.moveInsY = pageY - this.startY
          }
          // 往下
          if (pageY > this.startY) {
            this.moveInsY = pageY - this.startY
          }
          // console.log('moveInsX', this.moveInsX, 'moveInsY', this.moveInsY)
          if(this.moveMode === 'position') {
            this._topLeftMoveTargetEl()
          }else {
            this._translateMoveEl()
          }
          // 计算第三边的长度(勾股定理 a^2 + b^2 = c^2)
          let c = Math.round(Math.pow((this.moveInsX * this.moveInsX + this.moveInsY * this.moveInsY), 0.5))
          this.callback(this.moveInsX, this.moveInsY, c)
        }
      }
​
      // 鼠标按下事件
      _mousedownHandler = (e) => {
        const pageX = this.h5 ? e.changedTouches[0].pageX : e.pageX
        const pageY = this.h5 ? e.changedTouches[0].pageY : e.pageY
        this.startX = pageX // 记录鼠标起始位置x
        this.startY = pageY // 记录鼠标起始位置y
        this.moveInsX = 0 // 将x轴移动距离清零
        this.moveInsY = 0 // 将y轴移动距离清零
        this.isMousedown = true // 标记鼠标按下状态
​
        // 计算目标元素的translate的值
        this._calcTargetTranlate()
​
        if (this.moveMode === 'position') {
          this._initTragetElInfo()
        }
      }
​
      // 鼠标松开事件
      _mouseupHandler = (e) => {
        this.isMousedown = false // 标记鼠标松开状态
        this.startX = 0 // 将x轴鼠标起始位置清零
        this.startY = 0 // 将y轴鼠标起始位置清零
      }
​
      // 初始化监听事件
      _initEvent() {
        const moveEvent = this.h5 ? 'touchmove' : 'mousemove'
        const downEvent = this.h5 ?  'touchstart' : 'mousedown'
        const upEvent = this.h5 ? 'touchend' : 'mouseup'
        document.addEventListener(moveEvent, this._mousemoveHandler)
        this.targetEl && this.targetEl.addEventListener(downEvent, this._mousedownHandler)
        document.addEventListener(upEvent, this._mouseupHandler)
      }
​
      // 销毁方法
      destroy() {
        const moveEvent = this.h5 ? 'touchmove' : 'mousemove'
        const downEvent = this.h5 ?  'touchstart' : 'mousedown'
        const upEvent = this.h5 ? 'touchend' : 'mouseup'
        this.targetEl && this.targetEl.removeEventListener(moveEvent, this._mousedownHandler)
        this.document.removeEventListener(downEvent, this._mousemoveHandler)
        this.document.removeEventListener(upEvent, this._mouseupHandler)
      }
    }
​
    const targetEl = document.getElementById('rect-1')
    const moveModel = new DragMoveModel({ targetEl: targetEl, limitMoveBorder: true })
​
    const targetEl2 = document.getElementById('rect-2')
    const moveModel2 = new DragMoveModel({ targetEl: targetEl2, moveMode: 'position', limitMoveBorder: true  })
​
  </script>
</body>
​
</html>

使用文档

<div id="rect-1" class="rect">1</div>
​
const targetEl = document.getElementById('rect-1')
const moveModel = new DragMoveModel({ targetEl: targetEl }, (x, y, z) => console.log(x, y, z))

构造函数初始化参数

  • config,个性化配置
  • callback, 回调函数,获取鼠标移动距离

config参数配置

属性说明类型默认值可选值
targetEl目标元素,需要拖动的元素HTMLElementdocument.body
limitMoveBorder是否限制拖动边界Booleanfalse
moveMode拖动实现方式,transform为transform-translate方式移动,position为top,left方式移动Stringtransformtransform,position
h5是否是h5Booleanfalse

销毁方法

moveModel.destroy()