mirror of
https://gitee.com/ElemeFE/element.git
synced 2024-12-02 04:08:10 +08:00
Tree: support drag and drop node (#9251)
This commit is contained in:
parent
2098e36b51
commit
d7c4fd2632
@ -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 | 说明 |
|
||||
|
@ -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) {
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user