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

絲滑的Flip動(dòng)畫(huà)

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

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

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

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

代碼如下 (沒(méi)有第三方庫(kù),基本都是自己手寫(xiě)實(shí)現(xiàn))
這里還使用了JS提供的 Web Animations API,具有極高的性能,不阻塞主線程。
但是由于API沒(méi)有提供動(dòng)畫(huà)完成的回調(diào),故這里使用定時(shí)器做回調(diào)觸發(fā)
/**位置的類(lèi)型 */
interface position {
x: number,
y: number
}
/**Flip動(dòng)畫(huà) */
export class Flip {
/**dom元素 */
private dom: Element
/**原位置 */
private firstPosition: position | null = null
/**動(dòng)畫(huà)時(shí)間 */
private duration: number
/**正在移動(dòng)的動(dòng)畫(huà)會(huì)有一個(gè)專(zhuān)屬的class類(lèi)名,可以用于標(biāo)識(shí) */
static movingClass = "__flipMoving__"
constructor(dom: Element, duration = 500) {
this.dom = dom
this.duration = duration
}
/**獲得元素的當(dāng)前位置信息 */
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 }
}
/**播放動(dòng)畫(huà) */
play(callback?: () => any) {
if (!this.firstPosition) {
console.warn('請(qǐng)先記錄原始位置');
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多元素同時(shí)觸發(fā) */
export class FlipList {
/**Flip列表 */
private flips: Flip[]
/**正在移動(dòng)的動(dòng)畫(huà)會(huì)有一個(gè)專(zhuān)屬的class類(lèi)名,可以用于標(biāo)識(shí) */
static movingClass = Flip.movingClass
/**Flip多元素同時(shí)觸發(fā) - 構(gòu)造函數(shù)
* @param domList 要監(jiān)聽(tīng)的DOM列表
* @param duration 動(dòng)畫(huà)時(shí)長(zhǎng),默認(rèn)500ms
*/
constructor(domList: Element[], duration?: number) {
this.flips = domList.map((k) => new Flip(k, duration))
}
/**記錄全部初始位置 */
recordFirst() {
this.flips.forEach((flip) => flip.recordFirst())
}
/**播放全部動(dòng)畫(huà) */
play(callback?: () => any) {
this.flips.forEach((flip) => flip.play(callback))
}
}
然后在特定的地方插入代碼,記錄元素位置,做動(dòng)畫(huà),插入了動(dòng)畫(huà)之后的代碼,見(jiàn)下面的“完整代碼”模塊
三、完整代碼
1.類(lèi)型定義
// type.ts
import { CSSProperties, ReactNode } from "react"
/**有孩子的,基礎(chǔ)的組件props,包含className style children */
interface baseChildrenProps {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**孩子 */
children?: ReactNode
}
/**ItemRender渲染函數(shù)的參數(shù) */
type itemProps<T> = {
/**當(dāng)前元素 */
item: T,
/**當(dāng)前索引 */
index: number,
/**父元素寬度 */
width: number
/**可拖拽的盒子,只有在這上面才能拖拽。自由放置位置。提供了一個(gè)默認(rèn)的拖拽圖標(biāo)。可以作為包圍盒,將某塊內(nèi)容作為拖拽區(qū)域 */
DragBox: (props: baseChildrenProps) => ReactNode
}
/**拖拽排序組件的props */
export interface DragSortProps<T> {
/**組件最外層的className */
className?: string
/**組件最外層的style */
style?: CSSProperties
/**列表,拖拽后會(huì)改變里面的順序 */
list: T[]
/**用作唯一key,在list的元素中的屬性名,比如id。必須傳遞 */
keyName: keyof T
/**一行個(gè)數(shù),默認(rèn)1 */
cols?: number
/**元素間距,單位px,默認(rèn)0 (因?yàn)橐恍心J(rèn)1) */
marginX?: number
/**當(dāng)列表長(zhǎng)度變化時(shí),是否需要Flip動(dòng)畫(huà),默認(rèn)開(kāi)啟 (可能有點(diǎn)略微的動(dòng)畫(huà)bug) */
flipWithListChange?: boolean
/**每個(gè)元素的渲染函數(shù) */
ItemRender: (props: itemProps<T>) => ReactNode
/**拖拽結(jié)束事件,返回排序好的新數(shù)組,在里面自己調(diào)用setList */
afterDrag: (list: T[]) => any
}
2. 部分不方便使用Tailwindcss的CSS
由于這段背景設(shè)置為tailwindcss過(guò)于麻煩,所以單獨(dú)提取出來(lái)
/* index.module.css */
/*拖拽時(shí),留在原地的元素*/
.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. 計(jì)算每個(gè)子元素寬度的Hook
一個(gè)響應(yīng)式計(jì)算寬度的hook,可以用于列表的多列布局
// hooks/alculativeWidth.ts
import { RefObject, useEffect, useState } from "react";
/**根據(jù)父節(jié)點(diǎn)的ref和子元素的列數(shù)等數(shù)據(jù),計(jì)算出子元素的寬度。用于響應(yīng)式布局
* @param fatherRef 父節(jié)點(diǎn)的ref
* @param marginX 子元素的水平間距
* @param cols 一行個(gè)數(shù) (一行有幾列)
* @param callback 根據(jù)瀏覽器寬度自動(dòng)計(jì)算大小后的回調(diào)函數(shù),參數(shù)是計(jì)算好的子元素寬度
* @returns 返回子元素寬度的響應(yīng)式數(shù)據(jù)
*/
const useCalculativeWidth = (fatherRef: RefObject<HTMLDivElement>, marginX: number, cols: number, callback?: (nowWidth: number) => void) => {
const [itemWidth, setItemWidth] = useState(200);
useEffect(() => {
/**計(jì)算單個(gè)子元素寬度,根據(jù)list的寬度計(jì)算 */
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)聽(tīng)綁定
window.addEventListener("resize", countWidth);
return () => window.removeEventListener("resize", countWidth);
}, [fatherRef, marginX, cols]);
return itemWidth
}
export default useCalculativeWidth
4. Flip動(dòng)畫(huà)實(shí)現(xiàn)
// lib/common/util/animation.ts
/**位置的類(lèi)型 */
interface position {
x: number,
y: number
}
/**Flip動(dòng)畫(huà) */
export class Flip {
/**dom元素 */
private dom: Element
/**原位置 */
private firstPosition: position | null = null
/**動(dòng)畫(huà)時(shí)間 */
private duration: number
/**正在移動(dòng)的動(dòng)畫(huà)會(huì)有一個(gè)專(zhuān)屬的class類(lèi)名,可以用于標(biāo)識(shí) */
static movingClass = "__flipMoving__"
constructor(dom: Element, duration = 500) {
this.dom = dom
this.duration = duration
}
/**獲得元素的當(dāng)前位置信息 */
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 }
}
/**播放動(dòng)畫(huà) */
play(callback?: () => any) {
if (!this.firstPosition) {
console.warn('請(qǐng)先記錄原始位置');
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多元素同時(shí)觸發(fā) */
export class FlipList {
/**Flip列表 */
private flips: Flip[]
/**正在移動(dòng)的動(dòng)畫(huà)會(huì)有一個(gè)專(zhuān)屬的class類(lèi)名,可以用于標(biāo)識(shí) */
static movingClass = Flip.movingClass
/**Flip多元素同時(shí)觸發(fā) - 構(gòu)造函數(shù)
* @param domList 要監(jiān)聽(tīng)的DOM列表
* @param duration 動(dòng)畫(huà)時(shí)長(zhǎng),默認(rèn)500ms
*/
constructor(domList: Element[], duration?: number) {
this.flips = domList.map((k) => new Flip(k, duration))
}
/**記錄全部初始位置 */
recordFirst() {
this.flips.forEach((flip) => flip.recordFirst())
}
/**播放全部動(dòng)畫(huà) */
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類(lèi)名 函數(shù)
* @param inputs 要合并的類(lèi)名
* @returns
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**查找符合條件的父節(jié)點(diǎn)
* @param node 當(dāng)前節(jié)點(diǎn)。如果當(dāng)前節(jié)點(diǎn)就符合條件,就會(huì)返回當(dāng)前節(jié)點(diǎn)
* @param target 參數(shù)是當(dāng)前找到的節(jié)點(diǎn),返回一個(gè)布爾值,為true代表找到想要的父節(jié)點(diǎn)
* @returns 沒(méi)找到則返回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"; //這個(gè)圖標(biāo)可以自己找喜歡的
import { FlipList } from "@/lib/common/util/animation";
/**拖拽時(shí),留在原位置的元素的樣式 */
const movingClass = [style.background]; //使用數(shù)組是為了方便以后添加其他類(lèi)名
/**拖拽時(shí),留在原位置的子元素的樣式 */
const opacityClass = ["opacity-0"]; //使用數(shù)組是為了方便以后添加其他類(lèi)名
/**拖拽排序組件 */
const DragSort = function <T>({
list,
ItemRender,
afterDrag,
keyName,
cols = 1,
marginX = 0,
flipWithListChange = true,
className,
style,
}: DragSortProps<T>) {
const listRef = useRef<HTMLDivElement>(null);
/**記錄當(dāng)前正在拖拽哪個(gè)元素 */
const nowDragItem = useRef<HTMLDivElement>();
const itemWidth = useCalculativeWidth(listRef, marginX, cols);
/**存儲(chǔ)flipList動(dòng)畫(huà)實(shí)例 */
const flipListRef = useRef<FlipList>();
const [dragOpen, setDragOpen] = useState(false); //是否開(kāi)啟拖拽 (鼠標(biāo)進(jìn)入指定區(qū)域開(kāi)啟)
/**創(chuàng)建記錄新的動(dòng)畫(huà)記錄,并立即記錄當(dāng)前位置 */
const createNewFlipList = (exceptTarget?: Element) => {
if (!listRef.current) return;
//記錄動(dòng)畫(huà)
const listenChildren = [...listRef.current.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都動(dòng)畫(huà)
flipListRef.current = new FlipList(listenChildren, 300);
flipListRef.current.recordFirst();
};
//下面這兩個(gè)是用于,當(dāng)列表變化時(shí),進(jìn)行動(dòng)畫(huà)
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
}, [list]);
useEffect(() => {
if (!flipWithListChange) return;
createNewFlipList();
return () => {
flipListRef.current?.play(() => flipListRef.current?.recordFirst());
};
}, [list.length]);
/**事件委托- 監(jiān)聽(tīng) 拖拽開(kāi)始 事件,添加樣式 */
const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
e.stopPropagation(); //阻止冒泡
/**這是當(dāng)前正在被拖拽的元素 */
const target = e.target as HTMLDivElement;
//設(shè)置被拖拽元素“留在原地”的樣式。為了防止設(shè)置正在拖拽的元素樣式,所以用定時(shí)器,宏任務(wù)更晚執(zhí)行
setTimeout(() => {
target.classList.add(...movingClass); //設(shè)置正被拖動(dòng)的元素樣式
target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都設(shè)置為透明,避免影響
}, 0);
//記錄元素的位置,用于Flip動(dòng)畫(huà)
createNewFlipList(target);
//記錄當(dāng)前拖拽的元素
nowDragItem.current = target;
//設(shè)置鼠標(biāo)樣式
e.dataTransfer.effectAllowed = "move";
};
/**事件委托- 監(jiān)聽(tīng) 拖拽進(jìn)入某個(gè)元素 事件,在這里只是DOM變化,數(shù)據(jù)順序沒(méi)有變化 */
const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
e.preventDefault(); //阻止默認(rèn)行為,默認(rèn)是不允許元素拖動(dòng)到人家身上的
if (!listRef.current || !nowDragItem.current) return;
/**孩子數(shù)組,每次都會(huì)獲取最新的 */
const children = [...listRef.current.children];
/**真正會(huì)被挪動(dòng)的元素(當(dāng)前正懸浮在哪個(gè)元素上面) */ //找到符合條件的父節(jié)點(diǎn)
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("這是正在動(dòng)畫(huà)的元素,跳過(guò)");
return;
}
//拿到兩個(gè)元素的索引,用來(lái)判斷這倆元素應(yīng)該怎么移動(dòng)
/**被拖拽元素在孩子數(shù)組中的索引 */
const nowDragtItemIndex = children.indexOf(nowDragItem.current);
/**被進(jìn)入元素在孩子數(shù)組中的索引 */
const enterItemIndex = children.indexOf(realTarget);
//當(dāng)用戶選中文字,然后去拖動(dòng)這個(gè)文字時(shí),就會(huì)觸發(fā) (可以通過(guò)禁止選中文字來(lái)避免)
if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
console.log("若第二個(gè)數(shù)為-1,說(shuō)明拖動(dòng)的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
return;
}
//Flip動(dòng)畫(huà) - 記錄原始位置
flipListRef.current?.recordFirst();
if (nowDragtItemIndex < enterItemIndex) {
// console.log("向下移動(dòng)");
listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
} else {
// console.log("向上移動(dòng)");
listRef.current.insertBefore(nowDragItem.current, realTarget);
}
//Flip動(dòng)畫(huà) - 播放
flipListRef.current?.play();
};
/**事件委托- 監(jiān)聽(tīng) 拖拽結(jié)束 事件,刪除樣式,設(shè)置當(dāng)前列表 */
const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
if (!listRef.current) return;
/**當(dāng)前正在被拖拽的元素 */
const target = e.target as Element;
target.classList.remove(...movingClass); //刪除前面添加的 被拖拽元素的樣式,回歸原樣式
target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //刪除所有子元素的透明樣式
/**拿到當(dāng)前DOM的id順序信息 */
const ids = [...listRef.current.children].map((k) => String(k.id)); //根據(jù)id,判斷到時(shí)候應(yīng)該怎么排序
//把列表按照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); //拖拽完成后,再次禁止拖拽
};
/**拖拽按鈕組件 */ //只有鼠標(biāo)懸浮在這上面的時(shí)候,才開(kāi)啟拖拽,做到“指定區(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()} //被拖動(dòng)的對(duì)象被拖到其它容器時(shí)(因?yàn)槟J(rèn)不能拖到其它元素上)
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. 效果圖的測(cè)試用例
一開(kāi)始展示的效果圖的實(shí)現(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[]>([]); //當(dāng)前列表
const [cols, setCols] = useState(1); //一行個(gè)數(shù)
/**創(chuàng)建一個(gè)新的元素 */
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}>
點(diǎn)我添加
</Button>
一行個(gè)數(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>序號(hào):{index},</div>
<div>ID:{item.id}</div>
{/* <DragBox className="bg-stone-400 text-white p-1">自定義拖拽位置</DragBox> */}
</div>
);
}}
/>
</div>
);
}
四、結(jié)語(yǔ)
以上就是React實(shí)現(xiàn)一個(gè)拖拽排序組件的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于React拖拽排序組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react實(shí)現(xiàn)記錄拖動(dòng)排序
這篇文章主要介紹了react實(shí)現(xiàn)記錄拖動(dòng)排序的相關(guān)資料,需要的朋友可以參考下2023-07-07
react+ts實(shí)現(xiàn)簡(jiǎn)單jira項(xiàng)目的最佳實(shí)踐記錄
這篇文章主要介紹了react+ts實(shí)現(xiàn)簡(jiǎn)單jira項(xiàng)目,本文通過(guò)圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07
react組件中debounce防抖功能失效問(wèn)題解決辦法
在React組件中,如果使用useState管理searchKey,每次輸入變化都會(huì)導(dǎo)致組件重新渲染,從而生成新的debounce函數(shù),導(dǎo)致防抖功能失效,解決方法是使用useRef定義searchKey為非響應(yīng)式數(shù)據(jù),從而維持debounce函數(shù)的穩(wěn)定,確保防抖功能有效,感興趣的朋友跟隨小編一起看看吧2024-10-10
ReactHooks批量更新state及獲取路由參數(shù)示例解析
這篇文章主要介紹了React Hooks如何實(shí)現(xiàn)批量更新state以及獲取路由參數(shù)的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10
React項(xiàng)目搭建與Echarts工具使用詳解
這篇文章主要介紹了React項(xiàng)目搭建與Echarts工具使用詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
react+zarm實(shí)現(xiàn)底部導(dǎo)航欄的示例代碼
本文主要介紹了react?+?zarm?實(shí)現(xiàn)底部導(dǎo)航欄的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

