dnd-kit拖拽库的使用

957 阅读6分钟

安装

npm install @dnd-kit/core

核心概念

DndContext:它类似 react 的 Context 向下传递状态,他必须在拖拽组件的最外层 Droppable: 用于放置可拖拽元素的盒子,使组件能够放置 Draggable: 用于包装可以拖拽元素的盒子,使组件能够拖拽 SortableContext: 创建可以放置拖拽排序元素,使其中的子元素可以拖拽排序

DndContext可以定义拖拽的修饰方式(方向),有以下值: restrictToVerticalAxis: 垂直方向拖拽

restrictToHorizontalAxis:水平方向拖拽

restrictToWindowEdges:限制拖拽到窗口边缘,防止拖拽元素超出窗口

SortableContext可以定义不同的排序策略,有以下值: rectSortingStrategy:默认值,适用于大多数排序,不支持虚拟化列表

rectSwappingStrategy:使用此策略可使用交换功能

verticalListSortingStrategy:此策略针对垂直方向列表进行了优化,并支持虚拟化列表

horizontalListSortingStrategy:此策略针对水平方向列表行了优化,并支持虚拟化列表

useDroppable

参数: id: 该放置区域的唯一id,支持string | number disabled: 该放置区域是否禁用 data: 可用于传递数据,支持任意类型

返回值: active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息 isOver: 是否正在有被拖拽元素覆盖在掉落区域上方 setNodeRef:用于绑定可掉落区域的ref

useDraggable

参数: id: 该拖拽元素的唯一id,支持string | number disabled: 该拖拽元素是否禁用 data: 可用于传递数据,支持任意类型

返回值: attributes:传递给拖拽元素的属性 listeners:拖拽手柄信息 transform:被拖拽元素的位置信息 isDragging:被拖拽元素是否正在被拖拽 setNodeRef:用于绑定被拖拽元素的ref active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息

useSortable

参数: id: 拖拽元素的唯一id,支持string | number disabled: 该拖拽元素是否禁用 data: 可用于传递数据,支持任意类型 transition: 过渡动画,不要动画设为null

返回值: attributes:传递给拖拽元素的属性 listeners:拖拽手柄信息 transform:被拖拽元素的位置信息 isDragging:被拖拽元素是否正在被拖拽 setNodeRef:用于绑定被拖拽元素的ref active:正在拖拽的子元素信息 over: 正在掉落区域(接收被拖拽元素)元素的信息 data: 传递的信息

示例

简单实现小色块拖动至一个容器中的功能,如图所示:

image.png

// 被拖拽的元素
type DraggableItem = {
  id: string
  color: string
}

// 被拖拽的元素
type DroppableProps = {
  id: string
  draggableItems: DraggableItem[]
}

// 接收被拖拽的元素的容器
function Droppable(props: DroppableProps) {
  const { id, draggableItems } = props
  const { isOver, setNodeRef } = useDroppable({
    id,
  })

  return (
    <div
      ref={setNodeRef}
      className={cn('w-full h-[300px] bg-white border-2 border-orange-400', {
        'border-cyan-500': isOver,
      })}
    >
      <SpaceItem align="left" styles={{ flexWrap: 'wrap' }}>
        {draggableItems.map((item, index) => (
          <div
            key={`{${item.color}-${index}`}
            className={cn('w-[50px] h-[50px]')}
            style={{ backgroundColor: item.color }}
          />
        ))}
      </SpaceItem>
    </div>
  )
}

// 被拖拽元素
function Draggable(props: DraggableItem) {
  const { id, color } = props
  const { attributes, listeners, setNodeRef, transform, isDragging } =
    useDraggable({
      id,
      data: props,
    })
  const style = {
    transform: CSS.Translate.toString(transform),
  }

  return (
    <div
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      className={cn(
        `w-[50px] h-[50px]`,
        isDragging ? 'opacity-30 shadow-md' : 'opacity-100',
      )}
      style={{ ...style, backgroundColor: color }}
    />
  )
}

// 拖拽
export default function DndKit() {
  const dragItems = [
    { id: '1', color: 'red' },
    { id: '2', color: 'blue' },
    { id: '3', color: 'green' },
    { id: '4', color: 'yellow' },
    { id: '5', color: 'orange' },
  ] // 色块
  const droppableId = 'droppable' // 可掉落元素id
  const [items, setItems] = useState<DraggableItem[]>([]) // 存储拖拽后的元素

  // 拖拽结束
  const onDragEnd = (event: DragEndEvent) => {
    if (event.over && event.over.id === droppableId) {
      setItems((prevItems) => [
        ...prevItems,
        event.active.data.current as DraggableItem,
      ])
    }
  }

  return (
    <Row gutter={24}>
      <Col span={8}>
        <DndContext onDragEnd={onDragEnd}>
          <Droppable id={droppableId} draggableItems={items} />
          <WhiteSpace gap={20} />
          <SpaceItem align="left">
            {dragItems.map((item) => (
              <Draggable key={item.id} {...item} />
            ))}
          </SpaceItem>
        </DndContext>
      </Col>
    </Row>
  )
}

