0、写在前面
前两天接到一个需求:在一个盒子中可以添加自定义的图片(如地铁站台平面图),而在图片中可以添加点位标记。同时要求背景图可以进行缩放拖拽,还要求对点位进行拖拽。
考虑到整个需求的实现放在一篇文章中会比较繁杂,所以准备把需求一分为二,拆分成两篇文章:
- 实现普通的点位拖拽。(本文)
- 实现背景图片的缩放、拖拽,并在背景图片上添加点位,对点位进行拖拽。(文章链接在此,感兴趣的同学可以看看)
本文只会讨论如何实现普通的点位拖拽,即使不需要实现其他需求的同学也能愉快阅读。
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所示:
归根结底,我们希望的是dragBox能够“跟着”鼠标移动,鼠标左键按下(mousedown事件)时开始拖拽,左键抬起(mouseup事件)停止拖拽。
这里我能够想到两种思路实现dragBox跟着鼠标移动:
方法一:计算鼠标移动距离,让dragBox也移动相同距离。(不需要知道dragBox应该去哪,直接通知它应该移动多少距离
)
方法二:左键按下时,计算dragBox边界与鼠标的距离,通过这段距离计算后续dragBox所在位置。(需要计算出dragBox应该去哪,告诉它具体位置让它过去
)
3、方法一实现
3.1 实现过程
使用方法一实现,我们不需要知道拖拽时dragBox应该移动到的具体坐标,只需计算鼠标移动的距离,然后让dragBox也移动相同距离就好了。
需要使用到的属性或方法:
- 事件对象属性:clientX,clientY:表示鼠标相对于浏览器窗口的X(Y)轴坐标。
- 元素对象属性: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)
})
进行拖拽时:
- 关键词:
鼠标移动的距离
- 计算鼠标相对于mousedown时,所
移动的距离
。 - 让dragBox的style.left和style.top加上
鼠标移动的距离
。(完成拖拽) - 拓展:判断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)。也就是下图蓝色字体的距离:
由图可知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 总结
本文提供了两种思路去实现点位拖拽。
思路一:
- 计算鼠标相对于mousedown时,所移动的距离x。
- 设置dragBox的style.left和style.top加上鼠标移动的距离x。
思路二:
- 保存mousedown时,鼠标与dragBox边界的距离。
- 拖拽时,使用
dragBox.style.left = 鼠标与可视区域x距离 - outer与可视区域距离 - mousedown时鼠标与dragBox的x距离
公式。dragBox.style.top同理。
个人感觉思路一的实现要更加简便。
当然,实现功能的思路有很多,每种思路的实现方式也是多样的,本文只是使用两种方式作为例子,欢迎大家留言讨论。
附上完整需求实现的文章链接,大家感兴趣的话可以看看。