这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
前言
拖放 在实际业务中也是一个常见的功能,比如通过拖放列表元素进行排序、拖放元素在不同的容器中穿梭等。提到拖放,我们想到的是原生 DOM 事件的操作、元素移动时位置的计算以及动画等等,第一反应必然是找第三方拖放库 ‘帮助’ 我们完成需求咯。然而,第三方库虽然功能强大,但很多功能你未必需要,杀鸡焉用牛刀呢?
所以我们可以根据实际场景的复杂度来选择实现方式,第三方库的强大功能 or 自己实现最精炼的代码。
自己动手(代码不过百)
实现一个列表拖放排序的功能,选中元素后,元素实时跟随鼠标移动,并且列表实时更新,如下图:
准备工作
在 React 中实现拖放的技术要点:
- 熟悉鼠标事件:
MouseDown
、MouseUp
、MouseMove
- 如何触发拖放开始(
MouseDown
)和判断拖放结束(MouseUp
) - 如何实现拖放元素位置的移动,可以用两种方案
- 原元素跟着鼠标移动
- 创建一个新元素,作为原元素的影子,跟随鼠标移动
- 在组件中维护拖放的状态
拖放开始
在行元素上监听 Mousedown 事件,在 Mousedown 中初始化拖放状态:
- dragging:true,拖放中
- startPageY,记录鼠标开始位置(纵向)
- draggingIndex,选中元素的 index
<li onMouseDown={(evt) => handleMounseDown(evt, i)} style={getDraggingStyle(i)}>
{text}
</li>
const handleMounseDown = (evt, index) => {
setDragging(true);
setStartPageY(evt.pageY);
setDraggingIndex(index);
};
样式上高亮拖放元素并且添加跟随鼠标效果,translateX
可以产生视觉上悬浮的效果,其中的 offsetPageY
的值将会在拖放过程中(MouseMove
)计算得来:
const getDraggingStyle = (index) => {
if (index !== draggingIndex) return {};
return {
backgroundColor: 'lightsteelblue',
transform: `translate(10px, ${offsetPageY}px)`,
};
};
拖动中
拖放中最关键的地方是:该在哪个节点上监听 MouseMove
事件?
用户拖动时,鼠标是可以全页面跑的,如果在拖动元素( <li/>
)或者父元素( <ul/>
)上监听事件都不合适。此时有一个方案:创建一个透明的全局的 fixed mask
,既覆盖了鼠标的运动轨迹,又防止触发其他元素。
{
dragging && (
<div
className="my-dnd-mask"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
/>
);
}
.my-dnd-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0);
}
在 MouseMove 中,就是核心计算代码:
- 计算偏移值(offset),
let offset = evt.pageY - startPageY;
- 当偏移值大于行高时,进行位置交换(
move
),并重新设置DraggingIndex
、StartPageY
。 - 当偏移值小于行高时,操作同上
- 当偏移值没有超过行高,只需要设置
offsetPageY
const move = (arr, startIndex, toIndex) => {
arr = arr.slice();
arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
return arr;
};
const handleMouseMove = (evt) => {
let offset = evt.pageY - startPageY;
if (offset > lineHeight && draggingIndex < list.length - 1) {
// move down
offset -= lineHeight;
setList((pre) => move(pre, draggingIndex, draggingIndex + 1));
setDraggingIndex((pre) => pre + 1);
setStartPageY((pre) => pre + lineHeight);
} else if (offset < -lineHeight && draggingIndex > 0) {
// move up
offset += lineHeight;
setList((pre) => move(pre, draggingIndex, draggingIndex - 1));
setDraggingIndex((pre) => pre - 1);
setStartPageY((pre) => pre - lineHeight);
}
setOffsetPageY(offset);
};
拖放结束
设置 dragging: false
,mask 层消失, startPageY
、draggingIndex
初始化。
const handleMouseUp = () => {
setDragging(false);
setStartPageY(0);
setDraggingIndex(-1);
};
完整代码
完整代码可以看 这里
拖放库
找了一个在 React 最有名的拖放库,react-beautiful-dnd 。
安装
# yarn
yarn add react-beautiful-dnd
# npm
npm install react-beautiful-dnd --save
使用 react-beautiful-dnd
:
import { DragDropContext } from 'react-beautiful-dnd';
基础使用
需要我们管理的部分:
- 展示的数据
- 拖放结束后的动作(列表重排序)
- 拖放时的变化(样式)
其余的事情,react-beautiful-dnd
会办好的。
Dom 结构
<DragDropContext onDragEnd={onDragEnd}>
<center>
<Droppable droppableId="droppable">
{(provided, snapshot) => {
return (
<div ref={provided.innerRef}>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef}>{item.content}</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
);
}}
</Droppable>
</center>
</DragDropContext>
拖放结束,重新排序
// 元素移动
const move = (arr, startIndex, toIndex) => {
arr = arr.slice();
arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
return arr;
};
const onDragEnd = (result) => {
if (!result.destination) {
return;
}
setItems((pre) => move(pre, result.source.index, result.destination.index));
};
拖放中,样式管理
// 设置样式
const getItemStyle = (isDragging, draggableStyle) => ({
userSelect: 'none',
padding: grid * 2,
margin: `0 0 ${grid}px 0`,
// 拖拽的时候,item 背景变化
background: isDragging ? 'lightgreen' : '#ffffff',
...draggableStyle,
});
const getListStyle = (isDraggingOver) => {
return {
// 拖拽的时候,list 背景变化
background: isDraggingOver ? 'darkgreen' : 'gray',
padding: 8,
width: 250,
};
};
<Droppable droppableId="droppable">
{(provided, snapshot) => {
return (
<div style={getListStyle(snapshot.isDraggingOver)}>
{items.map((item, index) => (
<Draggable>
{(provided, snapshot) => (
<div
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
)}
></div>
)}
</Draggable>
))}
</div>
);
}}
</Droppable>;
效果预览
完整代码
完整代码可以看 这里
进阶操作
详细的 API 介绍和进阶操作(容器间的拖拽 & 容器可滚动),看这边。