一百多行程式碼實現react拖拽hooks
前言
原始碼總共也就一百多行,看完這個大致可以理解一些成熟的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元素的訊息,後面會通過引數的形式傳遞給wASyTdrop元素
})
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.currewww.cppcns.comnt]) }
getBoundingClientRect的api圖解:
rectObject = object.getBoundingClientRect();
rectObject.top:元素上邊到視窗上邊的距離;
rectObject.right:元素右邊到視窗左邊的距離;
rectObject.bottom:元素下邊到視窗上邊的距離;
rectObject.left:元素左邊到視窗左邊的距離;
到此這篇關於一百多行程式碼實現react拖拽hooks的文章就介紹到這了,更多相關react拖拽hooks內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!