React Dnd 实现横向和纵向拖拽排序

2020-07-06  本文已影响0人  VioletJack

接昨天说到的 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 的看板功能了!

上一篇下一篇

猜你喜欢

热点阅读