处理拖动数据存储

DragEvent 接口有一个 dataTransfer 属性,它是一个 DataTransfer 对象。DataTransfer 对象代表拖动操作的主要上下文,并且在不同事件触发时保持一致。它包括拖动数据拖动图像放置效果等。本文重点介绍 dataTransfer数据存储部分。

拖动数据存储的结构

从根本上说,拖动数据存储是一个项目列表,表示为 DataTransferItem 对象的 DataTransferItemList。每个项目可以是以下两种类型之一:

此外,该项目还由一个类型标识,按照惯例,该类型采用 MIME 类型的形式。此类型可以指导消费者如何解析或解码有效负载。对于所有文本项,列表只能包含每种类型的一个项目,因此实际上,该列表包含两个不相交的集合:一个可能包含重复类型的文件列表,以及一个以类型为键的文本项 Map。通常,文件列表表示多个正在拖动的文件。文本映射表示正在传输多个资源,而是以不同方式编码的相同资源,以便接收端可以选择最合适的受支持解释。文本项按优先级降序排序。

此列表可通过 DataTransfer.items 属性访问。

HTML 拖放 API 经历了多次迭代,导致管理数据存储的方式有两种共存。在 DataTransferItemListDataTransferItem 接口之前,“旧方式”使用 DataTransfer 上的以下属性:

  • types:包含列表中文本项type 属性,以及如果存在任何文件项则包含值 "files"
  • setData()getData()clearData():使用“类型到有效负载映射”模型提供对列表中文本项的访问。
  • files:以 FileList 的形式提供对列表中文件项的访问。

您可能会看到文件项的类型未直接公开。它们仍然可以访问,但只能通过 files 列表中每个 File 对象的 type 属性访问,因此如果您无法读取文件,则也无法知道它们的类型(有关何时可读存储,请参阅读取拖动数据存储)。

要获取文件及其类型,我们建议使用 items 属性,因为它提供了一个更灵活和一致的接口。对于文本项,您也应该为了保持一致性而优先使用 items 属性,尽管 getData() 方法更方便访问或删除特定类型。

DataTransferDataTransferItem 接口之间的另一个关键区别是,前者使用同步的 getData() 方法访问文本有效负载,而后者则使用异步的 getAsString() 方法。

修改拖动数据存储

对于默认可拖动的项目,例如图像、链接和选区,拖动数据已由浏览器定义;对于使用 draggable 属性定义的自定义可拖动元素,您必须自行定义拖动数据。唯一可以修改数据存储的时间是在 dragstart 处理程序中——对于任何其他拖动事件的 dataTransfer,数据存储都是不可修改的。

要将文本数据添加到拖动数据存储,“新方式”使用 DataTransferItemList.add() 方法,而“旧方式”使用 DataTransfer.setData() 方法。

js
function dragstartHandler(ev) {
  // New way: add(data, type)
  ev.dataTransfer.items.add(ev.target.innerText, "text/plain");
  // Old way: setData(type, data)
  ev.dataTransfer.setData("text/html", ev.target.outerHTML);
}

const p1 = document.getElementById("p1");
p1.addEventListener("dragstart", dragstartHandler);

对于这两种方法,如果在数据存储不可修改时调用它们,则什么都不会发生。如果存在具有相同类型的文本项,add() 会抛出错误,而 setData() 会覆盖现有项。

要将文件数据添加到拖动数据存储,“新方式”仍然使用 DataTransferItemList.add() 方法。由于“旧方式”将文件项存储在 DataTransfer.files 属性中,这是一个只读的 FileList,因此没有直接的等效方法。

js
function dragstartHandler(ev) {
  // New way: add(data)
  ev.dataTransfer.items.add(new File([blob], "image.png"));
}

const p1 = document.getElementById("p1");
p1.addEventListener("dragstart", dragstartHandler);

请注意,在添加文件数据时,add() 会忽略 type 参数,并使用 File 对象的 type 属性。

注意:读写保护是按作业进行的,这意味着只有 dragstart 处理程序中的同步代码才能修改数据存储。如果您在异步操作后尝试访问数据存储,您将不再拥有写入权限。例如,这不起作用:

js
function dragstartHandler(ev) {
  canvas.toBlob((blob) => {
    ev.dataTransfer.items.add(new File([blob], "image.png"));
  });
}

删除数据类似,使用 DataTransferItemList.remove()DataTransferItemList.clear()DataTransfer.clearData() 方法。

读取拖动数据存储

除了 dragstart 事件(您拥有对数据存储的完全访问权限)之外,您唯一可以从数据存储中读取的时间是在 drop 事件期间,这允许放置目标检索数据。

