vue實現(xiàn)一個簡單的Grid拖拽布局
最近有個需求需要實現(xiàn)自定義首頁布局,需要將屏幕按照 6 列 4 行進(jìn)行等分成多個格子,然后將組件可拖拽對應(yīng)格子進(jìn)行渲染展示。
對比一些已有的插件,發(fā)現(xiàn)想要實現(xiàn)產(chǎn)品的交互效果,沒有現(xiàn)成可用的。本身功能并不是太過復(fù)雜,于是決定自己基于 vue 手?jǐn)]一個簡易的 Grid 拖拽布局。
完整源碼在此,在線體驗
概況
需要實現(xiàn) Grid 拖拽布局,主要了解這兩個東西就行
- 拖放 API,關(guān)于拖放 API 介紹文章有很多 ,可以直接看 MDN 里拖放 API介紹,可以說很詳細(xì)了。
- Grid 布局, Grid 布局與 Flex 布局很相似,但是 Grid 像是二維布局,F(xiàn)lex 則為一維布局,Grid 布局遠(yuǎn)比 Flex 布局強(qiáng)大。MDN 關(guān)于網(wǎng)格布局介紹
需要實現(xiàn)主要包含:
- 組件物料欄拖拽到布局容器
- 布局容器 Grid 布局
- 放置時是否重疊判斷
- 拖拽時樣式
- 放置后樣式
- 容器內(nèi)二次拖拽
拖放操作實現(xiàn)
拖拽中主要使用到的事件如下
被拖拽元素事件:
事件 | 觸發(fā)時刻 |
---|---|
dragstart | 當(dāng)用戶開始拖拽一個元素或選中的文本時觸發(fā)。 |
drag | 當(dāng)拖拽元素或選中的文本時觸發(fā)。 |
dragend | 當(dāng)拖拽操作結(jié)束時觸發(fā) |
放置容器事件:
事件 | 觸發(fā)時刻 |
---|---|
dragenter | 當(dāng)拖拽元素或選中的文本到一個可釋放目標(biāo)時觸發(fā)。 |
dragleave | 當(dāng)拖拽元素或選中的文本離開一個可釋放目標(biāo)時觸發(fā)。 |
dragover | 當(dāng)元素或選中的文本被拖到一個可釋放目標(biāo)上時觸發(fā)。 |
drop | 當(dāng)元素或選中的文本在可釋放目標(biāo)上被釋放時觸發(fā)。 |
可拖拽元素
讓一個元素能夠拖拽只需要給元素設(shè)置 draggable="true" 即可拖拽,拖拽事件 API 提供了 DataTransfer 對象,可以用于設(shè)置拖拽數(shù)據(jù)信息,但是僅僅只能 drop 事件中獲取到,但是我們需要在拖拽中就需要獲取到拖拽信息,用來顯示拖拽時樣式,所以需要我們自己存儲起來,以便讀取。
需要處理主要是,在拖拽時將 將當(dāng)前元素信息設(shè)置到 dragStore 中,結(jié)束時清空當(dāng)前信息
<script setup lang="ts"> import { dragStore } from "./drag"; const props = defineProps<{ data: DragItem; groupName?: string; }>(); const onDragstart = (e) => dragStore.set(props.groupName, { ...props.data }); const onDragend = () => dragStore.remove(props.groupName); </script> <template> <div class="drag-item__el" draggable="true" @dragstart="onDragstart" @dragend="onDragend"></div> </template>
封裝一個存儲方法,然后通過配置相同 key ,可以在同時存在多個放置區(qū)域時候,區(qū)分開來。
class DragStore<T extends DragItemData> { moveItem = new Map<string, DragItemData>(); set(key: string, data: T) { this.moveItem.set(key, data); } remove(key: string) { this.moveItem.delete(key); } get(key: string): undefined | DragItemData { return this.moveItem.get(key); } }
可放置區(qū)域
首先時需要告訴瀏覽器當(dāng)前區(qū)域是可以放置的,只需要在元素監(jiān)聽 dragenter、dragleave、dragover 事件即可,然后通過 preventDefault 來阻止瀏覽器默認(rèn)行為??梢栽谶@三個事件中處理判斷當(dāng)前位置是否可以放置等等。
示例:
<script setup lang="ts"> // 進(jìn)入放置目標(biāo) const onDragenter = (e) => { e.preventDefault(); }; // 在目標(biāo)中移動 const onDragover = (e) => { e.preventDefault(); }; // 離開目標(biāo) const onDragleave = (e) => { e.preventDefault(); }; </script> <template> <div @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)"></div> </template>
上面的代碼已經(jīng)可以讓,元素可以拖拽,然后當(dāng)元素拖到可防止區(qū)域時候,可以看到鼠標(biāo)樣式會變?yōu)榭煞胖脴邮搅恕?/p>
Grid 布局
我們是需要進(jìn)行 Grid 拖拽布局,所以先對上面放置容器進(jìn)行改造,首先就是需要將容器進(jìn)行格子劃分區(qū)域顯示。
計算 Grid 格子大小
我這里直接使用了 @vueuse/core 的 useElementSize 的 hooks 去獲取容器元素大小變動,也可以自己通過 ResizeObserver 去監(jiān)聽元素變動,然后根據(jù)設(shè)置列數(shù)、行數(shù)、間隔去計算單個格子大小。
import { useElementSize } from "@vueuse/core"; /** * 容器等分尺寸 * @param {*} target 容器 HTML * @param {*} column 列數(shù) * @param {*} row 行數(shù) * @param {*} gap 間隔 * @returns */ export const useBoxSize = (target: Ref<HTMLElement | undefined>, column: number, row: number, gap: number) => { const { width, height } = useElementSize(target); return computed(() => ({ width: (width.value - (column - 1) * gap) / column, height: (height.value - (row - 1) * gap) / row, })); };
設(shè)置 Grid 樣式
根據(jù)列數(shù)和行數(shù)循環(huán)生成格子數(shù),rowCount、columnCount為行數(shù)和列數(shù)。
<div class="drop-content__drop-container" @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)"> <template v-for="x in rowCount"> <div class="bg-column" v-for="y in columnCount" :key="`${x}-${y}`"></div> </template> </div>
設(shè)置 Grid 樣式,下面變量中 gap 為格子間隔,repeat 是 Grid 用來重復(fù)設(shè)置相同值的,grid-template-columns: repeat(2,100px) 等效于 grid-template-columns: 100px 100px。因為我們只需在容器里監(jiān)聽拖拽放置事件,所以我們還需要將 所有的 bg-column 事件去掉,設(shè)置 pointer-events: none 即可。
.drop-content__drop-container { display: grid; row-gap: v-bind("gap+'px'"); column-gap: v-bind("gap+'px'"); grid-template-columns: repeat(v-bind("columnCount"), v-bind("boxSize.width+'px'")); grid-template-rows: repeat(v-bind("rowCount"), v-bind("boxSize.height+'px'")); .bg-column { background-color: #fff; border-radius: 6px; pointer-events: none; } }
效果如下:
放置元素
放置元素時我們需要先計算出元素在 Grid 位置信息等,這樣才知道元素應(yīng)該放置那哪個地方。
拖拽位置計算
當(dāng)元素拖拽進(jìn)容器中時,我們可以通過 offsetX、offsetY兩個數(shù)據(jù)獲取當(dāng)前鼠標(biāo)距離容器左上角位置距離,我們可以根據(jù)這兩個值計算出對應(yīng)的在 Grid 中做坐標(biāo)。
計算方式:
// 計算 x 坐標(biāo) const getX = (num) => parseInt(num / (boxSizeWidth + gap)); // 計算 y 坐標(biāo) const getY = (num) => parseInt(num / (boxSizeHeight + gap));
需要注意的是上面計算坐標(biāo)是 0,0 開始的,而 Grid 是 1,1 開始的。
獲取拖拽信息
我們在進(jìn)入容器時,通過上面封裝 dragData 來獲取當(dāng)前拖拽元素信息,獲取它尺寸信息等等。
// 拖拽中的元素 const current = reactive({ show: <boolean>false, id: <undefined | number>undefined, column: <number>0, // 寬 row: <number>0, // 高 x: <number>0, // 列 y: <number>0, // 行 }); // 進(jìn)入放置目標(biāo) const onDragenter = (e) => { e.preventDefault(); const dragData = dragStore.get(props.groupName); if (dragData) { current.column = dragData.column; current.row = dragData.row; current.x = getX(e.offsetX); current.y = getY(e.offsetY); current.show = true; } }; // 在目標(biāo)中移動 const onDragover = (e) => { e.preventDefault(); const dragData = dragStore.get(props.groupName); if (dragData) { current.x = getX(e.offsetX); current.y = getY(e.offsetY); } }; const onDragleave = (e) => { e.preventDefault(); current.show = false; current.id = undefined; };
在 drop 事件中,我們將當(dāng)前拖拽元素存放起來,list 會存放每一次拖拽進(jìn)來元素信息。
計算碰撞
在上面還需要計算當(dāng)前拖拽的位置是否可以放置,需要處理是否包含在容器內(nèi),是否與其他已放置元素存在重疊等等。
計算是否在容器內(nèi)
這個是比較好計算的,只需要當(dāng)前拖拽位置左上角坐標(biāo) >= 容器左上角的坐標(biāo),然后右下角的坐標(biāo) <= 容器的右下角的坐標(biāo),就是在容器內(nèi)的。
代碼實現(xiàn):
const list = ref([]); // 放置在目標(biāo)上 const onDrop = async (e) => { e.preventDefault(); current.show = false; const item = dragStore.get(props.groupName); list.value.push({ ...item, x: current.x, y: current.y, id: new Date().getTime(), }); };
計算是否與現(xiàn)有的相交
兩個矩形相交情況有很多種,計算比較麻煩,但是我們可以計算他們不相交,然后在取反方式判斷是否相交。
不相交情況只有四種,假設(shè)有 p1、p2 連個矩形,它們不相交的情況只有四種:
- p1 在 p2 左邊
- p1 在 p2 右邊
- p1 在 p2 上邊
- p1 在 p2 下邊
代碼實現(xiàn):
/** * 判斷是否在當(dāng)前四邊形內(nèi) * @param {*} p1 父容器 * @param {*} p2 * 對應(yīng)是 左上角坐標(biāo) 和 右下角坐標(biāo) * [0,0,1,1] => 左上角坐標(biāo) 0,0 右下角 1,1 */ export const booleanWithin = (p1: [number, number, number, number], p2: [number, number, number, number]) => { return p1[0] <= p2[0] && p1[1] <= p2[1] && p1[2] >= p2[2] && p1[3] >= p2[3]; };
在放置前判斷
可以通過計算屬性去計算,在后面拖拽中處理樣式也可以用到。修改 drop 中方法,然后在 drop 中根據(jù) isPutDown 是否有效。
// 是否可以放置 const isPutDown = computed(() => { const currentXy = [current.x, current.y, current.x + current.column, current.y + current.row]; return ( booleanWithin([0, 0, columnCount.value, rowCount.value], currentXy) && // list.value.every((item) => item.id === current.id || !booleanIntersects([item.x, item.y, item.x + item.column, item.y + item.row], currentXy)) ); });
拖拽時樣式
上處理了基本拖放數(shù)據(jù)處理邏輯,為了更好的交互,我們可以在拖拽中顯示元素預(yù)占位信息,更加直觀的顯示元素占位大小,類似這樣:
我們可以根據(jù)上面 current 中信息去計算大小信息,還可以根據(jù) isPutDown 去判斷當(dāng)前位置是否可以放置,用來顯示不同交互效果。
可以直接通過 Grid 的 grid-area 屬性,快速計算出放置位置信息,應(yīng)為我們上面計算的 x 、y 是從 0 開始的,所以這里需要 +1。
grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`
預(yù)覽容器
在元素放置后,我們還需要根據(jù) list 中數(shù)據(jù),生成元素占位樣式處理,我們可以拖拽容器上層在放置一個容器,專門用來顯示放置后的樣式,也是可以直接使用 Grid 布局去處理。
預(yù)覽樣式
樣式基本上和 drop-container 樣式抱持一致即可,需要注意的時需要為預(yù)覽容器設(shè)置 pointer-events: none,避免遮擋了 drop-container 事件監(jiān)聽。
.drop-content__preview, .drop-content__drop-container { // ... }
每個元素位置信息計算方式,基本和拖拽時樣式計算方式一致,直接通過 grid-area 去布局就可以了。
grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`
二次拖拽
當(dāng)元素拖拽進(jìn)來后,我們還需要對放置的元素支持繼續(xù)拖拽。因為上面我們將預(yù)覽事件通過 pointer-events 去除了,所以我們需要給每個子元素都加上去。然后給子元素添加 draggable=true,然后處理拖拽事件,基本上和上面處理方式一樣,在 dragstart、dragend 處理拖拽元素信息。
然后我們還需在 onDrop 進(jìn)行一番修改,如果是二次拖拽時只需要修改坐標(biāo)信息,修改原 onDrop 處理方式:
if (item.id) { item.x = current.x; item.y = current.y; } else { list.value.push({ ...item, x: current.x, y: current.y, id: new Date().getTime(), }); }
位置偏移優(yōu)化
當(dāng)你對元素二次拖拽時,會發(fā)現(xiàn)元素會存在偏移問。比如你放置了一個 1x2 元素后,當(dāng)你從下面拖拽,你會發(fā)現(xiàn)拖拽中的占位樣式和你拖拽元素位置存在偏差。
效果如下圖
出現(xiàn)這情況應(yīng)為上面我們時根據(jù)鼠標(biāo)位置為左上角進(jìn)行計算的,所以會存在這種偏差問題,我們可在拖拽前計算出偏移量來校正位置。
我們可以在二次拖拽時,獲取到鼠標(biāo)在當(dāng)前元素內(nèi)位置信息
const onDragstart = (e) => { const data = props.data; data.offsetX = e.offsetX; data.offsetY = e.offsetY; dragStore.set(props.groupName, data); };
在 drop-container 內(nèi)計算 x、y 值時候減去偏移量,對 onDragenter、onDragover 進(jìn)行如下調(diào)整修改
current.x = getX(e.offsetX) - getX(dragData?.offsetX ?? 0); current.y = getY(e.offsetY) - getY(dragData?.offsetY ?? 0);
拖拽元素優(yōu)化
因為上面我們將預(yù)覽元素添加了 pointer-events: all,所以在我們拖拽到現(xiàn)有元素上時,會擋住 drop-container 事件的觸發(fā),在二次拖拽時,比如將一個 2x2 元素我們需要往下移動一格時,會發(fā)現(xiàn)也會被自己擋住。
預(yù)覽元素遮擋問題,可以在拖拽時將其他元素都設(shè)置為 none,二次拖拽時要做自己設(shè)置為 all 否則會無法拖拽
:style="{ pointerEvents: current.show && item.id !== current.id ? 'none' : 'all' }"`
二次拖拽時自己位置遮擋問題 我們可以在拖拽時增加標(biāo)識,將自己通過 transform 移除到多拽容器外去
moveing.value ? { opacity: 0, transform: `translate(-999999999px, -9999999999px)`, } : {};
結(jié)語
到目前為止基本上的 Grid 拖拽布局大致實現(xiàn)了,已經(jīng)滿足基本業(yè)務(wù)需求了,當(dāng)然有需要朋友還可以在上面增加支持拖拉調(diào)整大小、碰撞后自動調(diào)整位置等等。
以上就是vue實現(xiàn)一個簡單的Grid拖拽布局的詳細(xì)內(nèi)容,更多關(guān)于vue Grid拖拽布局的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue中render函數(shù)和h函數(shù)以及jsx的使用方式
這篇文章主要介紹了vue中render函數(shù)和h函數(shù)以及jsx的使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08Vuejs仿網(wǎng)易云音樂實現(xiàn)聽歌及搜索功能
這篇文章主要介紹了Vuejs仿網(wǎng)易云音樂實現(xiàn)聽歌及搜索功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-03-03Vue2.x 項目性能優(yōu)化之代碼優(yōu)化的實現(xiàn)
這篇文章主要介紹了Vue2.x 項目性能優(yōu)化之代碼優(yōu)化的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03antd Form組件方法getFieldsValue獲取自定義組件的值操作
這篇文章主要介紹了antd Form組件方法getFieldsValue獲取自定義組件的值操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10Vue局部組件數(shù)據(jù)共享Vue.observable()的使用
隨著組件的細(xì)化,就會遇到多組件狀態(tài)共享的情況,今天我們介紹的是 vue.js 2.6 新增加的 Observable API ,通過使用這個 api 我們可以應(yīng)對一些簡單的跨組件數(shù)據(jù)狀態(tài)共享的情況,感興趣的可以了解一下2021-06-06vue3+ts+elementui-plus二次封裝彈框?qū)崙?zhàn)教程
這篇文章主要介紹了vue3+ts+elementui-plus二次封裝彈框?qū)崙?zhàn)教程,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07vue日常開發(fā)基礎(chǔ)Axios網(wǎng)絡(luò)庫封裝
這篇文章主要為大家介紹了vue日常開發(fā)基礎(chǔ)Axios網(wǎng)絡(luò)庫封裝示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08