36

HTML 5 Drag and Drop 入门教程

 5 years ago
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 并松开鼠标,则路径上会触发的事件如下图所示:

vqYR3uF.png!web

这些事件的具体内容下面会讲到,你可以先跳过之后再回来查看,简单来说:

  1. dragstart :当我们“拖”起元素时会触发。
  2. dragenter :当拖动元素 A 进入另一个元素 B 时,会触发 B 的 dragenter 事件。
  3. dragleave :与 dragenter 相对应,当拖动元素 A 离开元素 B 时,触发 B 的 dragleave 事件。
  4. dragover :当拖动元素 A 在另一个元素 B 中移动/停止时触发 B 的 dragover 事件。文档说是每几百毫秒触发一次,Chrome 实测 1ms 左右触发;Firefox 大概是 300ms
  5. drop :当在拖动元素 A 到元素 B 上,释放鼠标时触发 B 的 drop 事件,相当于元素 B 接收了元素 A 。
  6. dragend :在 drop 事件之后,还会触发元素 A 的 dragend 事件,这里可以对元素 A 作一些清理工作。

除了上面的事件外,还有两个一般用不到的事件:

  1. drag :和 dragover 类似,当元素 A 被拖动时,每隔一段时间就会触发这个事件。与 dragover 不同, drag 事件是触发在源元素 A 上,而 dragover 是触发上潜在目标元素 B 上的。
  2. 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 都监听了 dragenterdragleave 事件,当拖动元素进入它们时,边框会变成虚线,离开时变回实线。这里有几个注意点:

  • dragenterdragover 里我们调用了 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/plaintext/htmltext/uri-list 等,但同时也可以是任意自定义的类型;不幸的是 data 只能是 stringfile
  • 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 并不容易,但换句话说这也就是它的几乎全部内容了,而你现在已经掌握了!恭喜!

定制拖放的行为时,还会有一些其它的需求,如拖放时的图标,到目标元素时鼠标的指针样式等,这里简单介绍一些。

当我们拖动元素时,浏览器默认生成了元素的缩略图,你可能需要自己设置,这时可以使用 DataTransfersetDragImage(image, xOffset, yOffset); 函数。参考 MDN 上的例子

event.dataTransfer.dropEffectevent.effectAllowed 共同决定了浏览器在执行拖动时的鼠标指针的行为,还有一些其它的用途。只是我实际测试时发现并不起作用, StackOverflow 的这个问题 说了一些自己的理解。

HTML5 还支持从操作系统中拖拽文件到浏览器中,或者从浏览器到操作系统中。如果从操作系统中获取文件,则可以访问 event.dataTransfer.files 字段,包含了操作系统中的文件内容。反之,在 dragstart 时正确设置 event.dataTransfer.files 则允许从浏览器中拖拽文件到操作系统中。

  • dataTransfer 的内容只在 drop 里可读,所以如果你想在 dragEnterdragOver 中通过 dataTransfer.getData() 返回的内容来决定一个目标元素是否允许放置是不可行的。其它的事件里只能通过一个个检查 dataTransfer.items 里的 type 来获取已经设置的 format 而无法获取 data
  • dropdragend 事件是顺序触发的,但在 dragend 里没有办法知道 drop 事件是否已经触发。

如果你遇到过其它的坑,也请在评论区留言~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK