前言
在 HTML5 还未普及之前,实现拖拽的大致思路是监听鼠标移动相关事件,来拖动目标元素到页面的任意位置,这要求目标元素必须满足绝对定位、脱离文档流才可以被移动,伪代码如下:
box.onmousedown = (event) => {
// 记录起始位置
document.onmousemove = (event) => {
// 计算移动位置,移动目标元素
}
document.onmouseup = () => {
// 松开鼠标,结束移动
}
}
HTML5 标准出来后,为标签元素新增了拖拽 draggable
属性,有了这个属性,我们可以通过监听元素的拖拽事件就能实现各种拖放功能。
默认情况下图片和链接可以被拖动,其他元素需要像下面这样,设置 draggable
属性:
<div draggable="true" id="draggableBox"></div>
下面我们先了解一下拖拽事件。
拖拽事件
拖拽事件涉及到两个步骤:拖起、放下
,事件可以分为两类:
- 一类是被拖起的元素所触发的事件,即拖起事件;
- 一类则是拖起放下后,所在区域的目标元素触发的事件,即拖放事件。
拖起事件(拖拽元素)
拖拽元素 自身的事件。
ondragstart
:当鼠标按下并且开始移动拖拽元素后,触发此事件,整个拖拽周期只触发一次;ondrag
:拖拽过程中会不断触发此事件;ondragend
:鼠标松开结束拖拽后,会触发此事件的执行,整个拖拽周期只触发一次。
const draggableBox = document.getElementById('draggableBox');
draggableBox.ondragstart = function (event) {
console.log('开始拖拽');
}
draggableBox.ondrag = function (event) {
console.log('拖拽中');
}
draggableBox.ondragend = function (event) {
console.log('拖拽结束');
}
拖放事件(目标元素)
当拖拽的元素拖到一个目标元素上时,目标元素 会触发以下事件:
ondragenter
:拖拽元素移动到目标元素时,触发此事件;ondragover
:拖拽元素停留在目标元素时,会持续触发此事件;ondragleave
:拖拽元素离开目标元素时(没有在目标元素上放下),触发此事件;ondrop
:拖拽元素在目标元素放下(松开鼠标),触发此事件。
const target = document.getElementById('target');
target.ondragenter = function (event) {
console.log('进入目标元素');
}
target.ondragover = function (event) {
event.preventDefault();
console.log('在目标元素中拖拽');
}
target.ondragleave = function (event) {
console.log('拖放离开目标元素');
}
target.ondrop = function (event) {
console.log('在目标元素中拖放');
}
注意
:目标元素默认是不能够被拖放的,即不会触发 ondrop
事件,可以通过在目标元素的 ondragover
事件中取消默认事件来解决此问题。
拖拽状态(视图体现)
当我们拖拽起元素时,可以为拖拽元素添加一些特定样式来让用户知道,当前他对哪个元素进行了拖拽,这在一组列表中来拖拽 item 非常需要。
我们可以在事件源 event
中拿到元素节点,设置样式如下:
const draggableBox = document.getElementById('draggableBox');
draggableBox.ondragstart = function (event) {
console.log('开始拖拽');
event.target.classList.add('draggable-style');
}
draggableBox.ondragend = function (event) {
console.log('拖拽结束');
event.target.classList.remove('draggable-style');
}
数据交换
有时候,我们在拖起元素时,需要携带一些数据,在拖放到目标元素后,将数据交给目标元素做处理。HTML5 拖拽
提供了数据交换的方式:event.dataTransfer
const draggableBox = document.getElementById('draggableBox');
const target = document.getElementById('target');
draggableBox.ondragstart = function (event) {
console.log('开始拖拽');
event.dataTransfer.setData('value', '数据传递');
}
// ...
target.ondrop = function (event) {
console.log('在目标元素中拖放');
const value = event.dataTransfer.getData('value');
}
注意
:event.dataTransfer.setData
默认只能传递 string
类型数据,如果是一个对象,可以使用 JSON.stringify
转换。
自定义原生拖动图像(拖拽效果)
原生默认的拖拽效果是拖拽元素的一个半透明的预览图,有时候我们需要自定义拖拽时的预览图来实现我们的需求,HTML5 拖拽
也提供了自定义方式:
语法如下:
event.dataTransfer.setDragImage(element, xOffset, yOffset);
使用示例:
const draggableBox = document.getElementById('draggableBox');
const target = document.getElementById('target');
draggableBox.ondragstart = function (event) {
console.log('开始拖拽');
e.dataTransfer.setDragImage(document.getElementById('customEle'), 0, 0);
}
上面代码中我们将 customEle 元素
作为拖动图像,拖动图像的显示位置与鼠标指针看齐。
注意
:我们自定义的元素必须是一个已经挂载到 DOM 树上的元素,否则自定义图像会失败,这一点在 React、Vue
框架中要特别注意,虚拟 DOM 对象
用在这里不生效。
在 React 中使用:
有了上面的基础,我们就可以很容易在 React 中去使用拖拽。
拖拽元素
场景:一组 list 列表中,每一个 item 都作为一个被拖拽的元素。
DOM 结构:
{list.map((item) => (
<li
draggable={item.allowMove ? true : false}
onDragStart={e => handleDragStart(e, item)}
onDragEnd={handleDragEnd}>
// ...
</li>
)
// 自定义拖动图像
<div
ref={draggableView}
style={{ display: 'none' }}
className="gl-draggable-preview">
// ...
</div>
方法定义:
const draggableView = useRef<HTMLDivElement>(null); // 拖拽图像
const [draggableData, setDraggableData] = useState({}); // 拖拽项的数据
// 获取拖拽图像的容器盒子
export const getDraggablePreview = () => {
let previewEle = document.querySelector('#draggable-preview'); // 要拖动渲染的预览图
if (!previewEle) {
previewEle = document.createElement('div');
previewEle.id = 'draggable-preview';
document.body.appendChild(previewEle);
}
return previewEle;
}
const handleDragStart = (e, data) => {
console.log('开始拖拽');
// 保存当前 item 数据,用于在自定义图片中展示
setDraggableData(data);
// 为拖拽的元素添加拖拽 class 样式
e.target.classList.add('list-item-draggable');
// 从 body 上获取(没有则创建)用于存放自定义拖动图像的盒子
const previewBox = getDraggablePreview();
// 拖动图像默认是隐藏的,让其显示
draggableView.current!.style.display = 'inline-flex';
// 将拖动图像添加到 DOM 树上的盒子中
previewBox.appendChild(draggableView.current as Node);
// 将 DOM 树上的拖动图像作为我们拖拽时显示的图像
e.dataTransfer.setDragImage(previewBox, 0, 0);
// 数据交换
e.dataTransfer.setData('data', JSON.stringify({ id: data.id, type: data.type }));
}
const handleDragEnd = (e) => {
console.log('结束拖拽');
// 移除拖拽 class 样式
e.target.classList.remove('list-item-draggable');
const previewBox = getDraggablePreview();
// removeChild,会将自定义图像从 previewBox 中移除,并且 draggableView.current 会置为 null
previewBox.removeChild(draggableView.current);
}
目标元素
DOM 结构:
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragover}
onDrop={handleDrop}
>
// ...
</div>
方法定义:
const handleDragEnter = (e) => {
console.log('进入目标元素', e.target.id);
// 添加拖放图像移动到目标元素的 class 样式
target.classList.add('target-draggable');
}
const handleDragLeave = (e) => {
console.log('拖放离开目标元素');
target.classList.remove('target-draggable');
}
const handleDragover = (e) => {
e.preventDefault();
}
const handleDrop = async (e, id) => {
target.classList.remove('target-draggable');
// 获取携带数据
const data = JSON.parse(e.dataTransfer.getData('data'));
// ...
}
文末
本文在编写中如有不足的地方,👏欢迎读者提出宝贵意见。