使用React DnD实现拖拽并根据拖拽位置调整顺序

472 阅读3分钟

前言

最近打算研究使用React DnD来实现不同容器的tag拖拽到一个共同的tag,如下图的左侧容器的tag拖拽到右侧容器的tag,并插入到当前拖拽到的顺序:

image.png

Hooks API

主要用到useDrag、useDrop;具体可参考文档:react-dnd.github.io/react-dnd/d…

1、useDrag

useDrag:提供了一种将组件作为拖动源连接到 DnD 系统的方法;

2、useDrop

useDrop:提供了一种将组件作为放置目标连接到 DnD 系统的方法。

代码实现

1、index.tsx

import React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Left from './Left';
import Right from './Right';
import './index.less';
const Index = () => {
  return (
    <DndProvider backend={HTML5Backend}>
      <div className={'content'}>
        <Left/>
        <Right/>
      </div>
    </DndProvider>
  );
};
export default Index;

2、Left.tsx

import React, { useRef, useState } from 'react';
import { Tag } from 'antd';
import { useDrag } from 'react-dnd';

const Left = () => {
  // 示例数据
  const dataSource = [
    {
      key: 1,
      title: '苹果',
    },
    {
      key: 2,
      title: '梨',
    },
    {
      key: 3,
      title: '芒果',
    },
    {
      key: 4,
      title: '波罗蜜',
    },
    {
      key: 5,
      title: '荔枝',
    },
    {
      key: 6,
      title: '龙眼',
    },
    {
      key: 7,
      title: '香蕉',
    },
    {
      key: 8,
      title: '榴莲',
    },
  ];
  const TagItem = ({ tag, draggable }: any) => {
    //实现拖拽
    const [, drag] = useDrag({
      type: 'LeftTag',
      item: (): API.FormulaItem => {
        // 使用时间戳来定义id值,避免移动后的数据的id值相同
        return { ...tag, id: new Date().getTime() };
      },
      collect: (monitor: any) => ({
        isDragging: monitor.isDragging(),
      }),
      canDrag: () => {
        return draggable;
      },
    });
    return (
      <Tag
        ref={drag}
        className={'formula-left-constant-tag-item'}
      >
        {tag.title}
      </Tag>
    );
  };
  return(
    <div className={'left'}>
      {
        dataSource?.map(item=><TagItem key={item.key} tag={item} draggable="true" />)
      }
    </div>
  )
}

export default Left;

3、Right.tsx

import React, { useRef, useState } from 'react';
import { Tag } from 'antd';
import {DragSourceMonitor, useDrag, useDrop} from 'react-dnd';

type ListItem = {
  key: number;
  title: string;
  id: number;
}

const Right = () => {
  const ref = useRef<HTMLDivElement>(null);
  const [dataList, setDataList] = useState<ListItem[]>([]);
  const [, dropRef] = useDrop({
    // 指明该区域允许接收的拖放物。可以是单个,也可以是数组里面的值就是useDrag所定义的type
    accept: ['LeftTag'],
    drop: (item: API.FormulaItem) => {
      if (item) {
        setDataList((data: ListItem[])=>{
          // 判断现有的数据源是否有id值相同的数据,若有则不做处理,若无则把数据插入到数组尾部
          const hasIndex = data.findIndex((v: ListItem) => v.id === item?.id);
          if(hasIndex === -1){
            return [...data, item] as ListItem[];
          }
          return [...data] as ListItem[];
        })
      }
      return item;
    },
  });
  const moveTag = (dragIndex: number, hoverIndex: number, item: ListItem) => {
    /**
     * 1、如果dragIndex 为 undefined,则此时为新增的元素,则此时修改 dataList 中的占位元素的位置即可
     * 2、如果此时dragIndex 不为 undefined,则此时为拖拽现有的元素,此时替换 dragIndex 和 hoverIndex 位置的元素即可
     */
    if (dragIndex !== undefined) {
      // 拖动元素
      let data = [...dataList];
      let temp = data[dragIndex];
      data.splice(dragIndex, 1);
      data.splice(hoverIndex, 0, temp);
      setDataList(data);
    } else {
      // 新增元素,如果是函数需要额外加上参数
      let data = [...dataList];
      data.splice(hoverIndex, 0, item);
      setDataList(data);
    }
  };
  const TagItem = ({ tag, index }: any) => {
    //实现拖拽
    const [, drag] = useDrag({
      type: 'RightTag',
      collect: (monitor: DragSourceMonitor) => ({
        isDragging: monitor.isDragging(),
      }),
      // item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
      item: { ...tag, index },
    });
    const [, drop] = useDrop({
      // 指明该区域允许接收的拖放物。可以是单个,也可以是数组里面的值就是useDrag所定义的type
      accept: ['LeftTag', 'RightTag'],
      drop: (item: any) => {
        if (!ref.current) return;
        let dragIndex = item.index;
        let hoverIndex = index;
        // 拖拽元素下标与鼠标悬浮元素下标一致时,不进行操作
        if (dragIndex === hoverIndex) {
          return;
        }
        // 执行 move 回调函数
        moveTag(dragIndex, hoverIndex, item);
        item.index = hoverIndex;
      },
    });
    drag(drop(ref));
    return (
      <Tag
        ref={ref}
      >
        {tag.title}
      </Tag>
    );
  };
  return (
    <div className={'right'} ref={dropRef}>
      {
        dataList?.map((item, index)=><TagItem key={item.key} tag={item} index={index} />)
      }
    </div>
  )
}

export default Right;

4、index.less

.content{
  display: flex;
  justify-content: space-between;
}
.left{
  width: 45%;
  background-color: #ffffff;
  border: 1px dashed #ddd;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px;
}
.right{
  width: 45%;
  background-color: #ffffff;
  border: 1px dashed #ddd;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px;
}

至此一个简单的tag拖动示例就实现了,既能拖动tag到容器里面又能改变tag的顺序。