HTML5拖拽API实现vue树形拖拽组件

12,057 阅读4分钟

因业务场景需要一个可拖拽修改节点位置的树形组件,因此动手撸了一个,乘此机会摸了一把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);
        }
      }
    }

作为子节点,改变层级

修改排序,将拖拽节点放在目标节点后面

修改排序,将拖拽节点放在目标节点前面

源码在此