要从拖动数据存储中读取文本数据,“新方式”使用 DataTransferItemList 对象,而“旧方式”使用 DataTransfer.getData() 方法。新方式更方便遍历所有项目,而旧方式更方便访问特定类型。

js
function dropHandler(ev) {
  // New way: loop through items
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      item.getAsString((data) => {
        // Do something with data
      });
    }
  }
  // Old way: getData(type)
  const data = ev.dataTransfer.getData("text/plain");
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

要从拖动数据存储中读取文件数据,“新方式”仍然使用 DataTransferItemList 对象,而“旧方式”使用 DataTransfer.files 属性。

js
function dropHandler(ev) {
  // New way: loop through items
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "file") {
      const file = item.getAsFile(); // A File object
    }
  }
  // Old way: loop through files
  for (const file of ev.dataTransfer.files) {
    // Do something with file
  }
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

保护模式

dragstartdrop 事件之外,数据存储处于保护模式,不允许代码访问任何有效负载。即:

  • 所有修改尝试都会静默地不执行任何操作或抛出 DOMException(仅适用于 items.add()items.remove())。
  • DataTransfer.getData() 始终返回空字符串。
  • DataTransfer.files 始终返回空列表。
  • DataTransferItem.getAsString() 返回而不调用回调。
  • DataTransferItem.getAsFile() 始终返回 null

同样,读写保护是按作业进行的,这意味着只有 drop 处理程序中的同步代码才能读取数据存储。如果您在异步操作后尝试访问数据存储,您将不再拥有写入权限。例如,这不起作用:

js
function getDataPromise(item) {
  return new Promise((resolve) => {
    item.getAsString((data) => {
      resolve(data);
    });
  });
}

async function dropHandler(ev) {
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      // Bad: by the second time this runs, we are no longer in the same job
      const data = await getDataPromise(item);
    }
  }
}

const p1 = document.getElementById("p1");
p1.addEventListener("drop", dropHandler);

相反,您必须同步调用所有访问方法,然后稍后等待它们的结果。

js
async function dropHandler(ev) {
  const promises = [];
  for (const item of ev.dataTransfer.items) {
    if (item.kind === "string") {
      // Bad: by the second time this runs, we are no longer in the same job
      promises.push(getDataPromise(item));
    }
  }
  const results = await Promise.all(promises);
}

常见的拖动数据类型

规范只定义了少数数据类型的行为,但浏览器有时对更多类型具有原生支持。通常,类型旨在像 MIME 类型一样作为一种协议,只要接收端(另一个网页、同一网页的另一部分,甚至浏览器外部的某个地方)理解它,您就可以使用任何类型。本节描述了一些常见约定和浏览器的默认行为。

请注意,以下场景指的是意图而不是行为。例如,当我们说“拖动链接”时,用户可能没有拖动实际的 <a> 元素;他们可能正在拖动一个包含一个或多个链接的容器,但意图是传输链接作为数据,因此您准备的数据存储可以与用户拖动实际链接时相同。

拖动文本

对于拖动文本,使用 text/plain 类型,拖动字符串作为值。例如:

js
event.dataTransfer.items.add("This is text to drag", "text/plain");

您应该始终添加 text/plain 类型的数据作为不支持其他类型的应用程序或放置目标的备用,除非没有逻辑上的文本替代方案。始终将此 text/plain 类型最后添加,因为它最不具体,不应被优先考虑。

getData()setData()clearData() 中,Text 类型(不区分大小写)被视为 text/plain

默认情况下,当选择被拖动时,会创建以下数据项:

  • text/plain:包含选定的文本。Firefox 和 Safari 将此项排在 text/html 之后,尽管规范要求它排在第一位。
  • text/html:包含选定元素的完整 HTML 源代码(所有样式内联)。

规范还要求另一个类型为 application/microdata+json 的项,其中包含从拖动选择中的元素中提取的微数据。目前没有浏览器实现此项。

当放置到可编辑文本字段(例如 <textarea><input type="text">)时,text/plain 项默认会被复制到字段中(没有任何事件处理)。

拖动的超链接应包含两种类型的数据:text/uri-listtext/plain两种类型都应使用链接的 URL 作为其数据。注意:URL 类型是 uri-list,带有 I,而不是 L

像往常一样,将 text/plain 类型放在最后,作为 text/uri-list 类型的备用。例如:

js
event.dataTransfer.items.add("https://www.mozilla.org", "text/uri-list");
event.dataTransfer.items.add("https://www.mozilla.org", "text/plain");

