本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
拖放功能在 Web 中是很常见的,比如拖拽排序,拖拽上传文件等等。本文将详细的介绍 DnD(Drag and Drop) API,让你对拖放功能有一定的认识。
概述
一个典型的拖放操作是这样的:用户选中一个可拖拽的(draggable) 元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable) 元素,然后释放鼠标。
在这个过程中,最重要的三个点是:
- 让元素可拖拽
- 让另一个元素支持可放置
- 可拖拽和可放置元素之间的数据传递
示例
接下来我们看一个简单的例子:
<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))
})
}
上述例子就一个简单的拖放操作,让我们看下例子中拖放的三个重要点都是怎么实现的。
- 元素可拖拽:DOM 属性上设置 draggable=true
- 元素可放置:监听 dragover 事件,事件中调用 preventDefault
- 拖放数据传递;可拖拽元素监听 dragstart 事件,调用 dataTransfer 对象的 setData 方法,可放置元素监听 drop 事件,使用 dataTransfer 对象的 getData 方法获取设置的数据。
可以看到,一个拖放操作实现起来还是很简洁的。
小结
最后我们从可拖拽元素和可放置元素的角度总结下:
元素要支持可拖拽,需要做三件事:
- 设置 draggable=true
- 添加 dragstart 事件监听
- 在监听事件中设置拖拽数据
另一个元素要支持可放置:
- 监听 dragover 事件,事件中调用 preventDefault
- 监听 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:恢复样式
同样的我们看个例子:
<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()
:用于设置自定义的拖动图像。
接下来我们将详细的介绍各个属性和方法。
dropEffect
和 effectAllowed
dropEffect
用于表示放置区接受什么行为的拖放,一般在 dragenter 和 dragover 中设置;对应的 effectAllowed
表示这次拖拽的行为是什么行为,要在 dragstart 中设置。
effectAllowed
支持的值:
none
: 所有拖拽行为都允许copy
: 支持复制行为move
: 支持移动行为link
: 支持链接关联行为copyMove
: 支持 copy 和 movecopyLink
: 支持 copy 和 linklinkMove
: 支持 link 和 moveall
: 支持 copy, move 和 linkuninitialized
: 未设置值,默认和 all 效果一样
dropEffect
支持的值:
none
: 不允许放置copy
: 支持复制行为move
: 支持移动行为link
: 支持链接关联行为
这两个属性的作用有俩个:
- 鼠标样式会根据设置值展示,可以看下之前的例子,里面设置的是 move,在元素放置到拖拽区时,鼠标光标旁有个方形的样式
dropEffect
的值必须在effectAllowed
范围中才可以支持放置,不然拖拽元素是无法在放置区中触发 drop 事件。
files
这个属性就如定义一样,如果拖拽的文件,这个数组就对应的就有 File 对象的数组项
items
这个属性保存了所有拖拽数据,我们在下面 DataTransferItemList 中介绍它。
types
这个数组保存了当前拖拽数据所有的数据类型值,我们可以看下默认图片拖拽时,这个数组的值情况:
可以看到图片拖拽的时候拖拽数据项有三个,它们的类型分别是 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 数组最后位置,但是如果数据的类型已存在,就会更新之前那条数据项的值。
数据项的设置应该要从最具体到最不具体,这个是什么意思呢,来看下链接拖拽的默认数据项。
我们可以看到,第一个数据项类型是 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 | filetype
: 数据的类型
方法:
getAsFile(callback)
返回 File,拖拽不是文件就返回 nullgetAsFileSystemHandle(callback)
返回文件或者文件夹 handle,拖拽不是文件就返回 nullgetAsString(callback)
返回字符串
注意 DataTransferItem 获取数据方法是异步的,dataTransfer 的 getData 的获取数据是同步的。通常我们直接是使用 dataTransfer 的方法即可。
兼容性
拖拽 API 主要还是在 PC 上使用,大部分移动设备都不支持。PC 端除了 ie 兼容性有些问题,其他浏览器兼容性良好。
总结
HTML 拖拽知识点中比较关键的有以下几点:
- 如何让元素支持拖拽
- 如何让元素变成可放置区
- 拖拽事件
- 拖拽中的数据:dataTransfer 对象
掌握以上几个知识点,就能很好完成拖拽相关功能的编写。