一百多行代码,实现react拖拽hooks

前言

源码总共也就一百多行,看完这个大体能够理解一些成熟的react拖拽库的实现思路,好比react-dnd,而后你上手这些库的时候就很是快了。react

使用hooks实现的大体效果动图以下:api

未命名.gif

咱们的目标是实现一个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,或者你想要其余信息均可以往里面加,咱们看看它的实现。markdown

const DragAndDropContext = React.createContext({ DragAndDropManager: {} });


const DragAndDrop = ({ children }) => (
  <DragAndDropContext.Provider value={{ DragAndDropManager: new DragAndDropManager() }}>
    {children}
  </DragAndDropContext.Provider>
)

复制代码

能够看到传递消息是用react的Context的api去实现的,重点就是这个DragAndDropManager,咱们看下实现dom

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元素传进去,是否是就知道当前拖拽的元素是哪一个了呢。ide

除此以外,我还增长了订阅事件的api,subscribe,目前我并无使用它,本次示例里你能够忽略这部分,知道能够添加订阅事件就行。函数

接着咱们看看,useDrag的使用,DragElement的实现以下:this

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的实现,很是简单spa

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作的事情很是简单,firefox

  • 首先经过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:元素左边到视窗左边的距离;
复制代码

image.png

大概就是这些,后面能够分享一些别的实战,有些代码已经写好了,只是项目太忙了,没时间写博客,最近在研究rxjs,这个是忽然兴致来了,从12点写到了两点写完了。。。

相关文章
相关标签/搜索