React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React dnd-kit拖曳排序

React中使用dnd-kit实现拖曳排序功能

作者:non_hana

在这篇文章中,我将带着大家一起探究React中使用dnd-kit实现拖曳排序功能,由于前阵子需要在开发 Picals 的时候,需要实现一些拖动排序的功能,文中通过代码示例介绍的非常详细,需要的朋友可以参考下

由于前阵子需要在开发 Picals 的时候,需要实现一些拖动排序的功能。虽然有原生的浏览器 dragger API,不过纯靠自己手写很难实现自己想要的效果,更多的是吃力不讨好。于是我四处去调研了一些 React 中比较常用的拖曳库,最终确定了 dnd-kit 作为我实现拖曳排序的工具。

当然,使用的时候肯定免不了踩坑。这篇文章的意义就是为了记录所踩的坑,希望能够为有需要的大家提供一点帮助。

在这篇文章中,我将带着大家一起实现如下的拖曳排序的例子:

那让我们开始吧。

安装

安装 dnd-kit 工具库很简单,只需要输入下面的命令进行安装即可:

pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities

这几个包分别有什么作用呢?

使用方法

首先我们需要知道的是,拖曳这个行为需要涉及到两个部分:

在使用 dnd-kit 时,需要对这两个部分分别进行定义。

父容器(DraggableList)的编写

我们首先进行拖曳父容器相关的功能配置。话不多说我们直接上代码:

import { FC, useEffect, useState } from "react";
import type { DragEndEvent, DragMoveEvent } from "@dnd-kit/core";
import { DndContext } from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  rectSortingStrategy,
} from "@dnd-kit/sortable";
import { restrictToParentElement } from "@dnd-kit/modifiers";
import "./index.scss";
import DraggableItem from "../draggable-item";

type ImgItem = {
  id: number;
  url: string;
};

const DraggableList: FC = () => {
  const [list, setList] = useState<ImgItem[]>([]);

  useEffect(() => {
    setList(
      Array.from({ length: 31 }, (_, index) => ({
        id: index + 1,
        url: String(index),
      }))
    );
  }, []);

  const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
    const { active, over } = dragItem;
    const activeIndex = array.findIndex((item) => item.id === active.id);
    const overIndex = array.findIndex((item) => item.id === over?.id);

    // 处理未找到索引的情况
    return {
      activeIndex: activeIndex !== -1 ? activeIndex : 0,
      overIndex: overIndex !== -1 ? overIndex : activeIndex,
    };
  };

  const dragEndEvent = (dragItem: DragEndEvent) => {
    const { active, over } = dragItem;
    if (!active || !over) return; // 处理边界情况

    const moveDataList = [...list];
    const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);

    if (activeIndex !== overIndex) {
      const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
      setList(newDataList);
    }
  };

  return (
    <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
      <SortableContext
        items={list.map((item) => item.id)}
        strategy={rectSortingStrategy}
      >
        <div className="drag-container">
          {list.map((item) => (
            <DraggableItem key={item.id} item={item} />
          ))}
        </div>
      </SortableContext>
    </DndContext>
  );
};

export default DraggableList;

对应的 index.scss

.drag-container {
  position: relative;
  width: 800px;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

return 的 DOM 元素结构非常简单,最主要的无外乎两个上下文组件:DndContextSortableContext

SortableContext 组件内部包裹的,就是我们正常的需要进行排序的列表容器了。当然,dnd-kit 也不是对任何的内容都可以进行排序的。要想实现排序功能,这个被包裹的 DOM 元素必须符合以下几个要求:

在这里附加一个说明,可以看到我初始化的数据的列表 id 是从 1 开始的,因为 从 0 开始会导致第一个元素无法触发移动 。现阶段还不知道是什么原因,大概的猜测是在 JavaScript 和 React 中,id0 可能会被视为“假值”(falsy value)。许多库和框架在处理数据时,会有意无意地忽略或处理“假值”。dnd-kit 可能在某些情况下忽略了 id0 的元素,导致其无法正常参与拖曳操作。总之, 避免第一个拖曳元素的 id 不要为 0 或者空字符串

对于 DndContext,需要传入几个 props 以处理拖曳事件本身。在这里,传入了 onDragEnd 函数与 modifiers 修饰符列表。实际上,这个上下文组件能够传入很多的 props,我在这里简单截个图:

可以看到,不仅是结束回调,也接受拖曳全过程的函数回调并通过回传值进行一些数据处理。

但是,一般用于完成拖曳排序功能我们可以不管这么多,只用管鼠标松开后的回调函数,然后拿到对象进行处理就可以了。

const dragEndEvent = (dragItem: DragEndEvent) => {
  const { active, over } = dragItem;
  if (!active || !over) return; // 处理边界情况

  const moveDataList = [...list];
  const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);

  if (activeIndex !== overIndex) {
    const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
    setList(newDataList);
  }
};

