React 多层级嵌套列表(树)的拖拽排序/移动,建议收藏

1,786 阅读3分钟

大家好,我是双越,也是 wangEditor 作者。

今年我致力于开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用,围观项目研发过程。

本文分享一下,如何使用 React + Dnd-kit 实现多层级嵌套列表(树)的拖拽排序和移动(如下图),源码链接在文章最后。

image.png

Dnd-kit 和 Sortable 组件

Dnd-kit 是 React 常用的拖拽工具,它也支持列表拖拽排序,文档和 demo docs.dndkit.com/presets/sor…

image.png

它的核心就是 <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> ,这和单层列表使用的方法一样。

image.png

然后,在 <SortableItem> 继续嵌套下级的 <SortableContext> ,用于显示 children

image.png

运行效果如下,这样的问题:同层级可以拖拽排序,但跨层级是不行的,因为不是一个 Context —— 想来这也合理

image.png

多层级转换为单一层级,可行

既然嵌套不可行,那就需要把多层级转换为第一层级。

但是需要为每个 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 排序之后不会生效。

image.png

另外,我们还可以通过 ancestorsIds 层级关系判断是否可以移动,父节点不能移动到它的子节点,否则死循环了。

例如上图中,我们要拖拽 B2 到 B2a 的位置,就发现 B2a 的 ancestorsIds 包含 B2 ,这样是不行的,因为你不能把一个 item 拖拽到它自己的下级。如下图

拖拽以后修改 state 数据

为了方便操作,把数据放在 Zustand 全局 store

image.png

Dnd-kit 把拖拽的元素较多 activeItem ,把放置的目标位置较多 overItem 。所以修改 state 数据,就是把 activeItem 移动到 overItem 的位置。

如果是单一层级,Dnd-kit 提供了一个方法 arrayMove 可以直接修改,文档 docs.dndkit.com/presets/sor…

image.png

但在多层级嵌套列表(树)中,就需要自己实现,麻烦一些。核心代码在这里,可以下载源代码(文章末尾)查阅。

image.png

遇到一个问题

如下图,把 A 拖拽到 B 下面时,A 会移动到 B 这个整体的下面,而不是 B 里面

image.png

解决这个问题,需要判断 B 后面是否有 B 的子元素,有的话就把 over 赋值给它的子元素

image.png

然后把当前的 active 元素插入到 items 的第一个元素

image.png

最后

demo 源码链接 github.com/wangfupeng1…

前端转全栈,可关注我开发的 Node 全栈 AIGC 知识库 划水AI ,AI 智能写作,多人协同编辑,复杂业务,真实上线。