該外掛vue-dragrid功能類似vue-gridlayout,預覽效果點選這裡。下面會一個個commit來進行詳細講解。
準備工作
- 先clone專案到本地。
git reset --hard commit
命令可以使當前head指向某個commit。
完成html的基本佈局
點選複製按鈕來複制整個commit id。然後在專案根路徑下執行git reset
。用瀏覽器開啟index.html來預覽效果,該外掛的html主要結果如下:
<!-- 節點容器 -->
<div class="dragrid">
<!-- 可拖拽的節點,使用translate控制位移 -->
<div class="dragrid-item" style="transform: translate(0px, 0px)">
<!-- 通過slot可以插入動態內容 -->
<div class="dragrid-item-content">
</div>
<!-- 拖拽控制程式碼 -->
<div class="dragrid-drag-bar"></div>
<!-- 縮放控制程式碼 -->
<div class="dragrid-resize-bar"></div>
</div>
</div>
複製程式碼
使用vue完成nodes簡單排版
先切換commit,安裝需要的包,執行如下命令:
git reset --hard 83842ea107e7d819761f25bf06bfc545102b2944
npm install
<!-- 啟動,埠為7777,在package.json中可以修改 -->
npm start
複製程式碼
這一步一個是搭建環境,這個直接看webpack.config.js配置檔案就可以了。
另一個就是節點的排版(layout),主要思路是把節點容器看成一個網格,每個節點就可以通過橫座標(x)和縱座標(y)來控制節點的位置,左上角座標為(0, 0);通過寬(w)和高(h)來控制節點大小;每個節點還必須有一個唯一的id。這樣節點node的資料結構就為:
{
id: "uuid",
x: 0,
y: 0,
w: 6,
h: 8
}
複製程式碼
其中w和h的值為所佔網格的格數,例如容器是24格,且寬度為960px,每格寬度就為40px,則上面節點渲染為240px * 320px, 且在容器左上角。
來看一下dragrid.vue與之對應的邏輯:
computed: {
cfg() {
let cfg = Object.assign({}, config);
cfg.cellW = Math.floor(this.containerWidth / cfg.col);
cfg.cellH = cfg.cellW; // 1:1
return cfg;
}
},
methods: {
getStyle(node) {
return {
width: node.w * this.cfg.cellW + 'px',
height: node.h * this.cfg.cellH + 'px',
transform: "translate("+ node.x * this.cfg.cellW +"px, "+ node.y * this.cfg.cellH +"px)"
};
}
}
複製程式碼
其中cellW、cellH為每個格子的寬和高,這樣計算節點的寬和高及位移就很容易了。
完成單個節點的拖拽
拖拽事件
- 使用mousedown、mousemove、mouseup來實現拖拽。
- 這些事件繫結在document上,只需要繫結一次就可以。
執行流程大致如下:
滑鼠在拖拽控制程式碼上按下,onMouseDown
方法觸發,在eventHandler中儲存一些值之後,滑鼠移動則觸發onMouseMove
方法,第一次進入時eventHandler.drag
為false,其中isDrag方法會根據位移來判斷是否是拖拽行為(橫向或縱向移動5畫素),如果是拖拽行為,則將drag屬性設定為true,同時執行dragdrop.dragStart
方法(一次拖拽行為只會執行一次),之後滑鼠繼續移動,則就開始執行dragdrop.drag
方法了。最後滑鼠鬆開後,會執行onMouseUp
方法,將一些狀態重置回初始狀態,同時執行dragdrop.dragEnd
方法。
拖拽節點
拖拽節點的邏輯都封裝在dragdrop.js這個檔案裡,主要方法為dragStart
、drag
、dragEnd
。
dragStart
在一次拖拽行為中,該方法只執行一次,因此適合做一些初始化工作,此時程式碼如下:
dragStart(el, offsetX, offsetY) {
// 要拖拽的節點
const dragNode = utils.searchUp(el, 'dragrid-item');
// 容器
const dragContainer = utils.searchUp(el, 'dragrid');
// 拖拽例項
const instance = cache.get(dragContainer.getAttribute('name'));
// 拖拽節點
const dragdrop = dragContainer.querySelector('.dragrid-dragdrop');
// 拖拽節點id
const dragNodeId = dragNode.getAttribute('dg-id');
// 設定拖拽節點
dragdrop.setAttribute('style', dragNode.getAttribute('style'));
dragdrop.innerHTML = dragNode.innerHTML;
instance.current = dragNodeId;
const offset = utils.getOffset(el, dragNode, {offsetX, offsetY});
// 容器偏移
const containerOffset = dragContainer.getBoundingClientRect();
// 快取資料
this.offsetX = offset.offsetX;
this.offsetY = offset.offsetY;
this.dragrid = instance;
this.dragElement = dragdrop;
this.dragContainer = dragContainer;
this.containerOffset = containerOffset;
}
複製程式碼
- 引數el為拖拽控制程式碼元素,offsetX為滑鼠距離拖拽控制程式碼的橫向偏移,offsetY為滑鼠距離拖拽控制程式碼的縱向偏移。
- 通過el可以向上遞迴查詢到拖拽節點(dragNode),及拖拽容器(dragContainer)。
- dragdrop元素是真正滑鼠控制拖拽的節點,同時與之對應的佈局節點會變為佔位節點(placeholder),視覺上顯示為陰影效果。
- 設定拖拽節點其實就將點選的dragNode的innerHTML設定到dragdrop中,同時將樣式也應用過去。
- 拖拽例項,其實就是dragrid.vue例項,它在created鉤子函式中將其例項快取到cache中,在這裡根據name就可以從cache中得到該例項,從而可以呼叫該例項中的方法了。
instance.current = dragNodeId;
設定之後,dragdrop節點及placeholder節點的樣式就應用了。- 快取資料中的offsetX、offsetY是拖拽控制程式碼相對於節點左上角的偏移。
drag
發生拖拽行為之後,滑鼠move都會執行該方法,通過不斷更新拖拽節點的樣式來是節點發生移動效果。
drag(event) {
const pageX = event.pageX, pageY = event.pageY;
const x = pageX - this.containerOffset.left - this.offsetX,
y = pageY - this.containerOffset.top - this.offsetY;
this.dragElement.style.cssText += ';transform:translate('+ x +'px, '+ y +'px)';
}
複製程式碼
主要是計算節點相對於容器的偏移:滑鼠距離頁面距離-容器偏移-滑鼠距離拽節點距離就為節點距離容器的距離。
dragEnd
主要是重置狀態。邏輯比較簡單,就不再細說了。
到這裡已經單個節點已經可以跟隨滑鼠進行移動了。
使placeholder可以跟隨拖拽節點運動
本節是要講佔位節點(placeholder陰影部分)跟隨拖拽節點一起移動。主要思路是:
- 通過拖拽節點距離容器的偏移(drag方法中的x, y),可以將其轉化為對應網格的座標。
- 轉化後的座標如果發生變化,則更新佔位節點的座標。
drag方法中增加的程式碼如下:
// 座標轉換
const nodeX = Math.round(x / opt.cellW);
const nodeY = Math.round(y / opt.cellH);
let currentNode = this.dragrid.currentNode;
// 發生移動
if(currentNode.x !== nodeX || currentNode.y !== nodeY) {
currentNode.x = nodeX;
currentNode.y = nodeY;
}
複製程式碼
nodes重排及上移
本節核心點有兩個:
- 用一個二維陣列來表示網格,這樣節點的位置資訊就可以在此二維陣列中標記出來了。
- nodes中只要某個節點發生變化,就要重新排版,要將每個節點儘可能地上移。
二維陣列的構建
getArea(nodes) {
let area = [];
nodes.forEach(n => {
for(let row = n.y; row < n.y + n.h; row++){
let rowArr = area[row];
if(rowArr === undefined){
area[row] = new Array();
}
for(let col = n.x; col < n.x + n.w; col++){
area[row][col] = n.id;
}
}
});
return area;
}
複製程式碼
按需可以動態擴充套件該二維資料,如果某行沒有任何節點佔位,則實際儲存的是一個undefined值。否則儲存的是節點的id值。
佈局方法
dragird.vue中watch了nodes,發生變化後會呼叫layout方法,程式碼如下:
/**
* 重新佈局
* 只要有一個節點發生變化,就要重新進行排版佈局
*/
layout() {
this.nodes.forEach(n => {
const y = this.moveup(n);
if(y < n.y){
n.y = y;
}
});
},
// 向上查詢節點可以冒泡到的位置
moveup(node) {
let area = this.area;
for(let row = node.y - 1; row > 0; row--){
// 如果一整行都為空,則直接繼續往上找
if(area[row] === undefined) continue;
for(let col = node.x; col < node.x + node.w; col++){
// 改行如果有內容,則直接返回下一行
if(area[row][col] !== undefined){
return row + 1;
}
}
}
return 0;
}
複製程式碼
佈局方法layout中遍歷所有節點,moveup方法返回該節點縱向可以上升到的位置座標,如果比實際座標小,則進行上移。moveup方法預設從上一行開始找,直到發現二維陣列中存放了值(改行已經有元素了),則返回此時行數加1。
到這裡,拖拽節點移動時,佔位節點會盡可能地上移,如果只有一個節點,那麼佔位節點一直在最上面移動。
相關節點的下移
拖拽節點移動時,與拖拽節點發生碰撞的節點及其下發的節點,都先下移一定距離,這樣拖拽節點就可以移到相應位置,最後節點都會發生上一節所說的上移。
請看dragrid.vue中的overlap方法:
overlap(node) {
// 下移節點
this.nodes.forEach(n => {
if(node !== n && n.y + n.h > node.y) {
n.y += node.h;
}
});
}
複製程式碼
n.y + n.h > node.y
表示可以與拖拽節點發生碰撞,以及在拖拽節點下方的節點。
在dragdrop.drag中會呼叫該方法。
注意目前該方法會有問題,沒有考慮到如果碰撞節點比較高,則n.y += node.h
並沒有將該節點下沉到拖拽節點下方,從而拖拽節點會疊加上去。後面會介紹解決方法。
縮放
上面的思路都理解之後,縮放其實也是一樣的,主要還是要進行座標轉換,座標發生變化後,就會呼叫overlap方法。
resize(event) {
const opt = this.dragrid.cfg;
// 之前
const x1 = this.currentNode.x * opt.cellW + this.offsetX,
y1 = this.currentNode.y * opt.cellH + this.offsetY;
// 之後
const x2 = event.pageX - this.containerOffset.left,
y2 = event.pageY - this.containerOffset.top;
// 偏移
const dx = x2 - x1, dy = y2 - y1;
// 新的節點寬和高
const w = this.currentNode.w * opt.cellW + dx,
h = this.currentNode.h * opt.cellH + dy;
// 樣式設定
this.dragElement.style.cssText += ';width:' + w + 'px;height:' + h + 'px;';
// 座標轉換
const nodeW = Math.round(w / opt.cellW);
const nodeH = Math.round(h / opt.cellH);
let currentNode = this.dragrid.currentNode;
// 發生移動
if(currentNode.w !== nodeW || currentNode.h !== nodeH) {
currentNode.w = nodeW;
currentNode.h = nodeH;
this.dragrid.overlap(currentNode);
}
}
複製程式碼
根據滑鼠距拖拽容器的距離的偏移,來修改節點的大小(寬和高),其中x1為滑鼠點選後距離容器的距離,x2為移動一段距離之後距離容器的距離,那麼差值dx就為滑鼠移動的距離,dy同理。
到這裡,外掛的核心邏輯基本上已經完成了。
[fix]解決碰撞位置靠上的大塊,並沒有下移的問題
overlap修改為:
overlap(node) {
let offsetUpY = 0;
// 碰撞檢測,查詢一起碰撞節點裡面,位置最靠上的那個
this.nodes.forEach(n => {
if(node !== n && this.checkHit(node, n)){
const value = node.y - n.y;
offsetUpY = value > offsetUpY ? value : offsetUpY;
}
});
// 下移節點
this.nodes.forEach(n => {
if(node !== n && n.y + n.h > node.y) {
n.y += (node.h + offsetUpY);
}
});
}
複製程式碼
offsetUpY 最終存放的是與拖拽節點發生碰撞的所有節點中,位置最靠上的節點與拖拽節點之間的距離。然後再下移過程中會加上該offsetUpY值,確保所有節點下移到拖拽節點下方。
這個外掛的核心邏輯就說到這裡了,讀者可以自己解決如下一些問題:
- 縮放限制,達到最小寬度就不能再繼續縮放了。
- 拖拽控制滾動條。
- 拖拽邊界的限制。
- 向下拖拽,達到碰撞節點1/2高度就發生換位。