react实现可拖拽的列表

本文介绍了如何在React中使用DndProvider和HTML5Backend实现一个可拖拽的列表,重点讲解了如何处理卡片组件的拖拽事件、设置唯一key以及优化多区域拖拽功能。作者推荐使用AntDesign的可拖拽Table进行生产环境应用。
摘要由CSDN通过智能技术生成

一个比较简单的拖拽列表,参考了react的官方demo,学习使用。

如果想要用于生产,推荐Ant Design的可拖拽Table

整个drag_list分为列表body和上面的若干数据项card

body

body文件命名为index.tsx,代码如下。

核心是对传入的包含数据项items遍历创建对应的card。拖拽依靠的是react提供的DndProvider

必须为每一个card赋予一个唯一的key={item.id}

因为react渲染机制的问题,如果不赋予唯一key,会造成在拖拽时没有“腾出位置”动画的问题(能够成功拖拽,但没有视觉效果)

moveCard响应卡片拖拽card事件

insertCard响应上插/下插card事件

deleteCard响应删除card事件

updateCatd响应修改catd内容事件

'use client'

import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"
import type { Item } from '@/app/components/app/drag_item'
import { useCallback } from "react";
import { Card } from "./card"


export type NewProfileItemProps = {
    items: Item[]
    moveCard: (dragIndex: number, hoverIndex: number) => void
    insertCard: (index: number) => void
    deleteCard: (index: number) => void
    updateCard: (index: number, content: string) => void
}

const DragList = ({
    items, moveCard, insertCard, deleteCard, updateCard,
}: NewProfileItemProps) => {
    const renderCard = useCallback(
        (item: Item, index: number) => {
            return (
                <Card
                    key={item.id}
                    id={item.id}
                    index={index}
                    item={item}
                    moveCard={moveCard}
                    insertCard={insertCard}
                    deleteCard={deleteCard}
                    updateCard={updateCard}
                />
            );
        },
        []
    )

    return (
        <DndProvider backend={HTML5Backend}>
            <div className="w-full space-y-[10px] overflow-y-auto">
                {
                    items.map((item, i) => renderCard(item, i))
                }
            </div>
        </DndProvider>
    )
}

export default DragList

card

card核心是依靠useDrop和useDrag来实现

通过计算拖拽时移动的距离、移动的方向等就能知道被拖拽的catd正处于的位置hoverIndex,松手就便调用moveCard(dragIndex, hoverIndex),将items数组的dragIndex项移动到hoverIndex,随后触发react的重绘,便达到了拖拽排序的目的。

useDrop中accept: 'value_card'和useDrag中的type: 'value_card'必须拥有相同的值

因为可能存在多个DndProvider区域,拖拽card是不会触发type值和accept值不同的DndProvider区域的useDrop事件

可以做两个accept: 'value_card'的列表,这样可以稍加改进就可以实现两个列表的card相互拖拽的效果。

import type { Identifier, XYCoord } from "dnd-core"
import type { FC } from "react"
import { useRef } from "react"
import { useDrag, useDrop } from "react-dnd"
import type { Item } from '@/app/components/app/drag_item'
import cn from 'classnames'
import style from './drag.list.module.css'
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'

const dragContainerStyle = {
    backgroundColor: "white",
    cursor: "move",
}

export interface CardProps {
    id: any;
    index: number
    item: Item
    moveCard: (dragIndex: number, hoverIndex: number) => void
    insertCard: (index: number) => void
    deleteCard: (index: number) => void
    updateCard: (index: number, content: string) => void
}

interface DragItem {
    index: number
    id: string
    type: string
}

export const Card: FC<CardProps> = ({ id, index, item, moveCard, insertCard, deleteCard, updateCard, changeTag ,changeRole}) => {
    const dragRef = useRef<HTMLDivElement>(null);

    const [{ handlerId }, drop] = useDrop<
        DragItem,
        void,
        { handlerId: Identifier | null }
    >({
        accept: 'value_card',
        collect(monitor) {
            return {
                handlerId: monitor.getHandlerId(),
            };
        },
        hover(item: DragItem, monitor) {
            if (!dragRef.current) {
                return
            }
            const dragIndex = item.index
            const hoverIndex = index
            if (dragIndex === hoverIndex) {
                return
            }

            //获取屏幕上的矩形
            const hoverBoundingRect = dragRef.current?.getBoundingClientRect()
            //获得垂直轴中间点
            const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
            //确定鼠标位置
            const clientOffset = monitor.getClientOffset()
            //将像素移到顶部
            const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top

            //只有当鼠标越过项目高度的一半时才执行移动
            //向下拖动时,仅当光标低于50%时移动
            //向上拖动时,仅当光标高于50%时移动
            //向下拖动
            if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
                return
            }
            //向上拖动
            if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
                return
            }
            //移动item
            moveCard(dragIndex, hoverIndex)
            item.index = hoverIndex
        },
    })

    const [{ isDragging }, drag] = useDrag({
        type: 'value_card',
        item: () => {
            return { id, index }
        },
        collect: (monitor: any) => ({
            isDragging: monitor.isDragging(),
        }),
    })

    const opacity = isDragging ? 0 : 1

    drag(drop(dragRef))

    return (
        <div className="block w-full hover:border-blue-700 hover:border rounded-lg" ref={dragRef} style={{ ...dragContainerStyle, opacity }} data-handler-id={handlerId}>
            <div className="ml-4 flex" >
                <Button className='mb-1 mr-1' onClick={(e) => { e.stopPropagation(); insertCard(index) }}>上插</Button>
                <Button className='mb-1 mr-1'  onClick={(e) => { e.stopPropagation(); insertCard(index + 1) }}>下插</Button>
                <Button className='mb-1 mr-1' onClick={(e) => { e.stopPropagation(); deleteCard(index) }}>删除</Button>
            </div>
            <div className="ml-4 flex-grow">
                <AutoHeightTextarea
                    value={item.content}
                    onChange={e => updateCard(index, e.target.value)}
                    className={`${cn(style.textArea)} resize-none block w-full pl-3 bg-gray-50 border border-gray-200 rounded-md  focus:outline-none sm:text-sm text-gray-700`}
                />
            </div>
        </div>
    )
}

export default Card

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值