Browse Source

Tree: support drag and drop node (#9251)

Harlan 7 years ago
parent
commit
d7c4fd2632

+ 176 - 0
examples/docs/zh-CN/tree.md

@@ -151,6 +151,52 @@
     }]
   }];
 
+  const data6 = [{
+    id: 1,
+    label: '一级 1',
+    children: [{
+      id: 4,
+      label: '二级 1-1',
+      children: [{
+        id: 9,
+        label: '三级 1-1-1'
+      }, {
+        id: 10,
+        label: '三级 1-1-2'
+      }]
+    }]
+  }, {
+    id: 2,
+    label: '一级 2',
+    children: [{
+      id: 5,
+      label: '二级 2-1'
+    }, {
+      id: 6,
+      label: '二级 2-2'
+    }]
+  }, {
+    id: 3,
+    label: '一级 3',
+    children: [{
+      id: 7,
+      label: '二级 3-1'
+    }, {
+      id: 8,
+      label: '二级 3-2',
+      children: [{
+       id: 11,
+        label: '三级 3-2-1'
+      }, {
+        id: 12,
+        label: '三级 3-2-2'
+      }, {
+        id: 13,
+        label: '三级 3-2-3'
+      }]
+    }]
+  }];
+
   let id = 1000;
 
   const regions = [{
@@ -191,6 +237,27 @@
       handleNodeClick(data) {
         console.log(data);
       },
+      handleDragStart(node, ev) {
+        console.log('drag start', node);
+      },
+      handleDragEnter(node, ev) {
+        console.log('tree drag enter: ', node.label);
+      },
+      handleDragLeave(node, ev) {
+        console.log('tree drag leave: ', node.label);
+      },
+      handleDragEnd(from, target, position, ev) {
+        console.log('tree drag end: ', target.label);
+        if (position !== null) {
+          console.log(`target position: parent node: ${position.parent.label}, index: ${position.index}`);
+        }
+      },
+      allowDrop(from, target) {
+        return target.data.label !== '二级 3-1';
+      },
+      allowDrag(node) {
+        return node.data.label.indexOf('三级 3-1-1') === -1;
+      },
       loadNode(node, resolve) {
         if (node.level === 0) {
           return resolve([{ name: 'region1' }, { name: 'region2' }]);
@@ -300,6 +367,7 @@
         data3,
         data4: JSON.parse(JSON.stringify(data2)),
         data5: JSON.parse(JSON.stringify(data2)),
+        data6,
         regions,
         defaultProps,
         props,
@@ -995,6 +1063,107 @@
 ```
 :::
 
+### 可拖拽节点
+
+通过draggable属性可让节点变为可拖拽,节点只能放到相同level节点旁边。
+
+:::demo
+```html
+<el-tree
+  :data="data6"
+  node-key="id"
+  default-expand-all
+  @node-drag-start="handleDragStart"
+  @node-drag-enter="handleDragEnter"
+  @node-drag-leave="handleDragLeave"
+  @node-drag-end="handleDragEnd"
+  draggable
+  :allow-drop="allowDrop"
+  :allow-drag="allowDrag">
+</el-tree>
+
+<script>
+  export default {
+    data() {
+      return {
+        data6: [{
+          id: 1,
+          label: '一级 1',
+          children: [{
+            id: 4,
+            label: '二级 1-1',
+            children: [{
+              id: 9,
+              label: '三级 1-1-1'
+            }, {
+              id: 10,
+              label: '三级 1-1-2'
+            }]
+          }]
+        }, {
+          id: 2,
+          label: '一级 2',
+          children: [{
+            id: 5,
+            label: '二级 2-1'
+          }, {
+            id: 6,
+            label: '二级 2-2'
+          }]
+        }, {
+          id: 3,
+          label: '一级 3',
+          children: [{
+            id: 7,
+            label: '二级 3-1'
+          }, {
+            id: 8,
+            label: '二级 3-2',
+            children: [{
+             id: 11,
+              label: '三级 3-2-1'
+            }, {
+              id: 12,
+              label: '三级 3-2-2'
+            }, {
+              id: 13,
+              label: '三级 3-2-3'
+            }]
+          }]
+        }],
+        defaultProps: {
+          children: 'children',
+          label: 'label'
+        }
+      };
+    },
+    methods: {
+      handleDragStart(node, ev) {
+        console.log('drag start', node);
+      },
+      handleDragEnter(node, ev) {
+        console.log('tree drag enter: ', node.label);
+      },
+      handleDragLeave(node, ev) {
+        console.log('tree drag leave: ', node.label);
+      },
+      handleDragEnd(from, target, position, ev) {
+        console.log('tree drag end: ', target.label);
+        if (position !== null) {
+          console.log(`target position: parent node: ${position.parent.label}, index: ${position.index}`);
+        }
+      },
+      allowDrop(from, target) {
+        return target.data.label !== '二级 3-1';
+      },
+      allowDrag(node) {
+        return node.data.label.indexOf('三级 3-1-1') === -1;
+      },
+  };
+</script>
+```
+:::
+
 ### Attributes
 | 参数                  | 说明                                               | 类型                        | 可选值  | 默认值   |
 | --------------------- | ---------------------------------------- | --------------------------- | ---- | ----- |
@@ -1017,6 +1186,9 @@
 | accordion             | 是否每次只打开一个同级树节点展开                   | boolean                     | —    | false |
 | indent                | 相邻级节点间的水平缩进,单位为像素                 | number                     | —    | 16 |
 | lazy                  | 是否懒加载子节点,需与 load 方法结合使用           | boolean                     | —    | false |
+| draggable             | 是否开启拖拽节点功能                                   | boolean            | —    | false |
+| allow-drag            | 判断节点能否被拖拽                  | Function(Node)  | —  | —  |
+| allow-drop            | 拖拽时判定位置能否被放置             | Function(fromNode, toNode)  | —    | —     |
 
 ### props
 | 参数       | 说明                | 类型     | 可选值  | 默认值  |
@@ -1061,6 +1233,10 @@
 | current-change | 当前选中节点变化时触发的事件 | 共两个参数,依次为:当前节点的数据,当前节点的 Node 对象          |
 | node-expand    | 节点被展开时触发的事件    | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 |
 | node-collapse  | 节点被关闭时触发的事件    | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 |
+| node-drag-start| 节点开始拖拽时触发的事件  | 共两个参数,依次为:被拖拽节点对应的 Node、Vue传来的drag event。   |
+| node-drag-enter| 拖拽进入其他节点时触发的事件  | 共两个参数,依次为:所进入节点对应的 Node、Vue传来的drag event。   |
+| node-drag-leave| 拖拽离开某个节点时触发的事件  | 共两个参数,依次为:所离开节点对应的 Node、Vue传来的drag event。(注意:上个节点的leave事件有可能在下个节点enter之后执行)   |
+| node-drag-end  | 拖拽结束时触发的事件  | 共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后指向的节点、被拖拽节点的放置位置{ parent: 位置的父节点, index: 在父节点中的序号 }、Vue传来的drag event。|
 
 ### Scoped slot
 | name | 说明 |

+ 19 - 0
packages/theme-chalk/src/tree.scss

@@ -2,6 +2,7 @@
 @import "common/var";
 
 @include b(tree) {
+  position: relative;
   cursor: default;
   background: $--color-white;
   color: $--tree-text-color;
@@ -21,6 +22,13 @@
     transform: translate(-50%, -50%);
     color: mix($--color-primary, rgb(158, 68, 0), 50%);
   }
+
+  @include e(drag-indicator) {
+    position: absolute;
+    width: 100%;
+    height: 1px;
+    background-color: $--color-primary;
+  }
 }
 
 @include b(tree-node) {
@@ -46,6 +54,17 @@
     &:hover {
       background-color: $--tree-node-hover-color;
     }
+
+    .el-tree.dragging & {
+      cursor: move;
+
+      & * {
+        pointer-events: none;
+      }
+    }
+    .el-tree.dragging.drop-not-allow & {
+      cursor: not-allowed;
+    }
   }
 
   @include e(expand-icon) {

+ 5 - 0
packages/tree/src/model/tree-store.js

@@ -6,6 +6,11 @@ export default class TreeStore {
     this.currentNode = null;
     this.currentNodeKey = null;
 
+    this.dragSourceNode = null;
+    this.dragTargetNode = null;
+    this.dragTargetDom = null;
+    this.allowDrop = true;
+
     for (let option in options) {
       if (options.hasOwnProperty(option)) {
         this[option] = options[option];

+ 94 - 0
packages/tree/src/tree-node.vue

@@ -16,6 +16,14 @@
     :aria-expanded="expanded"
     :aria-disabled="node.disabled"
     :aria-checked="node.checked"
+    :draggable="tree.draggable"
+    @dragstart.stop="handleDragStart"
+    @dragenter.stop="handleDragEnter"
+    @dragleave.stop="handleDragLeave"
+    @dragover.stop="handleDragOver"
+    @dragend.stop="handleDragEnd"
+    @drop.stop="handleDrop"
+    ref="node"
   >
     <div class="el-tree-node__content"
       :style="{ 'padding-left': (node.level - 1) * tree.indent + 'px' }">
@@ -199,6 +207,92 @@
       handleChildNodeExpand(nodeData, node, instance) {
         this.broadcast('ElTreeNode', 'tree-node-expand', node);
         this.tree.$emit('node-expand', nodeData, node, instance);
+      },
+
+      handleDragStart(ev) {
+        if (typeof this.tree.allowDrag === 'function' && !this.tree.allowDrag(this.node)) {
+          ev.preventDefault();
+          return false;
+        }
+        ev.dataTransfer.effectAllowed = 'move';
+        ev.dataTransfer.setData('text/plain', this.node.label);
+        this.node.store.dragSourceNode = this.node;
+        this.node.store.dragFromDom = this.$refs.node;
+        this.node.store.allowDrop = true;
+        this.tree.$emit('node-drag-start', this.node, ev);
+      },
+
+      handleDragEnter(ev) {
+        ev.preventDefault();
+        const store = this.node.store;
+        const from = store.dragSourceNode;
+        let node = this.node;
+        let dom = this.$refs.node;
+
+        if (!from) return;
+
+        while (node.level > from.level && node.level > 1) {
+          node = node.parent
+          dom = this.$parent.$refs.node;
+        }
+        store.dragTargetNode = node;
+        store.dragTargetDom = dom;
+
+        if (!this.tree.dropAt) {
+          ev.dataTransfer.dropEffect = 'none';
+          store.allowDrop = false;
+        } else {
+          ev.dataTransfer.dropEffect = 'move';
+          store.allowDrop = true;
+        }
+
+        this.tree.$emit('node-drag-enter', this.node, ev);
+      },
+
+      handleDragLeave(ev) {
+        ev.preventDefault();
+        if (!this.node.store.dragSourceNode) return;
+        this.tree.$emit('node-drag-leave', this.node, ev);
+      },
+
+      handleDragOver(ev) {
+        ev.dataTransfer.dropEffect = this.node.store.allowDrop ? 'move' : 'none';
+        ev.preventDefault();
+      },
+
+      handleDrop(ev) {
+        ev.preventDefault();
+      },
+
+      handleDragEnd(ev) {
+        const from = this.node.store.dragSourceNode;
+        const target = this.node.store.dragTargetNode;
+        let position = this.tree.dropAt;
+
+        if (!from) return;
+
+        if (typeof this.tree.allowDrop === 'function' && !this.tree.allowDrop(from, target)) {
+          position = null;
+        }
+        ev.preventDefault();
+        ev.dataTransfer.dropEffect = 'move';
+
+        if (target && from && from !== target && position) {
+          const index = from.parent.childNodes.indexOf(from);
+          from.parent.childNodes.splice(index, 1);
+          if (from.parent.childNodes.length === 0) {
+            from.parent.isLeaf = true;
+          }
+          position.parent.childNodes.splice(position.index, 0, from);
+          from.parent = position.parent;
+          from.parent.isLeaf = false;
+        }
+        this.tree.$emit('node-drag-end', from, target, position, ev);
+        this.node.store.dragTargetNode = null;
+        this.node.store.dragSourceNode = null;
+        this.node.store.dragTargetDom = null;
+
+        return false;
       }
     },
 

+ 51 - 1
packages/tree/src/tree.vue

@@ -1,7 +1,11 @@
 <template>
   <div
     class="el-tree"
-    :class="{ 'el-tree--highlight-current': highlightCurrent }"
+    :class="{
+      'el-tree--highlight-current': highlightCurrent,
+      dragging: !!store.dragSourceNode,
+      'drop-not-allow': !store.allowDrop
+    }"
     role="tree"
   >
     <el-tree-node
@@ -16,6 +20,12 @@
     <div class="el-tree__empty-block" v-if="!root.childNodes || root.childNodes.length === 0">
       <span class="el-tree__empty-text">{{ emptyText }}</span>
     </div>
+    <div
+      v-if="!!dropAt"
+      class="el-tree__drag-indicator"
+      :style="{top: dragIndicatorOffset}"
+      ref="drag-indicator">
+    </div>
   </div>
 </template>
 
@@ -81,6 +91,12 @@
         type: Boolean,
         default: false
       },
+      draggable: {
+        type: Boolean,
+        default: false
+      },
+      allowDrag: Function,
+      allowDrop: Function,
       props: {
         default() {
           return {
@@ -116,6 +132,40 @@
       },
       treeItemArray() {
         return Array.prototype.slice.call(this.treeItems);
+      },
+      dragIndicatorOffset() {
+        if (!this.dropAt) return;
+
+        const dom = this.store.dragTargetDom;
+        if (this.store.dragSourceNode.level !== this.store.dragTargetNode.level) {
+          return (dom.offsetTop + dom.querySelector('.el-tree-node__content').scrollHeight) + 'px';
+        } else {
+          return (dom.offsetTop + dom.scrollHeight) + 'px';
+        }
+      },
+      dropAt() {
+        let target = this.store.dragTargetNode;
+        let from = this.store.dragSourceNode;
+        if (!target || !from) {
+          return null;
+        }
+        if (typeof this.allowDrop === 'function' && !this.allowDrop(from, target)) {
+          return null;
+        }
+
+        if (target.level === from.level - 1) {
+          return {
+            parent: target,
+            index: 0
+          };
+        }
+        if (target.level === from.level) {
+          return {
+            parent: target.parent,
+            index: target.parent.childNodes.indexOf(target) + 1
+          };
+        }
+        return null;
       }
     },