antd table 基于 react-dnd 实现拖拽无限滚动的排序

需求场景

antdtable 表单数据中,需要对行进行拖拽排序,并且需要支持拖拽期间的加载数据和无限滚动。

antd table 基于 react-dnd 实现拖拽无限滚动的排序

在线体验

完整代码,直接放 码上掘金 了,有需要可以在里面自提。

实现方式

代码结构简述

  1. 首先是要对 Table 进行数据的渲染,这步没什么好说的,相信大伙都会,不会的看 文档 cv大法就好。

  2. 接着就是对 Table 的内容实现往下滚动加载更多的需求。主要是基于 react-infinite-scroll-component 实现的,可以直接参考 Antd 滚动加载的 Demo

此时 html 结构大致如下.

<div id={id}>
  <InfiniteScroll
    scrollableTarget={id}
    // ...
  >
    <Table
      // ...
    />
  </InfiniteScroll>
</div>
  1. 根据 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>
  1. 最后根据封装的 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 组件主要是实现了行的拖拽功能。通过 useDraguseDrog 两个钩子,实现了整行元素(即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-dnduseDragLayer 获取当前的拖拽信息和状态进而进行处理。

其他地方都没什么好说的,主要看 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 作者:滑动变滚动的蜗牛

(0)
上一篇 2024年3月19日 上午10:33
下一篇 2024年3月19日 上午10:43

相关推荐

发表回复

登录后才能评论