Tree: support drag and drop node (#9251)

This commit is contained in:
Harlan 2018-03-20 13:42:04 +08:00 committed by FuryBean
parent 2098e36b51
commit d7c4fd2632
5 changed files with 345 additions and 1 deletions

View File

@ -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 | 说明 |

View File

@ -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) {

View File

@ -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];

View File

@ -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;
}
},

View File

@ -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;
}
},