一百多行代碼,實現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點寫到了兩點寫完了。。。

相關文章
相關標籤/搜索