基于react-dnd简单实现拖拽、排序
基于react-dnd简单实现拖拽、排序
·
前言
react-dnd官网地址:https://react-dnd.github.io/react-dnd/about
一、react-dnd 安装
npm install react-dnd react-dnd-html5-backend
二、使用步骤
1. 引入DndProvider
代码如下(示例):
DndProvider 组件为您的应用程序提供 React-DnD 功能。通过backend props 注入
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Home from '@/pages/Home';
export default function BaseLayout() {
return (
<DndProvider backend={ HTML5Backend }>
<Home />
</DndProvider>
)
}
2. 拖拽,排序
如下图所示:
2.1. 截图
2.2. 代码
1、拖拽的数据都保存在targetBox中,没有数据时,targetBox设置默认可拖拽的区域,同时像子组件传入增删改、移动的方法。
2、通过ref={ drop },使当前元素具备可放置功能
3、useDrop提供的drop方法,该方法主要在模块拖动放置在当前目标上时,会触发
4、当拖动左侧模块到targetBox容器中,触发hover,不存在id,此时显示上下可放置区域,放置时触发drop方法,根据落点新增数据,并且给该数据加上唯一id字段。
5、targetBox中,拖动元素,触发hover 存在id时,移动当前模块,
代码如下(示例):
// index.tsx
import React, { memo, useCallback, useContext, useState, useEffect } from 'react'
import type { FC, Dispatch } from 'react'
import { useDrop } from 'react-dnd'
import update from 'immutability-helper'
import { v4 as uuidv4 } from 'uuid'
import Card from './Card'
import { base, cardBase } from './type'
import styles from './index.scss'
interface TargetBoxProps {
accept: string
className?: string,
dispatch: Dispatch<any>
}
const TargetBox: FC<TargetBoxProps> = memo((props) => {
const {
dispatch,
className,
accept
} = props
const [cards, setCards] = useState<cardBase[]>([])
// 根据当前id查询,返回card对象,及位置
const findCard = useCallback(
(id: number) => {
const card = cards.length > 0 ? cards.filter((c) => c.id === id)[0] as base : { } as never
return {
card,
index: cards.indexOf(card),
}
},
[cards],
)
// 移动当前模块
const moveCard = useCallback(
(id: number, atIndex: number) => {
const { card, index } = findCard(id)
setCards(
update(cards, {
$splice: [
[index, 1],
[atIndex, 0, card],
],
}),
)
},
[findCard, cards, setCards],
)
// 在当前位置插入模块
const insertCard = useCallback(
(atIndex: number, element:cardBase) => {
setCards(
update(cards, {
$splice: [
[atIndex, 0, element],
],
}),
)
},
[cards, setCards],
)
// 删除当前模块
const deleteCard = useCallback(
(atIndex: number) => {
setCards(
update(cards, {
$splice: [
[atIndex, 1],
],
}),
)
},
[cards, setCards],
)
const onDrop = useCallback((item,monitor)=> {
const { id: currentId } = item
if(currentId) return
let newItem = { ...item, id:uuidv4() }
insertCard(0, newItem)
}, [])
// useDrop react-dnd api
const [{ isOver, canDrop }, drop] = useDrop({
accept,
drop: onDrop,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
})
const isActive = isOver && canDrop
let backgroundColor = ''
if (isActive) {
backgroundColor = '#F7EDEA'
}
return (
<div className={ styles.realBox }>
{ cards.length === 0 && <div ref={ drop } style={{ background:backgroundColor }} className={ styles.tzTag }>拖拽到此处</div> }
{ cards.map((item, index) => (
<Card
key={ item.id }
card={ item }
len={ cards.length }
moveCard={moveCard}
findCard={findCard}
insertCard={ insertCard }
deleteCard={ deleteCard }
setProperty={ setProperty }
/>
)) }
</div>
)
})
export default connect()(TargetBox)
1、通过ref={(node) => drag(drop(node))},使当前元素具备可拖拽可放置功能
2、useDrop api 提供的方法中,drop主要在模块拖动放置在当前目标上时,会触发,useDrop 提供的方法中,hover主要在模块悬停时会触发。
3、useDrag api 提供的方法中,end主要在拖动结束时触发。
代码如下(示例):
// card.tsx
import type { Dispatch, FC, ReactElement } from 'react'
import React, { memo, useEffect, useRef, useState, useContext } from 'react'
import { useDrag, useDrop } from 'react-dnd'
import { connect } from 'dva'
import { ItemTypes } from '@/utils/ItemTypes'
import { v4 as uuidv4 } from 'uuid'
import { cardBase, base, stringAndnull, element } from './type'
import styles from './index.scss'
import _ from 'lodash'
interface modelProps {
currentCard:base,
}
interface CardProps extends modelProps {
dispatch:Dispatch<any>;
card:base
len:number;
moveCard: (id: number, to: number) => void
findCard: (id: number) => { index: number, card:cardBase }
insertCard: (to: number, element: cardBase) => void
deleteCard: (to: number ) => void
setProperty: (data: base) => void
}
interface OperationTagProps {
delClick: () => void
originalIndex:number;
}
const Card: FC<CardProps> = memo(({
dispatch,
card,
len,
moveCard,
findCard,
insertCard,
deleteCard,
setProperty,
currentCard,
}) =>{
const { id, name, componentName, data, formId } = card
let nodeRef = useRef<HTMLDivElement | null>(null)
let positionRef = useRef<stringAndnull>(null)
const originalIndex = findCard(id).index
const [{ isDragging }, drag, dragPreview] = useDrag(
() => ({
type: ItemTypes.Box,
item: { id, originalIndex },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
// 拖动结束触发
end: (item, monitor) => {
const { id: droppedId, originalIndex } = item
const didDrop = monitor.didDrop()
if (!didDrop) {
moveCard(droppedId, originalIndex)
return
}
},
}),
[id, originalIndex, moveCard],
)
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
accept: ItemTypes.Box,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
// 模块悬停时触发
hover:(item: cardBase, monitor) =>{
const { id: draggedId } = item
// 不存在id,说明不是当前targetBox中的数据
if(!draggedId) {
const hoverBoundingRect = nodeRef?.current?.getBoundingClientRect() as DOMRect;
const hoverMiddleY = (hoverBoundingRect?.bottom - hoverBoundingRect?.top) / 2;
const clientOffset = monitor.getClientOffset();
if (clientOffset) {
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (hoverClientY <= hoverMiddleY) {
positionRef.current = 'top'
}
if (hoverClientY > hoverMiddleY) {
positionRef.current = 'bottom'
}
}
return
}
// 存在id,id不同时,移动
if (draggedId !== id) {
const { index: overIndex } = findCard(id)
moveCard(draggedId, overIndex)
}
},
// 模块掉落时触发
drop:(item: cardBase, monitor)=> {
const { id: currentId } = item
if(currentId) return
let { index: overIndex } = findCard(id)
if(positionRef.current === 'bottom') overIndex += 1
const newItem = { ...item, id: uuidv4() }
insertCard(overIndex, newItem)
positionRef.current = null
},
}),
[findCard, moveCard],
)
const beforeSetProperty = () => {
setProperty(card)
}
const ondelClick = () => {
deleteCard(originalIndex)
}
const opacity = isDragging ? 0.4 : 1
const borderStyle = (currentCard && currentCard.id === id) ? styles.borderCard : ''
return (
<div className={ styles.dragContainer }>
<div className={ styles.dragNode } onClick={ beforeSetProperty } ref={ nodeRef as any }>
{ (isOver && canDrop && positionRef.current === 'top') && <div className={ styles.blankTopCard }>拖拽到此处</div> }
<div className={`${ styles.cardStyle } ${ borderStyle }`} style={{ opacity }} ref={ dragPreview }>
{ name }
</div>
{ (isOver && canDrop && positionRef.current === 'bottom' && originalIndex !== len-1 ) && <div className={ styles.blankBottomCard }>拖拽到此处</div> }
{ (dragging && canDrop && originalIndex === len-1 && positionRef.current !== 'top') && <div className={ styles.blankBottomCard }>拖拽到此处</div> }
</div>
</div>
)
})
export default connect()(Card)
三、最终实现效果
最终实现效果如下图所示:
提示:拖拽后的渲染、输入同步展示等需配合其他功能实现
更多推荐
已为社区贡献3条内容
所有评论(0)