背景
工作中遇到一个项目,需要实现拖拽调整顺序,所以调研了一下拖动排序的实现方式。
原生实现
相关事件
HTML5 提供了 拖拽API ,要实现一个简单的拖拽功能只需要使用以下三个事件
- dragstart:在拖动开始时做一些处理,比如记录拖动的元素,给拖动的元素添加一些样式等
- dragover:控制元素移动过程中的元素交换表现
- dragend:在拖动结束时清除拖动开始时设定的事件
一个简单的实现
// 根据拖动元素拖动到的位置,来重新排序列表,实现动效
const updateList = useCallback(
(positionX, positionY) => {
const wrapperRect = dragWrapperRef.current?.getBoundingClientRect();
if (!wrapperRect) {
return;
}
const offsetX = positionX - wrapperRect.left;
const offsetY = positionY - wrapperRect.top;
if (
!dragItemRef ||
!dragItemRef.current ||
offsetX < 0 ||
offsetY < 0 ||
offsetX > wrapperRect.width ||
offsetY > wrapperRect.height
) {
return null;
}
const newIndex =
Math.floor(offsetX / (ITEM_MARGIN * 2 + ITEM_WIDTH)) +
Math.floor(offsetY / (ITEM_MARGIN * 2 + ITEM_HEIGHT)) * COLUMN;
let copy = [...dataList];
let dragIndex = dataList.indexOf(dragItemRef.current);
if (dragIndex < 0) {
return null;
}
copy.splice(dragIndex, 1);
if (dragIndex < newIndex) {
copy.splice(newIndex + 1, 0, dragItemRef.current);
} else {
copy.splice(newIndex, 0, dragItemRef.current);
}
setDataList(copy);
},
[dataList]
);
// 得到拖动元素的引用,在拖动时给拖动背景加上样式
const handleDragStart = (event: DragEvent<HTMLElement>, item: DragDataItem) => {
dragItemRef.current = item;
const el = dragWrapperRef.current?.querySelector(
`[data_id="data_id_${item.id}"]`
);
if (dragWrapperRef) {
el.classList.add(styles.dragItemActive);
}
};
// 当元素拖动过来时更新列表,交换位置
const handleDragOver = useCallback(
(event: DragEvent<HTMLElement>) => {
event.preventDefault();
updateList(event.clientX, event.clientY);
},
[updateList]
);
// 拖动结束后,清空拖动开始的设置
const handleDragEnd = (event: DragEvent<HTMLElement>) => {
const item = dragItemRef.current;
const el = dragWrapperRef.current?.querySelector(
`[data_id="data_id_${item.id}"]`
);
if (el) {
el.classList.remove(styles.dragItemActive);
}
dragItemRef.current = null;
};
复制代码
实现效果
缺点
- 封装性差,如果要做一个通用的组件的话需要自己封装
- 样式需要自己设置,动效什么的也需要自己写
- 要达到理想的效果,需要自己进行大量开发,代码量大,开发周期长
react-dnd
安装
npm i -s react-dnd react-dnd-html5-backend
复制代码
原理
react-dnd 是一个高阶组件库,用相应的API包裹子组件,即可使其可以拖拽
- item 描述拖拽组件的纯JS对象
- type 代表 item 的类型,标志哪些拖拽源可以放在哪些目标上
- monitor 对象暴露组件的拖拽状态,通过其 API 可以实现当拖拽状态改变时更新组件的 props
- connector 对象定义事件监听的组件,给组件绑定预定义的角色例如拖拽源,拖放目标。返回的结果是记忆的,不会破坏 shouldComponentUpdate 优化
- collect 函数定义函数,从 monitor 中获取状态, 从 connector 中获取对象,react-dnd 定时调用 collect函数,并将其返回值合并后传递给你组件的 props,决定注入到组件的 props
- dragSource 可拖拽源,注册为特定的 type,根据 props 返回特定的组件
- dropTarget 类似 dragSource,不同之处是可以注册多个 type,可以处理多种 type 的组件的 hover 或 drop
- Backends 使用的拖拽事件 API,默认是 HTML5,但是有兼容问题,对触屏和 IE 不友好
- Hook API:useDrag,useDrop和useDragLayer
一个简单实现
const Demo: FunctionComponent = () => {
const [componentList, setComponentList] = useState(ComponentList)
// 排序方法
const moveCard = useCallback((dragIndex, hoverIndex) => {
let copy = [...componentList]
let item = copy[dragIndex]
copy.splice(dragIndex, 1)
copy.splice(hoverIndex, 0, item)
setComponentList(copy)
}, [componentList])
return (
<div className={s.dndWrapper}>
<DndProvider backend={HTML5Backend}>
<div className={`${s.dropAreaWrapper} wrapper`}>
{
componentList.map((item, index)=>{
return <ComponentListItem key={item.id} id={item.id} index={index} text={item.text} index={index} moveCard={moveCard}/>
})
}
</div>
</DndProvider>
</div>
)
}
export default Demo
// 基础拖拽组件
const ComponentListItem: FunctionComponent = ({id, text, index, moveCard}) => {
const ref = useRef(null)
const [{handlerId}, drop] = useDrop({
accept: ComponentDropType,
// 实现实时交换的动效
hover(item, monitor) {
if(!ref.current){
return null
}
const dragIndex = item.index
const hoverIndex = index
if(dragIndex === hoverIndex){
return
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveCard(dragIndex, hoverIndex);
item.index = hoverIndex;
}
})
const [{ isDragging }, drag] = useDrag({
type: ComponentDropType,
item: () => {
return {
id,
index,
itemType: ComponentDropType,
}
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (<div ref={ref} style={{ opacity }} data-handler-id={handlerId} className={s.componentListItem}>
{text}
</div>);
}
复制代码
实现效果
优点
- 可以用于不规则元素的拖拽,适用范围广
- 可扩展性强,可以实现复杂的拖拽需求
缺点
- 学习成本高,需要自己实现的东西过多,开发周期长
- 没有默认样式,需要自定义动画等表现
react-beautiful-dnd
安装
npm install react-beautiful-dnd --save
复制代码
原理
是一个专用于列表的react拖放组件库,自带样式,支持鼠标和键盘事件,抽象性高,简单易用。
主要包含以下三个概念:
- DragDropContext:最外层包装组件,拖放背景,不支持嵌套
- Droppable:可放置拖动组件的区域,包装Draggable组件的外层组件
- Draggable:可以拖放的组件,必须放在Droppable组件里面
一个简单实现
const Demo: FC = () => {
const [dropDataSource, setDropDataSource] = useState(DataSourceGenerator(12));
const onDragStart = () => {};
const onDragUpdate = (result, ...props) => {
const { source, destination } = result;
};
// 只需要在拖动结束时做一些处理,拖动中间过程中元素的移动表现组件库都处理好了
const onDragEnd = (result) => {
const { source, destination } = result;
if (!destination) {
return;
}
const fromIndex = source.index;
const fromDropId = source.droppableId;
const toIndex = destination.index;
const toDropId = destination.droppableId;
// 判断一下拖动来源是不是目标拖动来源
if (fromDropId == DropAreaId && toDropId == DropAreaId) {
const copy = [...dropDataSource];
const [from] = copy.splice(fromIndex, 1);
copy.splice(toIndex, 0, from);
setDropDataSource(copy);
}
};
return (
<DragDropContext
onDragStart={onDragStart}
onDragUpdate={onDragUpdate}
onDragEnd={onDragEnd}
>
<Droppable droppableId={DropAreaId}>
{(provided, snapshot) => {
return (
<div
key="REACT_BEAUTIFUL_DND_DROP_WRAPPER"
ref={provided.innerRef}
className={`${s.dragComponentWrapper} ${
snapshot.isDraggingOver ? s.dragComponentWrapperActive : ""
}`}
{...provided.droppableProps}
style={{ height: 45 * dropDataSource.length + 20 }}
>
{dropDataSource.map((item, index) => {
return (
<>
<Draggable
key={`${item.id}_drop`}
draggableId={`${item.id}_drop`}
index={index}
>
{(provided, snapshot) => {
return (
<>
<div
key={`${item.id}_drop`}
ref={provided.innerRef}
className={`${s.dragComponentItem} ${
snapshot.isDragging
? s.dragComponentItemActive
: ""
}`}
{...provided.dragHandleProps}
{...provided.draggableProps}
>
<span key={1} className={s.dragItemIndex}>
[{index}]
</span>
<span key={2} className={s.dragItemContent}>
{item.title}
</span>
</div>
</>
);
}}
</Draggable>
</>
);
})}
{/* 用于拖动到可放置区域的末尾时,自动扩展可放置区域 */}
{provided.placeholder}
</div>
);
}}
</Droppable>
</DragDropContext>
);
};
复制代码
实现效果
优点
- 开箱即用,封装性好,支持快速开发
- 对动画做了很好的处理,拖动过程十分顺滑
缺点
- 适用范围有限,只支持列表形式,不支持不规则区域的拖动
总结
使用了以上三种方式来实现拖动排序后,个人选择了react-beautiful-dnd来完成项目,因为使用场景是列表拖动,没有其他复杂的拖动场景,而且基于其强大的功能,可以加快项目进度。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END