接昨天说到的 React Dnd 基本拖放功能实现及 API 整理 我们试着做一个简单的需求,拖拽排序。
原理
用的东西还是 React Dnd,其实要完成 DOM 元素的排序,其实就是操作数组的排序。所以定一个数组,并且在拖拽的时候修改数组内容的位置,再由 React 渲染出来就可以了。
所以重点就在如何监听拖拽元素拖拽的位置和放置的位置。这个就要提到上一篇将 API 时提到的 useDrag 的 end(item, monitor)
和 useDrop 的 hover(item, monitor)
了。
可能有人会有疑问,为什么拖拽元素还会使用 useDrop?因为其实元素所在位置是有拖拽和放置两个行为的(并非放在容器上)。
横向排序
import React, { useState, useCallback, useRef } from 'react'
import styles from './index.less'
import {
useDrop,
useDrag,
DndProvider,
DropTargetMonitor,
XYCoord,
} from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import update from 'immutability-helper'
const ItemTypes = {
CARD: 'card',
CONTAINER: 'container',
}
interface CardProps {
id: any
text: string
index: number
moveCard: (dragIndex: number, hoverIndex: number) => void
}
interface DragItem {
index: number
id: string
type: string
}
const Card = ({ id, text, index, moveCard }: CardProps) => {
const ref = useRef<HTMLDivElement>(null)
const [, drop] = useDrop({
accept: ItemTypes.CARD,
hover (item: DragItem, monitor: DropTargetMonitor) {
if (!ref.current) {
return
}
const dragIndex = item.index
const hoverIndex = index
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect()
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.right - hoverBoundingRect.left) / 2
// Determine mouse position
const clientOffset = monitor.getClientOffset()
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).x - hoverBoundingRect.left
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return
}
// Time to actually perform the action
moveCard(dragIndex, hoverIndex)
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex
},
})
const [{ isDragging }, drag] = useDrag({
item: { type: ItemTypes.CARD, id, index },
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
})
const opacity = isDragging ? 0 : 1
drag(drop(ref))
return (
<div ref={ref} className={styles.card} style={{ opacity }}>
{text}
</div>
)
}
const Container = () => {
const [, drag] = useDrag({
item: {
type: ItemTypes.CONTAINER,
},
})
const [cards, setCards] = useState([
{
id: 1,
text: 'Write a cool JS library',
},
{
id: 2,
text: 'Make it generic enough',
},
{
id: 3,
text: 'Write README',
},
{
id: 4,
text: 'Create some examples',
},
{
id: 5,
text:
'Spam in Twitter and IRC to promote it (note that this element is taller than the others)',
},
{
id: 6,
text: '???',
},
{
id: 7,
text: 'ABC',
},
{
id: 8,
text: 'DDD',
},
{
id: 9,
text: 'AAA',
},
{
id: 10,
text: 'SSS',
},
{
id: 11,
text: 'VVV',
},
])
const moveCard = useCallback(
(dragIndex: number, hoverIndex: number) => {
const dragCard = cards[dragIndex]
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
}),
)
},
[cards],
)
const renderCard = (card: { id: number; text: string }, index: number) => {
return (
<Card
key={card.id}
index={index}
id={card.id}
text={card.text}
moveCard={moveCard}
/>
)
}
return (
<>
<div ref={drag} className={styles.container}>
{cards.map((card, i) => renderCard(card, i))}
</div>
</>
)
}
export default () => {
return (
<DndProvider backend={HTML5Backend}>
<div className={styles.app}>
<Container />
</div>
</DndProvider>
)
}
.app {
background: #ffffff;
}
.container {
display: block;
border: #cccccc solid 1px;
padding: 10px;
height: 220px;
overflow-x: scroll;
white-space: nowrap;
}
.card {
display: inline-block;
vertical-align: middle;
height: 200px;
width: 200px;
border: 1px dashed gray;
padding: 5px;
margin-right: 5px;
background-color: #ffffff;
white-space: normal;
cursor: move;
}
其实这个还有个 bug,就是横向拖拽到容易边缘的时候最好能够滑动滚动条。这个后续优化。
纵向排序
import React, { useState, useCallback, useRef } from 'react'
import styles from './index.less'
import {
useDrop,
useDrag,
DndProvider,
DropTargetMonitor,
XYCoord,
DragSourceMonitor,
} from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import update from 'immutability-helper'
const ItemTypes = {
CARD: 'card',
CONTAINER: 'container',
}
interface CardProps {
id: any
text: string
index: number
moveCard: (dragIndex: number, hoverIndex: number) => void
}
interface DragItem {
index: number
id: string
type: string
}
const Card = ({ id, text, index, moveCard }: CardProps) => {
const ref = useRef<HTMLDivElement>(null)
const [, drop] = useDrop({
accept: ItemTypes.CARD,
hover (item: DragItem, monitor: DropTargetMonitor) {
if (!ref.current) {
return
}
const dragIndex = item.index
const hoverIndex = index
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect()
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
// Determine mouse position
const clientOffset = monitor.getClientOffset()
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return
}
// Time to actually perform the action
moveCard(dragIndex, hoverIndex)
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex
},
})
const [{ isDragging }, drag] = useDrag({
item: { type: ItemTypes.CARD, id, index },
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
})
const opacity = isDragging ? 0 : 1
drag(drop(ref))
return (
<div ref={ref} className={styles.card} style={{ opacity }}>
{text}
</div>
)
}
const Container = () => {
const [, drag] = useDrag({
item: {
type: ItemTypes.CONTAINER,
},
})
const [cards, setCards] = useState([
{
id: 1,
text: 'Write a cool JS library',
},
{
id: 2,
text: 'Make it generic enough',
},
{
id: 3,
text: 'Write README',
},
{
id: 4,
text: 'Create some examples',
},
{
id: 5,
text:
'Spam in Twitter and IRC to promote it (note that this element is taller than the others)',
},
{
id: 6,
text: '???',
},
{
id: 7,
text: 'PROFIT',
},
])
const moveCard = useCallback(
(dragIndex: number, hoverIndex: number) => {
const dragCard = cards[dragIndex]
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
}),
)
},
[cards],
)
const renderCard = (card: { id: number; text: string }, index: number) => {
return (
<Card
key={card.id}
index={index}
id={card.id}
text={card.text}
moveCard={moveCard}
/>
)
}
return (
<>
<div ref={drag} className={styles.container}>
{cards.map((card, i) => renderCard(card, i))}
</div>
</>
)
}
export default () => {
return (
<DndProvider backend={HTML5Backend}>
<div className={styles.app}>
<Container />
</div>
</DndProvider>
)
}
.app {
background: #ffffff;
}
.container {
display: inline-block;
border: #cccccc solid 1px;
width: 400px;
padding: 10px;
}
.card {
border: 1px dashed gray;
padding: 0.5rem 1rem;
margin-bottom: 0.5rem;
background-color: #ffffff;
cursor: move;
}
如何在拖拽到边缘的时候进行滑动操作?
参照 react-trello-board 可以发现它用的就是父级元素的 scrollLeft 和 scrollRight 属性。
scrollRight() {
function scroll() {
document.getElementsByTagName('main')[0].scrollLeft += 10;
}
this.scrollInterval = setInterval(scroll, 10);
}
scrollLeft() {
function scroll() {
document.getElementsByTagName('main')[0].scrollLeft -= 10;
}
this.scrollInterval = setInterval(scroll, 10);
}
结合 useDrop 的 hover 事件来实现具体的行为。
最后
如此一来,横向和纵向的拖拽排序都解决了,明天可以来实现一个类似于 teambition 的看板功能了!
网友评论