使用vue實現grid-layout功能

該插件vue-dragrid功能相似vue-gridlayout,預覽效果點擊這裏。下面會一個個commit來進行詳細講解。javascript

準備工做

  1. 先clone項目到本地。
  2. git reset --hard commit命令可使當前head指向某個commit。

完成html的基本佈局

點擊複製按鈕來複制整個commit id。而後在項目根路徑下運行git reset。用瀏覽器打開index.html來預覽效果,該插件的html主要結果以下:css

<!-- 節點容器 -->
<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,安裝須要的包,運行以下命令:html

git reset --hard 83842ea107e7d819761f25bf06bfc545102b2944
npm install
<!-- 啓動,端口爲7777,在package.json中能夠修改 -->
npm start 
複製代碼

這一步一個是搭建環境,這個直接看webpack.config.js配置文件就能夠了。vue

另外一個就是節點的排版(layout),主要思路是把節點容器當作一個網格,每一個節點就能夠經過橫座標(x)和縱座標(y)來控制節點的位置,左上角座標爲(0, 0);經過寬(w)和高(h)來控制節點大小;每一個節點還必須有一個惟一的id。這樣節點node的數據結構就爲:java

{
  id: "uuid",
  x: 0,
  y: 0,
  w: 6,
  h: 8
}
複製代碼

其中w和h的值爲所佔網格的格數,例如容器是24格,且寬度爲960px,每格寬度就爲40px,則上面節點渲染爲240px * 320px, 且在容器左上角。node

來看一下dragrid.vue與之對應的邏輯:webpack

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爲每一個格子的寬和高,這樣計算節點的寬和高及位移就很容易了。git

完成單個節點的拖拽

拖拽事件

  1. 使用mousedown、mousemove、mouseup來實現拖拽。
  2. 這些事件綁定在document上,只須要綁定一次就能夠。

執行流程大體以下:github

鼠標在拖拽句柄上按下,onMouseDown方法觸發,在eventHandler中存儲一些值以後,鼠標移動則觸發onMouseMove方法,第一次進入時eventHandler.drag爲false,其中isDrag方法會根據位移來判斷是不是拖拽行爲(橫向或縱向移動5像素),若是是拖拽行爲,則將drag屬性設置爲true,同時執行dragdrop.dragStart方法(一次拖拽行爲只會執行一次),以後鼠標繼續移動,則就開始執行dragdrop.drag方法了。最後鼠標鬆開後,會執行onMouseUp方法,將一些狀態重置回初始狀態,同時執行dragdrop.dragEnd方法。web

拖拽節點

拖拽節點的邏輯都封裝在dragdrop.js這個文件裏,主要方法爲dragStartdragdragEnd

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;
}
複製代碼
  1. 參數el爲拖拽句柄元素,offsetX爲鼠標距離拖拽句柄的橫向偏移,offsetY爲鼠標距離拖拽句柄的縱向偏移。
  2. 經過el能夠向上遞歸查找到拖拽節點(dragNode),及拖拽容器(dragContainer)。
  3. dragdrop元素是真正鼠標控制拖拽的節點,同時與之對應的佈局節點會變爲佔位節點(placeholder),視覺上顯示爲陰影效果。
  4. 設置拖拽節點其實就將點擊的dragNode的innerHTML設置到dragdrop中,同時將樣式也應用過去。
  5. 拖拽實例,其實就是dragrid.vue實例,它在created鉤子函數中將其實例緩存到cache中,在這裏根據name就能夠從cache中獲得該實例,從而能夠調用該實例中的方法了。
  6. instance.current = dragNodeId;設置以後,dragdrop節點及placeholder節點的樣式就應用了。
  7. 緩存數據中的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陰影部分)跟隨拖拽節點一塊兒移動。主要思路是:

  1. 經過拖拽節點距離容器的偏移(drag方法中的x, y),能夠將其轉化爲對應網格的座標。
  2. 轉化後的座標若是發生變化,則更新佔位節點的座標。

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重排及上移

本節核心點有兩個:

  1. 用一個二維數組來表示網格,這樣節點的位置信息就能夠在此二維數組中標記出來了。
  2. 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. 拖拽控制滾動條。
  3. 拖拽邊界的限制。
  4. 向下拖拽,達到碰撞節點1/2高度就發生換位。
相關文章
相關標籤/搜索