HTML5拖拽API實現vue樹形拖拽元件

花捲愛學習發表於2018-05-10

因業務場景需要一個可拖拽修改節點位置的樹形元件,因此動手擼了一個,乘此機會摸了一把html5原生拖拽。近期有時間將核心部分程式碼抽出,簡單說下實現方式。

1.樹形結構-元件遞迴使用

樹形結構非常簡單,tree元件作為父元件,結構如下

tree.vue


<template>
  <div>
    <Tree-Node v-for="item in data" :key="item.title" :node-data="item"></Tree-Node>
  </div>
</template>

複製程式碼

vue元件允許在它們自己的模板中呼叫自身,因此可以形成樹形結構,在元件中必須填寫唯一的name。

tree-node.vue

<template>
  <transition name="slide-up">
    <ul :class="classes">
      <li>
        <div :class="[prefixCls + '-item']">
          <i class="sp-icon sp-icon-arrow-right" :class="arrowClasses" @click.stop="toggleCollapseStatus()"></i>
          <span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
            <span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
          </span>
        </div>
        <Tree-Node v-for="item in nodeData.children" :key="item.title" :node-data="item" v-show="nodeData.children.length && nodeData.isExpand"></Tree-Node>
      </li>
    </ul>
  </transition>
</template>

複製程式碼

2.HTML5拖拽api

1.draggable屬性規定元素是否可拖動,目前Internet Explorer 9+, Firefox, Opera, Chrome, and Safari 支援 draggable 屬性 2.HTML 5 拖放api

  • ondragstart: 元素開始被拖動時觸發 作用在拖拽元素上
  • ondragenter:當拖曳元素進入目標元素的時候觸發的事件,作用在目標元素上
  • ondragover:拖拽元素在目標元素上移動的時候觸發的事件,作用在目標元素上
  • ondragleave:拖拽元素拖離開了目標元素時觸發,作用在目標元素上
  • ondrop:被拖拽的元素在目標元素上同時滑鼠放開觸發的事件,作用在目標元素上
  • ondragend:當拖拽完成後觸發的事件,作用在被拖曳元素上

3.拖拽節點

定義變數

處理拖拽節點需要幾個關鍵變數

  • 當前拖拽的節點
  • 拖拽時經過的節點
  • 最終放置的節點

因此定義了一個用於儲存拖拽資訊的物件

dragOverStatus: {
    overNodeKey: "",
    dropPosition: "",
    dragNode: {}
}
複製程式碼

繫結拖拽事件

這裡將ondragstart事件繫結在子元素上,將其他事件繫結在父元素上,因為在測試真機IE10的時候,發現ondragstart和其他事件繫結在同一個元素上,無法觸發ondragenter等事件。

<span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
    <span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
</span>

複製程式碼
 mounted() {
    //繫結拖拽事件
    if (this.root.draggable) {
      this.$refs.draggAbleDom.draggable = !this.nodeData.noDrag;
      this.$refs.draggAbleDom.ondragstart = this.onDragStart;

      this.$refs.dropTarget.ondragenter = this.onDragEnter;
      this.$refs.dropTarget.ondragover = this.onDragOver;
      this.$refs.dropTarget.ondragleave = this.onDragLeave;
      this.$refs.dropTarget.ondrop = this.onDrop;
      this.$refs.dropTarget.ondragend = this.onDragEnd;
    }
  }
複製程式碼

觸發某節點的拖拽事件時,就可以從拖拽事件裡拿到當前節點例項。 使用HTML5提供的專門的拖拽與拖放API,原生的實現了複雜的操作,不需要自己用滑鼠事件模擬,因此實現拖拽效果非常簡單。

(1).開始拖拽:在拖拽元素上觸發,事件內只需要儲存當前拖拽節點的資訊即可

