最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 一百多行代码,实现react拖拽hooks

    正文概述 掘金(孟祥_成都)   2021-03-21   326

    前言

    源码总共也就一百多行,看完这个大致可以理解一些成熟的react拖拽库的实现思路,比如react-dnd,然后你上手这些库的时候就非常快了。

    使用hooks实现的大致效果动图如下:

    一百多行代码,实现react拖拽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:元素左边到视窗左边的距离;
    

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

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


    起源地下载网 » 一百多行代码,实现react拖拽hooks

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元