源碼總共也就一百多行,看完這個大體能夠理解一些成熟的react拖拽庫的實現思路,好比react-dnd,而後你上手這些庫的時候就很是快了。react
使用hooks實現的大體效果動圖以下:api
咱們的目標是實現一個useDrag和useDrop的hooks,相似如下用法就能夠輕鬆讓元素能夠拖拽,而且在拖拽的各個生命週期,以下,能夠自定義傳遞消息(順便介紹幾個拖拽會觸發的事件)。瀏覽器
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
其中,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:元素左邊到視窗左邊的距離;
複製代碼
大概就是這些,後面能夠分享一些別的實戰,有些代碼已經寫好了,只是項目太忙了,沒時間寫博客,最近在研究rxjs,這個是忽然興致來了,從12點寫到了兩點寫完了。。。