实现点位拖拽

1,625 阅读7分钟

0、写在前面

前两天接到一个需求:在一个盒子中可以添加自定义的图片(如地铁站台平面图),而在图片中可以添加点位标记。同时要求背景图可以进行缩放拖拽,还要求对点位进行拖拽。

考虑到整个需求的实现放在一篇文章中会比较繁杂,所以准备把需求一分为二,拆分成两篇文章:

  1. 实现普通的点位拖拽。(本文)
  2. 实现背景图片的缩放、拖拽,并在背景图片上添加点位,对点位进行拖拽。(文章链接在此,感兴趣的同学可以看看)

本文只会讨论如何实现普通的点位拖拽,即使不需要实现其他需求的同学也能愉快阅读。

1、搭建布局架构

<style>
    .outer {
      width: 800px;
      height: 600px;
      border: 1px solid pink;
      margin: 0 auto;
      position: relative;
    }
    .drag-box {
      width: 400px;
      height: 250px;
      position: absolute;
      background-image: url('https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg');
      background-size: 100%;
    }
</style>

<div class="outer">
    <div class="drag-box"></div>
</div>

如上html代码所示,outer为外层盒子,dragBox为需要进行拖拽的盒子,拖拽的范围就是outer,拖拽时不能越过outer的边界。 dragBox以outer为基准进行绝对定位。

2、实现效果及理论

我们希望实现的效果如下gif所示: 动画1.gif

归根结底,我们希望的是dragBox能够“跟着”鼠标移动,鼠标左键按下(mousedown事件)时开始拖拽,左键抬起(mouseup事件)停止拖拽。

这里我能够想到两种思路实现dragBox跟着鼠标移动:

