美文网首页
React Dnd 实现横向和纵向拖拽排序

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

作者: VioletJack | 来源:发表于2020-07-06 18:55 被阅读0次

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

    相关文章

      网友评论

          本文标题:React Dnd 实现横向和纵向拖拽排序

          本文链接:https://www.haomeiwen.com/subject/esxgqktx.html