HTML5 拖放 API

本文介绍了拖放操作的基础知识。

在大多数浏览器中,默认情况下,选定的文字、图片和链接是可拖动的。 例如,如果您在网页上拖动链接,则会看到一个包含标题和网址的小框,您可以将其拖放到地址栏或桌面上,以创建快捷方式或导航到链接。如需使其他类型的内容可拖动,您需要使用 HTML5 拖放 API。

如需使对象可拖动,请在该元素上设置 draggable=true。几乎所有内容都可以拖动,包括图片、文件、链接、文件或网页上的任何标记。

以下示例创建了一个界面,用于重新排列使用 CSS Grid 布局的列。列的基本标记如下所示,其中每列的 draggable 属性均设为 true

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

以下是容器和框元素的 CSS。与拖动功能相关的唯一 CSS 是 cursor: move 属性。其余代码用于控制容器和框元素的布局和样式。

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

此时,您可以拖动项目,但不会发生任何其他情况。如需添加行为,您需要使用 JavaScript API。

监听拖动事件

如需监控拖动过程,您可以监听以下任一事件:

如需处理拖动流程,您需要某种来源元素(拖动开始的位置)、数据载荷(被拖动的内容)和目标(用于捕获拖放内容的区域)。来源元素可以是几乎任何类型的元素。目标是接受用户尝试拖放的数据的拖放区或一组拖放区。并非所有元素都可以用作目标。例如,您的目标不能是图片。

开始和结束拖动序列

在内容上定义 draggable="true" 属性后,请附加 dragstart 事件处理脚本,以启动每列的拖动序列。

此代码会在用户开始拖动列时将其不透明度设置为 40%,然后在拖动事件结束时将其恢复为 100%。

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

您可以在以下 Glitch 演示中查看结果。拖动某个项,其不透明度会发生变化。由于来源元素具有 dragstart 事件,因此将 this.style.opacity 设置为 40% 会向用户提供视觉反馈,告知该元素是当前正在移动的选择。当您放下项时,即使您尚未定义放下行为,源元素也会恢复为 100% 不透明度。

添加其他视觉提示

为了帮助用户了解如何与界面互动,请使用 dragenterdragoverdragleave 事件处理脚本。在此示例中,除了可拖动之外,列还可用作拖放目标。当用户将拖动的项悬停在列上时,使边框呈虚线,以帮助用户了解这一点。例如,您可以在 CSS 中为作为拖放目标的元素创建 over 类:

.box.over {
  border: 3px dotted #666;
}

然后,在 JavaScript 中设置事件处理脚本,在用户拖动列时添加 over 类,并在拖动的元素离开时移除该类。在 dragend 处理脚本中,我们还会确保在拖动结束时移除这些类。

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

此代码中有一些值得注意的要点:

  • dragover 事件的默认操作是将 dataTransfer.dropEffect 属性设置为 "none"。本页面后面会介绍 dropEffect 属性。目前,只需知道它会阻止 drop 事件触发即可。如需替换此行为,请调用 e.preventDefault()。另一个好做法是在同一处理程序中返回 false

  • dragenter 事件处理脚本用于切换 over 类,而不是 dragover。如果您使用 dragover,则在用户将拖动项悬停在列上时,系统会重复触发事件,导致 CSS 类重复切换。这会导致浏览器执行大量不必要的渲染工作,进而影响用户体验。我们强烈建议您尽量减少重新绘制次数,如果您需要使用 dragover,请考虑节流或取消调试事件监听器

完成限量发售

如需处理放下操作,请为 drop 事件添加事件监听器。在 drop 处理程序中,您需要阻止浏览器针对掉落的默认行为(通常是某种令人讨厌的重定向)。为此,请调用 e.stopPropagation()

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

请务必将新处理程序与其他处理程序一起注册:

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

如果您在此时运行代码,则项目不会放置到新位置。为此,请使用 DataTransfer 对象。

dataTransfer 属性用于存储在拖动操作中发送的数据。dataTransferdragstart 事件中设置,并在 drop 事件中读取或处理。通过调用 e.dataTransfer.setData(mimeType, dataPayload),您可以设置对象的 MIME 类型和数据载荷。

在此示例中,我们将允许用户重新排列列的顺序。为此,您首先需要在拖动开始时存储源元素的 HTML:

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

drop 事件中,您可以通过将源列的 HTML 设置为您将数据拖放到的目标列的 HTML 来处理列拖放。这包括检查用户是否将内容放回了从中拖动内容的同一列。

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

您可以在以下演示中查看结果。为此,您需要使用桌面浏览器。移动设备不支持拖放 API。将 A 列拖放到 B 列上方,然后观察它们如何更换位置:

更多拖动属性

dataTransfer 对象会公开属性,以便在拖动过程中向用户提供可视反馈,并控制每个拖放目标对特定数据类型的响应方式。

  • dataTransfer.effectAllowed 用于限制用户对元素可以执行的“拖动类型”。它在拖放处理模型中用于在 dragenterdragover 事件期间初始化 dropEffect。此属性可以具有以下值:nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized
  • dataTransfer.dropEffect 用于控制用户在 dragenterdragover 事件期间获得的反馈。当用户将光标悬停在目标元素上时,浏览器的光标会指示将执行哪种类型的操作,例如复制或移动。该效果可以采用以下值之一:nonecopylinkmove
  • e.dataTransfer.setDragImage(imgElement, x, y) 表示您可以设置拖动图标,而不是使用浏览器的默认“虚化图片”反馈。

上传文件

此简单示例将列同时用作拖动来源和拖动目标。这种情况可能会在要求用户重新排列项的界面中发生。在某些情况下,拖动目标和来源可能是不同的元素类型,例如在某个界面中,用户需要将所选图片拖动到目标上,以选择该图片作为商品的主图片。

拖放操作通常用于让用户将内容从桌面拖动到应用中。主要区别在于 drop 处理脚本。其数据包含在 dataTransfer.files 属性中,而不是使用 dataTransfer.getData() 访问文件:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

如需详细了解,请参阅自定义拖放

更多资源