方法一:计算鼠标移动距离,让dragBox也移动相同距离。(不需要知道dragBox应该去哪,直接通知它应该移动多少距离

方法二:左键按下时,计算dragBox边界与鼠标的距离,通过这段距离计算后续dragBox所在位置。(需要计算出dragBox应该去哪,告诉它具体位置让它过去

3、方法一实现

3.1 实现过程

使用方法一实现,我们不需要知道拖拽时dragBox应该移动到的具体坐标,只需计算鼠标移动的距离,然后让dragBox也移动相同距离就好了。

需要使用到的属性或方法:

  1. 事件对象属性:clientX,clientY:表示鼠标相对于浏览器窗口的X(Y)轴坐标。
  2. 元素对象属性:offsetWidth,offsetHeight:元素的宽高

需要使用到的公共变量:

var dragBox = document.getElementsByClassName('drag-box')[0]
var oOuter = document.getElementsByClassName('outer')[0]

// 是否处于拖拽状态,否的话不作任何处理
var isDragging = false
// 鼠标按下时的原始位置
var pointPosition = {x: 0, y: 0}
// dragBox的原始位置(让它在这基础上移动鼠标移动的距离)
var dragBoxPosition = {x: 0, y: 0}

拖拽第一步,按下左键,开启拖拽模式,记录鼠标原始位置(以便后续计算鼠标移动距离):

dragBox.addEventListener('mousedown', function(e){
  isDragging = true
  // 记录左键按下时,鼠标相对于浏览器可视区域的距离
  pointPosition.x = e.clientX
  pointPosition.y = e.clientY
  // 记录dragBox相对于outer的left和top(注意是绝对定位的left和top),转化为number类型
  dragBoxPosition.x = parseFloat(dragBox.style.left || 0)
  dragBoxPosition.y = parseFloat(dragBox.style.top || 0)
})

进行拖拽时:

  1. 关键词:鼠标移动的距离
  2. 计算鼠标相对于mousedown时,所移动的距离
  3. 让dragBox的style.left和style.top加上鼠标移动的距离。(完成拖拽)
  4. 拓展:判断dragBox是否越过outer,防止其越界。
// 值得注意的是,mousemove的事件处理函数,是绑定在document中的,
// 因为鼠标拖动速度太快时,有那么一瞬间,dragBox是跟不上鼠标的速度,
// 鼠标会处于dragBox外面,甚至出于outer外面,
// 所以这个事件处理函数需要由document来绑定
document.addEventListener('mousemove', function(e){
  // 当拖拽模式关闭时,什么都不执行
  if(!isDragging)return;
  
  // 计算鼠标相对于mousedown时,在x,y轴分别移动的距离
  var changeX = e.clientX - pointPosition.x
  var changeY = e.clientY - pointPosition.y
  // 让dragBox的style.left和style.top也移动鼠标移动的距离
  dragBox.style.left = dragBoxPosition.x + changeX + 'px'
  dragBox.style.top = dragBoxPosition.y + changeY + 'px'

  // 上述操作已完成拖拽,下面的操作是防止拖拽的div越过外层盒子
  if(dragBox.offsetLeft < 0) dragBox.style.left = 0;
  if(dragBox.offsetTop < 0) dragBox.style.top = 0;
  // 下面两个判断分别是防止dragBox右边界和上边界越界,可以将代码跑起来理解
  if(dragBox.offsetLeft > (oOuter.offsetWidth - dragBox.offsetWidth)) {
    dragBox.style.left = oOuter.offsetWidth - dragBox.offsetWidth + 'px';
  }
  if(dragBox.offsetTop > (oOuter.offsetHeight - dragBox.offsetHeight)) {
    dragBox.style.top = oOuter.offsetHeight - dragBox.offsetHeight + 'px';
  }
})

拖拽最后一步,松开左键,关闭拖拽模式:

// 值得注意的是,mouseup的事件处理函数,是绑定在document中的,
// 因为用户在拖动结束时,鼠标可能会处于outer之外,
// 所以这个事件处理函数需要由document来绑定
document.addEventListener('mouseup', function(e){
  isDragging = false
})

3.2 完整代码

<style>
    .outer {
      width: 800px;
      height: 600px;
      border: 1px solid pink;
      margin: 0 auto;
      position: relative;
    }

    .drag-box {
      width: 400px;
      height: 250px;
      position: absolute;
      background-image: url('https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg');
      background-size: 100%;
    }
  </style>

  <div class="outer">
    <div class="drag-box"></div>
  </div>

  <script type="text/javascript">
    var dragBox = document.getElementsByClassName('drag-box')[0]
    var oOuter = document.getElementsByClassName('outer')[0]

    // 是否处于拖拽状态,否的话不作任何处理
    var isDragging = false
    // 鼠标按下时的原始位置
    var pointPosition = { x: 0, y: 0 }
    // dragBox的原始位置(让它在这基础上移动鼠标移动的距离)
    var dragBoxPosition = { x: 0, y: 0 }

    dragBox.addEventListener('mousedown', function (e) {
      isDragging = true
      // 记录左键按下时,鼠标相对于浏览器可视区域的距离
      pointPosition.x = e.clientX
      pointPosition.y = e.clientY
      // 记录dragBox相对于outer的left和top(注意是绝对定位的left和top),转化为number类型
      dragBoxPosition.x = parseFloat(dragBox.style.left || 0)
      dragBoxPosition.y = parseFloat(dragBox.style.top || 0)
    })

    // 值得注意的是,mousemove的事件处理函数,是绑定在document中的,
    // 因为鼠标拖动速度太快时,有那么一瞬间,dragBox是跟不上鼠标的速度,
    // 鼠标会处于dragBox外面,甚至出于outer外面,
    // 所以这个事件处理函数需要由document来绑定
    document.addEventListener('mousemove', function (e) {
      // 当拖拽模式关闭时,什么都不执行
      if (!isDragging) return;

      // 计算鼠标相对于mousedown时,在x,y轴分别移动的距离
      var changeX = e.clientX - pointPosition.x
      var changeY = e.clientY - pointPosition.y
      // 让dragBox的style.left和style.top也移动鼠标移动的距离
      dragBox.style.left = dragBoxPosition.x + changeX + 'px'
      dragBox.style.top = dragBoxPosition.y + changeY + 'px'

      // 上述操作已完成拖拽,下面的操作是防止拖拽的div越过外层盒子
      if (dragBox.offsetLeft < 0) dragBox.style.left = 0;
      if (dragBox.offsetTop < 0) dragBox.style.top = 0;
      // 下面两个判断分别是防止dragBox右边界和上边界越界,可以将代码跑起来理解
      if (dragBox.offsetLeft > (oOuter.offsetWidth - dragBox.offsetWidth)) {
        dragBox.style.left = oOuter.offsetWidth - dragBox.offsetWidth + 'px';
      }
      if (dragBox.offsetTop > (oOuter.offsetHeight - dragBox.offsetHeight)) {
        dragBox.style.top = oOuter.offsetHeight - dragBox.offsetHeight + 'px';
      }
    })
    // 值得注意的是,mouseup的事件处理函数,是绑定在document中的,
    // 因为用户在拖动结束时,鼠标可能会处于outer之外,
    // 所以这个事件处理函数需要由document来绑定
    document.addEventListener('mouseup', function (e) {
      isDragging = false
    })
  </script>

4、方法二实现

4.1 实现过程

使用方法二实现,我们需要保存mousedown时,鼠标与dragBox边界的距离。在拖拽时需要根据鼠标的位置和其与dragBox的距离计算出dragBox的style.left和style.top(相对于outer而言)。

这个过程相比于方法一需要更多步骤,可以结合代码理解。

需要使用到的属性或方法:
元素对象方法:getBoundingClientRect:返回值是一个DOMRect,里面有很多使用属性,具体可见NDM文档。
事件对象属性:
clientX,clientY:表示鼠标相对于浏览器窗口的X(Y)轴坐标。
offsetX, offsetY: 鼠标与事件源对象(target)的x,y轴的偏移量。
元素对象属性:offsetWidth,offsetHeight:元素的宽高

公共变量:

var dragBox = document.getElementsByClassName('drag-box')[0]
var oOuter = document.getElementsByClassName('outer')[0]

// 是否处于拖拽状态
var isDragging = false
// mousedown时,鼠标与dragBox边界的距离
var distance = {x: 0, y: 0}

拖拽第一步,按下左键,记录鼠标的原始位置,以及鼠标相对于dragBox边界的距离

dragBox.addEventListener('mousedown', function(e){
  // 记录鼠标与dragBox(即事件源对象)的距离
  distance.x = e.offsetX
  distance.y = e.offsetY
  isDragging = true
})

拖拽时,我们需要得到的是dragBox的style.left和style.top(也就是它相对于outer绝对定位的left和top)。也就是下图蓝色字体的距离:

image.png

由图可知style.left = e.clientX - outer与可视区域的x边距 - 鼠标与dragBox的x边距

而我们只有outer与可视区域的x边距是未知的,但可使用getBoundingClientRect方法获取

// 值得注意的是,mousemove的事件处理函数,是绑定在document中的,
// 因为鼠标拖动速度太快时,有那么一瞬间,dragBox是跟不上鼠标的速度,
// 鼠标会处于dragBox外面,甚至出于outer外面,
// 所以这个事件处理函数需要由document来绑定
document.addEventListener('mousemove', function(e){
  if(!isDragging)return;

  // 可获取outer与可视区域的距离
  // rect.left是outer相对于浏览器可视区域的x轴距离,rect.top同理。
  // rect对象的属性不会实时变化,每次读取前都需要重新获取对象。
  var rect = oOuter.getBoundingClientRect()
  
  // 计算鼠标相对于outer的距离
  var pointToOuterX = e.clientX - rect.left
  var pointToOuterY = e.clientY - rect.top
  
  // 计算并赋值dragBox相对于outer的left和top
  dragBox.style.left = pointToOuterX - distance.x + 'px'
  dragBox.style.top = pointToOuterY - distance.y + 'px'
    
  // 上述操作已完成拖拽,下面的操作是防止拖拽的div越过外层盒子
  if(dragBox.offsetLeft < 0) dragBox.style.left = 0;
  if(dragBox.offsetTop < 0) dragBox.style.top = 0;
  // 下面两个判断分别是防止dragBox右边界和上边界越界,可以将代码跑起来理解
  if(dragBox.offsetLeft > (oOuter.offsetWidth - dragBox.offsetWidth)) {
    dragBox.style.left = oOuter.offsetWidth - dragBox.offsetWidth + 'px'
  }
  if(dragBox.offsetTop > (oOuter.offsetHeight - dragBox.offsetHeight)) {
    dragBox.style.top = oOuter.offsetHeight - dragBox.offsetHeight + 'px'
  }

})

最后一步,松开左键,关闭拖拽模式

// 值得注意的是,mouseup的事件处理函数,是绑定在document中的,
// 因为用户在拖动结束时,鼠标可能会处于outer之外,
// 所以这个事件处理函数需要由document来绑定
document.addEventListener('mouseup', function (e) {
  isDragging = false
})

4.2 完整代码

<style>
    .outer {
      width: 800px;
      height: 600px;
      border: 1px solid pink;
      margin: 0 auto;
      position: relative;
    }

    .drag-box {
      width: 400px;
      height: 250px;
      position: absolute;
      background-image: url('https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg');
      background-size: 100%;
    }
</style>

  <div class="outer">
    <div class="drag-box"></div>
  </div>

  <script>
    var dragBox = document.getElementsByClassName('drag-box')[0]
    var oOuter = document.getElementsByClassName('outer')[0]

    // 是否处于拖拽状态
    var isDragging = false
    // mousedown时,鼠标与dragBox边界的距离
    var distance = { x: 0, y: 0 }

    dragBox.addEventListener('mousedown', function (e) {
      // 记录鼠标与dragBox(即事件源对象)的距离
      distance.x = e.offsetX
      distance.y = e.offsetY

      isDragging = true
    })

    // 值得注意的是,mousemove的事件处理函数,是绑定在document中的,
    // 因为鼠标拖动速度太快时,有那么一瞬间,dragBox是跟不上鼠标的速度,
    // 鼠标会处于dragBox外面,甚至出于outer外面,
    // 所以这个事件处理函数需要由document来绑定
    document.addEventListener('mousemove', function (e) {
      if (!isDragging) return;

      // 可获取outer与可视区域的距离
      var rect = oOuter.getBoundingClientRect()

      // 计算鼠标相对于outer的距离
      var pointToOuterX = e.clientX - rect.left
      var pointToOuterY = e.clientY - rect.top

      // 计算并赋值dragBox相对于outer的left和top
      dragBox.style.left = pointToOuterX - distance.x + 'px'
      dragBox.style.top = pointToOuterY - distance.y + 'px'

      // 上述操作已完成拖拽,下面的操作是防止拖拽的div越过外层盒子
      if (dragBox.offsetLeft < 0) dragBox.style.left = 0;
      if (dragBox.offsetTop < 0) dragBox.style.top = 0;
      // 下面两个判断分别是防止dragBox右边界和上边界越界,可以将代码跑起来理解
      if (dragBox.offsetLeft > (oOuter.offsetWidth - dragBox.offsetWidth)) {
        dragBox.style.left = oOuter.offsetWidth - dragBox.offsetWidth + 'px'
      }
      if (dragBox.offsetTop > (oOuter.offsetHeight - dragBox.offsetHeight)) {
        dragBox.style.top = oOuter.offsetHeight - dragBox.offsetHeight + 'px'
      }

    })

    // 值得注意的是,mouseup的事件处理函数,是绑定在document中的,
    // 因为用户在拖动结束时,鼠标可能会处于outer之外,
    // 所以这个事件处理函数需要由document来绑定
    document.addEventListener('mouseup', function (e) {
      isDragging = false
    })
  </script>

5、补充与总结

5.1 补充

对于offsetX和offsetY我有话要说:
offsetX与offsetY是鼠标事件如mousedown的事件对象中的属性。
他们代表的是,触发事件时,鼠标与事件源对象(target)的x,y轴的偏移量。
值得注意的是,这两个属性只关注事件源对象(event.target):
如果事件源元素和事件处理函数所绑定的元素不是同一个元素(如父元素绑定了事件处理函数,但真正触发事件的是子元素),那么event.offsetX表示的是鼠标相对于事件源元素(子元素)的X轴偏移量,而不是绑定事件处理函数的父元素。
拓展:
除了上述的offsetX和offsetY之外,还有两组属性需要注意:
1.clientX,clientY:表示鼠标相对于浏览器窗口的X(Y)轴坐标。
2.pageX,pageY:表示鼠标对于文档的X(Y)轴坐标。

5.2 总结

本文提供了两种思路去实现点位拖拽。

思路一:

  1. 计算鼠标相对于mousedown时,所移动的距离x。
  2. 设置dragBox的style.left和style.top加上鼠标移动的距离x。

思路二:

  1. 保存mousedown时,鼠标与dragBox边界的距离。
  2. 拖拽时,使用dragBox.style.left = 鼠标与可视区域x距离 - outer与可视区域距离 - mousedown时鼠标与dragBox的x距离公式。dragBox.style.top同理。

个人感觉思路一的实现要更加简便。

当然,实现功能的思路有很多,每种思路的实现方式也是多样的,本文只是使用两种方式作为例子,欢迎大家留言讨论。

附上完整需求实现的文章链接,大家感兴趣的话可以看看。