一个比较简单的拖拽列表,参考了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