拖放操作

以下描述了拖放操作期间发生的步骤。

本文档中描述的拖放操作使用 DataTransfer 接口。本文档使用 DataTransferItem 接口或 DataTransferItemList 接口。

draggable 属性

在网页中,某些情况下会使用默认的拖放行为。这些包括文本选择、图像和链接。当拖动图像或链接时,图像或链接的 URL 将被设置为拖放数据,并开始拖放。对于其他元素,它们必须是选择的一部分才能进行默认拖放。要查看此效果,请选择网页的一部分,然后单击并按住鼠标,拖动选择。选择将呈现为操作系统特定的内容,并随着拖动发生时跟随鼠标指针。但是,此行为只是默认的拖放行为,如果没有任何监听器调整要拖动的数据,则不会发生。

在 HTML 中,除了图像、链接和选择的默认行为外,默认情况下其他元素不可拖动。

要使其他 HTML 元素可拖动,必须执行三件事

  1. draggable 属性设置为 "true",该属性用于您希望使其可拖动的元素。
  2. 添加 dragstart 事件的监听器。
  3. 在上述监听器中 设置拖放数据

以下是一个允许拖动内容部分的示例。

html
<p draggable="true">This text <strong>may</strong> be dragged.</p>
js
const draggableElement = document.querySelector('p[draggable="true"]');

draggableElement.addEventListener("dragstart", (event) =>
  event.dataTransfer.setData("text/plain", "This text may be dragged"),
);

draggable 属性设置为 "true",因此此元素变为可拖动。如果省略此属性或将其设置为 "false",则该元素将不会被拖动,而是会选择文本。

draggable 属性可以用于任何元素,包括图像和链接。但是,对于最后两个元素,默认值为 true,因此您只需使用值为 falsedraggable 属性来禁用这些元素的拖动。

注意:当元素变为可拖动时,其中的文本或其他元素将无法再通过单击鼠标并拖动以正常方式选择。相反,用户必须按住 Alt 键才能使用鼠标选择文本,或使用键盘。

开始拖放操作

在此示例中,我们使用 addEventListener() 方法为 dragstart 事件添加了一个监听器。

html
<p draggable="true">This text <strong>may</strong> be dragged.</p>
js
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragstart", (event) =>
  event.dataTransfer.setData("text/plain", "This text may be dragged"),
);

当用户开始拖动时,将触发 dragstart 事件。

在此示例中,dragstart 监听器已添加到可拖动元素本身。但是,您可以监听更高祖先,因为拖放事件与大多数其他事件一样向上冒泡。

dragstart 事件中,您可以指定拖放数据反馈图像拖放效果,所有这些将在下面描述。但是,只有拖放数据是必需的。(在大多数情况下,默认图像和拖放效果都适合。)

拖放数据

所有 DragEvent 对象都有一个名为 dataTransfer 的属性,该属性保存拖放数据(dataTransferDataTransfer 对象)。

当发生拖动时,必须将数据与拖动相关联,以识别正在拖动的内容。例如,当拖动文本框中的选中文本时,与拖放数据项相关联的数据是文本本身。同样,当拖动网页上的链接时,拖放数据项是链接的 URL。

DataTransfer 包含两部分信息,数据的类型(或格式)和数据的。格式是类型字符串(例如 text/plain 用于文本数据),而值是文本字符串。当拖动开始时,您通过提供类型和数据来添加数据。在拖动过程中,在 dragenterdragover 事件的事件监听器中,您使用被拖动数据的类型来检查是否允许放置。例如,接受链接的放置目标将检查类型 text/uri-list。在放置事件期间,监听器将检索被拖动的数据并将其插入到放置位置。

DataTransfertypes 属性返回一个 MIME 类型字符串列表,例如 text/plainimage/jpeg。您也可以创建自己的类型。最常用的类型在文章 推荐的拖放类型 中列出。

拖动可以包含几种不同类型的项目。这允许以更具体的类型(通常是自定义类型)提供数据,同时仍然为不支持更具体类型的放置目标提供备用数据。通常情况下,最不具体的类型将是使用类型 text/plain 的普通文本数据。此数据将是一个简单的文本表示。

要在 dataTransfer 中设置拖放数据项,请使用 setData() 方法。它接受两个参数:数据的类型和数据值。例如