实现容器内子元素卡片拖拽排序

image.png

// 被拖拽排序的元素
type ListItem = {
  id: number | string
  title: string
  content: string
}

// 用来拖拽排序的子元素
function SortableItem(props: ListItem) {
  const { id, title, content } = props
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id, data: props })

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  }

  return (
    <div
      ref={setNodeRef}
      className={cn('w-full bg-green-300 my-3 p-2 rounded-md', {
        'bg-red-500': isDragging,
      })}
      style={style}
      {...attributes}
      {...listeners}
    >
      <div>{id}</div>
      <div>{title}</div>
      <div>{content}</div>
    </div>
  )
}

// 拖拽
export default function DndKit() {
  const generateDatas = (length = 10, id = '') => {
    return Array.from({ length }, (_, index) => ({
      id: id ? `${id}${index}` : index,
      title: randomString(5),
      content: randomString(12),
    }))
  } // 生成内容
  const [lists, setLists] = useState<ListItem[]>(generateDatas()) // 所有卡片
  const itemss = useMemo(() => lists.map((ele) => ele.id), [lists]) // 所有卡片的id
  
  const onDragEnd1 = (event: DragEndEvent) => {
    const { active, over } = event

    if (active && over && active.id !== over.id) {
      const oldIndex = itemss.indexOf(active.id as number)
      const newIndex = itemss.indexOf(over.id as number)
      setLists((lists) => {
        return arrayMove(lists, oldIndex, newIndex)
      })
    }
  }

  return (
    <Row gutter={24}>
      <Col span={8}>
        <DndContext onDragEnd={onDragEnd1}>
          <div className="border-2 border-red-500 p-2 rounded-lg">
            <SortableContext items={itemss}>
              {lists.map((item) => (
                <SortableItem key={item.id} {...item} />
              ))}
            </SortableContext>
          </div>
        </DndContext>
      </Col>
    </Row>
  )
}

表格的拖拽排序

image.png

// 被拖拽排序的元素
type ListItem = {
  id: number | string
  title: string
  content: string
}

// 用来拖拽排序的TR
function SortableTr(props: any) {
  const { id, ...rests } = props
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id, data: props })

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  }

  return (
    <tr
      {...rests}
      ref={setNodeRef}
      className={cn({ 'bg-pink-400': isDragging })}
      style={style}
      {...attributes}
      {...listeners}
    />
  )
}

// 拖拽
export default function DndKit() {
  const generateDatas = (length = 10, id = '') => {
    return Array.from({ length }, (_, index) => ({
      id: id ? `${id}${index}` : index,
      title: randomString(5),
      content: randomString(12),
    }))
  }

  const [dataSource, setDataSource] = useState<ListItem[]>(generateDatas(15))
  const dataSourceIds = useMemo(
    () => dataSource.map((ele) => ele.id),
    [dataSource],
  )

  const onDragEnd2 = (event: DragEndEvent) => {
    const { active, over } = event

    if (active && over && active.id !== over.id) {
      const oldIndex = dataSourceIds.indexOf(active.id as number)
      const newIndex = dataSourceIds.indexOf(over.id as number)
      setDataSource((lists) => {
        return arrayMove(lists, oldIndex, newIndex)
      })
    }
  }

  return (
    <Row gutter={24}>
      <Col span={8}>
        <DndContext onDragEnd={onDragEnd2}>
          <SortableContext items={dataSourceIds}>
            <Table
              dataSource={dataSource}
              pagination={false}
              columns={[
                { title: 'ID', dataIndex: 'id' },
                { title: '标题', dataIndex: 'title' },
                { title: '内容', dataIndex: 'content' },
              ]}
              rowKey="id"
              components={{
                body: {
                  row: (props: any) => {
                    const rowKey = props['data-row-key']
                    return <SortableTr {...props} id={rowKey} />
                  },
                },
              }}
            />
          </SortableContext>
        </DndContext>
      </Col>
    </Row>
  )
}

大卡片之间互相拖拽排序,大卡片里面的小卡片相互拖拽排序,不同的大卡片里面的小卡片跨大卡片拖拽排序

image.png

// 被拖拽排序的元素
type ListItem = {
  id: number | string
  title: string
  content: string
}

// 大卡片
type SortableCard = {
  id: number | string
  cardItems: ListItem[]
}

// 大卡片里面的小卡片
type SortableCardItems = {
  pid: number | string // 父级id
  cardItem: ListItem
}

