需求场景
在 antd
的 table
表单数据中,需要对行进行拖拽排序,并且需要支持拖拽期间的加载数据和无限滚动。
在线体验
完整代码,直接放 码上掘金 了,有需要可以在里面自提。
实现方式
代码结构简述
-
首先是要对
Table
进行数据的渲染,这步没什么好说的,相信大伙都会,不会的看 文档 cv大法就好。 -
接着就是对
Table
的内容实现往下滚动加载更多的需求。主要是基于 react-infinite-scroll-component 实现的,可以直接参考 Antd 滚动加载的 Demo。
此时 html
结构大致如下.
<div id={id}>
<InfiniteScroll
scrollableTarget={id}
// ...
>
<Table
// ...
/>
</InfiniteScroll>
</div>
- 根据 react-dnd 实现
table
各行的拖拽排序功能,需要封装一个行的拖拽组件DragRow
(具体使用和封装见下文),其他组件都是引入第三方库的,此时html
结构大致如下:
<div id={id}>
<InfiniteScroll
scrollableTarget={id}
// ...
>
<DndProvider backend={HTML5Backend}>
<Table
components={{
body: {
// 这个就是要封装的行拖拽组件
row: DragRow,
},
}}
onRow={(_, index) => {
const attr: DragRowProps = {
index: index!,
moveRow,
};
return attr;
}}
// ...
/>
</DndProvider>
</InfiniteScroll>
</div>
- 最后根据封装的
DragScrollLayer
实现拖拽期间的自动滚动效果即可。此时html
结构大致如下:
<div ref={scrollWrapRef} id={id}>
<InfiniteScroll
scrollableTarget={id}
// ...
>
<DndProvider backend={HTML5Backend}>
<DragScrollLayer scrollWrapRef={scrollWrapRef}>
<Table
components={{
body: {
// 这个就是要封装的行拖拽组件
row: DragRow,
},
}}
onRow={(_, index) => {
const attr: DragRowProps = {
index: index!,
moveRow,
};
return attr;
}}
// ...
/>
</DragScrollLayer>
</DndProvider>
</InfiniteScroll>
</div>
react-dnd 和 DragRow 组件搭配实现拖拽
react-dnd
的简单使用方式是:首先使用 DndProvider
包裹内部组件,跟 useContext
的用法是类似的,目的就是让内部组件可以访问 react-dnd
的属性和方法。 当移动位置后,触发 moveRow
的回调,修改一下数组中的对应位置即可。
简单使用代码如下:
import { Table } from "antd";
import { useCallback, useState } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
function DndDemo() {
const [data, setData] = useState([
{ 'id': '1', 'name': 'User-1'},
{ 'id': '2', 'name': 'User-2'},
{ 'id': '3', 'name': 'User-3'},
]);
const moveRow = useCallback((dragIndex: number, targetIndex: number) => {
const dragRow = data[dragIndex];
setData((d) => {
const newData = [...d];
newData.splice(dragIndex, 1);
newData.splice(targetIndex, 0, dragRow);
return newData;
})
}, [data])
return (
<DndProvider backend={HTML5Backend}>
<Table
columns={[{
title: 'Name',
dataIndex: 'name',
key: 'name',
}]}
dataSource={data}
rowKey="id"
pagination={false}
components={{
body: {
row: DragRow,
},
}}
onRow={(_, index) => {
const attr: DragRowProps = {
index: index!,
moveRow,
};
return attr;
}}
/>
</DndProvider>
)
}
DragRow
组件主要是实现了行的拖拽功能。通过 useDrag
和 useDrog
两个钩子,实现了整行元素(即tr
)的拖拽和释放的功能。
组件代码如下:
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
const type = 'DragRow';
interface DragRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
index: number;
moveRow: (dragIndex: number, hoverIndex: number) => void;
disableDrop?: boolean;
}
const DragRow = ({
index,
moveRow,
className,
style,
disableDrop,
...restProps
}: DragRowProps) => {
const ref = useRef<HTMLTableRowElement>(null);
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = monitor.getItem() || {};
if (dragIndex !== index) {
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
}
return {};
},
drop: (item: { index: number }) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const canDrop = !disableDrop;
if (canDrop) {
drop(drag(ref));
}
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: canDrop ? 'move' : 'auto', ...style }}
{...restProps}
/>
);
};
DragScrollLayer 拖拽并滚动
要实现拖拽期间支持滚动,则需要通过 react-dnd
的 useDragLayer
获取当前的拖拽信息和状态进而进行处理。
其他地方都没什么好说的,主要看 onScroll
函数拖拽的时候判断当前元素的 offsetTop
值是否超过,外部盒子(table
)的 offsetTop
即可,然后根据拖拽的位置改变滚动盒子的 scrollTop
值就可实现滚动效果。
这里为了滚动得更加丝滑可以给滚动盒子的 css
加上 scroll-behavior: smooth;
最终源码如下:
import { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useDragLayer, XYCoord } from 'react-dnd';
/** 获取 offsetTop 值,因为 layout 使用了 overflow 所以需要冒泡获取 */
export function getOffsetTop(element: HTMLElement | null) {
let offsetTop = 0;
while (element) {
offsetTop += element.offsetTop;
element = element.offsetParent as HTMLElement;
}
return offsetTop;
}
export type DragScrollLayerParams = {
isDragging: boolean;
initialOffset: XYCoord | null;
currentOffset: XYCoord | null;
};
export type PropsType = {
children: ReactNode;
/** 需要滚动的盒子 */
scrollWrapRef?: React.RefObject<HTMLDivElement>;
onDrag?: (p: DragScrollLayerParams) => void;
};
/**
* 支持 dnd 拖拽并滚动
* 主要用于无限滚动的 table 列表的拖拽排序。
*/
export default ({ children, scrollWrapRef, onDrag }: PropsType) => {
const { isDragging, initialOffset, currentOffset } = useDragLayer(
(monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}),
);
const offsetTopRef = useRef(0);
useEffect(() => {
if (scrollWrapRef?.current) {
offsetTopRef.current = getOffsetTop(scrollWrapRef.current);
}
}, []);
const onScroll = useCallback(() => {
if (!isDragging || !currentOffset) {
return;
}
if (scrollWrapRef?.current) {
const y = currentOffset!.y;
const offsetTop = offsetTopRef.current;
const offsetY = y - offsetTop;
if (offsetY < 0) {
const scrollTop = offsetY > 50 ? 60 : 30;
scrollWrapRef.current.scrollTop -= scrollTop;
} else if (offsetY > scrollWrapRef.current.clientHeight) {
const scrollTop =
offsetY + scrollWrapRef.current.clientHeight > 50 ? 60 : 30;
scrollWrapRef.current.scrollTop += scrollTop;
}
}
onDrag?.({ isDragging, initialOffset, currentOffset });
}, [isDragging, initialOffset, currentOffset]);
useEffect(() => {
if (isDragging) {
const interval = setInterval(onScroll, 100);
return () => clearInterval(interval);
}
}, [isDragging, onScroll]);
return <>{children}</>;
};
end…
原文链接:https://juejin.cn/post/7347668434447302666 作者:滑动变滚动的蜗牛