最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時間:2023年11月06日 09:46:35   作者:煎餅狗子  
這篇文章主要給大家介紹了React實現(xiàn)一個拖拽排序組件?-?支持多行多列、支持TypeScript、支持Flip動畫、可自定義拖拽區(qū)域,文章通過代碼示例介紹的非常詳細,需要的朋友可以參考下

一、效果展示

排序:

絲滑的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ā) - 構造函數(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
    /**可拖拽的盒子,只有在這上面才能拖拽。自由放置位置。提供了一個默認的拖拽圖標??梢宰鳛榘鼑校瑢⒛硥K內(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ā) - 構造函數(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拖拽排序組件的資料請關注腳本之家其它相關文章!

相關文章

  • Redux使用方法和基本原理解讀

    Redux使用方法和基本原理解讀

    這篇文章主要介紹了Redux使用方法和基本原理,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • react實現(xiàn)記錄拖動排序

    react實現(xiàn)記錄拖動排序

    這篇文章主要介紹了react實現(xiàn)記錄拖動排序的相關資料,需要的朋友可以參考下
    2023-07-07
  • react+ts實現(xiàn)簡單jira項目的最佳實踐記錄

    react+ts實現(xiàn)簡單jira項目的最佳實踐記錄

    這篇文章主要介紹了react+ts實現(xiàn)簡單jira項目,本文通過圖文實例相結(jié)合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-07-07
  • React中如何使用scss

    React中如何使用scss

    這篇文章主要介紹了React中如何使用scss問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • react組件中debounce防抖功能失效問題解決辦法

    react組件中debounce防抖功能失效問題解決辦法

    在React組件中,如果使用useState管理searchKey,每次輸入變化都會導致組件重新渲染,從而生成新的debounce函數(shù),導致防抖功能失效,解決方法是使用useRef定義searchKey為非響應式數(shù)據(jù),從而維持debounce函數(shù)的穩(wěn)定,確保防抖功能有效,感興趣的朋友跟隨小編一起看看吧
    2024-10-10
  • ReactHooks批量更新state及獲取路由參數(shù)示例解析

    ReactHooks批量更新state及獲取路由參數(shù)示例解析

    這篇文章主要介紹了React Hooks如何實現(xiàn)批量更新state以及獲取路由參數(shù)的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2021-10-10
  • React項目搭建與Echarts工具使用詳解

    React項目搭建與Echarts工具使用詳解

    這篇文章主要介紹了React項目搭建與Echarts工具使用詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-03-03
  • ReactNative之FlatList的具體使用方法

    ReactNative之FlatList的具體使用方法

    本篇文章主要介紹了ReactNative之FlatList的具體使用方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-11-11
  • 詳解react-native-fs插件的使用以及遇到的坑

    詳解react-native-fs插件的使用以及遇到的坑

    本篇文章主要介紹了react-native-fs插件的使用以及遇到的坑,詳細的介紹了react-native-fs安裝使用,具有一定的參考價值,有興趣的可以了解一下
    2017-09-09
  • react+zarm實現(xiàn)底部導航欄的示例代碼

    react+zarm實現(xiàn)底部導航欄的示例代碼

    本文主要介紹了react?+?zarm?實現(xiàn)底部導航欄的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-05-05

最新評論

盘山县| 西贡区| 中宁县| 永安市| 米泉市| 保定市| 江华| 彭阳县| 三亚市| 吉隆县| 湘乡市| 东兰县| 临武县| 滨州市| 枣庄市| 论坛| 华阴市| 田东县| 穆棱市| 双柏县| 图们市| 延边| 兴国县| 泸水县| 贡山| 新巴尔虎左旗| 青河县| 惠安县| 兰考县| 岑巩县| 离岛区| 二手房| 海晏县| 花莲市| 石景山区| 阳新县| 宜春市| 勃利县| 阜宁县| 丰镇市| 石家庄市|