项目场景:
在网上看见很多jQuery和Vue实现元素拖拽的方法,心想如果使用原生JS该如何来来实现。
我的实现目标是当拖拽一个div元素,可以让元素跟着鼠标移动
问题描述:
开门就遇见一个bug。。
window.onload = () => {
const draggable = document.getElementById("draggable");
draggable.addEventListener("dragstart", handleDragstart);
let distanceX = 0;
let distanceY = 0;
function handleDragstart(e) {
//获取鼠标位置
const x = e.clientX;
const y = e.clientY;
console.log(x, y);
//获取draggable的位置
const offsetX = e.target.offsetLeft;
const offsetY = e.target.offsetTop;
console.log(offsetX, offsetY);
//保存鼠标悬停处与draggable边界的距离(是全局变量)
distanceX = x - offsetX;
distanceY = y - offsetY;
console.log(distanceX, distanceY);
}
draggable.addEventListener("drag", handleDrag);
function handleDrag(e) {
//获取实时鼠标位置
let x = e.clientX;
let y = e.clientY;
console.log(x, y);
// 计算出draggable的坐标
let left = x - distanceX;
let top = y - distanceY;
console.log(left, top);
// //设置draggable坐标
draggable.style.left = left + "px";
draggable.style.top = top + "px";
}
}
结果一松手就弹到左上角
查看控制台,哦,原来是
在拖拽鼠标松开的一瞬间,鼠标坐标e.clientX和Y都变成了0,导致计算left和top时变成了负数,所以一松手就弹到左上角了。趁我没注意啊,我大意了啊。
然后我想通过百度去找寻答案,结果因为问题难以描述所以没有找到答案,否则有解决方案了我也就懒得写文章了。
解决方案
大家学过drag事件群就知道,拖拽事件中还有个dragend事件,我尝试添加dragend事件监听
draggable.addEventListener("dragend", handleDragend);
function handleDragend(e) {
//获取实时鼠标位置
let x = e.clientX;
let y = e.clientY;
console.log(x, y);
}
结果正确打印出松手一瞬间的鼠标坐标
尝试在dragend再次修改一次坐标
draggable.addEventListener("dragend", handleDragend);
function handleDragend(e) {
//获取实时鼠标位置
let x = e.clientX;
let y = e.clientY;
console.log(x, y);
// 计算出draggable的坐标
let left = x - distanceX;
let top = y - distanceY;
// //设置draggable坐标
draggable.style.left = left + "px";
draggable.style.top = top + "px";
}
不考虑复杂情况,问题在这里已经解决了。
情景扩展
边界情况
虽然动画很流畅,也没有一瞬间闪到左上角的情况,但是实际上元素的确经过左上角
是弹回去后再过来,我可以用一个触发器来验证
if (draggable.offsetLeft < 0) {
draggable.style.backgroundColor = "#ff0055"
}
为了避免隐患,之后可能会进行一些边界判断,所以我使用了个笨方法(如果有懂为什么松手鼠标坐标会变成0的同学可以在评论区告诉我)
在之前的handleDrag函数(“drag”拖拽事件)中添加if判断。(这样设置一举两得,既可以避免元素弹回左上角,还能让元素不超过边界)
// //设置draggable坐标
if (left < 0 || top < 0) {
return;
}
draggable.style.left = left + "px";
draggable.style.top = top + "px";
虽然问题解决了,但还是不难看出一个问题,不流畅啊!!!
那是因为一碰到边界就阻止设置元素坐标了,如果x轴碰到边界,y轴上也无法移动了。
所以我们分开判断,将前面的改写成
// //设置draggable坐标
if (left >= 0) {
draggable.style.left = left + "px";
}
if (top >= 0) {
draggable.style.top = top + "px";
}
问题虽然解决了,但还是不难发现一个问题,当快速拖拽超出屏幕时,元素无法贴上边界,这里就是之前各位肯定听教程讲过的“吸边“:
当元素超过边界时,手动将其坐标设置为边界值
为了方便,我先计算出右边边界和下面边界的值
const maxTop = document.body.clientHeight - draggable.offsetHeight;
console.log(maxTop);
const maxLeft = document.body.clientWidth - draggable.offsetWidth;
console.log(maxLeft);
然后将前面的代码替换成如下
// //设置draggable坐标
if (left >= maxLeft) { //当超过右边界
draggable.style.left = maxLeft + "px";
} else if (left < 0) { //当超过左边界
draggable.style.left = "0px";
} else {
draggable.style.left = left + "px";
}
if (top >= maxTop) { //当超过下边界
draggable.style.top = maxTop + "px";
} else if (top < 0) { //当超过上边界
draggable.style.top = "0px"
} else {
draggable.style.top = top + "px"
}
那么问题就这样解决了。
补充:
相信跟着我做的同学一定发现了一个问题
这是怎么回事?发生甚磨事了?
我一看,哦,原来是:
maxTop = document.body.clientHeight - draggable.offsetHeight 等于 0 了
问题原因很简单,我没有在div外面套一层全屏容器,导致当前的body高度和div高度相同,也就是说body完全由div撑开其高度。
解决办法很简单,外面套一层全屏容器或者把body的宽高都改成100vh / 100vw就ok了
body {
width: 100vw;
height: 100vh;
}
但你以为这就解决问题了吗?
做到这里虽然还能通过修改代码做一部分改进,但是我已经不想改了,为什么?
因为一旦拖拽元素的鼠标指针超过浏览器边界,就会立刻像之前松开鼠标一样直接弹回左上角,如果说我可以再次通过判断鼠标超出浏览器来阻止触发坐标修改为(0,0)从而阻止其弹回左上角。但是我们已经做了很多步骤了,相同的实现方法有通过mousemove事件实现同样的功能简单而且没有bug,那为什么还要通过drag事件来实现拖拽移动呢?
完整代码贴出:
window.onload = () => {
const draggable = document.getElementById("draggable");
draggable.addEventListener("dragstart", handleDragstart);
let distanceX = 0;
let distanceY = 0;
const maxTop = document.body.clientHeight - draggable.offsetHeight;
console.log(maxTop);
const maxLeft = document.body.clientWidth - draggable.offsetWidth;
console.log(maxLeft);
function handleDragstart(e) {
//获取鼠标位置
const x = e.clientX;
const y = e.clientY;
console.log(x, y);
//获取draggable的位置
const offsetX = e.target.offsetLeft;
const offsetY = e.target.offsetTop;
console.log(offsetX, offsetY);
//保存鼠标悬停处与draggable边界的距离(是全局变量)
distanceX = x - offsetX;
distanceY = y - offsetY;
console.log(distanceX, distanceY);
}
draggable.addEventListener("drag", handleDrag);
function handleDrag(e) {
//获取实时鼠标位置
let x = e.clientX;
let y = e.clientY;
console.log(x, y);
// 计算出draggable的坐标
let left = x - distanceX;
let top = y - distanceY;
console.log(left, top);
// //设置draggable坐标
if (left >= maxLeft) { //当超过右边界
draggable.style.left = maxLeft + "px";
} else if (left < 0) { //当超过左边界
draggable.style.left = "0px";
} else {
draggable.style.left = left + "px";
}
if (top >= maxTop) { //当超过下边界
draggable.style.top = maxTop + "px";
} else if (top < 0) { //当超过上边界
draggable.style.top = "0px"
} else {
draggable.style.top = top + "px"
}
}
//添加松手事件判断
draggable.addEventListener("dragend", handleDragend);
function handleDragend(e) {
//检测是否超出左边界(是就变红)
if (draggable.offsetLeft < 0) {
draggable.style.backgroundColor = "#ff0055"
}
//再次调用一次拖拽处理函数,把元素从左上角拉回来
handleDrag(e);
}
}
不推荐使用drag进行元素拖拽移动的原因
因此,我总结了几条不推荐使用drag来做元素拖拽移动原因:
1.动画不流畅有明显残影,drag拖拽帧率很低(对比mousemove就能明显发现)
2.在松手的一瞬间会将鼠标位置设置为(0,0)从而导致元素弹回左上角
3.当拖拽离开浏览器时,元素仍然弹回左上角,鼠标坐标变成(0,0),(但事件没有中断)
所以以后遇到这类问题还是老老实实用mouseover来实现吧