HTML Drag & Drop API 指南

5,426 阅读9分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

拖放功能在 Web 中是很常见的,比如拖拽排序,拖拽上传文件等等。本文将详细的介绍 DnD(Drag and Drop) API,让你对拖放功能有一定的认识。

概述

一个典型的拖放操作是这样的:用户选中一个可拖拽的(draggable) 元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable) 元素,然后释放鼠标。

在这个过程中,最重要的三个点是:

  • 让元素可拖拽
  • 让另一个元素支持可放置
  • 可拖拽和可放置元素之间的数据传递

示例

接下来我们看一个简单的例子:

DnD-1.gif

代码地址:codepen.io/wuzhengyan2…

<div id="drop-area"></div>
<div id="drag-el" draggable="true">...</div>
window.onload = () => {
  const dragEl = document.querySelector('#drag-el')
  const dropArea = document.querySelector('#drop-area')

  dragEl.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', event.target.id)
  })
  dropArea.addEventListener('dragover', (event) => {
    event.preventDefault()
  })
  dropArea.addEventListener('drop', (event) => {
    const id = event.dataTransfer.getData('text/plain')
    dropArea.appendChild(document.getElementById(id))
  })
}

上述例子就一个简单的拖放操作,让我们看下例子中拖放的三个重要点都是怎么实现的。

  1. 元素可拖拽:DOM 属性上设置 draggable=true
  2. 元素可放置:监听 dragover 事件,事件中调用 preventDefault
  3. 拖放数据传递;可拖拽元素监听 dragstart 事件,调用 dataTransfer 对象的 setData 方法,可放置元素监听 drop 事件,使用 dataTransfer 对象的 getData 方法获取设置的数据。

可以看到,一个拖放操作实现起来还是很简洁的。

小结

最后我们从可拖拽元素和可放置元素的角度总结下:

元素要支持可拖拽,需要做三件事:

  1. 设置 draggable=true
  2. 添加 dragstart 事件监听
  3. 在监听事件中设置拖拽数据

另一个元素要支持可放置:

  1. 监听 dragover 事件,事件中调用 preventDefault
  2. 监听 drop 事件,处理对应的逻辑

从可拖拽元素和可放置元素的角度上来总结,就让拖放的实现变的很清晰。可拖拽元素定义好自己的拖拽数据,可放置元素定义要接受的数据和对应的处理逻辑,这样两个模块就是独立的模块,不会相互耦合。

DnD API

接下来我们详细的介绍 DnD API,熟悉这些 API 后你就可以完成相对复杂的功能。

draggable 属性

当我们想让元素变成可拖拽时,我们就需要设置 draggable 属性。

属性值未设置的情况下,默认是 auto,此时拖拽行为为浏览器默认行为,只有选中的文字,链接,图片可以拖动。

draggable 属性值不是 Boolean 类型,需要显式设置为 true 或者 false,true 为可拖动,false 为不可拖动

拖拽事件

拖放过程的各个事件和触发时刻如下:

事件触发时刻
dragstart当用户开始拖拽一个元素或选中的文本时触发
drag当拖拽元素或选中的文本时触发
dragend当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键)
dragenter当拖拽元素或选中的文本到一个可放置的目标时触发
dragover当元素或选中的文本被拖到一个可放置的目标上时触发(每100毫秒触发一次)
drop当元素或选中的文本在可放置的目标上被释放时触发
dragleave当拖拽元素或选中的文本离开一个可放置的目标时触发。
dragexit和dragleave类似,但是兼容性不好,建议不要使用。说明

事件分类:

  • 可拖拽元素:dragstart,drag,dragend
  • 可放置的元素:dragenter,dragover,drop,dragleave

事件的触发时机可以在这个例子中体验:codepen.io/wuzhengyan2…

拖拽事件的 event 对象 dragEvent 继承 mouseEvent,dragEvent 有个属性 dataTransfer,dataTransfer 属性是一个 DataTransfer 对象

