脚本之家

电脑版
提示:原网页已由神马搜索转码, 内容由www.jb51.net提供.
您的位置:首页网络编程JavaScriptjavascript类库React→ React dnd-kit拖曳排序

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

  更新时间:2024年06月27日 09:09:28  作者: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/core:核心库,提供基本的拖拽功能。
  • @dnd-kit/sortable:扩展库,提供排序功能和工具。
  • @dnd-kit/modifiers:修饰库,提供拖拽行为的限制和修饰功能。
  • @dnd-kit/utilities:工具库,提供 CSS 和实用工具函数。上述演示的平滑移动的样式就是来源于这个包。

使用方法

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

  • 能够允许被拖曳的有限空间(父容器)
  • 用户真正进行拖曳的子元素

在使用 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

  • DndContext:是 dnd-kit 的核心组件,用于提供拖放的上下文。
  • SortableContext:是一个上下文组件,用于提供排序的功能。

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

  • 必须是可排序的元素:SortableContext需要包裹的元素具有相同的父级容器,且这些元素需要具备可排序的能力。每个子元素应当是独立的可拖拽项,例如一个列表项、卡片或网格中的块。

  • 提供唯一的 id:每个可排序的子元素必须具有唯一的 idSortableContext会通过这些 id来识别和管理每个拖拽项的位置。你需要确保 items属性中提供的 id数组与实际渲染的子元素的 id一一对应。

  • 需要是同一个父容器的直接子元素:SortableContext内部的子元素必须是同一个父容器的直接子元素,不能有其他中间层级。这是因为排序和拖拽是基于元素的相对位置和布局来计算的。

  • 使用相同的布局策略:SortableContext的子元素应当使用相同的布局策略,例如使用 CSS Flexbox 或 Grid 进行布局。这样可以确保拖拽操作时,子元素之间的排列和移动逻辑一致。

  • 设置相同的样式属性:确保子元素具有相同的样式属性,例如宽度、高度、边距等。这些属性一致性有助于拖拽过程中视觉效果的一致性和准确性。

  • 添加必要的样式以支持拖拽:为了支持拖拽效果,子元素应具备必要的样式。例如,设置 positionrelative以便于绝对定位的拖拽项,设置 overflow以防止拖拽项溢出。

  • 确保有足够的拖拽空间:父容器应当有足够的空间来允许子元素的拖拽操作。如果空间不足,可能会导致拖拽操作不顺畅或无法完成。

  • 子元素必须具备 draggable属性:每个子元素应该具备 draggable属性,以表明该元素是可拖动的。这通常通过 dnd-kit 提供的组件如 DraggableSortable来实现。

  • 提供合适的拖拽处理程序:为子元素添加合适的拖拽处理程序,通常通过 dnd-kit 提供的钩子或组件实现。例如,使用 useDraggable钩子来处理拖拽逻辑。

  • 处理子元素布局变化:确保在拖拽过程中,子元素的布局变化能够被正确处理。例如,设置适当的动画效果以平滑地更新布局。

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

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

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

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

  • onDragEnd:顾名思义,就是用户鼠标松开后触发的拖曳事件的回调。触发时会自动传入类型为 DragEndEvent的对象,我们可以从其中拿出 activeover两个参数来具体处理拖曳事件。

    active 包含 正在拖曳的元素的相关信息,over 包含最后鼠标松开时所覆盖到的元素的相关信息

    结合到我的函数:

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,
};
};
  • 通过 findIndex获取 activeover项目的索引,如果未找到,默认返回 0。

  • modifiers:标识符,传入一个标识符数组以限制在父组件进行拖曳的行为。主要可选的一些标识符如下:

    • restrictToParentElement:限制在父元素内。
    • restrictToFirstScrollableAncestor:限制在第一个可滚动祖先元素。
    • restrictToVerticalAxis:限制在垂直轴上。
    • restrictToHorizontalAxis:限制在水平轴上。
    • restrictToBoundingRect:限制在指定矩形区域内。
    • snapCenterToCursor:使元素中心对齐到光标。

    在这里我选择了一个比较普通的限制在父元素内的标识符。可以按照具体的定制需要,配置不同的标识符组合来限制拖曳行为。

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

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

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

  • 因此,只要是每个 item 唯一的,无论传字符串或者数字都是可以的。

  • strategy:策略,用于定义排序算法,它指定了拖拽项目在容器内如何排序和移动。它通过提供一个函数来控制项目在拖拽过程中的排序行为。它决定了拖拽项目的排序方式和在拖拽过程中如何移动。例如,它可以控制项目按行、按列或者自由布局进行排序,并且不同的排序策略可以提供不同的用户交互体验。例如,矩形排序、水平排序或者垂直排序等。

    常用的排序策略有如下几种:

    • rectSortingStrategy

      • 适用场景:矩形网格布局,比如 flex 容器内部配置 flex-wrap: wrap换行之后,可以采用这种策略。

      • 说明:项目根据矩形区域进行排序,适用于二维网格布局。

    • horizontalListSortingStrategy

      • 适用场景:水平列表,只用于单行的 flex 布局。

      • 说明:项目按水平顺序排列,适用于水平滚动的列表。

    • verticalListSortingStrategy

      • 适用场景:垂直列表,只用于单列的 flex 布局,配置了 flex-direction: column之后使用。

      • 说明:项目按垂直顺序排列,适用于垂直滚动的列表。

    除了这几种以外,你还可以自定义一些策略,按照你自己的需求自己写。不过一般也用不到自己写 www

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

