从零开始-文件资源管理器-29-使用dnd-kit快速实现拖拽移动定位

1,005 阅读3分钟

使用 dnd-kit 实现一个类似 window 窗口鼠标长按标题栏,可拖拽移动。释放鼠标时窗口位置固定的功能。

拖拽核心

简单封装一个 dnd-kit hooks 文件

'use client'
import React, { CSSProperties } from 'react'
import { DndContext, useDraggable, useDroppable } from '@dnd-kit/core'
import { Button } from 'antd'
import { DragOutlined } from '@ant-design/icons'
import { useDockAction } from '@/components/desktop/dock/dock-context'

export const useWindowDraggable = ({ id }: { id: number }) => {
  const { attributes, listeners, setNodeRef, transform, activatorEvent } = useDraggable({
    id: `draggable-${id}`,
    data: { type: 'window', id: id },
  })
  const { getWindowItem } = useDockAction()

  const { position } = getWindowItem(id)

  const style: CSSProperties = transform
    ? {
        transform: `translate3d(${position.offsetX + transform.x}px, ${position.offsetY + transform.y}px, 0)`,
      }
    : {}

  return {
    setNodeRef,
    style,
    draggable_button: <Button {...listeners} {...attributes} icon={<DragOutlined />} />,
  }
}

export const Droppable: React.FC<React.PropsWithChildren & { id?: string }> = (props) => {
  const { isOver, setNodeRef } = useDroppable({
    id: `droppable-${props.id}`,
  })

  const style: CSSProperties = {
    backgroundColor: isOver ? 'rgba(255,255,255,.1)' : undefined,
  }

  return (
    <div ref={setNodeRef} style={style}>
      {props.children}
    </div>
  )
}

export const WindowDndContext: React.FC<React.PropsWithChildren> = ({ children }) => {
  const { changeWindowPositionItem } = useDockAction()

  return (
    <DndContext
      onDragEnd={(event) => {
        const { activatorEvent, active, delta } = event

        active &&
          changeWindowPositionItem(active.data.current?.id, {
            offsetY: delta.y,
            offsetX: delta.x,
          })
      }}
    >
      <Droppable>{children}</Droppable>
    </DndContext>
  )
}
  • useWindowDraggable 为长按拖拽 hooks,返回一个 ref、 使用 transform 改变位置的 style、draggable_button长按触发拖拽的按钮。
  • Droppable为开始拖拽可放置区域组件,这里仅改变背景色。
  • WindowDndContext为 Context 组件。

onDragEnd方法为释放鼠标时执行的回调函数,event 对象内的 delta 记录了释放时,窗口的偏移量 x 与 y。每次将释放时,将该值记录下来,供下次拖拽移动时相加使用,避免再次拖拽时transform偏移量错误。

再次拖拽移动时,需要与上次释放时的值开始进行偏移定位。

...
  const { position } = getWindowItem(id)

  const style: CSSProperties = transform
    ? {
        transform: `translate3d(${position.offsetX + transform.x}px, ${position.offsetY + transform.y}px, 0)`,
      }
    : {}
...

窗口定位列表

打开窗口列表的的 Context 组件。内部维护一个打开窗口的定位信息。

当需要打开一个窗口时,使用 push 方法新增一个。changeActivity可改变激活状态。

'use client'
import createCtx from '@/lib/create-ctx'
import React from 'react'

export const DockContext =
  createCtx<{ title: string; hidden: boolean; activity: boolean; position: { offsetY: number; offsetX: number } }[]>()

export const useDockAction = () => {
  const window_list = DockContext.useStore()
  const changeDock = DockContext.useDispatch()

  return {
    getWindowItem: (window_index: number) => {
      return window_list[window_index]
    },
    changeWindowPositionItem: (window_index: number, position: { offsetY: number; offsetX: number }) => {
      const item = window_list[window_index]
      item.position = {
        offsetX: item.position.offsetX + position.offsetX,
        offsetY: item.position.offsetY + position.offsetY,
      }

      changeDock([...window_list])
    },
    push: (window_type: string) => {
      const list = [
        ...window_list.map((item) => {
          item.activity = false
          return item
        }),
        { title: window_type, hidden: false, activity: true, position: { offsetY: 0, offsetX: 0 } },
      ]

      changeDock(list)

      return list.length
    },
    remove: (window_index: number) => {
      window_list.splice(window_index, 1)
      changeDock([...window_list])
    },
    changeActivity: (window_index: number) => {
      window_list.map((item) => {
        item.activity = false
        return item
      })

      const item = window_list[window_index]

      item.activity = true

      changeDock([...window_list])
    },
  }
}

export const DockContextProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
  return <DockContext.ContextProvider value={[]}>{children}</DockContext.ContextProvider>
}

接下来使用就很简单了。首先将 WindowDndContextDockContextProvider 插入所需要拖拽的组件顶层。注意层级关系,DockContextProvider为最外层。

import React from 'react'
import Window from '@/components/desktop/window'
import { DockContextProvider } from '@/components/desktop/dock/dock-context'
import { OpenAppList } from '@/components/desktop/open-app-list'
import { WindowDndContext } from '@/components/desktop/dnd'

const Desktop: React.FC = () => {
  return (
    <DockContextProvider>
      <WindowDndContext>
        <Window>
          <OpenAppList />
        </Window>
      </WindowDndContext>
    </DockContextProvider>
  )
}

export default Desktop

窗口拖拽组件

'use client'
import React from 'react'
import { WindowCloseButton, WindowItemStyle } from '@/components/desktop/style'
import { useDockAction } from '@/components/desktop/dock/dock-context'
import { useWindowDraggable } from '@/components/desktop/dnd'
import { Card, CardProps, Space } from 'antd'
import { CloseOutlined } from '@ant-design/icons'

const WindowBox: React.FC<{ window_id: number } & CardProps> = ({ children, window_id, ...props }) => {
  const { remove, getWindowItem, changeActivity } = useDockAction()
  const { setNodeRef, style, draggable_button } = useWindowDraggable({ id: window_id })

  const { position, activity } = getWindowItem(window_id)

  return (
    <WindowItemStyle
      ref={setNodeRef}
      style={{
        transform: `translate3d(${position.offsetX}px, ${position.offsetY}px, 0)`,
        zIndex: activity ? 101 : 100,
        ...style,
      }}
      onClick={() => {
        changeActivity(window_id)
      }}
    >
      <Card
        title={props.title}
        extra={
          <Space>
            {draggable_button}
            <WindowCloseButton
              icon={<CloseOutlined />}
              type="text"
              onClick={(e) => {
                e.stopPropagation()
                remove(window_id)
              }}
            />
          </Space>
        }
        styles={{ body: { flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
        style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
      >
        {children}
      </Card>
    </WindowItemStyle>
  )
}

export default WindowBox

总结

实现起来很简单,使用 dnd-kit 的 useDraggable hooks 可以快速实现一个拖拽样式与事件。每次释放时记录当前的 x、y 偏移量。再次拖拽时需要在当前的 x、y 偏移量的基础上进行叠加。

后续将使用该组件实现拖拽文件夹、文件实现“剪切、移动”功能。

效果

长按 + 按钮时可整体拖拽当前“窗口”

image.png

git-repo

yangWs29/share-explorer