首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用React DnD实现列表拖拽排序

使用React DnD实现列表拖拽排序

作者头像
IMWeb前端团队
发布2019-12-04 16:56:44
8.9K1
发布2019-12-04 16:56:44
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

本文作者:IMWeb howenhuo 原文出处:IMWeb社区 未经同意,禁止转载

概述

项目中需要对列表实现拖拽排序,同时要支持点击选中和删除功能。

主要实现以下功能:

  1. 鼠标hover到【列表项】,显示可【拖动图标】;
  2. 抓取【拖动图标】并拖动,【列表项】跟随鼠标;
  3. 拖动过程【其他列表项】自行挪动;
  4. 拖动到目标位置,释放鼠标,完成排序;

由于项目使用 React,因此用到 React DnD 来实现。

React DnD 是一组 React 高阶组件,使用的时候只需要将对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。

可以在 codesandbox 查看 React DnD 例子的源码,包含ES6、ES7的实现。

实现详解

实现列表

components/List.js

import React, { useState } from "react";
import { faTrashAlt, faArrowsAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classnames from "classnames";

function Item(props) {
  const { item, ...restProps } = props;
  return (
    <div {...restProps}>
      <p className="title">{item.title || "标题"}</p>
      <ul className="oper-list">
        <li className="oper-item icon-move"><FontAwesomeIcon icon={faArrowsAlt} /></li>
        <li className="oper-item"><FontAwesomeIcon icon={faTrashAlt} /></li>
      </ul>
    </div>
  );
}

function List(props) {
  let { list: propsList, activeItem } = props;
  propsList = propsList.map(item => {
    const isActive = activeItem.id === item.id;
    item = isActive ? activeItem : item;
    item.active = isActive;
    return item;
  });
  const [list, setList] = useState(propsList);
  const find = id => {
    const item = list.find(c => `${c.id}` === id);
    return {
      item,
      index: list.indexOf(item)
    };
  };
  const onClick = event => {
    const { id } = event.currentTarget;
    const { item } = find(id);
    props.onClick(item);
  };

  return (
    <ul className="list">
      {list.map((item, index) => (
        <li className={classnames("item", { active: item.active })} key={item.id}>
          <div className="index">{index + 1}</div>
          <Item
            className="info"
            id={`${item.id}`}
            item={item}
            onClick={onClick}
          />
        </li>
      ))}
    </ul>
  );
}

export default List;

App.js

import React, { useState } from "react";
import ReactDOM from "react-dom";
import List from "./components/List";
import "./styles.scss";

const defaultList = [
  { id: 1, title: "item1" },
  { id: 2, title: "item2" },
  { id: 3, title: "item3" },
  { id: 4, title: "item4" },
  { id: 5, title: "item5" }
];

function App() {
  const [list, setList] = useState(defaultList);
  const [activeItem, setActiveItem] = useState(list[0]);
  const onClick = item => {
    if (item.id !== activeItem.id) {
      setActiveItem(item);
    }
  };
  return <List list={list} activeItem={activeItem} onClick={onClick} />;
}

ReactDOM.render(<App />, document.getElementById("root"));

首先简单的实现一个列表,hover 列表项显示操作按钮,点击列表项可以选中。

安装 React DnD

# Using npm
npm i -s react-dnd react-dnd-html5-backend

# Using yarn
yarn add react-dnd react-dnd-html5-backend

这里 react-dnd-html5-backend 是使用 HTML5 的拖放API。也可以选择其他第三方库。

React DnD 核心 API

  • DragSource:用于包装需要拖动的组件,使组件能够被拖拽(make it draggable)。
  • DropTarget:用于包装接收拖拽元素的组件,使组件能够放置(dropped on it)。
  • DragDropContex:用于包装拖拽根组件,DragSourceDropTarget 都需要包裹在 DragDropContex 内。

详细用法请参考 React DnD 文档react-dnd 用法详解

实现列表拖拽排序

components/DndList.js

import React, { useState } from "react";
import { DragSource, DropTarget, DragDropContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { faTrashAlt, faArrowsAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classnames from "classnames";

function Item(props) {
  const {
    // 这些 props 由 React DnD注入,参考`collect`函数定义
    isDragging, connectDragSource, connectDragPreview, connectDropTarget,
    // 这些是组件收到的 props
    item, style = {}, find, move, change, remove, ...restProps
  } = props;
  const opacity = isDragging ? 0.5 : 1;
  const onRemove = event => {
    event.stopPropagation();
    remove(item);
  }
  return connectDropTarget( // 列表项本身作为 Drop 对象
    connectDragPreview( // 整个列表项作为跟随拖动的影像
      <div {...restProps} style={Object.assign(style, { opacity })}>
        <p className="title">{item.title || "任务标题"}</p>
        <ul className="oper-list">
          {
            connectDragSource(
                <li className="oper-item icon-move">
                  <FontAwesomeIcon icon={faArrowsAlt} />
                </li>
            ) // 拖动图标作为 Drag 对象
          }
          <li className="oper-item" onClick={onRemove}>
            <FontAwesomeIcon icon={faTrashAlt} />
          </li>
        </ul>
      </div>
    )
  );
}

const type = "item";
const dragSpec = {
  // 拖动开始时,返回描述 source 数据。后续通过 monitor.getItem() 获得
  beginDrag: props => ({
    id: props.id,
    originalIndex: props.find(props.id).index
  }),
  // 拖动停止时,处理 source 数据
  endDrag(props, monitor) {
    const { id: droppedId, originalIndex } = monitor.getItem();
    const didDrop = monitor.didDrop();
    // source 是否已经放置在 target
    if (!didDrop) {
      return props.move(droppedId, originalIndex);
    }
    return props.change(droppedId, originalIndex);
  }
};
const dragCollect = (connect, monitor) => ({
  connectDragSource: connect.dragSource(), // 用于包装需要拖动的组件
  connectDragPreview: connect.dragPreview(), // 用于包装需要拖动跟随预览的组件
  isDragging: monitor.isDragging() // 用于判断是否处于拖动状态
});
const dropSpec = {
  canDrop: () => false, // item 不处理 drop
  hover(props, monitor) {
    const { id: draggedId } = monitor.getItem();
    const { id: overId } = props;
    // 如果 source item 与 target item 不同,则交换位置并重新排序
    if (draggedId !== overId) {
      const { index: overIndex } = props.find(overId);
      props.move(draggedId, overIndex);
    }
  }
};
const dropCollect = (connect, monitor) => ({
  connectDropTarget: connect.dropTarget() // 用于包装需接收拖拽的组件
});

const DndItem = DropTarget(type, dropSpec, dropCollect)(
  DragSource(type, dragSpec, dragCollect)(Item)
);

function List(props) {
  let { list: propsList, activeItem, connectDropTarget } = props;
  propsList = propsList.map(item => {
    const isActive = activeItem.id === item.id;
    item = isActive ? activeItem : item;
    item.active = isActive;
    return item;
  });
  const [list, setList] = useState(propsList);
  const find = id => {
    const item = list.find(c => `${c.id}` === id);
    return {
      item,
      index: list.indexOf(item)
    };
  };
  const move = (id, toIndex) => {
    const { item, index } = find(id);
    list.splice(index, 1);
    list.splice(toIndex, 0, item);
    setList([...list]);
  };
  const change = (id, fromIndex) => {
    const { index: toIndex } = find(id);
    props.onDropEnd(list, fromIndex, toIndex);
  };
  const remove = item => {
    const newList = list.filter(it => it.id !== item.id);
    setList(newList);
    props.onDelete(newList);
  };
  const onClick = event => {
    const { id } = event.currentTarget;
    const { item } = find(id);
    props.onClick(item);
  };

  return connectDropTarget(
    <ul className="list">
      {list.map((item, index) => (
        <li
          className={classnames("item", { active: item.active })}
          key={item.id}
        >
          <div className="index">{index + 1}</div>
          <DndItem
            className="info"
            id={`${item.id}`}
            item={item}
            find={find}
            move={move}
            change={change}
            remove={remove}
            onClick={onClick}
          />
        </li>
      ))}
    </ul>
  );
}

const DndList = DropTarget(type, {}, connect => ({
  connectDropTarget: connect.dropTarget()
}))(List);

// 将 HTMLBackend 作为参数传给 DragDropContext
export default DragDropContext(HTML5Backend)(DndList);

App.js

import React, { useState } from "react";
import ReactDOM from "react-dom";
import List from "./components/DndList";

import "./styles.scss";

const defaultList = [
  { id: 1, title: "item1" },
  { id: 2, title: "item2" },
  { id: 3, title: "item3" },
  { id: 4, title: "item4" },
  { id: 5, title: "item5" }
];

function App() {
  const [list, setList] = useState(defaultList);
  const [activeItem, setActiveItem] = useState(list[0]);
  const onDropEnd = (list, fromIndex, toIndex) => {
    setList([...list]);
  };
  const onDelete = list => {
    setList([...list]);
  };
  const onClick = item => {
    if (item.id !== activeItem.id) {
      setActiveItem(item);
    }
  };
  return (
    <div className="list-wrap">
      <List
        list={list}
        activeItem={activeItem}
        onDropEnd={onDropEnd}
        onDelete={onDelete}
        onClick={onClick}
      />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

源码地址

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 实现详解
    • 实现列表
      • 安装 React DnD
        • React DnD 核心 API
          • 实现列表拖拽排序
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档