大家好,我是双越,也是 wangEditor 作者。
今年我致力于开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用,围观项目研发过程。
本文分享一下,如何使用 React + Dnd-kit 实现多层级嵌套列表(树)的拖拽排序和移动(如下图),源码链接在文章最后。
Dnd-kit 和 Sortable 组件
Dnd-kit 是 React 常用的拖拽工具,它也支持列表拖拽排序,文档和 demo docs.dndkit.com/presets/sor…
它的核心就是 <SortableContext>
组件,内部的元素即可拖拽排序,并且修改 items
数组的排序。
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={items}
strategy={verticalListSortingStrategy}
>
{items.map(id => <SortableItem key={id} id={id} />)}
</SortableContext>
</DndContext>
但它只支持单一层级的列表,想要实现多层级嵌套列表(树)的拖拽排序,还需要我们自己改造。
定义 state 数据结构
现代前端框架 React Vue 等都是数据驱动视图,所以凡事先定义数据结构,再考虑 UI 渲染。
多层级嵌套列表(树),最常见的数据结构定义如下,虚拟 DOM vnode
也是这样定义的。
const defaultItems = [
{ id: 'A', children: [] },
{
id: 'B',
children: [
{ id: 'B1', children: [] },
{
id: 'B2',
children: [
{ id: 'B2a', children: [] },
{ id: 'B2b', children: [] },
],
},
],
},
{ id: 'C', children: [] },
{
id: 'D',
children: [
{ id: 'D1', children: [] },
{ id: 'D2', children: [] },
],
},
{ id: 'E', children: [] },
]
多层嵌套 SortableContext 不可行
因为 state 数据结构是嵌套的,所以我首先想到的是一起嵌套渲染 UI 结构。
首先,在 <SortableContext>
中嵌套 <SortableItem>
,这和单层列表使用的方法一样。
然后,在 <SortableItem>
继续嵌套下级的 <SortableContext>
,用于显示 children
运行效果如下,这样的问题:同层级可以拖拽排序,但跨层级是不行的,因为不是一个 Context
—— 想来这也合理
多层级转换为单一层级,可行
既然嵌套不可行,那就需要把多层级转换为第一层级。
但是需要为每个 item 增加 ancestorsIds
属性,第一为展示层级深度,第二可以知道它的父节点有哪些。
interface IItem {
id: string
ancestorIds?: string[]
children?: IItem[]
}
function flatten(items: IItem[]): IItem[] {
return items.reduce<IItem[]>((acc, item) => {
acc.push(item)
if (item.children) {
const children = item.children.map((i) => ({
...i,
ancestorIds: [...(item.ancestorIds || []), item.id], // add ancestorIds
}))
acc.push(...flatten(children))
}
return acc
}, [])
}
转换以后的渲染效果如下,此时就可以拖拽排序了。只不过还未修改 state 排序之后不会生效。
另外,我们还可以通过 ancestorsIds
层级关系判断是否可以移动,父节点不能移动到它的子节点,否则死循环了。
例如上图中,我们要拖拽 B2 到 B2a 的位置,就发现 B2a 的 ancestorsIds
包含 B2 ,这样是不行的,因为你不能把一个 item 拖拽到它自己的下级。如下图
拖拽以后修改 state 数据
为了方便操作,把数据放在 Zustand 全局 store
中
Dnd-kit 把拖拽的元素较多 activeItem ,把放置的目标位置较多 overItem 。所以修改 state 数据,就是把 activeItem 移动到 overItem 的位置。
如果是单一层级,Dnd-kit 提供了一个方法 arrayMove
可以直接修改,文档 docs.dndkit.com/presets/sor…
但在多层级嵌套列表(树)中,就需要自己实现,麻烦一些。核心代码在这里,可以下载源代码(文章末尾)查阅。
遇到一个问题
如下图,把 A 拖拽到 B 下面时,A 会移动到 B 这个整体的下面,而不是 B 里面
解决这个问题,需要判断 B 后面是否有 B 的子元素,有的话就把 over 赋值给它的子元素
然后把当前的 active 元素插入到 items 的第一个元素
最后
demo 源码链接 github.com/wangfupeng1…
前端转全栈,可关注我开发的 Node 全栈 AIGC 知识库 划水AI ,AI 智能写作,多人协同编辑,复杂业务,真实上线。