子元素(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,主要用来启用子元素的排序功能。这个钩子返回了一组现成的属性和方法:

  • setNodeRef:用于将 DOM 节点与拖拽行为关联。
  • attributes:包含与可拖拽项目相关的属性,例如 roletabIndex
  • listeners:包含拖拽操作的事件监听器,例如 onMouseDownonTouchStart
  • transform:包含当前项目的转换属性,用于设置位置和旋转等。
  • transition:定义项目的过渡效果,用于动画处理。

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

  • id:在父容器组件中提到的唯一标识符,需要和父容器中传入 items 的列表的元素的属性是一致的,一般直接通过 map 来一次性传入。
  • transition:动画效果的配置,包含 durationeasing

之后我们定义了拖曳样式 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>
);
};
  • ref={setNodeRef}:通过 setNodeRefdiv关联到拖拽功能。
  • {...attributes}:将所有与可拖拽项目相关的属性应用到 div,例如 role="button"tabIndex="0"
  • {...listeners}:将所有拖拽操作的事件监听器应用到 div,例如 onMouseDownonTouchStart,使其能够响应用户的拖拽操作。这里是因为我整个 DOM 元素都要支持拖曳,所以我把它直接加到了最外层。如果需要只在子元素特定的区域内实现拖曳,listeners 就加到需要真正鼠标拖动的那个 DOM 上即可。
  • style={styles}:应用定义好的 styles对象,设置 transformtransition样式,使拖拽时能够实现平滑过渡。
  • className="draggable-item":设置组件的样式类名,用于样式定义。

实现效果

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

可以看到,元素已经能够正常地被排序,而且列表也能够同样地被更新。结合到具体的例子,可以把这个列表 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拖曳排序的资料请关注脚本之家其它相关文章!

相关文章

    • 这篇文章主要介绍了详解React Native 采用Fetch方式发送跨域POST请求,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2017-11-11
    • 这篇文章主要为大家介绍了react性能优化useMemo与useCallback使用对比详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
      2022-08-08
    • 本篇文章主要介绍了React-Native之定时器Timer的实现代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2017-10-10
    • 这篇文章主要介绍了react组件基本用法,结合实例形式分析了react组件传值、生命周期、受控组件和非受控组件等相关操作技巧,需要的朋友可以参考下
      2020-04-04
    • 具体来说这是一种react非受控组件,其状态是在input的react内部控制,不受调用者控制。可以使用受控组件来实现。下面就说说这个React中的受控组件与非受控组件的相关知识,感兴趣的朋友一起看看吧
      2022-12-12
    • 这篇文章主要介绍了使用Node搭建reactSSR服务端渲染架构,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2018-08-08
    • 这篇文章主要为大家介绍了Taro React底部自定义TabBar使用React useContext解决底部选中异常(需要点两次才能选中的问题)示例详解,有需要的朋友可以借鉴参考下
      2023-08-08
    • 这篇文章主要介绍了详解基于React.js和Node.js的SSR实现方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
      2019-03-03
    • 使用useInfiniteScroll hook可以处理检测用户何时滚动到页面底部的逻辑,并触发回调函数以加载更多数据,它还提供了一种简单的方法来管理加载和错误消息的状态,今天通过实例代码介绍下react滚动加载useInfiniteScroll 相关知识,感兴趣的朋友跟随小编一起看看吧
      2023-09-09
    • 这篇文章主要为大家介绍了react自适应布局px转rem实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
      2023-08-08

    最新评论