Przeglądaj źródła

Tree: update drag and drop logic (#10372)

FuryBean 7 lat temu
rodzic
commit
4fe58a3d96

+ 106 - 0
examples/docs/en-US/tree.md

@@ -996,6 +996,103 @@ Only one node among the same level can be expanded at one time.
 ```
 :::
 
+### Draggable
+
+Tree nodes can be drag and drop.
+
+:::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-over="handleDragOver"
+  @node-drag-end="handleDragEnd"
+  @node-drop="handleDrop"
+  draggable
+  :allow-drop="allowDrop"
+  :allow-drag="allowDrag">
+</el-tree>
+
+<script>
+  export default {
+    data() {
+      return {
+        data6: [{
+          label: 'Level one 1',
+          children: [{
+            label: 'Level two 1-1',
+            children: [{
+              label: 'Level three 1-1-1'
+            }]
+          }]
+        }, {
+          label: 'Level one 2',
+          children: [{
+            label: 'Level two 2-1',
+            children: [{
+              label: 'Level three 2-1-1'
+            }]
+          }, {
+            label: 'Level two 2-2',
+            children: [{
+              label: 'Level three 2-2-1'
+            }]
+          }]
+        }, {
+          label: 'Level one 3',
+          children: [{
+            label: 'Level two 3-1',
+            children: [{
+              label: 'Level three 3-1-1'
+            }]
+          }, {
+            label: 'Level two 3-2',
+            children: [{
+              label: 'Level three 3-2-1'
+            }]
+          }]
+        }],
+        defaultProps: {
+          children: 'children',
+          label: 'label'
+        }
+      };
+    },
+    methods: {
+      handleDragStart(node, ev) {
+        console.log('drag start', node);
+      },
+      handleDragEnter(draggingNode, dropNode, ev) {
+        console.log('tree drag enter: ', dropNode.label);
+      },
+      handleDragLeave(draggingNode, dropNode, ev) {
+        console.log('tree drag leave: ', dropNode.label);
+      },
+      handleDragOver(draggingNode, dropNode, ev) {
+        console.log('tree drag over: ', dropNode.label);
+      },
+      handleDragEnd(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drag end: ', dropNode.label, dropType);
+      },
+      handleDrop(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drop: ', dropNode.label, dropType);
+      },
+      allowDrop(draggingNode, dropNode) {
+        return dropNode.data.label !== 'Level two 3-1';
+      },
+      allowDrag(draggingNode) {
+        return draggingNode.data.label.indexOf('Level three 3-1-1') === -1;
+      }
+    }
+  };
+</script>
+```
+:::
+
 ### Attributes
 | Attribute             | Description                              | Type                        | Accepted Values | Default |
 | --------------------- | ---------------------------------------- | --------------------------- | --------------- | ------- |
@@ -1018,6 +1115,9 @@ Only one node among the same level can be expanded at one time.
 | accordion             | whether only one node among the same level can be expanded at one time | boolean                     | —               | false   |
 | indent                | horizontal indentation of nodes in adjacent levels in pixels | number                     | —    | 16 |
 | lazy                  | whether to lazy load leaf node, used with `load` attribute  | boolean                     | —    | false |
+| draggable             | whether enable tree nodes drag and drop | boolean            | —    | false |
+| allow-drag            | this function will be executed before dragging a node. if return `false`, the node can not be drag.  | Function(node)  | —  | —  |
+| allow-drop            | this function will be executed when dragging enter a node. if return `false`, dragging node can not be drop at the node. | Function(draggingNode, dropNode)  | —    | —     |
 
 ### props
 | Attribute | Description                              | Type   | Accepted Values | Default |
@@ -1060,6 +1160,12 @@ Only one node among the same level can be expanded at one time.
 | current-change | triggers when current node changes       | two parameters: node object corresponding to the current node, `node` property of TreeNode |
 | node-expand    | triggers when current node open          | three parameters: node object corresponding to the node opened, `node` property of TreeNode, TreeNode itself |
 | node-collapse  | triggers when current node close         | three parameters: node object corresponding to the node closed, `node` property of TreeNode, TreeNode itself |
+| node-drag-start | triggers when dragging start   | two parameters: node object corresponding to the dragging node、event. |
+| node-drag-enter | triggers when dragging node enters a node  | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging enter node、event. |
+| node-drag-leave | triggers when dragging node leaves a node  | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging leave node、event.  |
+| node-drag-over | triggers when dragging over a node(like browser mouseover event) | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging over node、event.  |
+| node-drag-end  | triggers when dragging end  | four parameters: node object corresponding to the dragging node、node object corresponding to the dragging end node、node drop type (before、after、inner)、event. |
+| node-drop  | triggers after dragging and dropping onto a node  | four parameters: node object corresponding to the dragging node、node object corresponding to the dragging end node、node drop type (before、after、inner)、event. |
 
 ### Scoped slot
 | name | Description |

+ 106 - 0
examples/docs/es/tree.md

@@ -996,6 +996,103 @@ Solo puede ser expandido un nodo del mismo nivel a la vez.
 ```
 :::
 
+### Draggable
+
+Tree nodes can be drag and drop.
+
+:::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-over="handleDragOver"
+  @node-drag-end="handleDragEnd"
+  @node-drop="handleDrop"
+  draggable
+  :allow-drop="allowDrop"
+  :allow-drag="allowDrag">
+</el-tree>
+
+<script>
+  export default {
+    data() {
+      return {
+        data6: [{
+          label: 'Level one 1',
+          children: [{
+            label: 'Level two 1-1',
+            children: [{
+              label: 'Level three 1-1-1'
+            }]
+          }]
+        }, {
+          label: 'Level one 2',
+          children: [{
+            label: 'Level two 2-1',
+            children: [{
+              label: 'Level three 2-1-1'
+            }]
+          }, {
+            label: 'Level two 2-2',
+            children: [{
+              label: 'Level three 2-2-1'
+            }]
+          }]
+        }, {
+          label: 'Level one 3',
+          children: [{
+            label: 'Level two 3-1',
+            children: [{
+              label: 'Level three 3-1-1'
+            }]
+          }, {
+            label: 'Level two 3-2',
+            children: [{
+              label: 'Level three 3-2-1'
+            }]
+          }]
+        }],
+        defaultProps: {
+          children: 'children',
+          label: 'label'
+        }
+      };
+    },
+    methods: {
+      handleDragStart(node, ev) {
+        console.log('drag start', node);
+      },
+      handleDragEnter(draggingNode, dropNode, ev) {
+        console.log('tree drag enter: ', dropNode.label);
+      },
+      handleDragLeave(draggingNode, dropNode, ev) {
+        console.log('tree drag leave: ', dropNode.label);
+      },
+      handleDragOver(draggingNode, dropNode, ev) {
+        console.log('tree drag over: ', dropNode.label);
+      },
+      handleDragEnd(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drag end: ', dropNode.label, dropType);
+      },
+      handleDrop(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drop: ', dropNode.label, dropType);
+      },
+      allowDrop(draggingNode, dropNode) {
+        return dropNode.data.label !== 'Level two 3-1';
+      },
+      allowDrag(draggingNode) {
+        return draggingNode.data.label.indexOf('Level three 3-1-1') === -1;
+      }
+    }
+  };
+</script>
+```
+:::
+
 ### Atributos
 | Atributo              | Descripción                              | Tipo                              | Valores aceptados | Por defecto |
 | --------------------- | ---------------------------------------- | --------------------------------- | ----------------- | ----------- |
@@ -1017,6 +1114,9 @@ Solo puede ser expandido un nodo del mismo nivel a la vez.
 | filter-node-method    | Esta función se ejecutará en cada nodo cuando se use el método filtrtar, si devuelve `false` el nodo se oculta | Function(value, data, node)       | —                 | —           |
 | accordion             | Si solo un nodo de cada nivel puede expandirse a la vez | boolean                           | —                 | false       |
 | indent                | Indentación horizontal de los nodos en niveles adyacentes, en pixeles | number                            | —                 | 16          |
+| draggable             | whether enable tree nodes drag and drop | boolean            | —    | false |
+| allow-drag            | this function will be executed before dragging a node. if return `false`, the node can not be drag.  | Function(node)  | —  | —  |
+| allow-drop            | this function will be executed when dragging enter a node. if return `false`, dragging node can not be drop at the node. | Function(draggingNode, dropNode)  | —    | —     |
 
 ### props
 | Atributo | Descripción                              | Tipo                          | Valores aceptados | Por defecto |
@@ -1058,6 +1158,12 @@ Solo puede ser expandido un nodo del mismo nivel a la vez.
 | current-change    | cambia cuando el nodo actual cambia      | dos parámetros: objeto nodo que se corresponde al nodo actual y propiedad `node` del TreeNode |
 | node-expand       | se lanza cuando el nodo actual se abre   | tres parámetros: el objeto del nodo abierto, propiedad `node` de TreeNode y el TreeNode en si |
 | node-collapse     | se lanza cuando el nodo actual se cierra | tres parámetros: el objeto del nodo cerrado, propiedad `node` de TreeNode y el TreeNode en si |
+| node-drag-start | triggers when dragging start   | two parameters: node object corresponding to the dragging node、event. |
+| node-drag-enter | triggers when dragging node enters a node  | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging enter node、event. |
+| node-drag-leave | triggers when dragging node leaves a node  | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging leave node、event.  |
+| node-drag-over | triggers when dragging over a node(like browser mouseover event) | three parameters: node object corresponding to the dragging node、node object corresponding to the dragging over node、event.  |
+| node-drag-end  | triggers when dragging end  | four parameters: node object corresponding to the dragging node、node object corresponding to the dragging end node、node drop type (before、after、inner)、event. |
+| node-drop  | triggers after dragging and dropping onto a node  | four parameters: node object corresponding to the dragging node、node object corresponding to the dragging end node、node drop type (before、after、inner)、event. |
 
 ### Scoped slot
 | name | Description |

+ 28 - 20
examples/docs/zh-CN/tree.md

@@ -1065,7 +1065,7 @@
 
 ### 可拖拽节点
 
-通过draggable属性可让节点变为可拖拽,节点只能放到相同level节点旁边
+通过 draggable 属性可让节点变为可拖拽。
 
 :::demo
 ```html
@@ -1076,7 +1076,9 @@
   @node-drag-start="handleDragStart"
   @node-drag-enter="handleDragEnter"
   @node-drag-leave="handleDragLeave"
+  @node-drag-over="handleDragOver"
   @node-drag-end="handleDragEnd"
+  @node-drop="handleDrop"
   draggable
   :allow-drop="allowDrop"
   :allow-drag="allowDrag">
@@ -1141,24 +1143,28 @@
       handleDragStart(node, ev) {
         console.log('drag start', node);
       },
-      handleDragEnter(node, ev) {
-        console.log('tree drag enter: ', node.label);
+      handleDragEnter(draggingNode, dropNode, ev) {
+        console.log('tree drag enter: ', dropNode.label);
       },
-      handleDragLeave(node, ev) {
-        console.log('tree drag leave: ', node.label);
+      handleDragLeave(draggingNode, dropNode, ev) {
+        console.log('tree drag leave: ', dropNode.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}`);
-        }
+      handleDragOver(draggingNode, dropNode, ev) {
+        console.log('tree drag over: ', dropNode.label);
       },
