欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

一、效果展示

排序:

絲滑的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ì)話框

    如何在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-05
  • react-player實(shí)現(xiàn)視頻播放與自定義進(jìn)度條效果

    react-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à)

    這篇文章主要為大家介紹了前端開發(fā)使用Ant Design項(xiàng)目評(píng)價(jià),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • 如何將你的AngularJS1.x應(yīng)用遷移至React的方法

    如何將你的AngularJS1.x應(yīng)用遷移至React的方法

    本篇文章主要介紹了如何將你的AngularJS1.x應(yīng)用遷移至React的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-02-02
  • React視頻播放控制組件Video Controls的實(shí)現(xiàn)

    React視頻播放控制組件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-02
  • React 組件中的 bind(this)示例代碼

    React 組件中的 bind(this)示例代碼

    這篇文章主要介紹了 React 組件中的 bind(this) ,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2018-09-09
  • React中useCallback useMemo到底該怎么用

    React中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)防止子組件的rerender
    2023-02-02
  • 詳解React-Router中Url參數(shù)改變頁(yè)面不刷新的解決辦法

    詳解React-Router中Url參數(shù)改變頁(yè)面不刷新的解決辦法

    這篇文章主要介紹了詳解React-Router中Url參數(shù)改變頁(yè)面不刷新的解決辦法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-05-05
  • react MPA 多頁(yè)配置詳解

    react MPA 多頁(yè)配置詳解

    這篇文章主要介紹了react MPA 多頁(yè)配置詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • 使用react+redux實(shí)現(xiàn)彈出框案例

    使用react+redux實(shí)現(xiàn)彈出框案例

    這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-08-08

最新評(píng)論