读者看到这个标题,拖拽 API,可能会想到 JS 写过的拖拽 DOM 的效果,但这个拖拽 API 和 JS 实现的拖拽效果并不是一回事,这里介绍一下什么是拖拽 API。
读者应该知道,在网页中,文字,链接和图片是可以拖动的

读者可以看到,对于链接这些是可以拖出来的,并且可以拖到标签栏在新标签页打开,但是如果只是一个普通 DOM 元素为什么就没有这个效果呢?别忘了,网页里的东西其实都是前段技术实现的,甚至右键菜单都可以自定义,这个拖拽功能当然也可以。这里读者应该已经明白这个拖拽 API 干的是什么事了,这个拖拽并不像 JS 那里自行实现的那种实时的拖拽功能,而是类似一个链接的拖拽功能,只有松手后才有效果。

首先,我们这里有一个元素,但是我们发现他完全拖不动,我们要使用拖拽 API,首先就要保证这个元素可以被拖动是吧,要让这个元素可以被拖动,就需要在元素的属性上添加上 draggable 属性并让其值为 true 即可
注意,这个属性尽管只有 true 和 false 两个值,但是这个属性是一个枚举属性,而非布尔属性,也就意味着我们必须要显示地写明
draggable="true"才可以生效,如果想布尔属性那样只写一个属性名是完全不生效的,这是反直觉的地方,所以格外注意。
加上了这个属性之后,我们来看效果

我们发现这个元素能被拖出来了,尽管他好像还是拖不出来,起码已经有了拖动的效果了。
接下来要考虑的问题就是,我们如何真正把这个元素拖出来,如何真正产生拖拽效果,这很显然就是拖拽事件要干的事情了,WEBAPI 中给我们提供了如下事件用于处理拖拽
- dragstart:元素开始被拖拽的时候触发一次
- dragend:元素被拖拽结束后触发一次
- drag:元素处于拖拽状态时频繁触发
- dragenter:鼠标拖拽一个拖拽元素进入时触发
- dragleave:鼠标拖拽一个拖拽元素离开时触发
- dragover:鼠标拖拽一个元素悬停时频繁触发(100 毫秒触发一次)
- drop:拖拽元素在自己身上释放的时候触发
这么多事件大致可以分为两组,第一组为 dragstart,dragend,drag,这三个为一组,因为这三个事件是加在被拖拽元素本身的。我们光给被拖拽元素本身添加事件其实还不够,既然涉及到拖拽,就一定有释放这样一个操作,而释放操作肯定是拖拽到一个特定的位置释放,或者说一定是拖拽到一个特定的 DOM 元素中释放,我们可以称这个 DOM 元素为拖拽容器(这个名字是我瞎取的),而后面的几个事件,dragenter,dragleave, dragover, drop 其实都是添加在拖拽容器上的事件。
关于后面四个属性,也就是加在拖拽容器上的属性,说的更具体一点,当元素被拖拽着走的时候,我们可以看到鼠标样式发生了变化,我们可以理解为鼠标目前就已经变成了拖拽状态,dragenter 就是当鼠标处于拖拽状态时的 mouseenter,dragleave 就是鼠标处于拖拽状态时的 mouseleave,而 dragover 就类似于鼠标处于拖拽状态时的 mousemove,当然,mousemove 还要求鼠标移动,实际上 dragover 只要悬停在上面,哪怕不移动也会频繁触发,drop 就是鼠标处于拖拽状态时的 mouseup,也就是在上面释放鼠标的时候触发。
这里我们先测试前面三个事件
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
.box {
height: 100px;
width: 100px;
background-color: red;
}
.container {
height: 500px;
width: 500px;
background-color: blue;
}
</style>
</head>
<body>
<div class="box" draggable="true"></div>
<div class="container"></div>
</body>
<script>
const box = document.querySelector(".box");
box.addEventListener("dragstart", (e) => {
console.log("dragstart");
});
box.addEventListener("drag", (e) => {
console.log("drag");
});
box.addEventListener("dragend", (e) => {
console.log("dragend");
});
</script>
</html>
可以看到,当 box 开始被拖拽的时候,立刻触发 dragstart 事件,处于拖拽过程中的时候会频繁触发 drag 事件,而拖拽结束后会触发 dragend 事件
接下来测试后面拖拽容器上的事件
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
.box {
height: 100px;
width: 100px;
background-color: red;
}
.container {
height: 500px;
width: 500px;
background-color: blue;
}
</style>
</head>
<body>
<div class="box" draggable="true"></div>
<div class="container"></div>
</body>
<script>
const box = document.querySelector(".box");
const container = document.querySelector(".container");
// box.addEventListener("dragstart", (e) => {
// console.log("dragstart");
// });
// box.addEventListener("drag", (e) => {
// console.log("drag");
// });
// box.addEventListener("dragend", (e) => {
// console.log("dragend");
// });
container.addEventListener("dragenter", e => {
console.log("dragenter");
});
container.addEventListener("dragleave", e => {
console.log("dragleave");
});
container.addEventListener("drop", e => {
console.log("drop");
});
container.addEventListener("dragover", e => {
console.log("dragover");
});
</script>
</html>
这里我们发现出现了一些问题,dragenter 和 dragover 实际上都没有问题,当鼠标处于拖拽状态移入时会触发 dragenter ,当鼠标处于拖拽状态并停留时会频繁触发 drapover,这两个应该没有任何问题,这里关键有一个问题,为什么鼠标在拖拽容器上释放的时候,触发的是容器的 dragleave 而不是 drop 呢?
读者是否注意到这样一个问题,当鼠标处于拖拽状态的时候一直是一个禁止的图标,这是由于这些 DOM 元素默认是不允许有元素在上面被释放的,也就是说它有一个默认行为阻止了其他元素在其上面释放,所以才会有这个禁止的图标,也就解释了为什么这个元素无法在拖拽容器上被释放。
我们只需要取消掉 dragover 的默认事件即可消除掉这个问题

