安装
npm install @dnd-kit/core
核心概念
DndContext:它类似 react 的 Context 向下传递状态,他必须在拖拽组件的最外层 Droppable: 用于放置可拖拽元素的盒子,使组件能够放置 Draggable: 用于包装可以拖拽元素的盒子,使组件能够拖拽 SortableContext: 创建可以放置拖拽排序元素,使其中的子元素可以拖拽排序
DndContext可以定义拖拽的修饰方式(方向),有以下值: restrictToVerticalAxis: 垂直方向拖拽
restrictToHorizontalAxis:水平方向拖拽
restrictToWindowEdges:限制拖拽到窗口边缘,防止拖拽元素超出窗口
SortableContext可以定义不同的排序策略,有以下值: rectSortingStrategy:默认值,适用于大多数排序,不支持虚拟化列表
rectSwappingStrategy:使用此策略可使用交换功能
verticalListSortingStrategy:此策略针对垂直方向列表进行了优化,并支持虚拟化列表
horizontalListSortingStrategy:此策略针对水平方向列表行了优化,并支持虚拟化列表
useDroppable
参数: id: 该放置区域的唯一id,支持string | number disabled: 该放置区域是否禁用 data: 可用于传递数据,支持任意类型
返回值: active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息 isOver: 是否正在有被拖拽元素覆盖在掉落区域上方 setNodeRef:用于绑定可掉落区域的ref
useDraggable
参数: id: 该拖拽元素的唯一id,支持string | number disabled: 该拖拽元素是否禁用 data: 可用于传递数据,支持任意类型
返回值: attributes:传递给拖拽元素的属性 listeners:拖拽手柄信息 transform:被拖拽元素的位置信息 isDragging:被拖拽元素是否正在被拖拽 setNodeRef:用于绑定被拖拽元素的ref active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息
useSortable
参数: id: 拖拽元素的唯一id,支持string | number disabled: 该拖拽元素是否禁用 data: 可用于传递数据,支持任意类型 transition: 过渡动画,不要动画设为null
返回值: attributes:传递给拖拽元素的属性 listeners:拖拽手柄信息 transform:被拖拽元素的位置信息 isDragging:被拖拽元素是否正在被拖拽 setNodeRef:用于绑定被拖拽元素的ref active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息 data: 传递的信息
示例
简单实现小色块拖动至一个容器中的功能,如图所示:
// 被拖拽的元素
type DraggableItem = {
id: string
color: string
}
// 被拖拽的元素
type DroppableProps = {
id: string
draggableItems: DraggableItem[]
}
// 接收被拖拽的元素的容器
function Droppable(props: DroppableProps) {
const { id, draggableItems } = props
const { isOver, setNodeRef } = useDroppable({
id,
})
return (
<div
ref={setNodeRef}
className={cn('w-full h-[300px] bg-white border-2 border-orange-400', {
'border-cyan-500': isOver,
})}
>
<SpaceItem align="left" styles={{ flexWrap: 'wrap' }}>
{draggableItems.map((item, index) => (
<div
key={`{${item.color}-${index}`}
className={cn('w-[50px] h-[50px]')}
style={{ backgroundColor: item.color }}
/>
))}
</SpaceItem>
</div>
)
}
// 被拖拽元素
function Draggable(props: DraggableItem) {
const { id, color } = props
const { attributes, listeners, setNodeRef, transform, isDragging } =
useDraggable({
id,
data: props,
})
const style = {
transform: CSS.Translate.toString(transform),
}
return (
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className={cn(
`w-[50px] h-[50px]`,
isDragging ? 'opacity-30 shadow-md' : 'opacity-100',
)}
style={{ ...style, backgroundColor: color }}
/>
)
}
// 拖拽
export default function DndKit() {
const dragItems = [
{ id: '1', color: 'red' },
{ id: '2', color: 'blue' },
{ id: '3', color: 'green' },
{ id: '4', color: 'yellow' },
{ id: '5', color: 'orange' },
] // 色块
const droppableId = 'droppable' // 可掉落元素id
const [items, setItems] = useState<DraggableItem[]>([]) // 存储拖拽后的元素
// 拖拽结束
const onDragEnd = (event: DragEndEvent) => {
if (event.over && event.over.id === droppableId) {
setItems((prevItems) => [
...prevItems,
event.active.data.current as DraggableItem,
])
}
}
return (
<Row gutter={24}>
<Col span={8}>
<DndContext onDragEnd={onDragEnd}>
<Droppable id={droppableId} draggableItems={items} />
<WhiteSpace gap={20} />
<SpaceItem align="left">
{dragItems.map((item) => (
<Draggable key={item.id} {...item} />
))}
</SpaceItem>
</DndContext>
</Col>
</Row>
)
}
实现容器内子元素卡片拖拽排序
// 被拖拽排序的元素
type ListItem = {
id: number | string
title: string
content: string
}
// 用来拖拽排序的子元素
function SortableItem(props: ListItem) {
const { id, title, content } = props
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, data: props })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div
ref={setNodeRef}
className={cn('w-full bg-green-300 my-3 p-2 rounded-md', {
'bg-red-500': isDragging,
})}
style={style}
{...attributes}
{...listeners}
>
<div>{id}</div>
<div>{title}</div>
<div>{content}</div>
</div>
)
}
// 拖拽
export default function DndKit() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(12),
}))
} // 生成内容
const [lists, setLists] = useState<ListItem[]>(generateDatas()) // 所有卡片
const itemss = useMemo(() => lists.map((ele) => ele.id), [lists]) // 所有卡片的id
const onDragEnd1 = (event: DragEndEvent) => {
const { active, over } = event
if (active && over && active.id !== over.id) {
const oldIndex = itemss.indexOf(active.id as number)
const newIndex = itemss.indexOf(over.id as number)
setLists((lists) => {
return arrayMove(lists, oldIndex, newIndex)
})
}
}
return (
<Row gutter={24}>
<Col span={8}>
<DndContext onDragEnd={onDragEnd1}>
<div className="border-2 border-red-500 p-2 rounded-lg">
<SortableContext items={itemss}>
{lists.map((item) => (
<SortableItem key={item.id} {...item} />
))}
</SortableContext>
</div>
</DndContext>
</Col>
</Row>
)
}
表格的拖拽排序
// 被拖拽排序的元素
type ListItem = {
id: number | string
title: string
content: string
}
// 用来拖拽排序的TR
function SortableTr(props: any) {
const { id, ...rests } = props
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, data: props })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<tr
{...rests}
ref={setNodeRef}
className={cn({ 'bg-pink-400': isDragging })}
style={style}
{...attributes}
{...listeners}
/>
)
}
// 拖拽
export default function DndKit() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(12),
}))
}
const [dataSource, setDataSource] = useState<ListItem[]>(generateDatas(15))
const dataSourceIds = useMemo(
() => dataSource.map((ele) => ele.id),
[dataSource],
)
const onDragEnd2 = (event: DragEndEvent) => {
const { active, over } = event
if (active && over && active.id !== over.id) {
const oldIndex = dataSourceIds.indexOf(active.id as number)
const newIndex = dataSourceIds.indexOf(over.id as number)
setDataSource((lists) => {
return arrayMove(lists, oldIndex, newIndex)
})
}
}
return (
<Row gutter={24}>
<Col span={8}>
<DndContext onDragEnd={onDragEnd2}>
<SortableContext items={dataSourceIds}>
<Table
dataSource={dataSource}
pagination={false}
columns={[
{ title: 'ID', dataIndex: 'id' },
{ title: '标题', dataIndex: 'title' },
{ title: '内容', dataIndex: 'content' },
]}
rowKey="id"
components={{
body: {
row: (props: any) => {
const rowKey = props['data-row-key']
return <SortableTr {...props} id={rowKey} />
},
},
}}
/>
</SortableContext>
</DndContext>
</Col>
</Row>
)
}
大卡片之间互相拖拽排序,大卡片里面的小卡片相互拖拽排序,不同的大卡片里面的小卡片跨大卡片拖拽排序
// 被拖拽排序的元素
type ListItem = {
id: number | string
title: string
content: string
}
// 大卡片
type SortableCard = {
id: number | string
cardItems: ListItem[]
}
// 大卡片里面的小卡片
type SortableCardItems = {
pid: number | string // 父级id
cardItem: ListItem
}
// 用来拖拽排序的Card
function SortableCard(props: SortableCard) {
const { id, cardItems } = props
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, data: { card: true } })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div
ref={setNodeRef}
className={cn(
'w-[160px] border-orange-300 border-2 rounded-md pt-3 px-2',
{
'bg-pink-400': isDragging,
},
)}
style={style}
{...attributes}
{...listeners}
>
<div className="text-center text-red-500 font-bold">{id}</div>
{cardItems.map((item) => (
<SortableCardItem
key={item.id}
pid={id}
cardItem={item}
onlyOne={cardItems.length === 1}
/>
))}
</div>
)
}
// 用来拖拽排序的CardItem
function SortableCardItem(props: SortableCardItems & { onlyOne: boolean }) {
const { pid, cardItem, onlyOne } = props
const { id, content, title } = cardItem
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: cardItem.id,
data: { cardItem: true, pid },
disabled: onlyOne,
})
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div
ref={setNodeRef}
className={cn('w-full bg-green-300 my-3 p-2 rounded-md', {
'bg-red-500': isDragging,
})}
style={style}
{...attributes}
{...listeners}
>
<div>{id}</div>
<div>{title}</div>
<div className="break-words">{content}</div>
</div>
)
}
// 拖拽
export default function DndKit() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(12),
}))
}
const [cards, setCards] = useState<number[]>(Array.from(Array(6).keys()))
const [cardItems, setCardItems] = useState<Record<number, ListItem[]>>(() => {
const cardItems = cards.reduce((acc, cur) => {
acc[cur] = generateDatas(10, cur + '')
return acc
}, {} as Record<number, ListItem[]>)
return cardItems
})
const allIds = useMemo(() => {
return Object.values(cardItems).reduce((acc, cur) => {
acc.push(...cur.map((item) => item.id as string))
return acc
}, [] as string[])
}, [cardItems]) // 所有的小卡片id
const onDragEnd3 = (event: DragEndEvent) => {
const { active, over } = event
// 大卡片拖拽
if (active.data.current!.card === true) {
if (active && over && active.id !== over.id) {
const oldIndex = cards.indexOf(active.id as number)
const newIndex = cards.indexOf(over.id as number)
setCards((lists) => {
return arrayMove(lists, oldIndex, newIndex)
})
}
} else {
// 小卡片拖拽
if (active && over && active.id !== over.id) {
const oldCardId = active.data.current!.pid
const newCardId = over.data.current!.pid
// 同一个大卡片内拖拽
if (oldCardId === newCardId) {
const cardItem = cardItems[oldCardId]
const oldIndex = cardItem.findIndex((item) => item.id === active.id)
const newIndex = cardItem.findIndex((item) => item.id === over.id)
const newCardItems = arrayMove(cardItem, oldIndex, newIndex)
setCardItems((items) => {
return { ...items, [newCardId]: Array.from(newCardItems) }
})
} else {
// 跨大卡片拖拽
const oldCardItem = cardItems[oldCardId] // 旧大卡片里面的数据
const newCardItem = cardItems[newCardId] // 新大卡片里面的数据
const activeIndex = oldCardItem.findIndex(
(ele) => ele.id === active.id,
) // 旧大卡片里面的索引
const activeCardItem = oldCardItem[activeIndex] // 旧大卡里面的索引对应的数据
const overIndex = newCardItem.findIndex((ele) => ele.id === over.id) // 新大卡片里面的索引
// 去除oldCardItem的被拖拽元素
oldCardItem.splice(activeIndex, 1)
// newCardItem中增加被拖拽元素
newCardItem.splice(overIndex, 0, activeCardItem)
setCardItems((items) => {
return {
...items,
[oldCardId]: Array.from(oldCardItem),
[newCardId]: Array.from(newCardItem),
}
})
}
}
}
}
return (
<Row gutter={24}>
<Col>
<DndContext onDragEnd={onDragEnd3}>
{/* 第一层 */}
<SortableContext items={[...cards, ...allIds]}>
<SpaceItem align="left" styles={{ alignItems: 'flex-start' }}>
{/* 第二层 */}
{cards.map((card) => (
<SortableCard
key={card}
id={card}
cardItems={cardItems[card]}
/>
))}
</SpaceItem>
</SortableContext>
</DndContext>
</Col>
</Row>
)
}
tips: 以上示例代码中出现的部分SpaceItem WhiteSpace cn randomString 均为本人项目中的封装,仅供参考。