接下来介绍这几个事件常用的方式:

  • dragstart: 设置拖拽数据,调整拖拽元素样式等
  • drag:拖拽的移动跟踪
  • dragend:判断拖拽操作是否成功(即是否在放置区释放,通过 dropEffect 判断),恢复拖拽元素样式等
  • dragenter:设置样式,设置dropEffect等等
  • dragover:设置为可放置区,设置dropEffect等等
  • drop:处理放置事件
  • dragexit:恢复样式

同样的我们看个例子:

DnD-2.gif

代码地址:codepen.io/wuzhengyan2…

<div id="drop-area"></div>
<div id="drag-el" draggable="true">...</div>
<div>放置结果:<span id="result"></span></div>
window.onload = () => {
  const carEl = document.querySelector('#drag-el')
  const dropArea = document.querySelector('#drop-area')
  const result = document.querySelector('#result')

  carEl.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', event.target.id)
    event.dataTransfer.effectAllowed = 'move'
    event.target.style.opacity = 0.5
  })
  carEl.addEventListener('drag', (event) => {
    console.log(event.clientX, event.clientY)
  })
  carEl.addEventListener('dragend', (event) => {
    event.target.style.opacity = 1
    const isDrop = event.dataTransfer.dropEffect !== 'none'
    result.innerText = isDrop ? '成功' : '未放置'
  })
  dropArea.addEventListener('dragenter', (event) => {
    event.dataTransfer.dropEffect = 'move'
    event.target.style.borderStyle = 'dashed'
  })
  dropArea.addEventListener('dragover', (event) => {
    event.dataTransfer.dropEffect = 'move'
    event.preventDefault()
  })
  dropArea.addEventListener('drop', (event) => {
    const id = event.dataTransfer.getData('text/plain')
    dropArea.appendChild(document.getElementById(id))
    event.target.style.borderStyle = 'solid'
  })
  dropArea.addEventListener('dragleave', (event) => {
    event.target.style.borderStyle = 'solid'
  })
}

上面这个例子中就使用到了各个事件,每个事件都有对应的用法。

拖拽数据对象

拖拽数据对象涉及到三个类:DataTransfer, DataTransferItemList, DataTransferItem

DataTransfer

DataTransfer 对象用于保存在拖放操作期间拖动的数据,同时还可以设置拖拽样式,读取拖拽文件等等。它可以包含一个或多个数据项,每个数据项包含一个或多个数据类型。

所有拖拽事件中我们都可以通过 event.dataTransfer 访问到它。

属性:

  • dropEffect :当前选定的拖放操作类型,一般在可放置元素的相关事件中设置
  • effectAllowed :提供可用的操作类型,一般在拖拽元素的相关事件中设置
  • files :包含数据传输中可用的所有本地文件的列表。如果拖动操作不涉及拖动文件,则此属性为空列表
  • items :提供一个包含所有拖动数据列表的 DataTransferItemList 对象 ,只读属性。
  • types :所有数据项类型的数组

方法:

  • clearData() :删除与给定类型关联的数据。类型参数是可选的。如果类型为空或未指定,则删除与所有类型关联的数据
  • setData() :设置给定类型的数据。如果该类型的数据不存在,则将其添加到末尾,类型列表中的最后一项将是新的类型。如果该类型的数据已经存在,则在相同位置替换现有数据
  • getData() :获取给定类型的数据,如果该类型的数据不存在或 dataTransfer不包含数据,则返回空字符串
  • setDragImage() :用于设置自定义的拖动图像。

接下来我们将详细的介绍各个属性和方法。

dropEffecteffectAllowed

dropEffect 用于表示放置区接受什么行为的拖放,一般在 dragenter 和 dragover 中设置;对应的 effectAllowed 表示这次拖拽的行为是什么行为,要在 dragstart 中设置。

effectAllowed 支持的值:

  • none: 所有拖拽行为都允许
  • copy: 支持复制行为
  • move: 支持移动行为
  • link: 支持链接关联行为
  • copyMove: 支持 copy 和 move
  • copyLink: 支持 copy 和 link
  • linkMove: 支持 link 和 move
  • all: 支持 copy, move 和 link
  • uninitialized: 未设置值,默认和 all 效果一样