onDragStart(e, treeNode) {
      this.dragOverStatus.dragNode = {
        nodeData: treeNode.nodeData,
        parentNode: treeNode.parentNodeData
      };
      this.$emit("on-dragStart", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
    }
複製程式碼

(2).進入目標節點:在目標元素上觸發,主要儲存當前經過的節點的key,然後向外層發出事件,供元件呼叫者做其他操作。為了避免拖拽一個元素快速經過許多個節點時頻繁發出事件,設定定時器當停留一定時間後觸發。

onDragEnter(e, treeNode) {
      //當沒有設定拖拽節點時,禁止作為目標節點
      if (!this.hasDragNode()) {
        return;
      }
      this.dragOverStatus.overNodeKey = "";
      //拖拽節點與目標節點是同一個,return掉
      if (
        treeNode.nodeData._hash === this.dragOverStatus.dragNode.nodeData._hash
      ) {
        return;
      }
      this.dragOverStatus.overNodeKey = treeNode.nodeData._hash; //當前經過的可放置的節點的key
      //當前節點禁止做為放置節點時
      if (treeNode.nodeData.noDrop) {
        return;
      }
      //設定dragEnter定時器,停留250毫秒後觸發事件
      if (!this.delayedDragEnterLogic) {
        this.delayedDragEnterLogic = {};
      }
      Object.keys(this.delayedDragEnterLogic).forEach(key => {
        clearTimeout(this.delayedDragEnterLogic[key]);
      });
      this.delayedDragEnterLogic[
        treeNode.nodeData._hash
      ] = setTimeout(() => {
        if (!treeNode.nodeData.isExpand) {
          treeNode.toggleCollapseStatus();
        }
        this.$emit("on-dragEnter", {
          treeNode: treeNode.nodeData,
          parentNode: treeNode.parentNodeData,
          event: e
        });
      }, 250);
    }
複製程式碼

(3).在目標節點上經過:在目標元素上觸發,即時計算滑鼠在目標節點上的位置,用於判斷最終的放置位置,0(作為目標節點的子節點),-1(放置在目標節點的前面),1(放置在目標節點的後面),顯示相應的樣式。

onDragOver(e, treeNode) {
      //當沒有設定拖拽節點時,禁止作為目標節點
      if (!this.hasDragNode()) {
        return;
      }
      if (
        this.dragOverStatus.overNodeKey === treeNode.nodeData._hash
      ) {
        this.dragOverStatus.dropPosition = this.calDropPosition(e); //放置標識0,-1,1
      }
      this.$emit("on-dragOver", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
      this.dragOverClass = this.setDragOverClass();//設定滑鼠經過樣式
    },
複製程式碼

當滑鼠處於目標節點內目標節點偏上方(1/5處),則意為放在目標節點前面-同級,當滑鼠處於目標節點內目標節點偏下方(1/5處),意為放在目標節點後面-同級,否則作為目標節點的子節點

calDropPosition(e) {
      var offsetTop = this.getOffset(e.target).top;
      var offsetHeight = e.target.offsetHeight;
      var pageY = e.pageY;
      var gapHeight = 0.2 * offsetHeight;
      if (pageY > offsetTop + offsetHeight - gapHeight) {
        //放在目標節點後面-同級
        return 1;
      }
      if (pageY < offsetTop + gapHeight) {
        //放在目標節點前面-同級
        return -1;
      }
      //放在目標節點裡面-作為子節點
      return 0;
    }
複製程式碼

(4).放置節點:在目標元素上觸發,此時將拖拽的資訊變數作為引數將事件發射到外層,其餘操作由外層來決定即可。

onDrop(e, treeNode) {
      //當沒有設定拖拽節點時,禁止作為目標節點
      if (!this.hasDragNode()) {
        return;
      }
      //當前節點禁止拖拽時
      if (treeNode.nodeData.noDrop) {
        return;
      }
      //拖拽節點與目標節點是同一個,不做任何操作
      if (
        this.dragOverStatus.dragNode.nodeData._hash === treeNode.nodeData._hash
      ) {
        return;
      }

      var res = {
        event: e,
        dragNode: this.dragOverStatus.dragNode,
        dropNode: {
          nodeData: treeNode.nodeData,
          parentNode: treeNode.parentNodeData
        },
        dropPosition: this.dragOverStatus.dropPosition
      };
      this.$emit("on-drop", res);
    }
複製程式碼

(5).拖拽結束:作用在拖拽元素上,拖拽結束後將清除變數,恢復樣式。

onDragEnd(e, treeNode) {
      //當沒有設定拖拽節點時,禁止作為目標節點
      if (!this.hasDragNode()) {
        return;
      }
      //當前節點禁止拖拽時
      if (treeNode.nodeData.noDrop) {
        return true;
      }
      this.dragOverStatus.dragNode = null;
      this.dragOverStatus.overNodeKey = "";

      this.$emit("on-dragEnd", {
        treeNode: treeNode.nodeData,
        parentNode: treeNode.parentNodeData,
        event: e
      });
    }
複製程式碼

4.應用

呼叫樹形拖拽元件,獲取拖拽過程中的拖拽節點,目標節點,以及放置位置,具體處理拖拽結果由呼叫方決定,可以是通過調介面更新樹結構,也可以由前端處理輸入資料,更新檢視。

<template>
    <Tree :data="data1" draggable @on-drop="getDropData">
    </Tree>
</template>
複製程式碼
getDropData(info) {
      var dragData = info.dragNode.nodeData;
      var dragParent = info.dragNode.parentNode;
      var dropData = info.dropNode.nodeData;
      var dropParent = info.dropNode.parentNode;
      var dropPosition = info.dropPosition; //0作為子級,-1放在目標節點前面,1放在目標節點後面

      //把拖拽元素從父節點中刪除
      dragParent.children.splice(dragParent.children.indexOf(dragData), 1);
      if (dropPosition === 0) {
        dropData.children.push(dragData);
      } else {
        var index = dropParent.children.indexOf(dropData);
        if (dropPosition === -1) {
          dropParent.children.splice(index, 0, dragData);
        } else {
          dropParent.children.splice(index + 1, 0, dragData);
        }
      }
    }
複製程式碼

作為子節點,改變層級

HTML5拖拽API實現vue樹形拖拽元件

HTML5拖拽API實現vue樹形拖拽元件

修改排序,將拖拽節點放在目標節點後面

HTML5拖拽API實現vue樹形拖拽元件

HTML5拖拽API實現vue樹形拖拽元件
修改排序,將拖拽節點放在目標節點前面

HTML5拖拽API實現vue樹形拖拽元件

HTML5拖拽API實現vue樹形拖拽元件

原始碼在此

相關文章