React實現(xiàn)一個拖拽排序組件的示例代碼
一、效果展示
排序:

絲滑的Flip動畫

自定義列數(shù) (并且寬度會隨著屏幕寬度自適應)

自定義拖拽區(qū)域:(擴展性高,可以全部可拖拽、自定義拖拽圖標)

二、主要思路
Tip: 本代碼的CSS使用Tailwindcss, 如果沒安裝的可以自行安裝這個庫,也可以去問GPT,讓它幫忙改成普通的CSS版本的代碼
1. 一些ts類型:
import { CSSProperties, MutableRefObject, ReactNode } from "react"
/**有孩子的,基礎的組件props,包含className style children */
interface baseChildrenProps {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**孩子 */
children?: ReactNode
}
/**ItemRender渲染函數(shù)的參數(shù) */
type itemProps<T> = {
/**當前元素 */
item: T,
/**當前索引 */
index: number,
/**父元素寬度 */
width: number
/**可拖拽的盒子,只有在這上面才能拖拽。自由放置位置。提供了一個默認的拖拽圖標。可以作為包圍盒,將某塊內(nèi)容作為拖拽區(qū)域 */
DragBox: (props: baseChildrenProps) => ReactNode
}
/**拖拽排序組件的props */
export interface DragSortProps<T> {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**列表,拖拽后會改變里面的順序 */
list: T[]
/**用作唯一key,在list的元素中的屬性名,比如id。必須傳遞 */
keyName: keyof T
/**一行個數(shù),默認1 */
cols?: number
/**元素間距,單位px,默認0 (因為一行默認1) */
marginX?: number
/**當列表長度變化時,是否需要Flip動畫,默認開啟 (可能有點略微的動畫bug) */
flipWithListChange?: boolean
/**每個元素的渲染函數(shù) */
ItemRender: (props: itemProps<T>) => ReactNode
/**拖拽結(jié)束事件,返回排序好的新數(shù)組,在里面自己調(diào)用setList */
afterDrag: (list: T[]) => any
}
2. 使用事件委托
監(jiān)聽所有子元素的拖拽開始、拖拽中、拖拽結(jié)束事件,減少綁定事件數(shù)量的同時,還能優(yōu)化代碼。
/**拖拽排序組件 */
const DragSort = function <T>({
list,
ItemRender,
afterDrag,
keyName,
cols = 1,
marginX = 0,
flipWithListChange = true,
className,
style,
}: DragSortProps<T>) {
const listRef = useRef<HTMLDivElement>(null);
/**記錄當前正在拖拽哪個元素 */
const nowDragItem = useRef<HTMLDivElement>();
const itemWidth = useCalculativeWidth(listRef, marginX, cols);//使用計算寬度鉤子,計算每個元素的寬度 (代碼后面會有)
const [dragOpen, setDragOpen] = useState(false); //是否開啟拖拽 (鼠標進入指定區(qū)域開啟)
/**事件委托- 監(jiān)聽 拖拽開始 事件,添加樣式 */
const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
e.stopPropagation(); //阻止冒泡
/**這是當前正在被拖拽的元素 */
const target = e.target as HTMLDivElement;
//設置被拖拽元素“留在原地”的樣式。為了防止設置正在拖拽的元素樣式,所以用定時器,宏任務更晚執(zhí)行
setTimeout(() => {
target.classList.add(...movingClass); //設置正被拖動的元素樣式
target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都設置為透明,避免影響
}, 0);
//記錄當前拖拽的元素
nowDragItem.current = target;
//設置鼠標樣式
e.dataTransfer.effectAllowed = "move";
};
/**事件委托- 監(jiān)聽 拖拽進入某個元素 事件,在這里只是DOM變化,數(shù)據(jù)順序沒有變化 */
const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
e.preventDefault(); //阻止默認行為,默認是不允許元素拖動到人家身上的
if (!listRef.current || !nowDragItem.current) return;
/**孩子數(shù)組,每次都會獲取最新的 */
const children = [...listRef.current.children];
/**真正會被挪動的元素(當前正懸浮在哪個元素上面) */ //找到符合條件的父節(jié)點
const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);
//邊界判斷
if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
// console.log("拖到自身或者拖到外面");
return;
}
//拿到兩個元素的索引,用來判斷這倆元素應該怎么移動
/**被拖拽元素在孩子數(shù)組中的索引 */
const nowDragtItemIndex = children.indexOf(nowDragItem.current);
/**被進入元素在孩子數(shù)組中的索引 */
const enterItemIndex = children.indexOf(realTarget);
//當用戶選中文字,然后去拖動這個文字時,就會觸發(fā) (可以通過禁止選中文字來避免)
if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
console.log("若第二個數(shù)為-1,說明拖動的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
return;
}
if (nowDragtItemIndex < enterItemIndex) {
// console.log("向下移動");
listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
} else {
// console.log("向上移動");
listRef.current.insertBefore(nowDragItem.current, realTarget);
}
};
/**事件委托- 監(jiān)聽 拖拽結(jié)束 事件,刪除樣式,設置當前列表 */
const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
/**當前正在被拖拽的元素 */
const target = e.target as Element;
target.classList.remove(...movingClass);//刪除前面添加的 被拖拽元素的樣式,回歸原樣式
target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass));//刪除所有子元素的透明樣式
/**拿到當前DOM的id順序信息 */
const ids = [...listRef.current.children].map((k) => String(k.id)); //根據(jù)id,判斷到時候應該怎么排序
//把列表按照id排序
const newList = [...list].sort(function (a, b) {
const aIndex = ids.indexOf(String(a[keyName]));
const bIndex = ids.indexOf(String(b[keyName]));
if (aIndex === -1 && bIndex === -1) return 0;
else if (aIndex === -1) return 1;
else if (bIndex === -1) return -1;
else return aIndex - bIndex;
});
afterDrag(newList);//觸發(fā)外界傳入的回調(diào)函數(shù)
setDragOpen(false);//拖拽完成后,再次禁止拖拽
};
/**拖拽按鈕組件 */ //只有鼠標懸浮在這上面的時候,才開啟拖拽,做到“指定區(qū)域拖拽”
const DragBox = ({ className, style, children }: baseChildrenProps) => {
return (
<div
style={{ ...style }}
className={cn("hover:cursor-grabbing", className)}
onMouseEnter={() => setDragOpen(true)}
onMouseLeave={() => setDragOpen(false)}
>
{children || <DragIcon size={20} color="#666666" />}
</div>
);
};
return (
<div
className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
style={style}
ref={listRef}
onDragStart={onDragStart}
onDragEnter={onDragEnter}
onDragOver={(e) => e.preventDefault()} //被拖動的對象被拖到其它容器時(因為默認不能拖到其它元素上)
onDragEnd={onDragEnd}
>
{list.map((item, index) => {
const key = item[keyName] as string;
return (
<div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
{ItemRender({ item, index, width: itemWidth, DragBox })}
</div>
);
})}
</div>
);
};
3. 使用Flip做動畫
對于這種移動位置的動畫,普通的CSS和JS動畫已經(jīng)無法滿足了:

