React DnD列表拖拽排序

概述

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

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

主要实现以下功能:

  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" }

];

functionApp() {

const [list, setList] = useState(defaultList);

const [activeItem, setActiveItem] = useState(list[0]);

const onClick = item => {

if (item.id !== activeItem.id) {

setActiveItem(item);

}

};

return<Listlist={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-touch-backend。

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" }

];

functionApp() {

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 (

<divclassName="list-wrap">

<List

list={list}

activeItem={activeItem}

onDropEnd={onDropEnd}

onDelete={onDelete}

onClick={onClick}

/>

</div>

);

}

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

以上是 React DnD列表拖拽排序 的全部内容, 来源链接: utcz.com/a/115920.html

回到顶部