js
event.dataTransfer.setData("text/plain", "Text to drag");

在本例中,数据值为“要拖动的文本”,格式为 text/plain

您可以以多种格式提供数据。为此,使用不同的格式多次调用 setData() 方法。您应该按照从最具体到最不具体的顺序调用它。

js
const dt = event.dataTransfer;
dt.setData("application/x.bookmark", bookmarkString);
dt.setData("text/uri-list", "https://www.mozilla.org");
dt.setData("text/plain", "https://www.mozilla.org");

这里,数据以三种不同的类型添加。第一个类型 application/x.bookmark 是自定义类型。其他应用程序不支持此类型,但您可以将自定义类型用于同一站点或应用程序的不同区域之间的拖动。

通过也提供其他类型的 data,我们也可以支持以不太具体的形式拖动到其他应用程序。application/x.bookmark 类型可以提供更详细的数据供应用程序内使用,而其他类型可以包含单个 URL 或文本版本。

请注意,text/uri-listtext/plain 在本示例中包含相同的数据。这通常是正确的,但并不一定是必需的。

如果您尝试使用相同格式添加两次数据,新数据将替换旧数据,但在类型列表中的位置与旧数据相同。

您可以使用 clearData() 方法清除数据,该方法接受一个参数:要删除的数据类型。

js
event.dataTransfer.clearData("text/uri-list");

clearData() 方法的 type 参数是可选的。如果未指定 type,则将删除与所有类型相关联的数据。如果拖动不包含任何拖放数据项,或者所有项随后都被清除,则不会发生拖动。

设置拖放反馈图像

当发生拖动时,将从拖放目标(触发 "dragstart" 事件的元素)生成一个半透明的图像,并在拖动期间跟随用户的指针。此图像会自动创建,因此您无需自行创建。但是,您可以使用 setDragImage() 指定自定义拖放反馈图像。

js
event.dataTransfer.setDragImage(image, xOffset, yOffset);

需要三个参数。第一个是对图像的引用。此引用通常是指向 <img> 元素,但也可能指向 <canvas> 或任何其他元素。反馈图像将从图像在屏幕上的显示方式自动生成,尽管对于图像,它们将以原始大小绘制。setDragImage() 方法的第二个和第三个参数是图像相对于鼠标指针应显示的位置的偏移量。

也可以使用不在文档中的图像和画布。当使用画布元素绘制自定义拖放图像时,此技术非常有用,如下例所示

js
function dragWithCustomImage(event) {
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = 50;

  const ctx = canvas.getContext("2d");
  ctx.lineWidth = 4;
  ctx.moveTo(0, 0);
  ctx.lineTo(50, 50);
  ctx.moveTo(0, 50);
  ctx.lineTo(50, 0);
  ctx.stroke();

  const dt = event.dataTransfer;
  dt.setData("text/plain", "Data to Drag");
  dt.setDragImage(canvas, 25, 25);
}

在此示例中,我们使一个画布成为拖放图像。由于画布是 50×50 像素,因此我们使用其一半的偏移量(25),以便图像显示在鼠标指针的中心。

拖放效果

拖动时,可以执行几种操作。copy 操作用于指示将被拖动的數據将从其当前位置复制到放置位置。move 操作用于指示将被拖动的數據将被移动,而 link 操作用于指示将在源位置和放置位置之间创建某种形式的关系或连接。

您可以通过在 effectAllowed 属性中设置 dragstart 事件监听器来指定三种操作中允许哪些操作。

js
event.dataTransfer.effectAllowed = "copy";

在此示例中,仅允许 **复制**。

您可以通过多种方式组合这些值

none

不允许任何操作

copy

copy

move

move

link

copyMove

copymove

copylink

linkMove

linkmove

all

copymovelink

uninitialized

效果未设置时的默认值,等效于 all

请注意,这些值必须按上面列出的方式使用。例如,将 effectAllowed 属性设置为 copyMove 允许复制或移动操作,但会阻止用户执行链接操作。如果您不更改 effectAllowed 属性,则允许任何操作,就像使用 'all' 值一样。因此,除非您想要排除特定类型,否则您无需调整此属性。

