HTML 5 Drag and Drop 入门教程
source link: http://lotabout.me/2018/HTML-5-Drag-and-Drop/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在 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 大概是 300ms -
drop
:当在拖动元素 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"> <div class="dropzone"> <div id="draggable" draggable="true"> Drag Me </div> </div> <div class="dropzone"></div> <div class="dropzone"></div> </div>
draggable
元素上加了 draggable="true"
,这样我们就能拖动它了,起码在 Chrome 里可以,在 Firefox 里我们还需要在 dragstart
里为 dataTransfer
设置一些数据,因此需要加上下面的代码。具体的作用我们之后会说。
let draggable = document.getElementById('draggable'); draggable.addEventListener('dragstart', (ev) => { ev.dataTransfer.setData('text/plain', null); });
于是效果如下(CSS 没有贴出):
这样红色的 Drag Me
元素就可以拖动了。下面我们增加一些拖动时的反馈,让交互更真实。
首先,我们想在拖起元素让原始的元素变成半透明,这样当我们拖动时就会知道它是“真的可以拖动的”,而不是浏览器的什么奇怪行为。为此,我们可以监听 dragstart
事件:
draggable.addEventListener("dragstart", (ev) => { ev.target.style.opacity = ".5"; });
这样一来我们开始拖动元素,它就变得透明了,然而我们松开鼠标,它依旧保持透明!这可不是我们想要的结果,因此我们需要监听 dragend
在拖动结束后还原透明度:
draggable.addEventListener("dragend", (ev) => { ev.target.style.opacity = ""; });
下面,我们希望拖着元素 A 进入目标 B 时让 B 的边框变成虚线,以示意我们可以放入元素。
let dropzones = document.querySelectorAll('.dropzone'); dropzones.forEach((dropzone) => { dropzone.addEventListener('dragenter', (ev) => { ev.preventDefault(); dropzone.style.borderStyle = 'dashed'; return false; }); dropzone.addEventListener('dragover', (ev) => { ev.preventDefault(); return false; }); dropzone.addEventListener('dragleave', (ev) => { dropzone.style.borderStyle = 'solid'; }); });
我们为所有的 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) => { ev.target.style.opacity = ".5"; // 设置 ID ev.dataTransfer.setData('text/plain', ev.target.id); }); dropzones.forEach((dropzone) => { dropzone.addEventListener('drop', (ev) => { ev.preventDefault() ev.target.style.borderStyle = 'solid'; // 获取 ID const sourceId = ev.dataTransfer.getData('text/plain') ev.target.appendChild(document.getElementById(sourceId)) }) });
- 在
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 一些常见的坑
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK