Improve Tree:

1. Add props: renderContent, highlightCurrent
2. Fix Bug: tree do not change when data changed
pull/470/head
furybean 2016-10-18 00:31:37 +08:00
parent baf3d192d8
commit 785bed20df
7 changed files with 152 additions and 65 deletions

View File

@ -235,6 +235,8 @@
| props | 配置选项,具体看下表 | object | — | — |
| load | 加载子树数据的方法 | function(node, resolve) | — | — |
| show-checkbox | 节点是否可被选择 | boolean | — | false |
| render-content | 树节点的内容区的渲染 Function会传入两个参数h 与 { node: node }。 | Function | - | - |
| highlight-current | 是否高亮当前选中节点,默认值是 false。| boolean | - | false |
### props
@ -253,5 +255,5 @@
### Events
| 事件名称 | 说明 | 回调参数 |
|---------- |-------- |---------- |
| node-click | 节点被点击时的回调 | 传递给 `data` 属性的数组中该节点所对应的对象 |
| node-click | 节点被点击时的回调 | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 |
| check-change | 节点选中状态发生变化时的回调 | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、<br>节点本身是否被选中、节点的子树中是否有被选中的节点 |

View File

@ -3,7 +3,6 @@
@component-namespace el {
@b tree {
overflow: auto;
cursor: default;
background: #ffffff;
border: 1px solid #d3dce6;
@ -82,6 +81,11 @@
}
}
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: #eff7ff;
}
.collapse-transition {
transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
}

View File