在拖动操作期间,用于 dragenterdragover 事件的监听器可以检查 effectAllowed 属性以查看允许哪些操作。一个相关的属性,dropEffect,应在这些事件中的一个中设置,以指定应执行哪个单一操作。dropEffect 的有效值是 nonecopymovelink。组合值不用于此属性。

使用 dragenterdragover 事件,dropEffect 属性被初始化为用户请求的效果。用户可以通过按修饰键来修改所需的效果。尽管使用的确切键因平台而异,但通常会使用 ShiftControl 键在复制、移动和链接之间切换。鼠标指针将发生变化以指示所需的操作。例如,对于 copy,光标可能会在旁边显示加号。

您可以在 dragenterdragover 事件期间修改 dropEffect 属性,例如,如果特定放置目标仅支持某些操作。您可以修改 dropEffect 属性以覆盖用户效果,并强制执行特定放置操作。请注意,此效果必须是 effectAllowed 属性中列出的效果之一。否则,它将被设置为允许的替代值。

js
event.dataTransfer.dropEffect = "copy";

在此示例中,复制是执行的效果。

您可以使用值 none 来指示不允许在此位置放置,尽管在这种情况下最好不要取消事件。

dropdragend 事件中,您可以检查 dropEffect 属性以确定最终选择了哪个效果。如果选定的效果为“move”,则应在 dragend 事件中从拖动的来源中删除原始数据。

指定放置目标

用于 dragenterdragover 事件的监听器用于指示有效的放置目标,即允许放置拖动项目的区域。网页或应用程序的大多数区域都不是放置数据的有效区域。因此,对这些事件的默认处理是不允许放置。

如果您想允许放置,您必须通过取消 dragenterdragover 两个事件来阻止默认行为。您可以通过调用它们的 preventDefault() 方法来做到这一点。

html
<div id="drop-target">You can drag and then drop a draggable item here</div>
js
const dropElement = document.getElementById("drop-target");

dropElement.addEventListener("dragenter", (event) => {
  event.preventDefault();
});

dropElement.addEventListener("dragover", (event) => {
  event.preventDefault();
});

dragenterdragover 事件期间调用 preventDefault() 方法将指示允许在该位置放置。但是,您通常希望仅在某些情况下调用 preventDefault() 方法(例如,仅当拖动链接时)。

为此,请调用一个检查条件的函数,并且仅在满足条件时取消事件。如果条件不满足,请不要取消事件,并且如果用户释放鼠标按钮,则不会在该位置发生放置。

最常见的是根据数据传输中的拖动数据类型来接受或拒绝放置——例如,允许图像、链接或两者。为此,您可以检查事件的 types 属性的 dataTransfer(属性)。types 属性返回一个以从最显著到最不显著的顺序添加的字符串类型数组。

js
function doDragOver(event) {
  const isLink = event.dataTransfer.types.includes("text/uri-list");
  if (isLink) {
    event.preventDefault();
  }
}

在此示例中,我们使用 includes 方法检查类型 text/uri-list 是否存在于类型列表中。如果是,我们将取消事件,以便允许放置。如果拖动数据不包含链接,则不会取消事件,并且无法在该位置放置。

如果您希望更具体地说明将执行的操作类型,您可能还想同时设置 effectAlloweddropEffect 属性或两者。当然,如果您也不取消事件,则更改任何属性都不会有任何效果。

放置反馈

您可以通过多种方式向用户指示在特定位置允许放置。根据 dropEffect 属性的值,鼠标指针将根据需要更新。

尽管确切的外观取决于用户的平台,但通常会为 'copy' 显示加号图标,例如,当不允许放置时,会显示 '无法在此处放置' 图标。这种鼠标指针反馈在许多情况下已经足够了。

对于更复杂的可视效果,您可以在 dragenter 事件期间执行其他操作。例如,通过在将要放置的位置插入元素。这可能是一个插入标记,或代表拖动元素在新位置的元素。为此,您可以在 dragenter 事件期间创建一个 <img> 元素并将其插入文档中。

当鼠标指向的元素时,dragover 事件将触发。当然,您可能还需要在 dragover 事件中移动插入标记。您可以使用事件的 clientXclientY 属性,就像其他鼠标事件一样,来确定鼠标指针的位置。

