react拖拽hooks 一百多行代码实现react拖拽hooks
孟祥_成都 人气:0前言
源码总共也就一百多行,看完这个大致可以理解一些成熟的react拖拽库的实现思路,比如react-dnd,然后你上手这些库的时候就非常快了。
使用hooks实现的大致效果动图如下:
我们的目标是实现一个useDrag和useDrop的hooks,类似以下用法就可以轻松让元素可以拖拽,并且在拖拽的各个生命周期,如下,可以自定义传递消息(顺便介绍几个拖拽会触发的事件)。
- dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。
- dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。
- dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。
- dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。
使用方法 + 源码讲解
class Hello extends React.Component<any, any> { constructor(props: any) { super(props) this.state = {} } render() { return ( <DragAndDrop> <DragElement /> <DropElement /> </DragAndDrop> ) } } ReactDOM.render(<Hello />, window.document.getElementById("root"))
如上,DragAndDrop组件的作用是给所有的使用useDrag和useDrop的组件传递消息,比如当前拖拽的元素是那个dom,或者你想要其他信息都可以往里面加,我们看看它的实现。
const DragAndDropContext = React.createContext({ DragAndDropManager: {} }); const DragAndDrop = ({ children }) => ( <DragAndDropContext.Provider value={{ DragAndDropManager: new DragAndDropManager() }}> {children} </DragAndDropContext.Provider> )
可以看到传递消息是用react的Context的api去实现的,重点就是这个DragAndDropManager,我们看下实现
export default class DragAndDropManager { constructor() { this.active = null this.subscriptions = [] this.id = -1 } setActive(activeProps) { this.active = activeProps this.subscriptions.forEach((subscription) => subscription.callback()) } subscribe(callback) { this.id += 1 this.subscriptions.push({ callback, id: this.id, }) return this.id } unsubscribe(id) { this.subscriptions = this.subscriptions.filter((sub) => sub.id !== id) } }
setActive的作用是用来记录当前drag的元素是哪个,useDrag里面会用到,我们在看useDrag的hooks实现的时候就会明白只要调用setActive方法把drag的dom元素传进去,是不是就知道当前拖拽的元素是哪个了呢。
除此之外,我还增加了订阅事件的api,subscribe,目前我并没有使用它,本次示例里你可以忽略这部分,知道可以添加订阅事件就行。
接着我们看看,useDrag的使用,DragElement的实现如下:
function DragElement() { const input = useRef(null) const hanleDrag = useDrag({ ref: input, collection: {}, // 这里可以填写任意你想传递给drop元素的消息,后面会通过参数的形式传递给drop元素 }) return ( <div ref={input}> <h1 role="button" onClick={hanleDrag}> drag元素 </h1> </div> ) }
我们就来看下useDrag的实现,非常简单
export default function useDrag(props) { const { DragAndDropManager } = useContext(DragAndDropContext) const handleDragStart = (e) => { DragAndDropManager.setActive(props.collection) if (e.dataTransfer !== undefined) { e.dataTransfer.effectAllowed = "move" e.dataTransfer.dropEffect = "move" e.dataTransfer.setData("text/plain", "drag") // firefox fix } if (props.onDragStart) { props.onDragStart(DragAndDropManager.active) } } useEffect(() => { if (!props.ref) return () => {} const { ref: { current }, } = props if (current) { current.setAttribute("draggable", true) current.addEventListener("dragstart", handleDragStart) } return () => { current.removeEventListener("dragstart", handleDragStart) } }, [props.ref.current]) return handleDragStart }
useDrag做的事情非常简单,
- 首先通过useContext,来把获取最外层store的数据,也就是上面代码的DragAndDropManager
- 在useEffect里面,如果外界传入了ref,就将这个dom元素的属性draggable设为true,也就是可拖拽状态
- 然后给这个元素绑定dragstart事件,注意了,销毁组件的时候我们要移除事件,以防内存泄漏
- handleDragStart事件首先把外界传的props.collection更新到我们的外界仓库里,这样每一个要drag,也就是拖拽的元素都可以将我们useDrag中传是入的useDrag({collection: {}})信息,通过DragAndDropManager.setActive(props.collection)的方式,传入到外界的store
- 接着我们dataTransder属性上做一些事,目的是设置元素的拖拽属性为move,并且为了兼容firefox做了处理。
- 最后每当出发drag事件的时候,外界传入的onDragStart事件也会触发,并且我们将store里的数据传入进去
其中,useDrop的使用,DropElement的实现如下:
function DropElement(props: any): any { const input = useRef(null) useDrop({ ref: input, // e代表dragOver事件发生时,正在被over的元素的event对象 // collection是store存储的数据 // showAfter是表示,是否鼠标拖拽元素时,鼠标经过drop元素的上方(上方就是上半边,下方就是下半边) onDragOver: (e, collection, showAfter) => { // 如果经过上半边,drop元素的上边框就是红色 if (!showAfter) { input.current.style = "border-bottom: none;border-top: 1px solid red" } else { // 如果经过下半边,drop元素的上边框就是红色 input.current.style = "border-top: none;border-bottom: 1px solid red" } }, // 如果在drop元素上放开鼠标,则样式清空 onDrop: () => { input.current.style = "" }, // 如果在离开drop元素,则样式清空 onDragLeave: () => { input.current.style = "" }, }) return ( <div> <h1 ref={input}>drop元素</h1> </div> ) }
最后,我们来看看useDrop的实现
export default function useDrop(props) { // 获取最外层store里的数据 const { DragAndDropManager } = useContext(DragAndDropContext) const handleDragOver = (e) => { // e就是拖拽的event对象 e.preventDefault() // getBoundingClientRect的图请看下面 const overElementHeight = e.currentTarget.getBoundingClientRect().height / 2 const overElementTopOffset = e.currentTarget.getBoundingClientRect().top // clientY就是鼠标到浏览器页面可视区域的最顶端的距离 const mousePositionY = e.clientY // mousePositionY - overElementTopOffset就是鼠标在元素内部到元素border-top的距离 const showAfter = mousePositionY - overElementTopOffset > overElementHeight if (props.onDragOver) { props.onDragOver(e, DragAndDropManager.active, showAfter) } } // drop事件 const handledDop = (e: React.DragEvent) => { e.preventDefault() if (props.onDrop) { props.onDrop(DragAndDropManager.active) } } // dragLeave事件 const handledragLeave = (e: React.DragEvent) => { e.preventDefault() if (props.onDragLeave) { props.onDragLeave(DragAndDropManager.active) } } // 注册事件,注意销毁组件时要注销事件,避免内存泄露 useEffect(() => { if (!props.ref) return () => {} const { ref: { current }, } = props if (current) { current.addEventListener("dragover", handleDragOver) current.addEventListener("drop", handledDop) current.addEventListener("dragleave", handledragLeave) } return () => { current.removeEventListener("dragover", handleDragOver) current.removeEventListener("drop", handledDop) current.removeEventListener("dragleave", handledragLeave) } }, [props.ref.current]) }
getBoundingClientRect的api图解:
rectObject = object.getBoundingClientRect();
rectObject.top:元素上边到视窗上边的距离;
rectObject.right:元素右边到视窗左边的距离;
rectObject.bottom:元素下边到视窗上边的距离;
rectObject.left:元素左边到视窗左边的距离;
加载全部内容