在 HTML 5 之前,想要实现 Drag and Drop(拖拽/拖放)一般需要求助于 JQuery,所幸 HTML 5 已经把 DnD 标准化,现在我们能“轻易”地为几乎任意元素实现拖放功能。只是它的难度取决于你对 API 的理解程度,而官方文档并不好懂。这篇文章会一步步带你了解它的 API。
最终效果如下:
拖动事件
继续之前,有必要先了解拖动时会触发哪些事件。考虑拖动 Source Element,途中经过 Intermediate Element,最终进入 Target Element 并松开鼠标,则路径上会触发的事件如下图所示:
这些事件的具体内容下面会讲到,你可以先跳过之后再回来查看,简单来说:
dragstart
:当我们“拖”起元素时会触发。dragenter
:当拖动元素 A 进入另一个元素 B 时,会触发 B 的dragenter
事件。dragleave
:与dragenter
相对应,当拖动元素 A 离开元素 B 时,触发 B 的dragleave
事件。dragover
:当拖动元素 A 在另一个元素 B 中移动/停止时触发 B 的dragover
事件。文档说是每几百毫秒触发一次,Chrome 实测 1ms 左右触发;Firefox 大概是 300msdrop
:当在拖动元素 A 到元素 B 上,释放鼠标时触发 B 的drop
事件,相当于元素 B 接收了元素 A 。dragend
:在drop
事件之后,还会触发元素 A 的dragend
事件,这里可以对元素 A 作一些清理工作。
除了上面的事件外,还有两个一般用不到的事件:
drag
:和dragover
类似,当元素 A 被拖动时,每隔一段时间就会触发这个事件。与dragover
不同,drag
事件是触发在源元素 A 上,而dragover
是触发上潜在目标元素 B 上的。dragexit
:这个事件只有 Firefox 支持,和dragleave
作用几乎相同,发生在dragleave
之前。
如果想实际验证一下这些事件是何时触发的,可以看看这个 jsfiddle,console 里会输出拖放的元素及对应的事件。下面我们开始一起实现咱们的拖放示例吧。
让元素可拖放
一般在 HTML 里,元素默认是不可以作为源元素的(除了 <a>
,<img>
),例如一个div
,我们是“拖不动”它的。这时只需要为它加上 draggable="true"
属性它就能“拖”了。下面是我们的 DOM 结构:
<div id="drag-container"> |
draggable
元素上加了 draggable="true"
,这样我们就能拖动它了,起码在 Chrome
里可以,在 Firefox 里我们还需要在 dragstart
里为 dataTransfer
设置一些数据,因此需要加上下面的代码。具体的作用我们之后会说。
let draggable = document.getElementById('draggable'); |
于是效果如下(CSS 没有贴出):
这样红色的 Drag Me
元素就可以拖动了。下面我们增加一些拖动时的反馈,让交互更真实。
添加拖动特效
首先,我们想在拖起元素让原始的元素变成半透明,这样当我们拖动时就会知道它是“真的可以拖动的”,而不是浏览器的什么奇怪行为。为此,我们可以监听 dragstart
事件:
draggable.addEventListener("dragstart", (ev) => { |
这样一来我们开始拖动元素,它就变得透明了,然而我们松开鼠标,它依旧保持透明!这可不是我们想要的结果,因此我们需要监听 dragend
在拖动结束后还原透明度:
draggable.addEventListener("dragend", (ev) => { |
下面,我们希望拖着元素 A 进入目标 B 时让 B 的边框变成虚线,以示意我们可以放入元素。
let dropzones = document.querySelectorAll('.dropzone'); |
我们为所有的 dropzone
都监听了 dragenter
及 dragleave
事件,当拖动元素进入它们时,边框会变成虚线,离开时变回实线。这里有几个注意点:
- 在
dragenter
与dragover
里我们调用了ev.preventDefault()
,事实上几乎所有元素默认都是不允许 drop 发生的,这里调用ev.preventDefault()
可以阻止默认行为。 - 在
dragenter
中我们通过dropzone
变量来修改样式而不是ev.target
,你可能觉得ev.target
指向的是目标 B 元素,然而它指向的是源元素 A。 - 我们在
dragenter
而不是dragover
中修改样式,是因为dragover
会触发太频繁了。
我们完成了“拖”的操作,最后需要完成“放”的操作了。
数据传输 DataTransfer
拖动是最终目的是为了对源和目标元素做一些操作。为了完成操作,需要在源和目标传输数据,我们可以通过设置/读取全局变量来完成,这并不是一个好习惯。在 HTML 5 中,我们通过 DataTransfer 完成。
我们在 dragstart
时设置需要传输的数据,在 drop 中获取需要的数据。
event.dataTransfer
提供了两个主要函数:
setData(format, data)
:用于添加数据,一般 format 对应于 MIME 类型字符串,常见的有text/plain
、text/html
及text/uri-list
等,但同时也可以是任意自定义的类型;不幸的是 data 只能是string
或file
。getData(format)
:用于获取数据。
我们要实现将 Drag Me
放到其它蓝色元素中,需要传输它的 ID ,通过下面的代码实现:
draggable.addEventListener('dragstart', (ev) => { |
- 在
dragstart
时通过setData
将 ID 放入DataTransfer
中 - 在
drop
事件中,通过getData
获取元素 ID 并通过appendChild
加入到蓝色元素中。
至此我们的简单示例就结束了,为了实现这么一个简单的示例,我们用到了全部的 6 个事件。因此从入门的角度来说 DnD API 并不容易,但换句话说这也就是它的几乎全部内容了,而你现在已经掌握了!恭喜!
其它用法
定制拖放的行为时,还会有一些其它的需求,如拖放时的图标,到目标元素时鼠标的指针样式等,这里简单介绍一些。
当我们拖动元素时,浏览器默认生成了元素的缩略图,你可能需要自己设置,这时可以使用 DataTransfer
的 setDragImage(image, xOffset, yOffset);
函数。参考
MDN 上的例子。
event.dataTransfer.dropEffect
和 event.effectAllowed
共同决定了浏览器在执行拖动时的鼠标指针的行为,还有一些其它的用途。只是我实际测试时发现并不起作用,
StackOverflow 的这个问题
说了一些自己的理解。
HTML5 还支持从操作系统中拖拽文件到浏览器中,或者从浏览器到操作系统中。如果从操作系统中获取文件,则可以访问 event.dataTransfer.files
字段,包含了操作系统中的文件内容。反之,在 dragstart
时正确设置 event.dataTransfer.files
则允许从浏览器中拖拽文件到操作系统中。
一些坑
dataTransfer
的内容只在drop
里可读,所以如果你想在dragEnter
或dragOver
中通过dataTransfer.getData()
返回的内容来决定一个目标元素是否允许放置是不可行的。其它的事件里只能通过一个个检查dataTransfer.items
里的 type 来获取已经设置的format
而无法获取data
。drop
与dragend
事件是顺序触发的,但在dragend
里没有办法知道drop
事件是否已经触发。
如果你遇到过其它的坑,也请在评论区留言~
参考
- Native HTML5 Drag and Drop 经典的入门教程,一步步带你入门
- Working with HTML5 Drag-and-Drop 相对更完整的介绍
- Drag and drop W3C DnD 标准
- HTML 5 drag and drop API DnD 一些常见的坑