dropEffect 支持的值:

  • none: 不允许放置
  • copy: 支持复制行为
  • move: 支持移动行为
  • link: 支持链接关联行为

这两个属性的作用有俩个:

  1. 鼠标样式会根据设置值展示,可以看下之前的例子,里面设置的是 move,在元素放置到拖拽区时,鼠标光标旁有个方形的样式
  2. dropEffect 的值必须在 effectAllowed 范围中才可以支持放置,不然拖拽元素是无法在放置区中触发 drop 事件。

DnD-2.gif

代码地址:codepen.io/wuzhengyan2…

files

这个属性就如定义一样,如果拖拽的文件,这个数组就对应的就有 File 对象的数组项

items

这个属性保存了所有拖拽数据,我们在下面 DataTransferItemList 中介绍它。

types

这个数组保存了当前拖拽数据所有的数据类型值,我们可以看下默认图片拖拽时,这个数组的值情况:

image.png

代码地址:codepen.io/wuzhengyan2…

可以看到图片拖拽的时候拖拽数据项有三个,它们的类型分别是 text/uri-list, text/html, Files。

这边我们顺便介绍下拖拽数据项的常见的类型值:text/plain, text/uri-list, text/html, Files,除了这些值,你自定义格类型值,如 application/x.bookmark。

clearData
DataTransfer.clearData([format]);

这个方法就是清除拖拽数据项的方法,不传参数清除所有数据,有传就清除对应类型的数据。

setData
void dataTransfer.setData(format, data);

设置拖拽数据的方法,format 可以使用一些通用的值,如 text/plain,或者自定义值。data 为字符串,如果你数据是对象的话,需要序列化处理下。

同时设置数据默认会加到 dataTransfer.items 数组最后位置,但是如果数据的类型已存在,就会更新之前那条数据项的值。

数据项的设置应该要从最具体到最不具体,这个是什么意思呢,来看下链接拖拽的默认数据项。

image.png

我们可以看到,第一个数据项类型是 text/uri-lsit,对应数据是链接,后面数据项是文本和 html,对于;链接来说,链接地址是最具体的,相对 html 是最不具体的。

getData
DOMString dataTransfer.getData(format);

获取拖拽项数据的方法,传入数据类型获取。

setDragImage
void dataTransfer.setDragImage(img, xOffset, yOffset);

拖拽元素,浏览器默认会有一个图像。通过此方法可以自定义拖拽图像。

具体可以看这个例子:codepen.io/webgeeker/p…

DataTransferItemList

一个 DataTransferItem 数组,DataTransferItem 代表的是一个拖拽数据项。event.dataTransfer.items 即是此类型

属性:

  • length: 数组长度

方法:

  • add(data, type) 添加一个拖拽数据项,这个方法和 dataTransfer 的 getData 类似,不过在添加相同的类型数据的时候,这个方法会返回一个错误
  • remove(index) 移除一个数据项
  • clear() 清除数据项

DataTransferItem

拖拽时是数据项,event.dataTransfer.items 中的每一项即是此类型

属性:

  • kind: 指明数据是文件还是字符串,string | file
  • type: 数据的类型

方法:

  • getAsFile(callback) 返回 File,拖拽不是文件就返回 null
  • getAsFileSystemHandle(callback) 返回文件或者文件夹 handle,拖拽不是文件就返回 null
  • getAsString(callback) 返回字符串

注意 DataTransferItem 获取数据方法是异步的,dataTransfer 的 getData 的获取数据是同步的。通常我们直接是使用 dataTransfer 的方法即可。

兼容性

拖拽 API 主要还是在 PC 上使用,大部分移动设备都不支持。PC 端除了 ie 兼容性有些问题,其他浏览器兼容性良好。

image.png

总结

HTML 拖拽知识点中比较关键的有以下几点:

  • 如何让元素支持拖拽
  • 如何让元素变成可放置区
  • 拖拽事件
  • 拖拽中的数据:dataTransfer 对象

掌握以上几个知识点,就能很好完成拖拽相关功能的编写。

参考资料: developer.mozilla.org/en-US/docs/…