useDrop & useDrag
作用
处理元素拖拽的 Hook。
useDrop 可以单独使用来接收文件、文字和网址的拖拽。
useDrag 允许一个 DOM 节点被拖拽,需要配合 useDrop 使用。
向节点内触发粘贴动作也会被视为拖拽。
useDrop
原理
就是对各种事件的监听,然后调用对应的回调函数。主要的事件有:
源码
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