React中使用dnd-kit實現(xiàn)拖曳排序功能
由于前陣子需要在開發(fā) Picals 的時候,需要實現(xiàn)一些拖動排序的功能。雖然有原生的瀏覽器 dragger API,不過純靠自己手寫很難實現(xiàn)自己想要的效果,更多的是吃力不討好。于是我四處去調(diào)研了一些 React 中比較常用的拖曳庫,最終確定了 dnd-kit
作為我實現(xiàn)拖曳排序的工具。
當然,使用的時候肯定免不了踩坑。這篇文章的意義就是為了記錄所踩的坑,希望能夠為有需要的大家提供一點幫助。
在這篇文章中,我將帶著大家一起實現(xiàn)如下的拖曳排序的例子:
那讓我們開始吧。
安裝
安裝 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 和實用工具函數(shù)。上述演示的平滑移動的樣式就是來源于這個包。
使用方法
首先我們需要知道的是,拖曳這個行為需要涉及到兩個部分:
- 能夠允許被拖曳的有限空間(父容器)
- 用戶真正進行拖曳的子元素
在使用 dnd-kit
時,需要對這兩個部分分別進行定義。
父容器(DraggableList)的編寫
我們首先進行拖曳父容器相關(guān)的功能配置。話不多說我們直接上代碼:
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;
對應(yīng)的 index.scss
:
.drag-container { position: relative; width: 800px; display: flex; flex-wrap: wrap; gap: 20px; }
return 的 DOM 元素結(jié)構(gòu)非常簡單,最主要的無外乎兩個上下文組件:DndContext
和 SortableContext
。
DndContext
:是 dnd-kit 的核心組件,用于提供拖放的上下文。SortableContext
:是一個上下文組件,用于提供排序的功能。
在 SortableContext
組件內(nèi)部包裹的,就是我們正常的需要進行排序的列表容器了。當然,dnd-kit 也不是對任何的內(nèi)容都可以進行排序的。要想實現(xiàn)排序功能,這個被包裹的 DOM 元素必須符合以下幾個要求:
必須是可排序的元素:
SortableContext
需要包裹的元素具有相同的父級容器,且這些元素需要具備可排序的能力。每個子元素應(yīng)當是獨立的可拖拽項,例如一個列表項、卡片或網(wǎng)格中的塊。提供唯一的
id
:每個可排序的子元素必須具有唯一的id
。SortableContext
會通過這些id
來識別和管理每個拖拽項的位置。你需要確保items
屬性中提供的id
數(shù)組與實際渲染的子元素的id
一一對應(yīng)。需要是同一個父容器的直接子元素:
SortableContext
內(nèi)部的子元素必須是同一個父容器的直接子元素,不能有其他中間層級。這是因為排序和拖拽是基于元素的相對位置和布局來計算的。使用相同的布局策略:
SortableContext
的子元素應(yīng)當使用相同的布局策略,例如使用 CSS Flexbox 或 Grid 進行布局。這樣可以確保拖拽操作時,子元素之間的排列和移動邏輯一致。設(shè)置相同的樣式屬性:確保子元素具有相同的樣式屬性,例如寬度、高度、邊距等。這些屬性一致性有助于拖拽過程中視覺效果的一致性和準確性。
添加必要的樣式以支持拖拽:為了支持拖拽效果,子元素應(yīng)具備必要的樣式。例如,設(shè)置
position
為relative
以便于絕對定位的拖拽項,設(shè)置overflow
以防止拖拽項溢出。確保有足夠的拖拽空間:父容器應(yīng)當有足夠的空間來允許子元素的拖拽操作。如果空間不足,可能會導致拖拽操作不順暢或無法完成。
子元素必須具備
draggable
屬性:每個子元素應(yīng)該具備draggable
屬性,以表明該元素是可拖動的。這通常通過 dnd-kit 提供的組件如Draggable
或Sortable
來實現(xiàn)。提供合適的拖拽處理程序:為子元素添加合適的拖拽處理程序,通常通過 dnd-kit 提供的鉤子或組件實現(xiàn)。例如,使用
useDraggable
鉤子來處理拖拽邏輯。處理子元素布局變化:確保在拖拽過程中,子元素的布局變化能夠被正確處理。例如,設(shè)置適當?shù)膭赢嬓Ч云交馗虏季帧?/p>
在這里附加一個說明,可以看到我初始化的數(shù)據(jù)的列表 id 是從 1 開始的,因為 從 0 開始會導致第一個元素無法觸發(fā)移動 ?,F(xiàn)階段還不知道是什么原因,大概的猜測是在 JavaScript 和 React 中,
id
為0
可能會被視為“假值”(falsy value)。許多庫和框架在處理數(shù)據(jù)時,會有意無意地忽略或處理“假值”。dnd-kit 可能在某些情況下忽略了id
為0
的元素,導致其無法正常參與拖曳操作??傊?, 避免第一個拖曳元素的 id 不要為 0 或者空字符串 。
對于 DndContext
,需要傳入幾個 props 以處理拖曳事件本身。在這里,傳入了 onDragEnd
函數(shù)與 modifiers
修飾符列表。實際上,這個上下文組件能夠傳入很多的 props,我在這里簡單截個圖:
可以看到,不僅是結(jié)束回調(diào),也接受拖曳全過程的函數(shù)回調(diào)并通過回傳值進行一些數(shù)據(jù)處理。
但是,一般用于完成拖曳排序功能我們可以不管這么多,只用管鼠標松開后的回調(diào)函數(shù),然后拿到對象進行處理就可以了。
onDragEnd
:顧名思義,就是用戶鼠標松開后觸發(fā)的拖曳事件的回調(diào)。觸發(fā)時會自動傳入類型為DragEndEvent
的對象,我們可以從其中拿出active
和over
兩個參數(shù)來具體處理拖曳事件。active 包含 正在拖曳的元素的相關(guān)信息,over 包含最后鼠標松開時所覆蓋到的元素的相關(guān)信息。
結(jié)合到我的函數(shù):
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); } };
首先檢查 active
和 over
是否有效,避免邊界問題,之后創(chuàng)建 moveDataList
的副本,調(diào)用 getMoveIndex
函數(shù)獲取 active
和 over
項目的索引,如果兩個索引不同,使用 arrayMove
移動項目,并更新 list
狀態(tài)。
getMoveIndex
函數(shù)如下,用于獲取拖拽項目和目標位置的索引:
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
獲取active
和over
項目的索引,如果未找到,默認返回 0。modifiers
:標識符,傳入一個標識符數(shù)組以限制在父組件進行拖曳的行為。主要可選的一些標識符如下:restrictToParentElement
:限制在父元素內(nèi)。restrictToFirstScrollableAncestor
:限制在第一個可滾動祖先元素。restrictToVerticalAxis
:限制在垂直軸上。restrictToHorizontalAxis
:限制在水平軸上。restrictToBoundingRect
:限制在指定矩形區(qū)域內(nèi)。snapCenterToCursor
:使元素中心對齊到光標。
在這里我選擇了一個比較普通的限制在父元素內(nèi)的標識符??梢园凑站唧w的定制需要,配置不同的標識符組合來限制拖曳行為。
接下來是對 SortableContext
的配置解析。在這個組件中傳入了 items
和 strategy
兩個參數(shù)。同樣地,它也提供了很多的 props 以供個性化配置:
items
:用于定義可排序項目的唯一標識符數(shù)組,它告訴 SortableContext
哪些項目可以被拖拽和排序。它的類型剛好和上述的 active 和 over 的 id 屬性的類型相同,都是 UniqueIdentifier
。
這也就意味著,我們在 items 這邊傳入了什么數(shù)組來對排序列表進行唯一性表示,active 和 over 就按照什么來追蹤元素的排序索引。UniqueIdentifier
實際上是 string 和 number 的聯(lián)合類型。
因此,只要是每個 item 唯一的,無論傳字符串或者數(shù)字都是可以的。
strategy
:策略,用于定義排序算法,它指定了拖拽項目在容器內(nèi)如何排序和移動。它通過提供一個函數(shù)來控制項目在拖拽過程中的排序行為。它決定了拖拽項目的排序方式和在拖拽過程中如何移動。例如,它可以控制項目按行、按列或者自由布局進行排序,并且不同的排序策略可以提供不同的用戶交互體驗。例如,矩形排序、水平排序或者垂直排序等。常用的排序策略有如下幾種:
rectSortingStrategy
適用場景:矩形網(wǎng)格布局,比如 flex 容器內(nèi)部配置
flex-wrap: wrap
換行之后,可以采用這種策略。說明:項目根據(jù)矩形區(qū)域進行排序,適用于二維網(wǎng)格布局。
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;
對應(yīng)的 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,主要用來啟用子元素的排序功能。這個鉤子返回了一組現(xiàn)成的屬性和方法:
setNodeRef
:用于將 DOM 節(jié)點與拖拽行為關(guān)聯(lián)。attributes
:包含與可拖拽項目相關(guān)的屬性,例如role
和tabIndex
。listeners
:包含拖拽操作的事件監(jiān)聽器,例如onMouseDown
、onTouchStart
。transform
:包含當前項目的轉(zhuǎn)換屬性,用于設(shè)置位置和旋轉(zhuǎn)等。transition
:定義項目的過渡效果,用于動畫處理。
它接受一個配置對象,其中包含了:
id
:在父容器組件中提到的唯一標識符,需要和父容器中傳入 items 的列表的元素的屬性是一致的,一般直接通過 map 來一次性傳入。transition
:動畫效果的配置,包含duration
和easing
。
之后我們定義了拖曳樣式 styles
,使用了 @dnd-kit/utilities
提供的 CSS
工具庫,用于處理 CSS 相關(guān)的樣式轉(zhuǎn)換,因為這里的 transform
是從 hook 拿到的,是其自定義的 Transform
類型,需要借助其轉(zhuǎn)為正常的 css 樣式。我們傳入了從 useSortable
中拿到的 transform
和 transition
,用于處理拖曳 item 的樣式。
之后就是直接一股腦的將配置全部傳入要真正進行拖曳的 DOM 元素:
return ( <div ref={setNodeRef} {...attributes} {...listeners} style={styles} className="draggable-item" > <span>{item.url}</span> </div> ); };
ref={setNodeRef}
:通過setNodeRef
將div
關(guān)聯(lián)到拖拽功能。{...attributes}
:將所有與可拖拽項目相關(guān)的屬性應(yīng)用到div
,例如role="button"
和tabIndex="0"
。{...listeners}
:將所有拖拽操作的事件監(jiān)聽器應(yīng)用到div
,例如onMouseDown
和onTouchStart
,使其能夠響應(yīng)用戶的拖拽操作。這里是因為我整個 DOM 元素都要支持拖曳,所以我把它直接加到了最外層。如果需要只在子元素特定的區(qū)域內(nèi)實現(xiàn)拖曳,listeners 就加到需要真正鼠標拖動的那個 DOM 上即可。style={styles}
:應(yīng)用定義好的styles
對象,設(shè)置transform
和transition
樣式,使拖拽時能夠?qū)崿F(xiàn)平滑過渡。className="draggable-item"
:設(shè)置組件的樣式類名,用于樣式定義。
實現(xiàn)效果
父容器和子元素全都編寫完畢后,我們可以觀察一下總體的實現(xiàn)效果如何:
可以看到,元素已經(jīng)能夠正常地被排序,而且列表也能夠同樣地被更新。結(jié)合到具體的例子,可以把這個列表 item 結(jié)合更加復(fù)雜的類型進行處理即可。只要保證每個 item 有唯一的 id 即可。
對于原有點擊事件失效的處理
對于某些需要觸發(fā)點擊事件的拖曳 item,如果按照上述方式封裝了拖曳子元素所需的一些配置,那么 原有的點擊事件將會失效,因為原有的鼠標按下的點擊事件被拖曳事件給覆蓋掉了。當然,dnd-kit 肯定也是考慮到了這種情況。他們在其核心庫 @dnd-kit/core
當中封裝了一個 hook useSensors
,用來配置 鼠標拖動多少個像素之后才觸發(fā)拖曳事件,在此之前不觸發(fā)拖曳事件。
使用方法也非常簡單,首先從核心庫中導入這個 hook,之后進行如下的配置:
//拖拽傳感器,在移動像素5px范圍內(nèi),不觸發(fā)拖拽事件 const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 5, }, }) );
這里配置了在 5px 范圍內(nèi)不觸發(fā)拖曳事件,這樣就可以在這個范圍內(nèi)進行點擊事件的正常觸發(fā)了。
在上面的 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實現(xiàn)拖曳排序功能的詳細內(nèi)容,更多關(guān)于React dnd-kit拖曳排序的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react16.8.0以上MobX在hook中的使用方法詳解
這篇文章主要為大家介紹了react16.8.0以上MobX在hook中的使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07react國際化化插件react-i18n-auto使用詳解
這篇文章主要介紹了react國際化化插件react-i18n-auto使用詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03React?Hooks--useEffect代替常用生命周期函數(shù)方式
這篇文章主要介紹了React?Hooks--useEffect代替常用生命周期函數(shù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09React不能將useMemo設(shè)置為默認方法原因詳解
這篇文章主要為大家介紹了React不能將useMemo設(shè)置為默認方法原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪<BR>2022-07-07