最后,当拖动离开元素时,dragleave 事件将在该元素上触发。这是您应该删除任何插入标记或突出显示的时间。您无需取消此事件。即使取消了拖动,dragleave 事件也会始终触发,因此您可以始终确保在该事件期间可以完成任何插入点清理。

执行放置

当用户释放鼠标时,拖放操作结束。

如果鼠标在有效的放置目标(即取消了最后一个 dragenterdragover 事件的元素)上释放,则放置将成功,并且 drop 事件将在目标上触发。否则,拖动操作将被取消,并且不会触发任何 drop 事件。

drop 事件期间,您应该从事件中检索被放置的数据,并将其插入放置位置。您可以使用 dropEffect 属性来确定需要哪个拖动操作。

与所有与拖动相关的事件一样,事件的 dataTransfer 属性将保存正在拖动的數據。可以使用 getData() 方法再次检索数据。

js
function onDrop(event) {
  const data = event.dataTransfer.getData("text/plain");
  event.target.textContent = data;
  event.preventDefault();
}

getData() 方法接受一个参数,即要检索的数据类型。它将返回在拖动操作开始时调用 setData() 时设置的字符串值。如果不存在该类型的数据,将返回空字符串。(当然,您可能知道存在正确类型的数据,因为它在 dragover 事件期间已被检查过。)

在此示例中,检索到数据后,我们将字符串插入为目标的文本内容。这将把拖动的文本插入放置的位置,假设放置目标是文本区域,例如 pdiv 元素。

在网页中,如果您接受了放置,则应该调用事件的 preventDefault() 方法,以便浏览器不会被放置的数据触发默认处理。例如,当链接被拖动到网页时,Firefox 会打开链接。通过取消事件,可以阻止这种行为。

您也可以检索其他类型的数据。如果数据是链接,它应该具有类型 text/uri-list。然后,您可以将链接插入内容中。

js
function doDrop(event) {
  const lines = event.dataTransfer.getData("text/uri-list").split("\n");
  lines
    .filter((line) => !line.startsWith("#"))
    .forEach((line) => {
      const link = document.createElement("a");
      link.href = line;
      link.textContent = line;
      event.target.appendChild(link);
    });
  event.preventDefault();
}

此示例从拖动的数据中插入链接。顾名思义,text/uri-list 类型实际上可能包含一个 URL 列表,每个 URL 位于单独的行上。上面的代码使用 split 将字符串分解为行,然后迭代行列表,并将每个行作为链接插入文档中。(另请注意,以井号 (#) 开头的链接将被跳过,因为这些是注释。)

对于简单的情况,您可以使用特殊类型 URL 仅检索列表中的第一个有效 URL。例如

js
const link = event.dataTransfer.getData("URL");

这消除了您自己检查注释或迭代行的需要。但是,它仅限于列表中的第一个 URL。

URL 类型是一种特殊类型。它仅用作简写,并且不会出现在 types 属性中指定的类型列表中。

有时您可能支持一些不同的格式,并且您希望检索支持的最具体的格式的数据。在以下示例中,三个格式由放置目标支持。

以下示例返回与最佳支持格式相关联的数据

js
function doDrop(event) {
  const supportedTypes = [
    "application/x-moz-file",
    "text/uri-list",
    "text/plain",
  ];
  const types = event.dataTransfer.types.filter((type) =>
    supportedTypes.includes(type),
  );
  if (types.length) {
    const data = event.dataTransfer.getData(types[0]);
    // Use this type of data…
  }
  event.preventDefault();
}

结束拖放

拖动完成后,将向拖动的源 (接收 dragstart 事件的同一元素) 发射一个 dragend 事件。如果拖动成功或被取消,此事件将触发。但是,您可以使用 dropEffect 属性来确定执行了哪个放置操作。

如果 dropEffect 属性在 dragend 期间的值为 none,则拖动已取消。否则,效果将指定执行的操作。源可以在 move 操作后使用此信息从旧位置删除拖动的项目。

放置可以在同一窗口内或在另一个应用程序上发生。无论如何,dragend 事件始终会触发。事件的 screenXscreenY 属性将设置为发生放置的屏幕坐标。

dragend 事件完成传播后,拖放操作完成。

另请参阅