@dnd-kit 一个轻量级、模块化、高性能、可访问和可扩展的 React 拖放工具包。
原本使用的是react-sortable-hoc,在查阅该库相关文档发现作者建议采用dnd-kit替换。
因此最终决定直接使用dnd-kit库来实现拖拽排序功能。
下面给出实现代码和需要注意的点:
yarn add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @dnd-kit/modifiers
然后需要了解两个核心的组件
DndContext和SortableContext2.1 DndContext:可以理解为dnd的容器,想要使用拖拽这是必须的,并且相应的回调监听和配置都需要附加在这里
2.2 SortableContext:滑动列表的容器,可以类似理解为一个ul,内部的列表项配置拖拽
这里必须
注意:(items为必填项,表示哪些列表在我这个容器中滑动,items为唯一标识的数组,这个标志数组必须和后面useSortable传入的id属性一一对应)<SortableContext items={dataList.map((d) => d.key)} strategy={verticalListSortingStrategy} > {......} </SortableContext>所以基本结构就是
DndContext包括SortableContext再包括需要拖动的列表项<DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}> {/* 在这里配置拖动结束后的回调函数,modifiers可以限定元素可以拖动的区域 这里强烈建议配置这个限定区域,不然当元素拖出了对应区域, 而有功能逻辑涉及时会取到null,产生错误 */} {/* 拖拽结束后要实现的就是移动元素的位置,同时同步给状态再传给父组件modal dnd库里自带了一个arrayMove方法,但是需要传入开始和结束index 因为事件的回调参数是active和over对象,继续调用编写的getMoveIndex方法获得*/}接下来的核心的就是如何实现我们的
sortableItemconst { setNodeRef, attributes, listeners, transform, transition } = useSortable({ id: checkboxItem.key, // 这里传入的id属性必须和SortableContext的items数组一一对应 transition: { duration: 500, easing: 'cubic-bezier(0.25, 1, 0.5, 1)', }, }); {/* 这是核心的钩子,返回值如下: setNodeRef:设置ref={setNodeRef},使Dom成为一个可拖拽的项 listeners:包含一系列触发拖拽方法,默认是鼠标触发,附加这个属性可以灵活地设置拖拽部 位,结合配置拖拽手柄 transform:该节点被拖动时候的移动变化值 transition:过渡效果 isDragging:节点是否在拖拽 */} 所以,我们在li元素挂载ref,attributes(官方推荐一起传入,防止预期之外的错误),style, 在li元素的子元素中自定义组件内容,为拖拽手柄设置listeners即可最后需要注意的一点,一般我们都会把这个排序作为一个组件的子组件,因为我们采用的受控的方式,状态由自己维护,所以需要在初始化时将状态传给父组件,否则如果既不拖动也不点击,父组件modal则会可能拿到undefined
最后,我们需要注意,因为拖动事件和点击事件存在叠加,如果事件存在交叉
所以可以有这样的解决思路:精确化设置listeners,这样拖动开关只会生效在设置的dom元素,不然默认是在每个列表项的父元素上的
第二,如果确有特殊需求,可以阻止默认事件冒泡,手动调度事件触发顺序
/* eslint-disable jsx-a11y/label-has-associated-control */
import { DndContext } from '@dnd-kit/core';
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { restrictToParentElement } from '@dnd-kit/modifiers';
import React, { useState, useEffect } from 'react';
// index获取算法,事件的回调包含active和over对象,无法直接用indexOf获取在原数组中的index
// 用这个方法遍历获取在原数组对应的位置index,供arrayMove方法使用
const getMoveIndex = (array, dragItem) => {
const { active, over } = dragItem;
let activeIndex = 0;
let overIndex = 0;
try {
// 遍历数组,查找出active和over的index
array.forEach((item, index) => {
if (active.id === item.key) {
activeIndex = index;
}
if (over.id === item.key) {
overIndex = index;
}
});
} catch (error) {
overIndex = activeIndex; // 如果有问题,则复位
}
return { activeIndex, overIndex };
};
export default function DndSortComponent() {
const [dataList, setDataList] = useState([
{ key: 'name', width: 0.25, isChecked: true, title: '姓名' },
{ key: 'age', width: 0.25, isChecked: true, title: '年龄' },
{ key: 'sex', width: 0.25, isChecked: true, title: '性别' },
{ key: 'phone', width: 0.25, isChecked: true, title: '手机号' },
]);
useEffect(() => {
props.onChange(dataList);// 这里写你自己对父组件传值的方式
}, []);
// 拖拽结束后的操作
const dragEndEvent = (dragItem) => {
setDataList((prevDataList) => {
const moveDataList = [...prevDataList];
const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);
const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
return newDataList;
});
};
// 点击checkbox的操作,修改指定ID的对象的isChecked属性取反即可
const handleCheckedChange = (chosedItemKey) => {
setDataList((prevDataList) => {
const newDataList = [...prevDataList];
const updatedDataList = newDataList.map((item) => {
if (item.key === chosedItemKey) {
return { ...item, isChecked: !item.isChecked };
}
return item;
});
return updatedDataList;
});
};
// 拖拽项组件
const SortableItem = (itemProps) => {
// 父传子,从props里拿,建议使用其他名字(如itemProps)代替props,以免和父组件的props混淆
const { checkboxItem } = itemProps;
const { setNodeRef, attributes, listeners, transform, transition } = useSortable({
id: checkboxItem.key, // 这里传入的id属性必须和SortableContext的items数组一一对应
transition: {
duration: 500,
easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
},
});
const styles = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li ref={setNodeRef} {...attributes} style={styles}>
<label className="check-label">
<input
type="checkbox"
name={checkboxItem.key}
checked={checkboxItem.isChecked}
onChange={() => handleCheckedChange(checkboxItem.key)}
/>
<label className="name-label">{checkboxItem.ptitle || checkboxItem.title}</label>
{/* 为Dom添加listeners属性表示设置其为可拖动项 */}
<span className="btn all-scroll" {...listeners}>
♬
</span>
</label>
</li>
);
};
return (
<DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
<SortableContext items={dataList.map((c) => c.key)} strategy={verticalListSortingStrategy}>
{/* 这里的items接收一个数组,这个数组的值要和useSortable传入的id属性一一对应 */}
<div className="attrs">
<ul className="attrs-list">
{dataList.map((checkboxItem) => (
<SortableItem checkboxItem={checkboxItem} key={checkboxItem.key} />
))}
</ul>
</div>
</SortableContext>
</DndContext>
);
}