可以使用Flip動畫來做:FLIP是 First、Last、Invert和 Play四個單詞首字母的縮寫, 意思就是,記錄一開始的位置、記錄結(jié)束的位置、記錄位置的變化、讓元素開始動畫
主要的思路為: 記錄原位置、記錄現(xiàn)位置、記錄位移大小,最重要的點來了, 使用CSS的 transform ,讓元素在被改動位置的一瞬間, translate 定位到原本的位置上(通過我們前面計算的位移大?。?然后給元素加上 過渡 效果,再讓它慢慢回到原位即可。

代碼如下 (沒有第三方庫,基本都是自己手寫實現(xiàn))
這里還使用了JS提供的 Web Animations API,具有極高的性能,不阻塞主線程。
但是由于API沒有提供動畫完成的回調(diào),故這里使用定時器做回調(diào)觸發(fā)
/**位置的類型 */
interface position {
x: number,
y: number
}
/**Flip動畫 */
export class Flip {
/**dom元素 */
private dom: Element
/**原位置 */
private firstPosition: position | null = null
/**動畫時間 */
private duration: number
/**正在移動的動畫會有一個專屬的class類名,可以用于標識 */
static movingClass = "__flipMoving__"
constructor(dom: Element, duration = 500) {
this.dom = dom
this.duration = duration
}
/**獲得元素的當前位置信息 */
private getDomPosition(): position {
const rect = this.dom.getBoundingClientRect()
return {
x: rect.left,
y: rect.top
}
}
/**給原始位置賦值 */
recordFirst(firstPosition?: position) {
if (!firstPosition) firstPosition = this.getDomPosition()
this.firstPosition = { ...firstPosition }
}
/**播放動畫 */
play(callback?: () => any) {
if (!this.firstPosition) {
console.warn('請先記錄原始位置');
return
}
const lastPositon = this.getDomPosition()
const dif: position = {
x: lastPositon.x - this.firstPosition.x,
y: lastPositon.y - this.firstPosition.y,
}
// console.log(this, dif);
if (!dif.x && !dif.y) return
this.dom.classList.add(Flip.movingClass)
this.dom.animate([
{ transform: `translate(${-dif.x}px, ${-dif.y}px)` },
{ transform: `translate(0px, 0px)` }
], { duration: this.duration })
setTimeout(() => {
this.dom.classList.remove(Flip.movingClass)
callback?.()
}, this.duration);
}
}
/**Flip多元素同時觸發(fā) */
export class FlipList {
/**Flip列表 */
private flips: Flip[]
/**正在移動的動畫會有一個專屬的class類名,可以用于標識 */
static movingClass = Flip.movingClass
/**Flip多元素同時觸發(fā) - 構(gòu)造函數(shù)
* @param domList 要監(jiān)聽的DOM列表
* @param duration 動畫時長,默認500ms
*/
constructor(domList: Element[], duration?: number) {
this.flips = domList.map((k) => new Flip(k, duration))
}
/**記錄全部初始位置 */
recordFirst() {
this.flips.forEach((flip) => flip.recordFirst())
}
/**播放全部動畫 */
play(callback?: () => any) {
this.flips.forEach((flip) => flip.play(callback))
}
}
然后在特定的地方插入代碼,記錄元素位置,做動畫,插入了動畫之后的代碼,見下面的“完整代碼”模塊
三、完整代碼
1.類型定義
// type.ts
import { CSSProperties, ReactNode } from "react"
/**有孩子的,基礎的組件props,包含className style children */
interface baseChildrenProps {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**孩子 */
children?: ReactNode
}
/**ItemRender渲染函數(shù)的參數(shù) */
type itemProps<T> = {
/**當前元素 */
item: T,
/**當前索引 */
index: number,
/**父元素寬度 */
width: number
/**可拖拽的盒子,只有在這上面才能拖拽。自由放置位置。提供了一個默認的拖拽圖標??梢宰鳛榘鼑?,將某塊內(nèi)容作為拖拽區(qū)域 */
DragBox: (props: baseChildrenProps) => ReactNode
}
/**拖拽排序組件的props */
export interface DragSortProps<T> {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**列表,拖拽后會改變里面的順序 */
list: T[]
/**用作唯一key,在list的元素中的屬性名,比如id。必須傳遞 */
keyName: keyof T
/**一行個數(shù),默認1 */
cols?: number
/**元素間距,單位px,默認0 (因為一行默認1) */
marginX?: number
/**當列表長度變化時,是否需要Flip動畫,默認開啟 (可能有點略微的動畫bug) */
flipWithListChange?: boolean
/**每個元素的渲染函數(shù) */
ItemRender: (props: itemProps<T>) => ReactNode
/**拖拽結(jié)束事件,返回排序好的新數(shù)組,在里面自己調(diào)用setList */
afterDrag: (list: T[]) => any
}
2. 部分不方便使用Tailwindcss的CSS
由于這段背景設置為tailwindcss過于麻煩,所以單獨提取出來
/* index.module.css */
/*拖拽時,留在原地的元素*/
.background {
background: linear-gradient(
45deg,
rgba(0, 0, 0, 0.3) 0,
rgba(0, 0, 0, 0.3) 25%,
transparent 25%,
transparent 50%,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0.3) 75%,
transparent 75%,
transparent
);
background-size: 20px 20px;
border-radius: 5px;
}
3. 計算每個子元素寬度的Hook
一個響應式計算寬度的hook,可以用于列表的多列布局
// hooks/alculativeWidth.ts
import { RefObject, useEffect, useState } from "react";
/**根據(jù)父節(jié)點的ref和子元素的列數(shù)等數(shù)據(jù),計算出子元素的寬度。用于響應式布局
* @param fatherRef 父節(jié)點的ref
* @param marginX 子元素的水平間距
* @param cols 一行個數(shù) (一行有幾列)
* @param callback 根據(jù)瀏覽器寬度自動計算大小后的回調(diào)函數(shù),參數(shù)是計算好的子元素寬度
* @returns 返回子元素寬度的響應式數(shù)據(jù)
*/
const useCalculativeWidth = (fatherRef: RefObject<HTMLDivElement>, marginX: number, cols: number, callback?: (nowWidth: number) => void) => {
const [itemWidth, setItemWidth] = useState(200);
useEffect(() => {
/**計算單個子元素寬度,根據(jù)list的寬度計算 */
const countWidth = () => {
const width = fatherRef.current?.offsetWidth;
if (width) {
const _width = (width - marginX * (cols + 1)) / cols;
setItemWidth(_width);
callback && callback(_width)
}
};
countWidth(); //先執(zhí)行一次,后續(xù)再監(jiān)聽綁定
window.addEventListener("resize", countWidth);
return () => window.removeEventListener("resize", countWidth);
}, [fatherRef, marginX, cols]);
return itemWidth
}
export default useCalculativeWidth
4. Flip動畫實現(xiàn)
// lib/common/util/animation.ts
/**位置的類型 */
interface position {
x: number,
y: number
}
/**Flip動畫 */
export class Flip {
/**dom元素 */
private dom: Element
/**原位置 */
private firstPosition: position | null = null
/**動畫時間 */
private duration: number
/**正在移動的動畫會有一個專屬的class類名,可以用于標識 */
static movingClass = "__flipMoving__"
constructor(dom: Element, duration = 500) {
this.dom = dom
this.duration = duration
}
/**獲得元素的當前位置信息 */
private getDomPosition(): position {
const rect = this.dom.getBoundingClientRect()
return {
x: rect.left,
y: rect.top
}
}
/**給原始位置賦值 */
recordFirst(firstPosition?: position) {
if (!firstPosition) firstPosition = this.getDomPosition()
this.firstPosition = { ...firstPosition }
}
/**播放動畫 */
play(callback?: () => any) {
if (!this.firstPosition) {
console.warn('請先記錄原始位置');
return
}
const lastPositon = this.getDomPosition()
const dif: position = {
x: lastPositon.x - this.firstPosition.x,
y: lastPositon.y - this.firstPosition.y,
}
// console.log(this, dif);
if (!dif.x && !dif.y) return
this.dom.classList.add(Flip.movingClass)
this.dom.animate([
{ transform: `translate(${-dif.x}px, ${-dif.y}px)` },
{ transform: `translate(0px, 0px)` }
], { duration: this.duration })
setTimeout(() => {
this.dom.classList.remove(Flip.movingClass)
callback?.()
}, this.duration);
}
}
/**Flip多元素同時觸發(fā) */
export class FlipList {
/**Flip列表 */
private flips: Flip[]
/**正在移動的動畫會有一個專屬的class類名,可以用于標識 */
static movingClass = Flip.movingClass
/**Flip多元素同時觸發(fā) - 構(gòu)造函數(shù)
* @param domList 要監(jiān)聽的DOM列表
* @param duration 動畫時長,默認500ms
*/
constructor(domList: Element[], duration?: number) {
this.flips = domList.map((k) => new Flip(k, duration))
}
/**記錄全部初始位置 */
recordFirst() {
this.flips.forEach((flip) => flip.recordFirst())
}
/**播放全部動畫 */
play(callback?: () => any) {
this.flips.forEach((flip) => flip.play(callback))
}
}
5. 一些工具函數(shù)
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
/**Tailwindcss的 合并css類名 函數(shù)
* @param inputs 要合并的類名
* @returns
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**查找符合條件的父節(jié)點
* @param node 當前節(jié)點。如果當前節(jié)點就符合條件,就會返回當前節(jié)點
* @param target 參數(shù)是當前找到的節(jié)點,返回一個布爾值,為true代表找到想要的父節(jié)點
* @returns 沒找到則返回null,找到了返回Element
*/
export function findParent(node: Element, target: (nowNode: Element) => boolean) {
while (node && !target(node)) {
if (node.parentElement) {
node = node.parentElement;
} else {
return null;
}
}
return node;
}
6. 完整組件代碼
import { DragEventHandler, useEffect, useRef, useState } from "react";
import { DragSortProps } from "./type";
import useCalculativeWidth from "@/hooks/calculativeWidth";
import { cn, findParent } from "@/lib/util";
import style from "./index.module.css";
import { DragIcon } from "../../UI/MyIcon"; //這個圖標可以自己找喜歡的
import { FlipList } from "@/lib/common/util/animation";
/**拖拽時,留在原位置的元素的樣式 */
const movingClass = [style.background]; //使用數(shù)組是為了方便以后添加其他類名
/**拖拽時,留在原位置的子元素的樣式 */
const opacityClass = ["opacity-0"]; //使用數(shù)組是為了方便以后添加其他類名
/**拖拽排序組件 */
const DragSort = function <T>({
list,
ItemRender,
afterDrag,
keyName,
cols = 1,
marginX = 0,
flipWithListChange = true,
className,
style,
}: DragSortProps<T>) {
const listRef = useRef<HTMLDivElement>(null);
/**記錄當前正在拖拽哪個元素 */
const nowDragItem = useRef<HTMLDivElement>();
const itemWidth = useCalculativeWidth(listRef, marginX, cols);
/**存儲flipList動畫實例 */
const flipListRef = useRef<FlipList>();
const [dragOpen, setDragOpen] = useState(false); //是否開啟拖拽 (鼠標進入指定區(qū)域開啟)
/**創(chuàng)建記錄新的動畫記錄,并立即記錄當前位置 */
const createNewFlipList = (exceptTarget?: Element) => {
if (!listRef.current) return;
//記錄動畫
const listenChildren = [...listRef.current.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都動畫
flipListRef.current = new FlipList(listenChildren, 300);
flipListRef.current.recordFirst();
};
//下面這兩個是用于,當列表變化時,進行動畫
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
}, [list]);
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
return () => {
flipListRef.current?.play(() => flipListRef.current?.recordFirst());
};
}, [list.length]);
/**事件委托- 監(jiān)聽 拖拽開始 事件,添加樣式 */
const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
e.stopPropagation(); //阻止冒泡
/**這是當前正在被拖拽的元素 */
const target = e.target as HTMLDivElement;
//設置被拖拽元素“留在原地”的樣式。為了防止設置正在拖拽的元素樣式,所以用定時器,宏任務更晚執(zhí)行
setTimeout(() => {
target.classList.add(...movingClass); //設置正被拖動的元素樣式
target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都設置為透明,避免影響
}, 0);
//記錄元素的位置,用于Flip動畫
createNewFlipList(target);
//記錄當前拖拽的元素
nowDragItem.current = target;
//設置鼠標樣式
e.dataTransfer.effectAllowed = "move";
};
/**事件委托- 監(jiān)聽 拖拽進入某個元素 事件,在這里只是DOM變化,數(shù)據(jù)順序沒有變化 */
const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
e.preventDefault(); //阻止默認行為,默認是不允許元素拖動到人家身上的
if (!listRef.current || !nowDragItem.current) return;
/**孩子數(shù)組,每次都會獲取最新的 */
const children = [...listRef.current.children];
/**真正會被挪動的元素(當前正懸浮在哪個元素上面) */ //找到符合條件的父節(jié)點
const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);
//邊界判斷
if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
// console.log("拖到自身或者拖到外面");
return;
}
if (realTarget.className.includes(FlipList.movingClass)) {
// console.log("這是正在動畫的元素,跳過");
return;
}
//拿到兩個元素的索引,用來判斷這倆元素應該怎么移動
/**被拖拽元素在孩子數(shù)組中的索引 */
const nowDragtItemIndex = children.indexOf(nowDragItem.current);
/**被進入元素在孩子數(shù)組中的索引 */
const enterItemIndex = children.indexOf(realTarget);
//當用戶選中文字,然后去拖動這個文字時,就會觸發(fā) (可以通過禁止選中文字來避免)
if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
console.log("若第二個數(shù)為-1,說明拖動的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
return;
}
//Flip動畫 - 記錄原始位置
flipListRef.current?.recordFirst();
if (nowDragtItemIndex < enterItemIndex) {
// console.log("向下移動");
listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
} else {
// console.log("向上移動");
listRef.current.insertBefore(nowDragItem.current, realTarget);
}
//Flip動畫 - 播放
flipListRef.current?.play();
};
/**事件委托- 監(jiān)聽 拖拽結(jié)束 事件,刪除樣式,設置當前列表 */
const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
/**當前正在被拖拽的元素 */
const target = e.target as Element;
target.classList.remove(...movingClass); //刪除前面添加的 被拖拽元素的樣式,回歸原樣式
target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //刪除所有子元素的透明樣式
/**拿到當前DOM的id順序信息 */
const ids = [...listRef.current.children].map((k) => String(k.id)); //根據(jù)id,判斷到時候應該怎么排序
//把列表按照id排序
const newList = [...list].sort(function (a, b) {
const aIndex = ids.indexOf(String(a[keyName]));
const bIndex = ids.indexOf(String(b[keyName]));
if (aIndex === -1 && bIndex === -1) return 0;
else if (aIndex === -1) return 1;
else if (bIndex === -1) return -1;
else return aIndex - bIndex;
});
afterDrag(newList); //觸發(fā)外界傳入的回調(diào)函數(shù)
setDragOpen(false); //拖拽完成后,再次禁止拖拽
};
/**拖拽按鈕組件 */ //只有鼠標懸浮在這上面的時候,才開啟拖拽,做到“指定區(qū)域拖拽”
const DragBox = ({ className, style, children }: baseChildrenProps) => {
return (
<div
style={{ ...style }}
className={cn("hover:cursor-grabbing", className)}
onMouseEnter={() => setDragOpen(true)}
onMouseLeave={() => setDragOpen(false)}
>
{children || <DragIcon size={20} color="#666666" />}
</div>
);
};
return (
<div
className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
style={style}
ref={listRef}
onDragStart={onDragStart}
onDragEnter={onDragEnter}
onDragOver={(e) => e.preventDefault()} //被拖動的對象被拖到其它容器時(因為默認不能拖到其它元素上)
onDragEnd={onDragEnd}
>
{list.map((item, index) => {
const key = item[keyName] as string;
return (
<div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
{ItemRender({ item, index, width: itemWidth, DragBox })}
</div>
);
})}
</div>
);
};
export default DragSort;
7. 效果圖的測試用例
一開始展示的效果圖的實現(xiàn)代碼
"use client";
import { useState } from "react";
import DragSort from "@/components/base/tool/DragSort";
import { Button, InputNumber } from "antd";
export default function page() {
interface item {
id: number;
}
const [list, setList] = useState<item[]>([]); //當前列表
const [cols, setCols] = useState(1); //一行個數(shù)
/**創(chuàng)建一個新的元素 */
const createNewItem = () => {
setList((old) =>
old.concat([
{
id: Date.now(),
},
])
);
};
return (
<div className="p-2 bg-[#a18c83] w-screen h-screen overflow-auto">
<Button type="primary" onClick={createNewItem}>
點我添加
</Button>
一行個數(shù): <InputNumber value={cols} min={1} onChange={(v) => setCols(v!)} />
<DragSort
list={list}
keyName={"id"}
cols={cols}
marginX={10}
afterDrag={(list) => setList(list)}
ItemRender={({ item, index, DragBox }) => {
return (
<div className="flex items-center border rounded-sm p-2 gap-1 bg-white">
<DragBox />
<div>序號:{index},</div>
<div>ID:{item.id}</div>
{/* <DragBox className="bg-stone-400 text-white p-1">自定義拖拽位置</DragBox> */}
</div>
);
}}
/>
</div>
);
}
四、結(jié)語
以上就是React實現(xiàn)一個拖拽排序組件的示例代碼的詳細內(nèi)容,更多關于React拖拽排序組件的資料請關注腳本之家其它相關文章!
相關文章
react-player實現(xiàn)視頻播放與自定義進度條效果
本篇文章通過完整的代碼給大家介紹了react-player實現(xiàn)視頻播放與自定義進度條效果,代碼簡單易懂,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2022-01-01
如何將你的AngularJS1.x應用遷移至React的方法
本篇文章主要介紹了如何將你的AngularJS1.x應用遷移至React的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02
React視頻播放控制組件Video Controls的實現(xiàn)
本文主要介紹了React視頻播放控制組件Video Controls的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-02-02
React中useCallback useMemo到底該怎么用
在React函數(shù)組件中,當組件中的props發(fā)生變化時,默認情況下整個組件都會重新渲染。換句話說,如果組件中的任何值更新,整個組件將重新渲染,包括沒有更改values/props的函數(shù)/組件。在react中,我們可以通過memo,useMemo以及useCallback來防止子組件的rerender2023-02-02
詳解React-Router中Url參數(shù)改變頁面不刷新的解決辦法
這篇文章主要介紹了詳解React-Router中Url參數(shù)改變頁面不刷新的解決辦法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05

