React結(jié)合Drag?API實現(xiàn)拖拽示例詳解
認識拖拽
鼠標拖拽是一個常見的交互場景,在這個熟悉的過程將會發(fā)生哪些事件?
拖拽事件指用戶通過鼠標(或其他指針設(shè)備)將元素移到一個新的位置上。拖拽過程涉及兩個對象:被拖拽元素(上圖中 A )和可釋放目標(上圖中 B )
被拖拽元素
默認情況下,圖片、鏈接和文本是可拖動的。HTML5 在所有 HTML 元素上規(guī)定了一個 draggable
屬性, 表示元素是否可以拖動。圖片和鏈接的 draggable 屬性自動被設(shè)置為 true,而其他所有元素此屬性的默認值為 false。
某個元素被拖動時,會依次觸發(fā)以下事件:
ondragstart
:拖動開始,當鼠標按下并且開始移動鼠標時,觸發(fā)此事件;整個周期只觸發(fā)一次;ondrag
:只要元素仍被拖拽,就會持續(xù)觸發(fā)此事件;ondragend
:拖拽結(jié)束,當鼠標松開后,會觸發(fā)此事件;整個周期只觸發(fā)一次。
可釋放目標
當把拖拽元素移動到一個有效的放置目標時,目標對象會觸發(fā)以下事件:
ondragenter
:只要一把拖拽元素移動到目標時,就會觸發(fā)此事件;ondragover
:拖拽元素在目標中拖動時,會持續(xù)觸發(fā)此事件;ondragleave
或ondrop
:拖拽元素離開目標時(沒有在目標上放下),會觸發(fā)ondragleave
;當拖拽元素在目標放下(松開鼠標),則觸發(fā)ondrop
事件。
?? 目標元素默認是不能夠被拖放的,即不會觸發(fā)
ondrop
事件,可以通過在目標元素的ondragover
事件中取消默認事件來解決此問題。
生命周期
拖拽操作中的數(shù)據(jù)傳輸
除非數(shù)據(jù)受影響,否則簡單的拖放并沒有實際意義。為實現(xiàn)拖動操作中的數(shù)據(jù)傳輸,event
對象上暴露了 dataTransfer
對象,用于從被拖動元素向放置目標傳遞字符串數(shù)據(jù)。我們使用它來通知畫布,當前需要渲染的組件是什么。
dataTransfer 對象主要有兩個方法:getData() 和 setData(),分別用來獲取和存儲值。setData()的第一個參數(shù)以及 getData()的唯一參數(shù)是一個字符串,表示要設(shè)置的數(shù)據(jù)類型:"text"或"URL"
?? 雖然這兩種數(shù)據(jù)類型是 IE 最初引入的,但 HTML5 已經(jīng)將其擴展為允許任何 MIME 類型。為向后 兼容,HTML5 還會繼續(xù)支持"text"和"URL",但它們會分別被映射到"text/plain"和"text/uri-list”
需要注意的是:存儲在 dataTransfer
對象中的數(shù)據(jù)只能在放置事件中讀取。如果沒有在 ondrop
事件中取得這些數(shù)據(jù),dataTransfer
對象就會被銷毀,數(shù)據(jù)也會丟失。
代碼實現(xiàn)
我在項目中使用 React 來實現(xiàn),并且考慮到跨組件通信,我使用了 dva 來管理數(shù)據(jù)流。
如何標記當前拖拽的元素?
HTML5 支持的 data-x
屬性,我們可以將當前組件的類型 Rectangle 賦值給它,這樣處理和畫布組件通信方便一些
const Block = (props) => { const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => { // 向拖拽數(shù)據(jù)中添加項目 e.dataTransfer.setData('text', e.target.dataset.index); }; return ( <div onDragStart={handleDragStart}> <Button draggable data-index="Rectangle"> 二維碼 </Button> </div> ); };
在上文中講到,dataTransfer 的數(shù)據(jù)必須在 handleDrop 方法中獲取。實際的用來保存畫布中的所有組件的數(shù)據(jù):
function DragEditor(props) { const { dvaStore, dispatch } = props; // 阻止瀏覽器默認事件,否則 ondrop 不會觸發(fā) const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); }; const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); // 獲取拖拽元素的組件類型 const type = e.dataTransfer.getData('text'); // COMPONENT_LIST 定義了組件的數(shù)據(jù)格式,根據(jù) type 匹配 const component = COMPONENT_LIST.filter( (i) => i.component === type, )[0]; // 將組件數(shù)據(jù)添加到 store,畫布將會根據(jù)數(shù)據(jù)渲染出組件 if (component) { dispatch?.({ type: 'store/addComponent', payload: component, }); } }; return (...); }
在畫布中拖動
拖動主要依賴組件的初始位置,鼠標開始位置、結(jié)束位置。根據(jù)后兩組得到鼠標移動的距離,和初始位置相加后,得到最終位置。
function DragEditor(props: IEditorProps) { const { dvaStore, dispatch } = props; const [startAxis, setStartAxis] = React.useState({ x: 0, y: 0 }); // 鼠標開始拖動時的位置 const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => { setStartAxis({ x: e.clientX, y: e.clientY }); }; const handleDragEnd = (e: React.DragEvent<HTMLDivElement>, data: IComponentSchema) => { // 鼠標移動的距離 const displacementX = e.clientX - startAxis.x; const displacementY = e.clientY - startAxis.y; // 計算組件的終點位置:初始位置 + 鼠標移動的距離 const endX = Number(data.style.left) + displacementX; const endY = Number(data.style.top) + displacementY; // 限制坐標的最小值為 0 const top = Math.max(endY, 0); const left = Math.max(endX, 0); // 更新當前組件樣式 dispatch?.({ type: 'store/setShapeStyle', payload: { top, left }, }); }; return ( {dvaStore.componentsData.map((i) => { return ( <RenderComponent type={i.component} componentData={i} key={i.generateId} onDragStart={handleDragStart} onDragEnd={(e) => handleDragEnd(e, i)} /> ); })} ); }
數(shù)據(jù)結(jié)構(gòu)
最后,就是組件和數(shù)據(jù)結(jié)構(gòu)的設(shè)計,RenderComponent 是一個自定義的組件,會根據(jù)傳入的 type 屬性渲染對應(yīng)的組件。組件的數(shù)據(jù)結(jié)構(gòu)設(shè)計如下:
export const COMPONENT_LIST = [ { component: 'Rectangle', // 組件名稱 label: '矩形', // 左側(cè) Blocks 組件列表中顯示的名字 propValue: '', // 組件所使用的值 icon: 'BorderOuterOutlined', // 左側(cè)組件列表中顯示的 icon 圖標 animations: [], // 動畫列表 events: {}, // 事件列表 style: { // 組件樣式 width: 100, height: 100, top: 0, left: 0, }, }, { component: 'Text', label: '文字', propValue: '文字', icon: '', animations: [], events: {}, style: { width: 200, height: 33, fontSize: 14, fontWeight: 500, lineHeight: '', letterSpacing: 0, textAlign: '', color: '', }, }, ];
總結(jié)
拖拽是非常有趣的一種交互,特別是在低代碼場景下非常重要。使用原生 API 能夠讓我們更加了解底層的一些細節(jié),React 社區(qū)也有一些優(yōu)秀的第三方框架,如:react-dragable, react-beautiful-dnd,大家有興趣不妨再多了解下。
Links HTML 拖放 API
以上就是React結(jié)合Drag API實現(xiàn)拖拽示例詳解的詳細內(nèi)容,更多關(guān)于React Drag API拖拽的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用useImperativeHandle時父組件第一次沒拿到子組件的問題
這篇文章主要介紹了使用useImperativeHandle時父組件第一次沒拿到子組件的問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08React Hooks之使用useCallback和useMemo進行性能優(yōu)化方式
這篇文章主要介紹了React Hooks之使用useCallback和useMemo進行性能優(yōu)化方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06關(guān)于react hook useState連續(xù)更新對象的問題
這篇文章主要介紹了關(guān)于react hook useState連續(xù)更新對象的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03