React實(shí)現(xiàn)一個(gè)拖拽排序組件的示例代碼
一、效果展示
排序:
絲滑的Flip動(dòng)畫
自定義列數(shù) (并且寬度會(huì)隨著屏幕寬度自適應(yīng))
自定義拖拽區(qū)域:(擴(kuò)展性高,可以全部可拖拽、自定義拖拽圖標(biāo))
二、主要思路
Tip: 本代碼的CSS使用Tailwindcss, 如果沒(méi)安裝的可以自行安裝這個(gè)庫(kù),也可以去問(wèn)GPT,讓它幫忙改成普通的CSS版本的代碼
1. 一些ts類型:
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)畫,默認(rèn)開啟 (可能有點(diǎn)略微的動(dòng)畫bug) */ flipWithListChange?: boolean /**每個(gè)元素的渲染函數(shù) */ ItemRender: (props: itemProps<T>) => ReactNode /**拖拽結(jié)束事件,返回排序好的新數(shù)組,在里面自己調(diào)用setList */ afterDrag: (list: T[]) => any }
2. 使用事件委托
監(jiān)聽所有子元素的拖拽開始、拖拽中、拖拽結(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); //是否開啟拖拽 (鼠標(biāo)進(jìn)入指定區(qū)域開啟) /**事件委托- 監(jiān)聽 拖拽開始 事件,添加樣式 */ 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)聽 拖拽進(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)聽 拖拽結(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í)候,才開啟拖拽,做到“指定區(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)畫
對(duì)于這種移動(dòng)位置的動(dòng)畫,普通的CSS和JS動(dòng)畫已經(jīng)無(wú)法滿足了:
可以使用Flip動(dòng)畫來(lái)做:FLIP是 First、Last、Invert和 Play四個(gè)單詞首字母的縮寫, 意思就是,記錄一開始的位置、記錄結(jié)束的位置、記錄位置的變化、讓元素開始動(dòng)畫
主要的思路為: 記錄原位置、記錄現(xiàn)位置、記錄位移大小,最重要的點(diǎn)來(lái)了, 使用CSS的 transform ,讓元素在被改動(dòng)位置的一瞬間, translate 定位到原本的位置上(通過(guò)我們前面計(jì)算的位移大?。?, 然后給元素加上 過(guò)渡 效果,再讓它慢慢回到原位即可。
代碼如下 (沒(méi)有第三方庫(kù),基本都是自己手寫實(shí)現(xiàn))
這里還使用了JS提供的 Web Animations API,具有極高的性能,不阻塞主線程。
但是由于API沒(méi)有提供動(dòng)畫完成的回調(diào),故這里使用定時(shí)器做回調(diào)觸發(fā)
/**位置的類型 */ interface position { x: number, y: number } /**Flip動(dòng)畫 */ export class Flip { /**dom元素 */ private dom: Element /**原位置 */ private firstPosition: position | null = null /**動(dòng)畫時(shí)間 */ private duration: number /**正在移動(dòng)的動(dòng)畫會(huì)有一個(gè)專屬的class類名,可以用于標(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)畫 */ 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ì)有一個(gè)專屬的class類名,可以用于標(biāo)識(shí) */ static movingClass = Flip.movingClass /**Flip多元素同時(shí)觸發(fā) - 構(gòu)造函數(shù) * @param domList 要監(jiān)聽的DOM列表 * @param duration 動(dòng)畫時(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)畫 */ play(callback?: () => any) { this.flips.forEach((flip) => flip.play(callback)) } }
然后在特定的地方插入代碼,記錄元素位置,做動(dòng)畫,插入了動(dòng)畫之后的代碼,見下面的“完整代碼”模塊
三、完整代碼
1.類型定義
// 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)畫,默認(rèn)開啟 (可能有點(diǎn)略微的動(dòng)畫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)聽綁定 window.addEventListener("resize", countWidth); return () => window.removeEventListener("resize", countWidth); }, [fatherRef, marginX, cols]); return itemWidth } export default useCalculativeWidth
4. Flip動(dòng)畫實(shí)現(xiàn)
// lib/common/util/animation.ts /**位置的類型 */ interface position { x: number, y: number } /**Flip動(dòng)畫 */ export class Flip { /**dom元素 */ private dom: Element /**原位置 */ private firstPosition: position | null = null /**動(dòng)畫時(shí)間 */ private duration: number /**正在移動(dòng)的動(dòng)畫會(huì)有一個(gè)專屬的class類名,可以用于標(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)畫 */ 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ì)有一個(gè)專屬的class類名,可以用于標(biāo)識(shí) */ static movingClass = Flip.movingClass /**Flip多元素同時(shí)觸發(fā) - 構(gòu)造函數(shù) * @param domList 要監(jiān)聽的DOM列表 * @param duration 動(dòng)畫時(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)畫 */ 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é)點(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ù)組是為了方便以后添加其他類名 /**拖拽時(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); /**記錄當(dāng)前正在拖拽哪個(gè)元素 */ const nowDragItem = useRef<HTMLDivElement>(); const itemWidth = useCalculativeWidth(listRef, marginX, cols); /**存儲(chǔ)flipList動(dòng)畫實(shí)例 */ const flipListRef = useRef<FlipList>(); const [dragOpen, setDragOpen] = useState(false); //是否開啟拖拽 (鼠標(biāo)進(jìn)入指定區(qū)域開啟) /**創(chuàng)建記錄新的動(dòng)畫記錄,并立即記錄當(dāng)前位置 */ const createNewFlipList = (exceptTarget?: Element) => { if (!listRef.current) return; //記錄動(dòng)畫 const listenChildren = [...listRef.current.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都動(dòng)畫 flipListRef.current = new FlipList(listenChildren, 300); flipListRef.current.recordFirst(); }; //下面這兩個(gè)是用于,當(dāng)列表變化時(shí),進(jìn)行動(dòng)畫 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(); //阻止冒泡 /**這是當(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)畫 createNewFlipList(target); //記錄當(dāng)前拖拽的元素 nowDragItem.current = target; //設(shè)置鼠標(biāo)樣式 e.dataTransfer.effectAllowed = "move"; }; /**事件委托- 監(jiān)聽 拖拽進(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)畫的元素,跳過(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)畫 - 記錄原始位置 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)畫 - 播放 flipListRef.current?.play(); }; /**事件委托- 監(jiān)聽 拖拽結(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í)候,才開啟拖拽,做到“指定區(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è)試用例
一開始展示的效果圖的實(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項(xiàng)目中優(yōu)雅的使用對(duì)話框
在項(xiàng)目中,對(duì)話框和確認(rèn)框是使用頻率很高的組件,下面這篇文章主要給大家介紹了關(guān)于如何在React項(xiàng)目中優(yōu)雅的使用對(duì)話框的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05react-player實(shí)現(xiàn)視頻播放與自定義進(jìn)度條效果
本篇文章通過(guò)完整的代碼給大家介紹了react-player實(shí)現(xiàn)視頻播放與自定義進(jìn)度條效果,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-01-01前端開發(fā)使用Ant Design項(xiàng)目評(píng)價(jià)
這篇文章主要為大家介紹了前端開發(fā)使用Ant Design項(xiàng)目評(píng)價(jià),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08如何將你的AngularJS1.x應(yīng)用遷移至React的方法
本篇文章主要介紹了如何將你的AngularJS1.x應(yīng)用遷移至React的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02React視頻播放控制組件Video Controls的實(shí)現(xiàn)
本文主要介紹了React視頻播放控制組件Video Controls的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02React中useCallback useMemo到底該怎么用
在React函數(shù)組件中,當(dāng)組件中的props發(fā)生變化時(shí),默認(rèn)情況下整個(gè)組件都會(huì)重新渲染。換句話說(shuō),如果組件中的任何值更新,整個(gè)組件將重新渲染,包括沒(méi)有更改values/props的函數(shù)/組件。在react中,我們可以通過(guò)memo,useMemo以及useCallback來(lái)防止子組件的rerender2023-02-02詳解React-Router中Url參數(shù)改變頁(yè)面不刷新的解決辦法
這篇文章主要介紹了詳解React-Router中Url參數(shù)改變頁(yè)面不刷新的解決辦法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08