@ -1,8 +1,8 @@
let idSeed = 0;
let nodeIdSeed = 0;
import objectAssign from 'object-assign';
const reInitChecked = function(node) {
const siblings = node.children;
const siblings = node.childNodes;
let all = true;
let none = true;
@ -42,7 +42,7 @@ const getPropertyFromData = function(node, prop) {
export default class Node {
constructor(options) {
this.id = idSeed++;
this.id = nodeIdSeed++;
this.text = null;
this.checked = false;
this.indeterminate = false;
@ -61,7 +61,7 @@ export default class Node {
// internal
this.level = -1;
this.loaded = false;
this.children = [];
this.childNodes = [];
this.loading = false;
if (this.parent) {
@ -74,8 +74,17 @@ export default class Node {
}
setData(data) {
if (!Array.isArray(data) && !data.$treeNodeId) {
Object.defineProperty(data, '$treeNodeId', {
value: this.id,
enumerable: false,
configurable: false,
writable: false
});
}
this.data = data;
this.children = [];
this.childNodes = [];
let children;
if (this.level === -1 && this.data instanceof Array) {
@ -85,14 +94,7 @@ export default class Node {
}
for (let i = 0, j = children.length; i < j; i++) {
const child = children[i];
this.insertChild(new Node({
data: child,
parent: this,
lazy: this.lazy,
load: this.load,
props: this.props
}));
this.insertChild({ data: children[i] });
}
}
@ -107,26 +109,47 @@ export default class Node {
insertChild(child, index) {
if (!child) throw new Error('insertChild error: child is required.');
if (!child instanceof Node) {
throw new Error('insertChild error: child should an instance of Node.');
if (!(child instanceof Node)) {
objectAssign(child, {
parent: this,
lazy: this.lazy,
load: this.load,
props: this.props
});
child = new Node(child);
}
child.parent = this;
child.level = this.level + 1;
if (typeof index === 'undefined') {
this.children.push(child);
this.childNodes.push(child);
} else {
this.children.splice(index, 0, child);
this.childNodes.splice(index, 0, child);
}
}
removeChild(child) {
const index = this.children.indexOf(child);
const index = this.childNodes.indexOf(child);
if (index > -1) {
child.parent = null;
this.children.splice(child, index);
this.childNodes.splice(index, 1);
}
}
removeChildByData(data) {
let nodeIndex = -1;
let targetNode = null;
this.childNodes.forEach((node, index) => {
if (node.data === data) {
nodeIndex = index;
targetNode = node;
}
});
if (nodeIndex > -1) {
targetNode.parent = null;
this.childNodes.splice(nodeIndex, 1);
}
}
@ -147,13 +170,7 @@ export default class Node {
doCreateChildren(array, defaultProps = {}) {
array.forEach((item) => {
const node = new Node(objectAssign({
data: item,
lazy: this.lazy,
load: this.load,
props: this.props
}, defaultProps));
this.insertChild(node);
this.insertChild(objectAssign({ data: item }, defaultProps));
});
}
@ -170,9 +187,9 @@ export default class Node {
}
hasChild() {
const children = this.children;
const childNodes = this.childNodes;
if (!this.lazy || (this.lazy === true && this.loaded === true)) {
return children && children.length > 0;
return childNodes && childNodes.length > 0;
}
return true;
}
@ -183,9 +200,9 @@ export default class Node {
const handleDeep = () => {
if (deep) {
const children = this.children;
for (let i = 0, j = children.length; i < j; i++) {
const child = children[i];
const childNodes = this.childNodes;
for (let i = 0, j = childNodes.length; i < j; i++) {
const child = childNodes[i];
child.setChecked(value !== false, deep);
}
}
@ -225,15 +242,33 @@ export default class Node {
return data[children];
}
updateChildren() {
const newData = this.getChildren() || [];
const oldData = this.childNodes.map((node) => node.data);
const newDataMap = {};
const newNodes = [];
newData.forEach((item, index) => {
if (item.$treeNodeId) {
newDataMap[item.$treeNodeId] = { index, data: item };
} else {
newNodes.push({ index, data: item });
}
});
oldData.forEach((item) => { if (!newDataMap[item.$treeNodeId]) this.removeChildByData(item); });
newNodes.forEach(({ index, data }) => this.insertChild({ data }, index));
}
loadData(callback, defaultProps = {}) {
if (this.lazy === true && this.load && !this.loaded) {
this.loading = true;
const loadFn = this.load;
const resolve = (children) => {
this.loaded = true;
this.loading = false;
this.children = [];
this.childNodes = [];
this.doCreateChildren(children, defaultProps);
@ -242,7 +277,7 @@ export default class Node {
}
};
loadFn(this, resolve);
this.load(this, resolve);
} else {
if (callback) {
callback.call(this);

View File

@ -8,8 +8,6 @@ export default class Tree {
}
}
this._isTree = true;
this.root = new Node({
data: this.data,
lazy: this.lazy,
@ -28,9 +26,9 @@ export default class Tree {
getCheckedNodes(leafOnly) {
const checkedNodes = [];
const walk = function(node) {
const children = node.root ? node.root.children : node.children;
const childNodes = node.root ? node.root.childNodes : node.childNodes;
children.forEach(function(child) {
childNodes.forEach(function(child) {
if ((!leafOnly && child.checked) || (leafOnly && child.isLeaf && child.checked)) {
checkedNodes.push(child.data);
}

View File

@ -58,7 +58,7 @@ class Transition {
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
};
}
export default {
functional: true,

View File

@ -1,29 +1,42 @@
<template>
<div class="el-tree-node"
:class="{ expanded: childrenRendered && expanded }">
<div class="el-tree-node__content" :style="{ 'padding-left': node.level * 16 + 'px' }"
@click="handleExpandIconClick">
<span class="el-tree-node__expand-icon"
:class="{ 'is-leaf': node.isLeaf, expanded: !node.isLeaf && expanded }"
></span>
<el-checkbox v-if="showCheckbox" :indeterminate="node.indeterminate" v-model="node.checked" @change="handleCheckChange" @click.native="handleUserClick"></el-checkbox>
@click.stop="handleClick"
:class="{ expanded: childNodeRendered && expanded, 'is-current': $tree.currentNode === _self }">
<div class="el-tree-node__content"
:style="{ 'padding-left': node.level * 16 + 'px' }"
@click="handleExpandIconClick">
<span
class="el-tree-node__expand-icon"
:class="{ 'is-leaf': node.isLeaf, expanded: !node.isLeaf && expanded }">
</span>
<el-checkbox
v-if="showCheckbox"
v-model="node.checked"
:indeterminate="node.indeterminate"
@change="handleCheckChange"
@click.native="handleUserClick">
</el-checkbox>
<span
v-if="node.loading"
class="el-tree-node__icon el-icon-loading"
>
class="el-tree-node__icon el-icon-loading">
</span>
<span class="el-tree-node__label" v-html="node.label"></span>
<node-content :node="node"></node-content>
</div>
<collapse-transition>
<div class="el-tree-node__children"
<div
class="el-tree-node__children"
v-show="expanded">
<el-tree-node v-for="child in node.children" :node="child"></el-tree-node>
<el-tree-node
:render-content="renderContent"
v-for="child in node.childNodes"
:node="child">
</el-tree-node>
</div>
</collapse-transition>
</div>
</template>
<script type="text/ecmascript-6">
<script type="text/jsx">
import CollapseTransition from './transition';
export default {
@ -34,18 +47,35 @@
default() {
return {};
}
}
},
props: {},
renderContent: Function
},
components: {
CollapseTransition
CollapseTransition,
NodeContent: {
props: {
node: {
required: true
}
},
render(h) {
const parent = this.$parent;
return (
parent.renderContent
? parent.renderContent.call(parent._renderProxy, h, { _self: parent.$parent.$vnode.context, node: this.node })
: <span class="el-tree-node__label">{ this.node.label }</span>
);
}
}
},
data() {
return {
$tree: null,
expanded: false,
childrenRendered: false,
childNodeRendered: false,
showCheckbox: false,
oldChecked: null,
oldIndeterminate: null
@ -71,21 +101,25 @@
this.indeterminate = indeterminate;
},
handleClick() {
this.$tree.currentNode = this;
},
handleExpandIconClick(event) {
let target = event.target;
if (target.tagName.toUpperCase() !== 'DIV' &&
target.parentNode.nodeName.toUpperCase() !== 'DIV' ||
target.nodeName.toUpperCase() === 'LABLE') return;
target.parentNode.nodeName.toUpperCase() !== 'DIV' ||
target.nodeName.toUpperCase() === 'LABEL') return;
if (this.expanded) {
this.node.collapse();
this.expanded = false;
} else {
this.node.expand(() => {
this.expanded = true;
this.childrenRendered = true;
this.childNodeRendered = true;
});
}
this.$tree.$emit('node-click', this.node.data);
this.$tree.$emit('node-click', this.node.data, this.node, this);
},
handleUserClick() {
@ -111,6 +145,12 @@
}
const tree = this.$tree;
const props = this.props || {};
const childrenKey = props['children'] || 'children';
this.$watch(`node.data.${childrenKey}`, () => {
this.node.updateChildren();
});
if (!tree) {
console.warn('Can not find node\'s tree.');

View File

@ -1,6 +1,11 @@
<template>
<div class="el-tree">
<el-tree-node v-for="child in tree.root.children" :node="child"></el-tree-node>
<div class="el-tree" :class="{ 'el-tree--highlight-current': highlightCurrent }">
<el-tree-node
v-for="child in tree.root.childNodes"
:node="child"
:props="props"
:render-content="renderContent">
</el-tree-node>
</div>
</template>
@ -14,6 +19,7 @@
data: {
type: Array
},
renderContent: Function,
showCheckbox: {
type: Boolean,
default: false
@ -31,6 +37,7 @@
type: Boolean,
default: false
},
highlightCurrent: Boolean,
load: {
type: Function
}
@ -49,7 +56,8 @@
data() {
return {
tree: {}
tree: {},
currentNode: null
};
},