此时消除了容器上 dragover 的默认事件,这个容器也就不会阻止其他元素在它上面进行释放了,所以当我们拖拽状态的鼠标移入这个容器的时候,鼠标样式其实是发生了变化的。也可以顺利触发 drop 事件。
对于一些元素,例如 text 类型的 input 元素,实际上我们是可以直接把文字拖拽进去的,这是由于
input的drop事件的默认事件导致的,我们可以通过取消 input 元素dorp时间的默认事件来阻止这个行为。一般情况下,drop和dragover的默认事件我们都是要取消掉的,以防止一些奇奇怪怪的事件发生。
有了这套拖拽 API,我们其实可以很轻松地实现一个拖拽效果
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
.box {
height: 100px;
width: 100px;
background-color: red;
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div class="box" draggable="true"></div>
</body>
<script>
const box = document.querySelector('.box');//获取元素
//初始化left和top
let currentLeft = box.offsetLeft;
let currentTop = box.offsetTop;
//初始化鼠标点击位置
let clickX = 0;
let clickY = 0;
// console.log(currentLeft, currentTop);
box.addEventListener("dragstart", function (e) {
//当进入拖拽状态的时候,记录鼠标点击位置
clickX = e.pageX;
clickY = e.pageY;
})
document.addEventListener("drop", function (e) {
const { pageX: currentClickX, pageY: currentClickY } = e;//解构出鼠标位置
//计算出偏移量
const disX = currentClickX - clickX;
const disY = currentClickY - clickY;
//更新偏移量
currentLeft += disX;
currentTop += disY;
//修改样式
box.style.left = currentLeft + 'px';
box.style.top = currentTop + 'px';
});
//取消document的默认行为
document.addEventListener("dragover", function (e) {
e.preventDefault();
});
</script>
</html>
读者发现这个效果好像我们以前见过的拖拽效果不太一样,当然,我们也可以使用拖拽 API 实现我们以前见过的那种拖拽效果
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
.box {
height: 100px;
width: 100px;
background-color: red;
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div class="box" draggable="true"></div>
</body>
<script>
const box = document.querySelector('.box');//获取元素
//初始化left和top
let currentLeft = box.offsetLeft;
let currentTop = box.offsetTop;
//初始化鼠标点击位置
let clickX = 0;
let clickY = 0;
// console.log(currentLeft, currentTop);
box.addEventListener("dragstart", function (e) {
//当进入拖拽状态的时候,记录鼠标点击位置
clickX = e.pageX;
clickY = e.pageY;
})
document.addEventListener("dragover", function (e) {
const { pageX: currentClickX, pageY: currentClickY } = e;//解构出鼠标位置
//计算出偏移量
const disX = currentClickX - clickX;
const disY = currentClickY - clickY;
//更新偏移量
currentLeft += disX;
currentTop += disY;
//修改clickX和clickY
clickX = currentClickX;
clickY = currentClickY;
//修改样式
box.style.left = currentLeft + 'px';
box.style.top = currentTop + 'px';
});
//取消document的默认行为
document.addEventListener("dragover", function (e) {
e.preventDefault();
});
</script>
</html>
注意修改 clickX 和 clickY,这是由于拖拽 API 在悬停的时候依旧会频繁触发,而
mousemove这种在悬停的时候不会触发的事件就可以不更新这两个属性
注意,drop 事件应该在 dragend 事件触发之前被触发
上面我们用拖拽 API 去实现了以前用鼠标事件实现过的元素拖拽效果,当然读者可能会发现这玩意儿实现起来,不管是从代码量还是从实现难度上,都没有简单多少,确实是这样,拖拽 API 确实不是专门用来干这个事的,我们可以去查看一下拖拽事件的事件对象文档:DragEvent - Web API | MDN
从这里我们会发现,拖拽 API 的事件对象比起普通的鼠标事件就只增加了一个叫 dataTransfer 的属性,这里我们就来介绍一下这个 dataTransfer 是干嘛的。
首先,dataTransfer 属性其实就是一个对象,文档可参见:DataTransfer - Web API | MDN
说简单点 dataTransfer 的功能就一个,用来完成数据的传递,读者应该也能意识到,拖拽 API 其实是用来帮助我们传递数据的,而非帮助我们完成拖拽操作,所以为什么我们前面的拖拽操作实现起来比较繁琐。
要使用 dataTransfer 传递数据,其实非常简单,无非就是两个步骤,分别是设置数据和获取数据。
实际上 dataTransfer 对象中为我们提供了这么几个方法
setData():设置数据getData():获取数据clearData():清空数据
首先是 setData 方法,这个方法接受两个参数,第一个参数为一个 MIME 格式字符串,表示我这个数据是什么格式的,第二个参数为数据字符串。getData 方法只需要传入一个 MIME 格式字符串即可获取到指定格式的数据。
这里是一个示例
html
<!DOCTYPE html>
<html lang="cmn-hans">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
.box {
height: 100px;
width: 100px;
background-color: red;
}
.container {
height: 100px;
width: 100px;
background-color: blue;
}
</style>
</head>
<body>
<div class="box" draggable="true"></div>
<div class="container"></div>
</body>
<script>
const box = document.querySelector('.box');//获取元素
const container = document.querySelector('.container');//获取元素
box.addEventListener("dragstart", e => {
e.dataTransfer.setData("text/plain", "我是一个拖拽文本");
e.dataTransfer.setData("text/plain", "我是第二个拖拽文本");//同一种类型的数据只能添加一次,第二次会覆盖第一次
e.dataTransfer.setData("text/html", "<p>我是一个P标签</p>");
});
container.addEventListener("dragover", e => {
e.preventDefault();
});
container.addEventListener("drop", e => {
console.log(e.dataTransfer.getData("text/plain"));//输出纯文本
console.log(e.dataTransfer.getData("text/html"));//输出HTML
});
</script>
</html>
显然,我们只需要在 dragstart 中给 dataTransfer 添加一个数据,然后在 drop 事件时使用 dataTransfer 的 getData 方法拿到数据,就完成了一个数据传输。
我们可以通过 MIME 类型拿到数据,那这里有一个问题,既然我一次拖拽可以设置多种数据,那我如何遍历它设置的所有数据呢?答案就是
dataTransfer.types属性,这个属性为一个数组,里面存储的是本次拖拽所有数据的 MIME 类型,通过遍历这个数组就可以拿到所有已添加数据的 MIME 类型,从而调用getData方法拿到所有数据。
实际上, dataTransfer 里面还有一些属性,比如我们会发现,当元素进行拖拽的时候,它的鼠标样式是一个小加号,实际上这个是可以修改的,我们可以在容器上添加我们可以通过修改 dataTransfer 中的 dropEffect 属性来修改鼠标样式,默认是 copy,可以修改为 move,link,none
在拖拽元素的时候,会有一个半透明的缩略图跟着鼠标走,这个其实我们也可以设置,使用
dataTransfer.setDragImage()方法,第一个参数传入一个元素即可,如果是图片元素,则会使用该图片元素的图像,第二个参数和第三个参数分别传入缩略图基于鼠标的偏移量即可,具体可以参考:DataTransfer.setDragImage() - Web API | MDN