最近碰到了个需求,大概就是要通过可视化拖拽的方式配置一个冰柜,需要把预设好的冰柜内部架子模板一个个拖到冰箱内。一开始的想法是用鼠标事件(mousedown、mouseup等)那一套去实现,能实现但是过程过于复杂,需要控制的状态太多了。其实
Web Api
为 html 元素拖拽量身定制了一套HTML
拖放API
,用这个方法实现一些简单的拖拽功能简直不要太简单。为此写了这篇文章,下面将详细介绍HTML 拖放 API
的核心知识点
一、被拖拽元素和放置被拖拽元素的元素
通常我们所了解的拖放是按住鼠标左键不放然后移动鼠标把一个页面元素从某个位置移动到另一个位置,然后松开鼠标左键,至此完成了整个拖放过程。在这个过程中我们需要先重点关注两个东西,一个是
被拖拽元素
另一个是放置被拖拽元素的元素
。
1.1 被拖拽元素
我们得先有个概念,页面上显示的元素默认并不都是可以被拖拽的(除了图片、被选中的文字、链接),所以如果当前元素默认不可被拖拽那么就得先把它设置为可拖拽的。ps:可拖拽元素被拖拽时会有一个半透明的快照跟着鼠标移动。
将 HTML 元素的 draggable 属性设置为 true, 元素就可以变为可拖拽元素。效果如下图。
<div id="box" draggable="true">draggable box</div>
1.2 可放置被拖拽元素的元素
所有的元素区域默认是不支持放置被拖拽元素的,直观的表现是,当被拖拽元素经过不可放置区域时鼠标的样式是一个禁止放置的一个图标(圆圈带一个斜杠),所以需要将目标元素设置为一个可放置区域
默认情况下是这样:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
设置为放置区域需要给元素绑定一个事件 dragover 且要 阻止默认行为
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})
</script>
React:
放置区域
需要绑定onDragOver
事件,且要 阻止默认行为 – 其他事件一样加on
<div draggable="true">draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置区域</div>
设置为可放置区域后鼠标样式也变了不再是禁止图标,而是一个加号图标(图标可以设置,下面会讲解):
然而你会发现被拖放元素并没有真正的被放置到放置区域,这是必然的,放置操作需要开发者自行定义
,以上的设置只是是为了向用户表明这个区域是允许放东西的,那么至于怎么放需要开发者自行决定。
二、拖拽过程触发的一些事件
这一小节将带你了解整个拖放过程的其他细节,比如拖拽过程中会触发哪些事件
2.1 被拖放目标触发的事件
给被拖放目标元素绑定三个事件 dragstart、drag、dragend。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {console.log('开始拖动');})dragDom.addEventListener('drag', (e) => {console.log('拖动中');})dragDom.addEventListener('dragend', (e) => {console.log('结束拖动');})
</script>
React
<div draggable="true"onDragStart={(e) => {console.log("开始拖动", e);}}onDrag={(e) => {console.log("拖动中", e);}}onDragEnd={(e) => {console.log("结束拖动", e);}}
>
>draggable box</div>
<div onDragOver={(e) => {e.preventDefault()}}>放置区域</div>
开始拖动触发 dragstart
,拖动过程中(鼠标不松开)触发drag
,松开鼠标(或者按下 Esc
键)触发 dragend
。
2.2 被拖拽元素在放置区域内会触发的事件
先给放置目标元素绑定四个事件
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragenter', (e) => {console.log('进入到了放置区域~');})dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置区域内拖拽中~');})dropDom.addEventListener('dragleave', (e) => {console.log('离开了放置区域~');})dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')})
</script>
拖拽元素进入放置区域内时触发 dragenter
事件,在放置区域内移动被拖放(鼠标不松开)元素触发 dragover
事件,被拖放元素离开放置区域触发 dragleave
事件,在放置区域内松开鼠标触发 drop
事件。
三、实现真正意义上的元素拖放
通过上面触发的事件我们可以知道,用户真正在放置区域释放鼠标的时候只有 drop 事件能够监听到。所以开发者需要在这个事件里做真正的放置操作,放置什么由开发者决定,可以是被拖拽元素,也可以是自定义的一些内容。
放置被拖拽元素:
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')dropDom.addEventListener('dragover', (e) => {e.preventDefault()console.log('在放置区域内拖拽中~');})dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')e.target.appendChild(document.getElementById('box'))})
</script>
放置自定义内容
dropDom.addEventListener('drop', (e) => {console.log('在放置区域内,放下了被拖拽元素~')let customCOntent = '<p>自定义内容</p>'e.target.innerHTML = e.target.innerHTML + customCOntent
})
四、dataTransfer 对象
4.1 从被拖放元素向可放置元素传递数据
dataTransfer
对象提供了一个setData()
方法,它接受两个参数,第一个参数是传递数据的类型(一般是标准的MIME类型
),第二个数据是数据值。dataTransfer
还提供了getData()
的方法用于获取传递的数据,它接受一个参数,参数值为setData
对应的第一个参数。
传递一个简单的字符串数据
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')})dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let data = e.dataTransfer.getData('text/plain')console.log('你传递的数据为:', data);})
</script>
⚡注意:只能在 dragstart 事件中设置数据,在其他地方设置无效。且只能在 drop 事件中获取设置的数据,其他事件中获取不到。
案例:根据传递的数据放置不同的内容。
<div id="box" draggable="true">draggable box</div>
<div id="droppable">放置区域</div>
<script>let dropDom = document.getElementById('droppable')let dragDom = document.getElementById('box')dropDom.addEventListener('dragover', (e) => {e.preventDefault()})dropDom.addEventListener('drop', (e) => {let num = e.dataTransfer.getData('num')console.log(num);if(num > 5)e.target.innerHTML = e.target.innerHTML + '<p>传递的数字大于5</p>'else if(num == 5) e.target.innerHTML = e.target.innerHTML + '<p>传递的数字等于5</p>'elsee.target.innerHTML = e.target.innerHTML + '<p>传递的数字小于5</p>'})dragDom.addEventListener('dragstart', (e) => {let num = Math.floor(Math.random() * 10) + 1;e.dataTransfer.setData('num', num)})
</script>
4.2 自定义拖拽过程中跟随鼠标移动的内容
默认情况下元素被拖拽时会有一个半透明的元素快照跟随着鼠标移动。通过 dataTransfer 提供的 setDragImage(elemnt, xOffset, yOffset) 方法是可以自定义跟随内容。接受三个参数 elemnt 可以是 dom 节点或者一个图片对象,xOffset, yOffset 是相对于鼠标的偏移量。
设置为一个图片:
<script>let dragDom = document.getElementById('box')dragDom.addEventListener('dragstart', (e) => {let img = new Image()img.src = './ikunlogo.png'e.dataTransfer.setDragImage(img, 10, 10)})
</script>
4.3 设置放置前的反馈图标
dataTransfer 提供了一个
dropEffect
属性设置放置前的反馈图标,它有四种取值 none move copy link
在 dragover 中设置 dropEffect
的值
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'link' // none || move || copy || link
})
- 值为 none 或者经过不可放置区域,显示禁止放置图标
- 值为 move 时
- 值为 copy 时
- 值为 link 时
4.4 拖动文件上传
通过 dataTransfer 的
files
属性可以获取用户拖拽的文件信息
拖拽系统文件到放置区域,并打印拖拽的文件信息:
dropDom.addEventListener('drop', (e) => {e.preventDefault()// 上传的文件列表let fileList = e.dataTransfer.filesfor (let i = 0; i < fileList.length; i++) {const file = fileList[i];console.log('文件名:' + file.name);console.log('文件大小:' + file.size);// 后续操作 比如:调接口上传文件}
})
4.5 清除 setData() 的值
dataTransfer 提供了
clearData()
清除 setData 设置的值,传参数则删除指定类型的值,不传则全部清除。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.getData('text/plain'));console.log(e.dataTransfer.getData('text/html'));
})dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')e.dataTransfer.setData('text/html', '自定义数据2')e.dataTransfer.clearData('text/html')
})
4.6 查看设置了哪些类型的值
dataTransfer 提供了
types
属性查看 setData 设置了哪些类型的值。
dropDom.addEventListener('drop', (e) => {console.log(e.dataTransfer.types);
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.setData('text/plain', '自定义数据')e.dataTransfer.setData('text/html', '自定义数据2')
})
4.7 effectAllowed 属性取值会影响到 dropEffect 的取值效果。
effectAllowed 用于限制 dropEffect 只能设置哪些值
effectAllowed 的取值有: + none -> 此项表示 dropEffect 设置任何值都是禁止效果 + copy -> dropEffect 可以设置为 copy + copyLink -> dropEffect 可以设置为 copy 和 link + copyMove -> dropEffect 可以设置为 copy 和 Move + link -> dropEffect 可以设置为 link + linkMove -> dropEffect 可以设置为 link 和 Move + move -> dropEffect 可以设置为 Move + all -> dropEffect 可以设置为所有合法值 + uninitialized -> 等同 all 效果
dropDom.addEventListener('dragover', (e) => {e.preventDefault()e.dataTransfer.dropEffect = 'move'
})
dragDom.addEventListener('dragstart', (e) => {e.dataTransfer.effectAllowed = 'none'
})
上面即使 dropEffect 设置为 move, 但是 effectAllowed 的值为 none,所有还是禁止放置的反馈图标。
五、总结
- 实现一个拖拽功能时先定义好被拖拽元素和放置区域元素。
- 所有的放置操作都是在 drop 事件中完成。
- 放置前的反馈效果可以根据你传递的数据来设置 dropEffect 显示不同的效果。
- 被拖拽元素也可以是放置区域,放置区域也可以是被拖拽元素,两者没有明确的界限。
- 功能自定义按需求开发