要拖动多个链接,请使用 CRLF 换行符分隔 text/uri-list 数据中的每个链接。以井号(#)开头的行是注释,不应被视为 URL。您可以使用注释来指示 URL 的目的、与 URL 关联的标题或其他数据。

警告:多个链接的 text/plain 备用应包含所有 URL,但不包含注释。

例如,此示例 text/uri-list 数据包含两个链接和一个注释:

https://www.mozilla.org
#A second link
http://www.example.com

检索拖放的链接时,请确保处理多个链接被拖动的情况,包括任何注释。

getData()setData()clearData() 中,URL 类型(不区分大小写)被视为 text/uri-list。对于 getData(),结果只包含列表中的第一个 URL。

默认情况下,当拖动 <a> 元素时,会创建以下数据项:

  • text/x-moz-url (仅限 Firefox):包含 href 属性和链接文本,用换行符分隔。
  • text/x-moz-url-data (仅限 Firefox):仅包含 href
  • text/x-moz-url-desc (仅限 Firefox):仅包含链接文本。
  • text/uri-list:包含 href 属性。
  • text/html (仅限 Chrome 和 Firefox):包含 <a> 元素的完整 HTML 源代码(所有样式内联)。
  • text/plain:也包含 href 属性。Chrome 将此项排在 text/uri-list 之前。

拖动图像

直接拖动图像(即,数据是像素内容)不常见,并且可能在某些平台不受支持。相反,图像通常仅通过其 URL 拖动。为此,与其他 URL 一样,使用 text/uri-list 类型。数据应该是图像的 URL,如果图像未存储在网站或磁盘上,则为 data: URL

与链接一样,text/plain 类型的数据也应包含 URL。但是,data: URL 在文本上下文中通常没有用处,因此在这种情况下您可能希望排除 text/plain 数据。

js
event.dataTransfer.items.add(imageURL, "text/uri-list");
event.dataTransfer.items.add(imageURL, "text/plain");

默认情况下,当拖动 <img> 元素时,会创建以下数据项:

  • text/x-moz-url (仅限 Firefox):包含 src 属性和 alt 文本(如果 alt 为空,则再次为 src),用换行符分隔。
  • text/x-moz-url-data (仅限 Firefox):仅包含 src 属性。
  • text/x-moz-url-desc (仅限 Firefox):仅包含 alt 文本(如果 alt 为空,则为 src)。
  • text/uri-list:包含 src 属性。
  • text/html:包含 <img> 元素的完整 HTML 源代码(所有样式内联)。
  • text/plain (仅限 Firefox):包含 src 属性。

Safari 还会创建一个文件项,其中包含图像数据,并带有适当的 MIME 类型,例如 image/png

拖动元素

当拖动的项目是带有 draggable="true" 的任意元素时,要设置什么数据取决于您打算传输什么。

传输元素的常用方法是使用包含序列化 HTML 源代码的 text/html 类型,接收端可以解析并插入。例如,将其数据设置为元素的 outerHTML 属性的值是合适的。也可以使用 text/xml,但要确保数据是格式良好的 XML。

您还可以使用 text/plain 类型包含 HTML 或 XML 数据的纯文本表示。数据应仅包含文本,不含任何源标签或属性。例如:

js
event.dataTransfer.items.add("text/html", element.outerHTML);
event.dataTransfer.items.add("text/plain", element.innerText);

您还可以使用为自定义目的而发明的其他类型。尽量始终包含一个 text/plain 替代方案,除非拖动的对象是特定于某个站点或应用程序的。在这种情况下,自定义类型可确保数据无法放置到其他位置。

从操作系统文件浏览器拖动文件

当拖动的项目是文件时,会将一个类型为 file 的项目添加到拖动数据中。type 被设置为文件的 MIME 类型(由操作系统提供),如果类型未知,则为 application/octet-stream。目前,拖动的文件只能源自浏览器外部,例如来自文件浏览器。

Firefox 还会添加一个非标准的文本项,类型为 application/x-moz-file,其中包含用户文件系统上文件的完整路径。除非在特权代码(例如扩展程序)中,否则其值为一个空字符串。

将文件拖到操作系统文件浏览器

可以从浏览器传输的内容主要取决于浏览器及其拖动到的位置。将图像拖动到本地文件系统通常受支持,并导致图像被下载。

Chrome 支持非标准的 DownloadURL 类型。有效负载应为 <MIME type>:<file name>:<file URL> 格式的文本。例如:

js
event.dataTransfer.items.add(
  "DownloadURL",
  "image/png:example.png:data:image/png;base64,iVBORw0K...",
);

这允许在拖动到文件浏览器时下载任意文件,或者在拖放到另一个浏览器窗口时,就像正在拖放文件一样(尽管可能适用 CORS 限制)。有关实际用例,请参阅 像 Gmail 一样拖出文件

另见