前言
源碼總共也就一百多行,看完這個大致可以理解一些成熟的react拖拽庫的實現(xiàn)思路,比如react-dnd,然后你上手這些庫的時候就非??炝?。
使用hooks實現(xiàn)的大致效果動圖如下:
我們的目標是實現(xiàn)一個useDrag和useDrop的hooks,類似以下用法就可以輕松讓元素可以拖拽,并且在拖拽的各個生命周期,如下,可以自定義傳遞消息(順便介紹幾個拖拽會觸發(fā)的事件)。
- dragstart:用戶開始拖拉時,在被拖拉的節(jié)點上觸發(fā),該事件的target屬性是被拖拉的節(jié)點。
- dragenter:拖拉進入當前節(jié)點時,在當前節(jié)點上觸發(fā)一次,該事件的target屬性是當前節(jié)點。通常應該在這個事件的監(jiān)聽函數(shù)中,指定是否允許在當前節(jié)點放下(drop)拖拉的數(shù)據(jù)。如果當前節(jié)點沒有該事件的監(jiān)聽函數(shù),或者監(jiān)聽函數(shù)不執(zhí)行任何操作,就意味著不允許在當前節(jié)點放下數(shù)據(jù)。在視覺上顯示拖拉進入當前節(jié)點,也是在這個事件的監(jiān)聽函數(shù)中設置。
- dragover:拖拉到當前節(jié)點上方時,在當前節(jié)點上持續(xù)觸發(fā)(相隔幾百毫秒),該事件的target屬性是當前節(jié)點。該事件與dragenter事件的區(qū)別是,dragenter事件在進入該節(jié)點時觸發(fā),然后只要沒有離開這個節(jié)點,dragover事件會持續(xù)觸發(fā)。
- dragleave:拖拉操作離開當前節(jié)點范圍時,在當前節(jié)點上觸發(fā),該事件的target屬性是當前節(jié)點。如果要在視覺上顯示拖拉離開操作當前節(jié)點,就在這個事件的監(jiān)聽函數(shù)中設置。
使用方法 + 源碼講解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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,或者你想要其他信息都可以往里面加,我們看看它的實現(xiàn)。
1
2
3
4
5
6
|
const DragAndDropContext = React.createContext({ DragAndDropManager: {} }); const DragAndDrop = ({ children }) => ( <DragAndDropContext.Provider value={{ DragAndDropManager: new DragAndDropManager() }}> {children} </DragAndDropContext.Provider> ) |
可以看到傳遞消息是用react的Context的api去實現(xiàn)的,重點就是這個DragAndDropManager,我們看下實現(xiàn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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實現(xiàn)的時候就會明白只要調用setActive方法把drag的dom元素傳進去,是不是就知道當前拖拽的元素是哪個了呢。
除此之外,我還增加了訂閱事件的api,subscribe,目前我并沒有使用它,本次示例里你可以忽略這部分,知道可以添加訂閱事件就行。
接著我們看看,useDrag的使用,DragElement的實現(xiàn)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function DragElement() { const input = useRef( null ) const hanleDrag = useDrag({ ref: input, collection: {}, // 這里可以填寫任意你想傳遞給drop元素的消息,后面會通過參數(shù)的形式傳遞給drop元素 }) return ( <div ref={input}> <h1 role= "button" onClick={hanleDrag}> drag元素 </h1> </div> ) } |
我們就來看下useDrag的實現(xiàn),非常簡單
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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的數(shù)據(jù),也就是上面代碼的DragAndDropManager
- 在useEffect里面,如果外界傳入了ref,就將這個dom元素的屬性draggable設為true,也就是可拖拽狀態(tài)
- 然后給這個元素綁定dragstart事件,注意了,銷毀組件的時候我們要移除事件,以防內存泄漏
- handleDragStart事件首先把外界傳的props.collection更新到我們的外界倉庫里,這樣每一個要drag,也就是拖拽的元素都可以將我們useDrag中傳是入的useDrag({collection: {}})信息,通過DragAndDropManager.setActive(props.collection)的方式,傳入到外界的store
- 接著我們dataTransder屬性上做一些事,目的是設置元素的拖拽屬性為move,并且為了兼容firefox做了處理。
- 最后每當出發(fā)drag事件的時候,外界傳入的onDragStart事件也會觸發(fā),并且我們將store里的數(shù)據(jù)傳入進去
其中,useDrop的使用,DropElement的實現(xiàn)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
function DropElement(props: any): any { const input = useRef( null ) useDrop({ ref: input, // e代表dragOver事件發(fā)生時,正在被over的元素的event對象 // collection是store存儲的數(shù)據(jù) // showAfter是表示,是否鼠標拖拽元素時,鼠標經(jīng)過drop元素的上方(上方就是上半邊,下方就是下半邊) onDragOver: (e, collection, showAfter) => { // 如果經(jīng)過上半邊,drop元素的上邊框就是紅色 if (!showAfter) { input.current.style = "border-bottom: none;border-top: 1px solid red" } else { // 如果經(jīng)過下半邊,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的實現(xiàn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
export default function useDrop(props) { // 獲取最外層store里的數(shù)據(jù) 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就是鼠標到瀏覽器頁面可視區(qū)域的最頂端的距離 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:元素左邊到視窗左邊的距離;
到此這篇關于一百多行代碼實現(xiàn)react拖拽hooks的文章就介紹到這了,更多相關react拖拽hooks內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/6941786077237624840