拖拽組件是在前端開發中十分常見的一個功能,現在無論你是使用React還是Vue,都有很多現成的拖拽組件可以使用。不過,有些時候你可能還是需要自己去實現,那麼就必須需要理解其實現原理。接下來這篇文章,我將詳細介紹如何使用React框架來實現一個拖拽組件。
理解HTML5 拖放API
現如今,大部分的前端拖拽組件都依託於HTML5原生提供的拖放接口。那麼在開始用具體框架來封裝組件的之前,就需要搞清楚這些原生的接口功能。
HTML 5的DOM鼠標事件中添加了drag這個事件。對於一個設置了draggable屬性的頁面元素來說,只要將其拖動到一個同樣帶有droppable屬性的元素上,就算完成了一次完整的拖放功能。在這一過程中,會分別觸發一些如下事件類型:
事件類型 | 事件處理函數 | 含義 |
---|---|---|
drag | ondrag | 拖放進行中 |
dragend/dragstart | ondragend/ondragstart | 開始拖放和結束拖放 |
dragover | ondragover | 當元素或選中的文本被拖到一個目標目標上(每100毫秒觸發一次)。 |
dragenter/dragleave | ondragenter/ondragleave | 源對象開始進入/離開目標對象範圍內 |
drop | ondrop | 源對象被拖放到目標對象上 |
熟悉這些基本事件類型後,實現上就是在源對象和目標對象上分別綁定對應的事件處理函數,並監聽處理即可。
除了這些拖放的事件接口外,我們通常還需要處理數據的傳遞。HTML5中同樣提供了簡便的接口,在對應的監聽函數內,我們可以拿到event對象,在這個對象內部有個DataTransfer
接口,可專門用來保存事件的數據內容。對應的接口有:
- event.dataTransfer.setData: 添加拖拽數據,這個方法接收兩個參數,第一個參數是數據類型(可自定義),第二個參數是對應的數據
- event.dataTransfer.getData:反向操作,獲取數據,只接收一個參數,即數據類型
- event.dataTransfer.clearData: 清除數據
- event.dataTransfer.setDragImage: 可自定義拖放過程中鼠標旁邊的圖像
- event.dataTransfer.effectAllowed: 指定拖放操作所允許的一個效果,有多個屬性值,如link, move等,具體可參考https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/effectAllowed
瞭解完這些基本接口後,我們就可以着手使用React來編寫自己的拖放組件了:
實現Drag組件
我們第一個要實現的是Drag組件,它會作爲我們的源對象,其子組件都可以進行拖動。就像這樣:
<Drag dataItem="item">
<div>這個組件可以拖動</div>
</Drag>
我們先來實現最基礎的功能,通過setData
接口來傳遞數據:
const Drag = (props) => {
const startDrag = ev => {
// 傳輸數據
ev.dataTransfer.setData("drag-item", props.dataItem);
};
return (
<div draggable onDragStart={startDrag}>
{props.children}
</div>);
}
實現Drop組件
接着我們就要來實現目標組件了,需要定義一個對外暴露的接口用來接收拖拽完成後的事件:
<DropTarget onItemDropped={itemDropped}>
<div>
請將組件拖放到這裏
</div>
</DropTarget>
從實現上來說,監聽onDragOver
和onDrop
這兩個事件就可以了:
const DropTarget = (props) => {
const dragOver = ev => {
ev.preventDefault();
}
const drop = ev => {
// 獲取數據
const droppedItem = ev.dataTransfer.getData("drag-item");
if (droppedItem) {
// 觸發回調函數
props.onItemDropped(droppedItem);
}
}
return (
<div onDragOver={dragOver} onDrop={drop}>
{props.children}
</div>
)
}
添加拖放效果
要實現拖放的視覺效果,需要effectAllowed和dropEffect兩個屬性結合起來使用。
先在Drag組件上設置effectAllowed
屬性:
const Drag = (props) => {
const startDrag = ev => {
ev.dataTransfer.setData("drag-item", props.dataItem);
// 添加效果
ev.dataTransfer.effectAllowed = props.dropEffect;
};
return (
<div draggable onDragStart={startDrag}>
{props.children}
</div>);
}
接着我們設置一些效果常量:
export const All = "all";
export const Move = "move";
export const Copy = "copy";
export const Link = "link";
export const CopyOrMove = "copyMove";
export const CopyOrLink = "copyLink";
export const LinkOrMove = "linkMove";
export const None = "none";
然後在目標組件上,我們通過給dropEffect
屬性賦值來引用這些效果常量,修改代碼如下:
const DropTarget = (props) => {
const dragOver = ev => {
ev.preventDefault();
// 添加效果
ev.dataTransfer.dropEffect = props.dropEffect;
}
const dragEnter = ev => {
ev.dataTransfer.dropEffect = props.dropEffect;
}
const drop = ev => {
const droppedItem = ev.dataTransfer.getData("drag-item");
if (droppedItem) {
props.onItemDropped(droppedItem);
}
}
return (
<div onDragOver={dragOver} onDrop={drop} onDragEnter={dragEnter}>
{props.children}
</div>
)
}
Drag.defaultProps = {
dropEffect: dropEffects.All, // 設置默認的效果
};
進一步完善
到這一步,大體的功能我們都完成的七七八八了,最後還剩下一些收尾的工作。首先我們可以添加接口用來讓用戶可以自定義拖拽圖像:
const Drag = (props) => {
const image = React.useRef(null);
React.useEffect(() => {
image.current = null;
if (props.dragImage) {
image.current = new Image();
image.current.src = props.dragImage;
}
}, [props.dragImage]);
const startDrag = ev => {
ev.dataTransfer.setData("drag-item", props.dataItem);
ev.dataTransfer.effectAllowed = props.dropEffect;
// 設置圖片
if (image.current) {
ev.dataTransfer.setDragImage(image.current, 0, 0);
}
};
return (
<div draggable onDragStart={startDrag}>
{props.children}
</div>);
}
接着,我們再來添加樣式:
// 樣式
const draggingStyle = {
opacity: 0.25,
};
const Drag = props => {
const [isDragging, setIsDragging] = React.useState(false);
const image = React.useRef(null);
React.useEffect(() => {
image.current = null;
if (props.dragImage) {
image.current = new Image();
image.current.src = props.dragImage;
}
}, [props.dragImage]);
const startDrag = ev => {
setIsDragging(true);
ev.dataTransfer.setData("drag-item", props.dataItem);
ev.dataTransfer.effectAllowed = props.dropEffect;
if (image.current) {
ev.dataTransfer.setDragImage(image.current, 0, 0);
}
};
// 拖拽結束時,添加樣式
const dragEnd = () => setIsDragging(false);
return (
<div style={isDragging ? draggingStyle : {}} draggable onDragStart={startDrag} onDragEnd={dragEnd}>
{props.children}
</div>
);
};
最後,需要注意的是,如果需要處理移動端的兼容性,那麼可以使用如下庫:
https://github.com/timruffles/mobile-drag-drop
——本文首發於個人公衆號,轉載請註明出處———
最後,歡迎大家關注我的公衆號,一起學習交流。
參考資料
https://app.pluralsight.com/guides/drag-and-drop-react-components(本文代碼例子主要來源於此)
https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API