首先检查 activeover 是否有效,避免边界问题,之后创建 moveDataList 的副本,调用 getMoveIndex 函数获取 activeover 项目的索引,如果两个索引不同,使用 arrayMove 移动项目,并更新 list 状态。

getMoveIndex 函数如下,用于获取拖拽项目和目标位置的索引:

const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
  const { active, over } = dragItem;
  const activeIndex = array.findIndex((item) => item.id === active.id);
  const overIndex = array.findIndex((item) => item.id === over?.id);

  // 处理未找到索引的情况
  return {
    activeIndex: activeIndex !== -1 ? activeIndex : 0,
    overIndex: overIndex !== -1 ? overIndex : activeIndex,
  };
};

接下来是对 SortableContext 的配置解析。在这个组件中传入了 itemsstrategy 两个参数。同样地,它也提供了很多的 props 以供个性化配置:

items:用于定义可排序项目的唯一标识符数组,它告诉 SortableContext 哪些项目可以被拖拽和排序。它的类型刚好和上述的 active 和 over 的 id 属性的类型相同,都是 UniqueIdentifier

这也就意味着,我们在 items 这边传入了什么数组来对排序列表进行唯一性表示,active 和 over 就按照什么来追踪元素的排序索引。UniqueIdentifier 实际上是 string 和 number 的联合类型。

至此,父容器组件介绍完毕,我们来看子元素怎么写吧。

子元素(Draggable-item)的编写

上代码:

import { FC } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import "./index.scss";

type ImgItem = {
  id: number;
  url: string;
};

type DraggableItemProps = {
  item: ImgItem;
};

const DraggableItem: FC<DraggableItemProps> = ({ item }) => {
  const { setNodeRef, attributes, listeners, transform, transition } =
    useSortable({
      id: item.id,
      transition: {
        duration: 500,
        easing: "cubic-bezier(0.25, 1, 0.5, 1)",
      },
    });
  const styles = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <div
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      style={styles}
      className="draggable-item"
    >
      <span>{item.url}</span>
    </div>
  );
};

export default DraggableItem;

对应的 index.scss

.draggable-item {
  width: 144px;
  height: 144px;
  background-color: #f0f0f0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: large;
  cursor: pointer;
  user-select: none;
  border-radius: 10px;
  overflow: hidden;
}

子元素的编写相较于父容器要简单得多,需要手动配置的少,引入的包更多了。

首先是引入了 useSortable 这个 hook,主要用来启用子元素的排序功能。这个钩子返回了一组现成的属性和方法:

它接受一个配置对象,其中包含了:

之后我们定义了拖曳样式 styles ,使用了 @dnd-kit/utilities 提供的 CSS 工具库,用于处理 CSS 相关的样式转换,因为这里的 transform 是从 hook 拿到的,是其自定义的 Transform 类型,需要借助其转为正常的 css 样式。我们传入了从 useSortable 中拿到的 transformtransition,用于处理拖曳 item 的样式。

之后就是直接一股脑的将配置全部传入要真正进行拖曳的 DOM 元素:

  return (
    <div
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      style={styles}
      className="draggable-item"
    >
      <span>{item.url}</span>
    </div>
  );
};

实现效果

父容器和子元素全都编写完毕后,我们可以观察一下总体的实现效果如何:

可以看到,元素已经能够正常地被排序,而且列表也能够同样地被更新。结合到具体的例子,可以把这个列表 item 结合更加复杂的类型进行处理即可。只要保证每个 item 有唯一的 id 即可。

对于原有点击事件失效的处理

对于某些需要触发点击事件的拖曳 item,如果按照上述方式封装了拖曳子元素所需的一些配置,那么 原有的点击事件将会失效,因为原有的鼠标按下的点击事件被拖曳事件给覆盖掉了。当然,dnd-kit 肯定也是考虑到了这种情况。他们在其核心库 @dnd-kit/core 当中封装了一个 hook useSensors,用来配置 鼠标拖动多少个像素之后才触发拖曳事件,在此之前不触发拖曳事件

使用方法也非常简单,首先从核心库中导入这个 hook,之后进行如下的配置:

//拖拽传感器,在移动像素5px范围内,不触发拖拽事件
const sensors = useSensors(
  useSensor(MouseSensor, {
    activationConstraint: {
      distance: 5,
    },
  })
);

这里配置了在 5px 范围内不触发拖曳事件,这样就可以在这个范围内进行点击事件的正常触发了。

在上面的 DndContext 的 props 中,我们也看到了其提供了这一属性的配置。我们只用将编写好的 sensors 传入即可:

<DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
  <SortableContext
    items={list.map((item) => item.id)}
    strategy={rectSortingStrategy}
    sensors={sensors}
  >
    <div className="drag-container">
      {list.map((item) => (
        <DraggableItem key={item.id} item={item} />
      ))}
    </div>
  </SortableContext>
</DndContext>

以上就是React中使用dnd-kit实现拖曳排序功能的详细内容,更多关于React dnd-kit拖曳排序的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文