Skip to content

useDrop & useDrag

作用

处理元素拖拽的 Hook。

useDrop 可以单独使用来接收文件、文字和网址的拖拽。

useDrag 允许一个 DOM 节点被拖拽,需要配合 useDrop 使用。

向节点内触发粘贴动作也会被视为拖拽。

useDrop

原理

就是对各种事件的监听,然后调用对应的回调函数。主要的事件有:

  • dragenter : 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发
  • dragover : 当拖动的元素或被选择的文本在有效的放置目标上方时, dragover 事件被触发
  • dragleave : 当拖动的元素或被选择的文本离开有效的放置目标时, dragleave 事件被触发
  • drop : 当拖动的元素或被选择的文本在有效的放置目标上被释放时, drop 事件被触发
  • paste : 粘贴事件
  • DataTransfer : 用于保存拖拽过程中的数据

源码

ts
import useLatest from '../useLatest'
import type { BasicTarget } from '../utils/domTarget'
import { getTargetElement } from '../utils/domTarget'
import useEffectWithTarget from '../utils/useEffectWithTarget'
import { useRef } from 'react'

export interface Options {
  onFiles?: (files: File[], event?: React.DragEvent) => void
  onUri?: (url: string, event?: React.DragEvent) => void
  onDom?: (content: any, event?: React.DragEvent) => void
  onText?: (text: string, event?: React.ClipboardEvent) => void
  onDragEnter?: (event?: React.DragEvent) => void
  onDragOver?: (event?: React.DragEvent) => void
  onDragLeave?: (event?: React.DragEvent) => void
  onDrop?: (event?: React.DragEvent) => void
  onPaste?: (event?: React.ClipboardEvent) => void
}

const useDrop = (target: BasicTarget, options: Options = {}) => {
  const optionsRef = useLatest(options)

  // https://stackoverflow.com/a/26459269
  const dragEnterTarget = useRef<any>()

  useEffectWithTarget(
    () => {
      const targetElement = getTargetElement(target)
      if (!targetElement?.addEventListener) {
        return
      }

      const onData = (
        dataTransfer: DataTransfer,
        event: React.DragEvent | React.ClipboardEvent,
      ) => {
        const uri = dataTransfer.getData('text/uri-list')
        const dom = dataTransfer.getData('custom')

        if (dom && optionsRef.current.onDom) {
          let data = dom
          try {
            data = JSON.parse(dom)
          } catch (e) {
            data = dom
          }
          optionsRef.current.onDom(data, event as React.DragEvent)
          return
        }

        if (uri && optionsRef.current.onUri) {
          optionsRef.current.onUri(uri, event as React.DragEvent)
          return
        }

        if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) {
          optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent)
          return
        }

        if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) {
          dataTransfer.items[0].getAsString((text) => {
            optionsRef.current.onText!(text, event as React.ClipboardEvent)
          })
        }
      }

      const onDragEnter = (event: React.DragEvent) => {
        event.preventDefault()
        event.stopPropagation()

        dragEnterTarget.current = event.target
        optionsRef.current.onDragEnter?.(event)
      }

      const onDragOver = (event: React.DragEvent) => {
        event.preventDefault()
        optionsRef.current.onDragOver?.(event)
      }

      const onDragLeave = (event: React.DragEvent) => {
        if (event.target === dragEnterTarget.current) {
          optionsRef.current.onDragLeave?.(event)
        }
      }

      const onDrop = (event: React.DragEvent) => {
        event.preventDefault()
        onData(event.dataTransfer, event)
        optionsRef.current.onDrop?.(event)
      }

      const onPaste = (event: React.ClipboardEvent) => {
        onData(event.clipboardData, event)
        optionsRef.current.onPaste?.(event)
      }

      targetElement.addEventListener('dragenter', onDragEnter as any)
      targetElement.addEventListener('dragover', onDragOver as any)
      targetElement.addEventListener('dragleave', onDragLeave as any)
      targetElement.addEventListener('drop', onDrop as any)
      targetElement.addEventListener('paste', onPaste as any)

      return () => {
        targetElement.removeEventListener('dragenter', onDragEnter as any)
        targetElement.removeEventListener('dragover', onDragOver as any)
        targetElement.removeEventListener('dragleave', onDragLeave as any)
        targetElement.removeEventListener('drop', onDrop as any)
        targetElement.removeEventListener('paste', onPaste as any)
      }
    },
    [],
    target,
  )
}

export default useDrop

useDrag

原理

监听 dragstart 和 dragend 方法,将值设置进 dataTransfer 中,并触发相应的回调

  • dragstart : 当用户开始拖动一个元素或者一个文本选取区域时触发
  • dragend : 当拖动操作结束时触发
  • DataTransfer : 用于保存拖拽过程中的数据

源码

ts
import { useRef } from 'react'
import useLatest from '../useLatest'
import useMount from '../useMount'
import { isString } from '../utils'
import type { BasicTarget } from '../utils/domTarget'
import { getTargetElement } from '../utils/domTarget'
import useEffectWithTarget from '../utils/useEffectWithTarget'

export interface Options {
  onDragStart?: (event: React.DragEvent) => void
  onDragEnd?: (event: React.DragEvent) => void
  dragImage?: {
    image: string | Element
    offsetX?: number
    offsetY?: number
  }
}

const useDrag = <T>(data: T, target: BasicTarget, options: Options = {}) => {
  const optionsRef = useLatest(options)
  const dataRef = useLatest(data)
  const imageElementRef = useRef<Element>()

  const { dragImage } = optionsRef.current

  useMount(() => {
    if (dragImage?.image) {
      const { image } = dragImage

      if (isString(image)) {
        const imageElement = new Image()

        imageElement.src = image
        imageElementRef.current = imageElement
      } else {
        imageElementRef.current = image
      }
    }
  })

  useEffectWithTarget(
    () => {
      const targetElement = getTargetElement(target)
      if (!targetElement?.addEventListener) {
        return
      }

      const onDragStart = (event: React.DragEvent) => {
        optionsRef.current.onDragStart?.(event)
        event.dataTransfer.setData('custom', JSON.stringify(dataRef.current))

        if (dragImage?.image && imageElementRef.current) {
          const { offsetX = 0, offsetY = 0 } = dragImage

          event.dataTransfer.setDragImage(imageElementRef.current, offsetX, offsetY)
        }
      }

      const onDragEnd = (event: React.DragEvent) => {
        optionsRef.current.onDragEnd?.(event)
      }

      targetElement.setAttribute('draggable', 'true')

      targetElement.addEventListener('dragstart', onDragStart as any)
      targetElement.addEventListener('dragend', onDragEnd as any)

      return () => {
        targetElement.removeEventListener('dragstart', onDragStart as any)
        targetElement.removeEventListener('dragend', onDragEnd as any)
      }
    },
    [],
    target,
  )
}

export default useDrag

如有转载或 CV 的请标注本站原文地址