最近有个需求是拖动一个列表中的item, 使列表能够调整显示的顺序,调研了一下,成熟的的工具有React-dnd, 不过感觉没必要, 因为项目中其他地方不怎么使用拖拽。遂自己使用Drag&Drop API(理论上换成mouse/touch也能实现)实现了一个简单的demo。
组件
思路: 首先,根据数据->视图的思想, 在完成拖拽后,调整数组顺序,然后再重新渲染视图。
- Draggrable
- DragItem
- 用于拖拽
- DropLine
- 用于放置
- DragItem
- Draggrable
import React, { ReactNode, useState } from "react";
import DropLine from "./DropLine";
import DragItem from "./DragItem";
export interface BaseItem {
id: string;
}
interface DraggableProps<T extends BaseItem> {
data: Array<T>;
renderItem: (item: T) => ReactNode;
}
export default function Draggable<T extends BaseItem>({
data,
renderItem,
}: DraggableProps<T>) {
const [items, setItems] = useState(data);
const [selectedID, setSelectedID] = useState("");
const moveBefore = (anchorID: string, selectedID: string) => {
const selectedItem = items.find((item) => item.id === selectedID);
if (!selectedItem) return;
let newItems = items.filter((item) => item.id !== selectedID);
const anchorIndex = newItems.findIndex((item) => item.id === anchorID);
newItems = [
...newItems.slice(0, anchorIndex + 1),
{ ...selectedItem },
...newItems.slice(anchorIndex + 1),
];
setItems(newItems);
};
return (
<section>
{items.reduce(
(acc, item, idx) => [
...acc,
<DragItem key={item.id} {...{ selectedID, setSelectedID, item }}>
{renderItem(item)}
</DragItem>,
<DropLine
key={idx + 1}
{...{
anchorID: item.id,
selectedID,
onMove: moveBefore,
}}
/>,
],
[
<DropLine
key={0}
{...{
anchorID: "",
selectedID,
onMove: moveBefore,
}}
/>,
]
)}
</section>
);
}
- DropLine
- 每个DropLine绑定一个DragItem ID,表示它的前驱(第一条DropLine没有前驱)
- 当item进入的时候,在DragEnter设置active, 在DragLeave, Drop取消active
- 放置后,在Drop里面移动数组
import React, { useState } from "react";
interface DropLineProps {
selectedID: string;
anchorID: string;
onMove: (anchorID: string, selectedID: string) => void;
}
export default function DropLine({
selectedID,
anchorID,
onMove,
}: DropLineProps) {
const [active, setActive] = useState(false);
return (
<div
style={{
height: "2rem",
display: "flex",
alignItems: "center",
}}
onDragEnter={(e) => {
e.preventDefault();
setActive(true);
}}
onDragOver={(e) => e.preventDefault()}
onDrop={() => {
setActive(false);
if (selectedID === anchorID) return;
onMove(anchorID, selectedID);
}}
onDragLeave={() => {
setActive(false);
}}
>
<hr
style={{
height: "3px",
flex: "1",
opacity: `${active && selectedID !== anchorID ? 1 : 0}`,
backgroundColor: "green",
}}
/>
</div>
);
}
- DropItem
- 拖拽开始的时候, 在DragStart事件设置SelectedID, 拖拽结束后取消设置
import React, { ReactNode } from "react";
import type { BaseItem } from "./Draggrable";
interface DragItemProps<T extends BaseItem> {
item: T;
selectedID: string;
setSelectedID: (id: string) => void;
children: ReactNode;
}
export default function DragItem<T extends BaseItem>({
selectedID,
setSelectedID,
item,
children,
}: DragItemProps<T>) {
return (
<div
key={item.id}
draggable
onDragStart={() => {
setSelectedID(item.id);
}}
onDragEnd={() => {
setSelectedID("");
}}
style={{
height: "2rem",
color: `${selectedID === item.id ? "red" : "black"}`,
}}
>
{children}
</div>
);
}
总结
功能是实现了, 但是感觉还是很粗糙, 拖动过程的反馈还是太差了。