第三方库
1) React dnd
样例有点复杂 不维护
2) React-beautiful-dnd
不维护了,不支持React18严格模式
3) Sortable.js
不依赖于React和Vue
不建议用于生产环境
4) React-sortable-hoc
不再维护
5) dnd kit
安装: npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities --save
基本构造:
根据官网示例,
有两个关键的文件: SortableContainer.tsx, SortableItem
1) SortableContainer.tsx
import React, { FC, useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import DndItem from "./DndItem";
type ComponentType = {
fe_id: string;
title: string;
}
const Dnd: FC = () => {
const [items, setItems] = useState<ComponentType[]>([{ fe_id: "i1", title: "t1" }, { fe_id: "i2", title: "t2" }, { fe_id: "i3", title: "t3" },]);
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}))
// when dragging, the start position and end position
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over == null) return;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex(component => component.fe_id === active.id);
const newIndex = items.findIndex(component => component.fe_id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
const itemsWithId = items.map(component => {
return {
...component,
id: component.fe_id
}
});
return <DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={itemsWithId}
strategy={verticalListSortingStrategy}
>
{items.map(component => <DndItem key={component.fe_id} id={component.fe_id} title={component.title}/>)}
</SortableContext>
</DndContext>;
}
export default Dnd;
2) SortableItem.tsx
import React, { FC } from "react";
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
type PropsType = {
id: string;
title: string;
}
const DndItem: FC<PropsType> = (props: PropsType) => {
const { id, title } = props;
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
border: "1px, solid #ccc",
margin: "10px 0",
background: "#f1f1f1",
};
return <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
Item {id} {title}
</div>;
}
export default DndItem;
使用:
在我的项目中刚开始列表是这样的:
代码如下
{componentList.map((component) => {
const { fe_id, title, isHidden, isLocked } = component;
const itemClassName = classNames({
[styles.title]: true,
[styles.selected]: selectedId === fe_id,
});
return (
<div key={fe_id} className={styles.wrapper}>
<div
className={itemClassName}
onClick={() => {
handleTitleClicked(fe_id);
}}
>
{changingTitleId === fe_id && (
<Input
value={title}
onPressEnter={() => setChangingTitleId("")}
onBlur={() => setChangingTitleId("")}
onChange={handleInputChange}
/>
)}
{changingTitleId !== fe_id && title}
</div>
<div className={styles.handler}>
<Space>
<Button
size="small"
shape="circle"
icon={<EyeInvisibleOutlined />}
type={isHidden ? "primary" : "text"}
className={isHidden ? "" : styles.btn}
onClick={() => {
handleChangeHidden(fe_id, !isHidden);
}}
></Button>
<Button
size="small"
shape="circle"
icon={<LockOutlined />}
type={isLocked ? "primary" : "text"}
className={isLocked ? "" : styles.btn}
onClick={() => {
handleChangeLock(fe_id);
}}
></Button>
</Space>
</div>
</div>
);
})}
发现我的列表有个循环,刚好可以套入到SortableContainer return函数里面的那个{xx.map()}
并且我的项目中有两个地方会用到拖拽排序,一个地方是最左侧的图层,另一个地方是中间的画布,都需要拖拽排序的功能。所以考虑将所有元素都传入,使其可以复用在两个地方:
修改后的SortableContainer.tsx为:
import React, { FC, useState } from "react";
import {
DndContext,
closestCenter,
MouseSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
type PropsType = {
children: JSX.Element | JSX.Element[]; //被SortableContainer包裹起来的元素
items: Array<{ id: string; [key: string]: any }>;
onDragEnd: (oldIndex: number, newIndex: number) => void;
};
const SortableContainer: FC<PropsType> = (props: PropsType) => {
const { children, items, onDragEnd } = props;
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 8, // minimum trigger distance
},
})
);
// when dragging, the start position and end position
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over == null) return;
if (active.id !== over.id) {
const oldIndex = items.findIndex(
(component) => component.fe_id === active.id
);
const newIndex = items.findIndex(
(component) => component.fe_id === over.id
);
onDragEnd(oldIndex, newIndex);
return arrayMove(items, oldIndex, newIndex);
}
}
return (
// Outside container
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{children}
</SortableContext>
</DndContext>
);
};
export default SortableContainer;
可以发现,我的列表的内容就可以直接替代 {children}中的内容
然后修改后的SortableItem.tsx为:
import React, { FC } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
type PropsType = {
id: string;
children: JSX.Element;
};
const SortableItem: FC<PropsType> = ({ id, children }) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</div>
);
};
export default SortableItem;
直接将SortableContainer和SortableItem应用到我的列表中
<SortableContainer items={componentListWithId} onDragEnd={handleDragEnd}>
{componentList.map((component) => {
const { fe_id, title, isHidden, isLocked } = component;
const itemClassName = classNames({
[styles.title]: true,
[styles.selected]: selectedId === fe_id,
});
return (
<SortableItem key={fe_id} id={fe_id}>
<div key={fe_id} className={styles.wrapper}>
<div
className={itemClassName}
onClick={() => {
handleTitleClicked(fe_id);
}}
>
{changingTitleId === fe_id && (
<Input
value={title}
onPressEnter={() => setChangingTitleId("")}
onBlur={() => setChangingTitleId("")}
onChange={handleInputChange}
/>
)}
{changingTitleId !== fe_id && title}
</div>
<div className={styles.handler}>
<Space>
<Button
size="small"
shape="circle"
icon={<EyeInvisibleOutlined />}
type={isHidden ? "primary" : "text"}
className={isHidden ? "" : styles.btn}
onClick={() => {
handleChangeHidden(fe_id, !isHidden);
}}
></Button>
<Button
size="small"
shape="circle"
icon={<LockOutlined />}
type={isLocked ? "primary" : "text"}
className={isLocked ? "" : styles.btn}
onClick={() => {
handleChangeLock(fe_id);
}}
></Button>
</Space>
</div>
</div>
</SortableItem>
);
})}
</SortableContainer>
也可以直接套用到我的图层中:
<SortableContainer items={componentListWithId} onDragEnd={handleDragEnd}>
<div className={styles.canvas}>
{componentList
.filter((c) => !c.isHidden)
.map((component) => {
const { fe_id, isLocked } = component;
const wrapperDefaultClassName = styles["component-wrapper"];
const selectedClassName = styles.selected;
const lockedClassName = styles.locked;
const wrapperClassName = classNames({
[wrapperDefaultClassName]: true,
[selectedClassName]: selectedId === fe_id, // selectedId == fe_id?
[lockedClassName]: isLocked,
});
return (
<SortableItem key={fe_id} id={fe_id}>
<div
className={wrapperClassName}
onClick={(e) => handelClick(e, fe_id)}
>
<div className={styles.component}>
{genComponent(component)}
</div>
</div>
</SortableItem>
);
})}
</div>
</SortableContainer>
Comments
Post a Comment