-      allowDrop(from, target) {
-        return target.data.label !== '二级 3-1';
+      handleDragEnd(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drag end: ', dropNode.label, dropType);
       },
-      allowDrag(node) {
-        return node.data.label.indexOf('三级 3-1-1') === -1;
+      handleDrop(draggingNode, dropNode, dropType, ev) {
+        console.log('tree drop: ', dropNode.label, dropType);
+      },
+      allowDrop(draggingNode, dropNode) {
+        return dropNode.data.label !== '二级 3-1';
       },
+      allowDrag(draggingNode) {
+        return draggingNode.data.label.indexOf('三级 3-1-1') === -1;
+      }
+    }
   };
 </script>
 ```
@@ -1187,8 +1193,8 @@
 | indent                | 相邻级节点间的水平缩进,单位为像素                 | number                     | —    | 16 |
 | lazy                  | 是否懒加载子节点,需与 load 方法结合使用           | boolean                     | —    | false |
 | draggable             | 是否开启拖拽节点功能                                   | boolean            | —    | false |
-| allow-drag            | 判断节点能否被拖拽                  | Function(Node)  | —  | —  |
-| allow-drop            | 拖拽时判定位置能否被放置             | Function(fromNode, toNode)  | —    | —     |
+| allow-drag            | 判断节点能否被拖拽                  | Function(node)  | —  | —  |
+| allow-drop            | 拖拽时判定位置能否被放置             | Function(draggingNode, dropNode)  | —    | —     |
 
 ### props
 | 参数       | 说明                | 类型     | 可选值  | 默认值  |
@@ -1233,10 +1239,12 @@
 | 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。|
+| node-drag-start | 节点开始拖拽时触发的事件  | 共两个参数,依次为:被拖拽节点对应的 Node、event。 |
+| node-drag-enter | 拖拽进入其他节点时触发的事件  | 共三个参数,依次为:被拖拽节点对应的 Node、所进入节点对应的 Node、event。|
+| node-drag-leave | 拖拽离开某个节点时触发的事件  | 共三个参数,依次为:被拖拽节点对应的 Node、所离开节点对应的 Node、event。 |
+| node-drag-over | 在拖拽节点时触发的事件(类似浏览器的 mouseover 事件) | 共三个参数,依次为:被拖拽节点对应的 Node、当前进入节点对应的 Node、event。 |
+| node-drag-end  | 拖拽结束时(可能未成功)触发的事件  | 共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点(可能为空)、被拖拽节点的放置位置(before、after、inner)、event。|
+| node-drop  | 拖拽成功完成时触发的事件  | 共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event。|
 
 ### Scoped slot
 | name | 说明 |

+ 14 - 4
packages/theme-chalk/src/tree.scss

@@ -23,9 +23,10 @@
     color: mix($--color-primary, rgb(158, 68, 0), 50%);
   }
 
-  @include e(drag-indicator) {
+  @include e(drop-indicator) {
     position: absolute;
-    width: 100%;
+    left: 0;
+    right: 0;
     height: 1px;
     background-color: $--color-primary;
   }
@@ -39,6 +40,14 @@
       background-color: $--tree-node-hover-color;
     }
   }
+
+  @include when(drop-inner) {
+    > .el-tree-node__content .el-tree-node__label {
+      background-color: $--color-primary;
+      color: #fff;
+    }
+  }
+
   @include e(content) {
     display: flex;
     align-items: center;
@@ -55,14 +64,15 @@
       background-color: $--tree-node-hover-color;
     }
 
-    .el-tree.dragging & {
+    .el-tree.is-dragging & {
       cursor: move;
 
       & * {
         pointer-events: none;
       }
     }
-    .el-tree.dragging.drop-not-allow & {
+
+    .el-tree.is-dragging.is-drop-not-allow & {
       cursor: not-allowed;
     }
   }

+ 52 - 2
packages/tree/src/model/node.js

@@ -168,12 +168,58 @@ export default class Node {
     return getPropertyFromData(this, 'disabled');
   }
 
+  get nextSibling() {
+    const parent = this.parent;
+    if (parent) {
+      const index = parent.childNodes.indexOf(this);
+      if (index > -1) {
+        return parent.childNodes[index + 1];
+      }
+    }
+    return null;
+  }
+
+  get previousSibling() {
+    const parent = this.parent;
+    if (parent) {
+      const index = parent.childNodes.indexOf(this);
+      if (index > -1) {
+        return index > 0 ? parent.childNodes[index - 1] : null;
+      }
+    }
+    return null;
+  }
+
+  contains(target, deep = true) {
+    const walk = function(parent) {
+      const children = parent.childNodes || [];
+      let result = false;
+      for (let i = 0, j = children.length; i < j; i++) {
+        const child = children[i];
+        if (child === target || (deep && walk(child))) {
+          result = true;
+          break;
+        }
+      }
+      return result;
+    };
+
+    return walk(this);
+  }
+
+  remove() {
+    const parent = this.parent;
+    if (parent) {
+      parent.removeChild(this);
+    }
+  }
+
   insertChild(child, index, batch) {
     if (!child) throw new Error('insertChild error: child is required.');
 
     if (!(child instanceof Node)) {
       if (!batch) {
-        const children = this.getChildren() || [];
+        const children = this.getChildren(true);
         if (children.indexOf(child.data) === -1) {
           if (typeof index === 'undefined' || index < 0) {
             children.push(child.data);
@@ -357,7 +403,7 @@ export default class Node {
     }
   }
 
-  getChildren() { // this is data
+  getChildren(forceInit = false) { // this is data
     if (this.level === 0) return this.data;
     const data = this.data;
     if (!data) return null;
@@ -372,6 +418,10 @@ export default class Node {
       data[children] = null;
     }
 
+    if (forceInit && !data[children]) {
+      data[children] = [];
+    }
+
     return data[children];
   }
 

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

@@ -6,11 +6,6 @@ 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];

+ 11 - 0
packages/tree/src/model/util.js

@@ -14,3 +14,14 @@ export const getNodeKey = function(key, data) {
   if (!key) return data[NODE_KEY];
   return data[key];
 };
+
+export const findNearestComponent = (element, componentName) => {
+  let target = element;
+  while (target && target.tagName !== 'BODY') {
+    if (target.__vue__ && target.__vue__.$options.name === componentName) {
+      return target.__vue__;
+    }
+    target = target.parentNode;
+  }
+  return null;
+};

+ 10 - 81
packages/tree/src/tree-node.vue

@@ -18,8 +18,6 @@
     :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"
@@ -114,7 +112,7 @@
               ? parent.renderContent.call(parent._renderProxy, h, { _self: tree.$vnode.context, node, data, store })
               : tree.$scopedSlots.default
                 ? tree.$scopedSlots.default({ node, data })
-                : <span class="el-tree-node__label">{ this.node.label }</span>
+                : <span class="el-tree-node__label">{ node.label }</span>
           );
         }
       }
@@ -209,90 +207,21 @@
         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);
+      handleDragStart(event) {
+        this.tree.$emit('tree-node-drag-start', event, this);
       },
 
-      handleDragLeave(ev) {
-        ev.preventDefault();
-        if (!this.node.store.dragSourceNode) return;
-        this.tree.$emit('node-drag-leave', this.node, ev);
+      handleDragOver(event) {
+        this.tree.$emit('tree-node-drag-over', event, this);
+        event.preventDefault();
       },
 
-      handleDragOver(ev) {
-        ev.dataTransfer.dropEffect = this.node.store.allowDrop ? 'move' : 'none';
-        ev.preventDefault();
+      handleDrop(event) {
+        event.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;
+      handleDragEnd(event) {
+        this.tree.$emit('tree-node-drag-end', event, this);
       }
     },
 

+ 178 - 49
packages/tree/src/tree.vue

@@ -3,8 +3,9 @@
     class="el-tree"
     :class="{
       'el-tree--highlight-current': highlightCurrent,
-      dragging: !!store.dragSourceNode,
-      'drop-not-allow': !store.allowDrop
+      'is-dragging': !!dragState.draggingNode,
+      'is-drop-not-allow': !dragState.allowDrop,
+      'is-drop-inner': dragState.dropType === 'inner'
     }"
     role="tree"
   >
@@ -21,20 +22,20 @@
       <span class="el-tree__empty-text">{{ emptyText }}</span>
     </div>
     <div
-      v-if="!!dropAt"
-      class="el-tree__drag-indicator"
-      :style="{top: dragIndicatorOffset}"
-      ref="drag-indicator">
+      v-show="dragState.showDropIndicator"
+      class="el-tree__drop-indicator"
+      ref="dropIndicator">
     </div>
   </div>
 </template>
 
 <script>
   import TreeStore from './model/tree-store';
-  import { getNodeKey } from './model/util';
+  import { getNodeKey, findNearestComponent } from './model/util';
   import ElTreeNode from './tree-node.vue';
   import {t} from 'element-ui/src/locale';
   import emitter from 'element-ui/src/mixins/emitter';
+  import { addClass, removeClass } from 'element-ui/src/utils/dom';
 
   export default {
     name: 'ElTree',
@@ -51,7 +52,13 @@
         root: null,
         currentNode: null,
         treeItems: null,
-        checkboxItems: []
+        checkboxItems: [],
+        dragState: {
+          showDropIndicator: false,
+          draggingNode: null,
+          dropNode: null,
+          allowDrop: true
+        }
       };
     },
 
@@ -130,42 +137,9 @@
           return this.data;
         }
       },
+
       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;
       }
     },
 
@@ -174,13 +148,16 @@
         this.store.defaultCheckedKeys = newVal;
         this.store.setDefaultCheckedKey(newVal);
       },
+
       defaultExpandedKeys(newVal) {
         this.store.defaultExpandedKeys = newVal;
         this.store.setDefaultExpandedKeys(newVal);
       },
+
       data(newVal) {
         this.store.setData(newVal);
       },
+
       checkboxItems(val) {
         Array.prototype.forEach.call(val, (checkbox) => {
           checkbox.setAttribute('tabindex', -1);
@@ -193,9 +170,11 @@
         if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
         this.store.filter(value);
       },
+
       getNodeKey(node) {
         return getNodeKey(this.nodeKey, node.data);
       },
+
       getNodePath(data) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath');
         const node = this.store.getNode(data);
@@ -208,70 +187,89 @@
         }
         return path.reverse();
       },
+
       getCheckedNodes(leafOnly) {
         return this.store.getCheckedNodes(leafOnly);
       },
+
       getCheckedKeys(leafOnly) {
         return this.store.getCheckedKeys(leafOnly);
       },
+
       getCurrentNode() {
         const currentNode = this.store.getCurrentNode();
         return currentNode ? currentNode.data : null;
       },
+
       getCurrentKey() {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey');
         const currentNode = this.getCurrentNode();
         return currentNode ? currentNode[this.nodeKey] : null;
       },
+
       setCheckedNodes(nodes, leafOnly) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
         this.store.setCheckedNodes(nodes, leafOnly);
       },
+
       setCheckedKeys(keys, leafOnly) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
         this.store.setCheckedKeys(keys, leafOnly);
       },
+
       setChecked(data, checked, deep) {
         this.store.setChecked(data, checked, deep);
       },
+
       getHalfCheckedNodes() {
         return this.store.getHalfCheckedNodes();
       },
+
       getHalfCheckedKeys() {
         return this.store.getHalfCheckedKeys();
       },
+
       setCurrentNode(node) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
         this.store.setUserCurrentNode(node);
       },
+
       setCurrentKey(key) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
         this.store.setCurrentNodeKey(key);
       },
+
       getNode(data) {
         return this.store.getNode(data);
       },
+
       remove(data) {
         this.store.remove(data);
       },
+
       append(data, parentNode) {
         this.store.append(data, parentNode);
       },
+
       insertBefore(data, refNode) {
         this.store.insertBefore(data, refNode);
       },
+
       insertAfter(data, refNode) {
         this.store.insertAfter(data, refNode);
       },
+
       handleNodeExpand(nodeData, node, instance) {
         this.broadcast('ElTreeNode', 'tree-node-expand', node);
         this.$emit('node-expand', nodeData, node, instance);
       },
+
       updateKeyChildren(key, data) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
         this.store.updateChildren(key, data);
       },
-      initTabindex() {
+
+      initTabIndex() {
         this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
         this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
         const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]');
@@ -281,6 +279,7 @@
         }
         this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0);
       },
+
       handelKeydown(ev) {
         const currentItem = ev.target;
         if (currentItem.className.indexOf('el-tree-node') === -1) return;
@@ -297,14 +296,12 @@
           }
           this.treeItemArray[nextIndex].focus(); // 选中
         }
-        const hasInput = currentItem.querySelector('[type="checkbox"]');
         if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开
           currentItem.click(); // 选中
         }
-        if ([13, 32].indexOf(keyCode) > -1) { // space enter选中checkbox
-          if (hasInput) {
-            hasInput.click();
-          }
+        const hasInput = currentItem.querySelector('[type="checkbox"]');
+        if ([13, 32].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox
+          hasInput.click();
         }
       }
     },
@@ -329,11 +326,143 @@
       });
 
       this.root = this.store.root;
+
+      let dragState = this.dragState;
+      this.$on('tree-node-drag-start', (event, treeNode) => {
+        if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
+          event.preventDefault();
+          return false;
+        }
+        event.dataTransfer.effectAllowed = 'move';
+        event.dataTransfer.setData('text/plain', treeNode.node.label);
+        dragState.draggingNode = treeNode;
+        this.$emit('node-drag-start', event, treeNode.node);
+      });
+
+      this.$on('tree-node-drag-over', (event, treeNode) => {
+        const dropNode = findNearestComponent(event.target, 'ElTreeNode');
+        const oldDropNode = dragState.dropNode;
+        if (oldDropNode && oldDropNode !== dropNode) {
+          removeClass(oldDropNode.$el, 'is-drop-inner');
+        }
+        const draggingNode = dragState.draggingNode;
+        if (!draggingNode || !dropNode) return;
+
+        let allowDrop = true;
+        if (typeof this.allowDrop === 'function' && !this.allowDrop(draggingNode.node, dropNode.node)) {
+          allowDrop = false;
+        }
+        dragState.allowDrop = allowDrop;
+        event.dataTransfer.dropEffect = allowDrop ? 'move' : 'none';
+        if (allowDrop && oldDropNode !== dropNode) {
+          if (oldDropNode) {
+            this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event);
+          }
+          this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event);
+        }
+
+        if (allowDrop) {
+          dragState.dropNode = dropNode;
+        }
+
+        let dropPrev = allowDrop;
+        let dropInner = allowDrop;
+        let dropNext = allowDrop;
+
+        if (dropNode.node.nextSibling === draggingNode.node) {
+          dropNext = false;
+        }
+        if (dropNode.node.previousSibling === draggingNode.node) {
+          dropPrev = false;
+        }
+        if (dropNode.node.contains(draggingNode.node, false)) {
+          dropInner = false;
+        }
+        if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
+          dropPrev = false;
+          dropInner = false;
+          dropNext = false;
+        }
+
+        const targetPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect();
+        const treePosition = this.$el.getBoundingClientRect();
+
+        let dropType;
+        const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.5 : 1)) : -1;
+        const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.5 : 0)) : 1;
+
+        let indicatorTop = -9999;
+        const distance = event.clientY - targetPosition.top;
+        if (distance < targetPosition.height * prevPercent) {
+          dropType = 'before';
+        } else if (distance > targetPosition.height * nextPercent) {
+          dropType = 'after';
+        } else if (dropInner) {
+          dropType = 'inner';
+        } else {
+          dropType = 'none';
+        }
+
+        const dropIndicator = this.$refs.dropIndicator;
+        if (dropType === 'before') {
+          indicatorTop = targetPosition.top - treePosition.top;
+        } else if (dropType === 'after') {
+          indicatorTop = targetPosition.bottom - treePosition.top;
+        }
+        dropIndicator.style.top = indicatorTop + 'px';
+        dropIndicator.style.left = (targetPosition.right - treePosition.left) + 'px';
+
+        if (dropType === 'inner') {
+          addClass(dropNode.$el, 'is-drop-inner');
+        } else {
+          removeClass(dropNode.$el, 'is-drop-inner');
+        }
+
+        dragState.showDropIndicator = dropType === 'before' || dropType === 'after';
+        dragState.dropType = dropType;
+        this.$emit('node-drag-over', draggingNode.node, dropNode.node, event);
+      });
+
+      this.$on('tree-node-drag-end', (event) => {
+        const { draggingNode, dropType, dropNode } = dragState;
+        event.preventDefault();
+        event.dataTransfer.dropEffect = 'move';
+
+        if (draggingNode && dropNode) {
+          const data = draggingNode.node.data;
+          if (dropType === 'before') {
+            draggingNode.node.remove();
+            dropNode.node.parent.insertBefore({ data }, dropNode.node);
+          } else if (dropType === 'after') {
+            draggingNode.node.remove();
+            dropNode.node.parent.insertAfter({ data }, dropNode.node);
+          } else if (dropType === 'inner') {
+            dropNode.node.insertChild({ data });
+            draggingNode.node.remove();
+          }
+          removeClass(dropNode.$el, 'is-drop-inner');
+
+          this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
+          if (dropType !== 'none') {
+            this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event);
+          }
+        }
+        if (draggingNode && !dropNode) {
+          this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
+        }
+
+        dragState.showDropIndicator = false;
+        dragState.draggingNode = null;
+        dragState.dropNode = null;
+        dragState.allowDrop = true;
+      });
     },
+
     mounted() {
-      this.initTabindex();
+      this.initTabIndex();
       this.$el.addEventListener('keydown', this.handelKeydown);
     },
+
     updated() {
       this.treeItems = this.$el.querySelectorAll('[role=treeitem]');
       this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');