HTML 5 Drag and Drop 入门教程

|5090字|415阅读

在 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 里,元素默认是不可以作为源元素的(除了 , ),例如一个 div ,我们是“拖不动”它的。这时只需要为它加上 draggable="true" 属性它就能“拖”了。下面是我们的 DOM 结构:

1
2
3
4
5
6
7
8
9
10

<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 设置一些数据,因此需要加上下面的代码。具体的作用我们之后会说。

1
2
3
4
let draggable = document.getElementById('draggable');
draggable.addEventListener('dragstart', (ev) => {
  ev.dataTransfer.setData('text/plain', null);
});

于是效果如下(CSS 没有贴出):

这样红色的 Drag Me 元素就可以拖动了。下面我们增加一些拖动时的反馈,让交互更真实。

首先,我们想在拖起元素让原始的元素变成半透明,这样当我们拖动时就会知道它是“真的可以拖动的”,而不是浏览器的什么奇怪行为。为此,我们可以监听 dragstart 事件:

1
2
3
draggable.addEventListener("dragstart", (ev) => {
 ev.target.style.opacity = ".5";
});

这样一来我们开始拖动元素,它就变得透明了,然而我们松开鼠标,它依旧保持透明!这可不是我们想要的结果,因此我们需要监听 dragend 在拖动结束后还原透明度:

1
2
3
draggable.addEventListener("dragend", (ev) => {
  ev.target.style.opacity = "";
});

下面,我们希望拖着元素 A 进入目标 B 时让 B 的边框变成虚线,以示意我们可以放入元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ,通过下面的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 一些常见的坑

原文链接:

|5090字|415阅读
分类: HTML5
标签: HTML 博客
         
返回顶部