处理拖动数据存储
DragEvent
接口有一个 dataTransfer
属性,它是一个 DataTransfer
对象。DataTransfer
对象代表拖动操作的主要上下文,并且在不同事件触发时保持一致。它包括拖动数据、拖动图像、放置效果等。本文重点介绍 dataTransfer
的数据存储部分。
拖动数据存储的结构
从根本上说,拖动数据存储是一个项目列表,表示为 DataTransferItem
对象的 DataTransferItemList
。每个项目可以是以下两种类型之一:
string
:其有效负载是一个字符串,可以通过getAsString()
检索。file
:其有效负载是一个文件对象,可以通过getAsFile()
(或getAsFileSystemHandle()
或webkitGetAsEntry()
,如果需要更复杂的文件系统操作)检索。
此外,该项目还由一个类型标识,按照惯例,该类型采用 MIME 类型的形式。此类型可以指导消费者如何解析或解码有效负载。对于所有文本项,列表只能包含每种类型的一个项目,因此实际上,该列表包含两个不相交的集合:一个可能包含重复类型的文件列表,以及一个以类型为键的文本项 Map
。通常,文件列表表示多个正在拖动的文件。文本映射不表示正在传输多个资源,而是以不同方式编码的相同资源,以便接收端可以选择最合适的受支持解释。文本项按优先级降序排序。
此列表可通过 DataTransfer.items
属性访问。
HTML 拖放 API 经历了多次迭代,导致管理数据存储的方式有两种共存。在 DataTransferItemList
和 DataTransferItem
接口之前,“旧方式”使用 DataTransfer
上的以下属性:
types
:包含列表中文本项的type
属性,以及如果存在任何文件项则包含值"files"
。setData()
、getData()
、clearData()
:使用“类型到有效负载映射”模型提供对列表中文本项的访问。files
:以FileList
的形式提供对列表中文件项的访问。
您可能会看到文件项的类型未直接公开。它们仍然可以访问,但只能通过 files
列表中每个 File
对象的 type
属性访问,因此如果您无法读取文件,则也无法知道它们的类型(有关何时可读存储,请参阅读取拖动数据存储)。
要获取文件及其类型,我们建议使用 items
属性,因为它提供了一个更灵活和一致的接口。对于文本项,您也应该为了保持一致性而优先使用 items
属性,尽管 getData()
方法更方便访问或删除特定类型。
DataTransfer
和 DataTransferItem
接口之间的另一个关键区别是,前者使用同步的 getData()
方法访问文本有效负载,而后者则使用异步的 getAsString()
方法。
修改拖动数据存储
对于默认可拖动的项目,例如图像、链接和选区,拖动数据已由浏览器定义;对于使用 draggable
属性定义的自定义可拖动元素,您必须自行定义拖动数据。唯一可以修改数据存储的时间是在 dragstart
处理程序中——对于任何其他拖动事件的 dataTransfer
,数据存储都是不可修改的。
要将文本数据添加到拖动数据存储,“新方式”使用 DataTransferItemList.add()
方法,而“旧方式”使用 DataTransfer.setData()
方法。
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
,因此没有直接的等效方法。
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
处理程序中的同步代码才能修改数据存储。如果您在异步操作后尝试访问数据存储,您将不再拥有写入权限。例如,这不起作用:
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()
方法。新方式更方便遍历所有项目,而旧方式更方便访问特定类型。
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
属性。
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);
保护模式
在 dragstart
和 drop
事件之外,数据存储处于保护模式,不允许代码访问任何有效负载。即:
- 所有修改尝试都会静默地不执行任何操作或抛出
DOMException
(仅适用于items.add()
和items.remove()
)。 DataTransfer.getData()
始终返回空字符串。DataTransfer.files
始终返回空列表。DataTransferItem.getAsString()
返回而不调用回调。DataTransferItem.getAsFile()
始终返回null
。
同样,读写保护是按作业进行的,这意味着只有 drop
处理程序中的同步代码才能读取数据存储。如果您在异步操作后尝试访问数据存储,您将不再拥有写入权限。例如,这不起作用:
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);
相反,您必须同步调用所有访问方法,然后稍后等待它们的结果。
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
类型,拖动字符串作为值。例如:
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-list
和 text/plain
。两种类型都应使用链接的 URL 作为其数据。注意:URL 类型是 uri-list
,带有 I,而不是 L。
像往常一样,将 text/plain
类型放在最后,作为 text/uri-list
类型的备用。例如:
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
数据。
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 数据的纯文本表示。数据应仅包含文本,不含任何源标签或属性。例如:
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>
格式的文本。例如:
event.dataTransfer.items.add(
"DownloadURL",
"image/png:example.png:data:image/png;base64,iVBORw0K...",
);
这允许在拖动到文件浏览器时下载任意文件,或者在拖放到另一个浏览器窗口时,就像正在拖放文件一样(尽管可能适用 CORS 限制)。有关实际用例,请参阅 像 Gmail 一样拖出文件。