// 用来拖拽排序的Card
function SortableCard(props: SortableCard) {
  const { id, cardItems } = props
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id, data: { card: true } })

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  }

  return (
    <div
      ref={setNodeRef}
      className={cn(
        'w-[160px] border-orange-300 border-2 rounded-md pt-3 px-2',
        {
          'bg-pink-400': isDragging,
        },
      )}
      style={style}
      {...attributes}
      {...listeners}
    >
      <div className="text-center text-red-500 font-bold">{id}</div>
      {cardItems.map((item) => (
        <SortableCardItem
          key={item.id}
          pid={id}
          cardItem={item}
          onlyOne={cardItems.length === 1}
        />
      ))}
    </div>
  )
}

// 用来拖拽排序的CardItem
function SortableCardItem(props: SortableCardItems & { onlyOne: boolean }) {
  const { pid, cardItem, onlyOne } = props
  const { id, content, title } = cardItem
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({
    id: cardItem.id,
    data: { cardItem: true, pid },
    disabled: onlyOne,
  })

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  }

  return (
    <div
      ref={setNodeRef}
      className={cn('w-full bg-green-300 my-3 p-2 rounded-md', {
        'bg-red-500': isDragging,
      })}
      style={style}
      {...attributes}
      {...listeners}
    >
      <div>{id}</div>
      <div>{title}</div>
      <div className="break-words">{content}</div>
    </div>
  )
}

// 拖拽
export default function DndKit() {
  const generateDatas = (length = 10, id = '') => {
    return Array.from({ length }, (_, index) => ({
      id: id ? `${id}${index}` : index,
      title: randomString(5),
      content: randomString(12),
    }))
  }

  const [cards, setCards] = useState<number[]>(Array.from(Array(6).keys()))
  const [cardItems, setCardItems] = useState<Record<number, ListItem[]>>(() => {
    const cardItems = cards.reduce((acc, cur) => {
      acc[cur] = generateDatas(10, cur + '')
      return acc
    }, {} as Record<number, ListItem[]>)

    return cardItems
  })

  const allIds = useMemo(() => {
    return Object.values(cardItems).reduce((acc, cur) => {
      acc.push(...cur.map((item) => item.id as string))
      return acc
    }, [] as string[])
  }, [cardItems]) // 所有的小卡片id

  const onDragEnd3 = (event: DragEndEvent) => {
    const { active, over } = event

    // 大卡片拖拽
    if (active.data.current!.card === true) {
      if (active && over && active.id !== over.id) {
        const oldIndex = cards.indexOf(active.id as number)
        const newIndex = cards.indexOf(over.id as number)
        setCards((lists) => {
          return arrayMove(lists, oldIndex, newIndex)
        })
      }
    } else {
      // 小卡片拖拽
      if (active && over && active.id !== over.id) {
        const oldCardId = active.data.current!.pid
        const newCardId = over.data.current!.pid

        // 同一个大卡片内拖拽
        if (oldCardId === newCardId) {
          const cardItem = cardItems[oldCardId]
          const oldIndex = cardItem.findIndex((item) => item.id === active.id)
          const newIndex = cardItem.findIndex((item) => item.id === over.id)
          const newCardItems = arrayMove(cardItem, oldIndex, newIndex)
          setCardItems((items) => {
            return { ...items, [newCardId]: Array.from(newCardItems) }
          })
        } else {
          // 跨大卡片拖拽
          const oldCardItem = cardItems[oldCardId] // 旧大卡片里面的数据
          const newCardItem = cardItems[newCardId] // 新大卡片里面的数据

          const activeIndex = oldCardItem.findIndex(
            (ele) => ele.id === active.id,
          ) // 旧大卡片里面的索引
          const activeCardItem = oldCardItem[activeIndex] // 旧大卡里面的索引对应的数据

          const overIndex = newCardItem.findIndex((ele) => ele.id === over.id) // 新大卡片里面的索引

          // 去除oldCardItem的被拖拽元素
          oldCardItem.splice(activeIndex, 1)

          // newCardItem中增加被拖拽元素
          newCardItem.splice(overIndex, 0, activeCardItem)

          setCardItems((items) => {
            return {
              ...items,
              [oldCardId]: Array.from(oldCardItem),
              [newCardId]: Array.from(newCardItem),
            }
          })
        }
      }
    }
  }

  return (
    <Row gutter={24}>
      <Col>
        <DndContext onDragEnd={onDragEnd3}>
          {/* 第一层 */}
          <SortableContext items={[...cards, ...allIds]}>
            <SpaceItem align="left" styles={{ alignItems: 'flex-start' }}>
              {/* 第二层 */}
              {cards.map((card) => (
                <SortableCard
                  key={card}
                  id={card}
                  cardItems={cardItems[card]}
                />
              ))}
            </SpaceItem>
          </SortableContext>
        </DndContext>
      </Col>
    </Row>
  )
}

tips: 以上示例代码中出现的部分SpaceItem WhiteSpace cn randomString 均为本人项目中的封装,仅供参考。