Tree: refactor code & add insert, remove api for tree-store. (#1283)

pull/1298/head
FuryBean 2016-11-23 10:46:05 +08:00 committed by cinwell.li
parent 1f6cafebb1
commit 2d23618c8c
5 changed files with 168 additions and 72 deletions

View File

@ -1,4 +1,5 @@
import objectAssign from 'element-ui/src/utils/merge'; import objectAssign from 'element-ui/src/utils/merge';
import { markNodeData, NODE_KEY } from './util';
const reInitChecked = function(node) { const reInitChecked = function(node) {
const siblings = node.childNodes; const siblings = node.childNodes;
@ -26,7 +27,7 @@ const reInitChecked = function(node) {
}; };
const getPropertyFromData = function(node, prop) { const getPropertyFromData = function(node, prop) {
const props = node._tree.props; const props = node.store.props;
const data = node.data || {}; const data = node.data || {};
const config = props[prop]; const config = props[prop];
@ -68,27 +69,35 @@ export default class Node {
this.level = this.parent.level + 1; this.level = this.parent.level + 1;
} }
const tree = this._tree; const store = this.store;
if (!tree) { if (!store) {
throw new Error('[Node]_tree is required!'); throw new Error('[Node]store is required!');
} }
tree.registerNode(this); store.registerNode(this);
if (tree.lazy !== true && this.data) { const props = store.props;
if (props && typeof props.isLeaf !== 'undefined') {
const isLeaf = getPropertyFromData(this, 'isLeaf');
if (typeof isLeaf === 'boolean') {
this.isLeafByUser = isLeaf;
}
}
if (store.lazy !== true && this.data) {
this.setData(this.data); this.setData(this.data);
if (tree.defaultExpandAll) { if (store.defaultExpandAll) {
this.expanded = true; this.expanded = true;
} }
} else if (this.level > 0 && tree.lazy && tree.defaultExpandAll) { } else if (this.level > 0 && store.lazy && store.defaultExpandAll) {
this.expand(); this.expand();
} }
if (!this.data) return; if (!this.data) return;
const defaultExpandedKeys = tree.defaultExpandedKeys; const defaultExpandedKeys = store.defaultExpandedKeys;
const key = tree.key; const key = store.key;
if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) { if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) {
if (tree.autoExpandParent) { if (store.autoExpandParent) {
let parent = this.parent; let parent = this.parent;
while (parent.level > 0) { while (parent.level > 0) {
parent.expanded = true; parent.expanded = true;
@ -99,19 +108,16 @@ export default class Node {
this.expand(); this.expand();
} }
if (tree.lazy) { if (store.lazy) {
tree._initDefaultCheckedNode(this); store._initDefaultCheckedNode(this);
} }
this.updateLeafState();
} }
setData(data) { setData(data) {
if (!Array.isArray(data) && !data.$treeNodeId) { if (!Array.isArray(data)) {
Object.defineProperty(data, '$treeNodeId', { markNodeData(this, data);
value: this.id,
enumerable: false,
configurable: false,
writable: false
});
} }
this.data = data; this.data = data;
@ -138,7 +144,7 @@ export default class Node {
} }
get key() { get key() {
const nodeKey = this._tree.key; const nodeKey = this.store.key;
if (this.data) return this.data[nodeKey]; if (this.data) return this.data[nodeKey];
return null; return null;
} }
@ -149,28 +155,49 @@ export default class Node {
if (!(child instanceof Node)) { if (!(child instanceof Node)) {
objectAssign(child, { objectAssign(child, {
parent: this, parent: this,
_tree: this._tree store: this.store
}); });
child = new Node(child); child = new Node(child);
} }
child.level = this.level + 1; child.level = this.level + 1;
if (typeof index === 'undefined') { if (typeof index === 'undefined' || index < 0) {
this.childNodes.push(child); this.childNodes.push(child);
} else { } else {
this.childNodes.splice(index, 0, child); this.childNodes.splice(index, 0, child);
} }
this.updateLeafState();
}
insertBefore(child, ref) {
let index;
if (ref) {
index = this.childNodes.indexOf(ref);
}
this.insertChild(child, index);
}
insertAfter(child, ref) {
let index;
if (ref) {
index = this.childNodes.indexOf(ref);
if (index !== -1) index += 1;
}
this.insertChild(child, index);
} }
removeChild(child) { removeChild(child) {
const index = this.childNodes.indexOf(child); const index = this.childNodes.indexOf(child);
if (index > -1) { if (index > -1) {
this._tree && this._tree.deregisterNode(child); this.store && this.store.deregisterNode(child);
child.parent = null; child.parent = null;
this.childNodes.splice(index, 1); this.childNodes.splice(index, 1);
} }
this.updateLeafState();
} }
removeChildByData(data) { removeChildByData(data) {
@ -211,19 +238,20 @@ export default class Node {
} }
shouldLoadData() { shouldLoadData() {
return this._tree.lazy === true && this._tree.load && !this.loaded; return this.store.lazy === true && this.store.load && !this.loaded;
} }
get isLeaf() { updateLeafState() {
return !this.hasChild(); if (this.store.lazy === true && this.loaded !== true && typeof this.isLeafByUser !== 'undefined') {
this.isLeaf = this.isLeafByUser;
return;
} }
hasChild() {
const childNodes = this.childNodes; const childNodes = this.childNodes;
if (!this._tree.lazy || (this._tree.lazy === true && this.loaded === true)) { if (!this.store.lazy || (this.store.lazy === true && this.loaded === true)) {
return childNodes && childNodes.length > 0; this.isLeaf = !childNodes || childNodes.length === 0;
return;
} }
return true; this.isLeaf = false;
} }
setChecked(value, deep) { setChecked(value, deep) {
@ -240,7 +268,7 @@ export default class Node {
} }
}; };
if (!this._tree.checkStrictly && this.shouldLoadData()) { if (!this.store.checkStrictly && this.shouldLoadData()) {
// Only work on lazy load data. // Only work on lazy load data.
this.loadData(() => { this.loadData(() => {
handleDescendants(); handleDescendants();
@ -254,7 +282,7 @@ export default class Node {
const parent = this.parent; const parent = this.parent;
if (!parent || parent.level === 0) return; if (!parent || parent.level === 0) return;
if (!this._tree.checkStrictly) { if (!this.store.checkStrictly) {
reInitChecked(parent); reInitChecked(parent);
} }
} }
@ -263,7 +291,7 @@ export default class Node {
const data = this.data; const data = this.data;
if (!data) return null; if (!data) return null;
const props = this._tree.props; const props = this.store.props;
let children = 'children'; let children = 'children';
if (props) { if (props) {
children = props.children || 'children'; children = props.children || 'children';
@ -284,19 +312,26 @@ export default class Node {
const newNodes = []; const newNodes = [];
newData.forEach((item, index) => { newData.forEach((item, index) => {
if (item.$treeNodeId) { if (item[NODE_KEY]) {
newDataMap[item.$treeNodeId] = { index, data: item }; newDataMap[item[NODE_KEY]] = { index, data: item };
} else { } else {
newNodes.push({ index, data: item }); newNodes.push({ index, data: item });
} }
}); });
oldData.forEach((item) => { if (!newDataMap[item.$treeNodeId]) this.removeChildByData(item); }); oldData.forEach((item) => {
newNodes.forEach(({ index, data }) => this.insertChild({ data }, index)); if (!newDataMap[item[NODE_KEY]]) this.removeChildByData(item);
});
newNodes.forEach(({ index, data }) => {
this.insertChild({ data }, index);
});
this.updateLeafState();
} }
loadData(callback, defaultProps = {}) { loadData(callback, defaultProps = {}) {
if (this._tree.lazy === true && this._tree.load && !this.loaded && !this.loading) { if (this.store.lazy === true && this.store.load && !this.loaded && !this.loading) {
this.loading = true; this.loading = true;
const resolve = (children) => { const resolve = (children) => {
@ -311,7 +346,7 @@ export default class Node {
} }
}; };
this._tree.load(this, resolve); this.store.load(this, resolve);
} else { } else {
if (callback) { if (callback) {
callback.call(this); callback.call(this);

View File

@ -1,6 +1,7 @@
import Node from './node'; import Node from './node';
import { getNodeKey } from './util';
export default class Tree { export default class TreeStore {
constructor(options) { constructor(options) {
for (let option in options) { for (let option in options) {
if (options.hasOwnProperty(option)) { if (options.hasOwnProperty(option)) {
@ -12,7 +13,7 @@ export default class Tree {
this.root = new Node({ this.root = new Node({
data: this.data, data: this.data,
_tree: this store: this
}); });
if (this.lazy && this.load) { if (this.lazy && this.load) {
@ -65,6 +66,36 @@ export default class Tree {
} }
} }
getNodeByData(data) {
const key = typeof data === 'string' ? data : getNodeKey(this.key, data);
return this.nodesMap[key];
}
insertBefore(data, refData) {
const refNode = this.getNodeByData(refData);
refNode.parent.insertBefore({ data }, refNode);
}
insertAfter(data, refData) {
const refNode = this.getNodeByData(refData);
refNode.parent.insertAfter({ data }, refNode);
}
remove(data) {
const node = this.getNodeByData(data);
if (node) {
node.parent.removeChild(node);
}
}
append(data, parentData) {
const parentNode = parentData ? this.getNodeByData(parentData) : this.root;
if (parentNode) {
parentNode.insertChild({ data });
}
}
_initDefaultCheckedNodes() { _initDefaultCheckedNodes() {
const defaultCheckedKeys = this.defaultCheckedKeys || []; const defaultCheckedKeys = this.defaultCheckedKeys || [];
const nodesMap = this.nodesMap; const nodesMap = this.nodesMap;

View File

@ -0,0 +1,16 @@
export const NODE_KEY = '$treeNodeId';
export const markNodeData = function(node, data) {
if (data[NODE_KEY]) return;
Object.defineProperty(data, NODE_KEY, {
value: node.id,
enumerable: false,
configurable: false,
writable: false
});
};
export const getNodeKey = function(key, data) {
if (!key) return data[NODE_KEY];
return data[key];
};

View File

@ -2,7 +2,7 @@
<div class="el-tree-node" <div class="el-tree-node"
@click.stop="handleClick" @click.stop="handleClick"
v-show="node.visible" v-show="node.visible"
:class="{ 'is-expanded': childNodeRendered && expanded, 'is-current': $tree.currentNode === _self, 'is-hidden': !node.visible }"> :class="{ 'is-expanded': childNodeRendered && expanded, 'is-current': tree.currentNode === _self, 'is-hidden': !node.visible }">
<div class="el-tree-node__content" <div class="el-tree-node__content"
:style="{ 'padding-left': (node.level - 1) * 16 + 'px' }" :style="{ 'padding-left': (node.level - 1) * 16 + 'px' }"
@click="handleExpandIconClick"> @click="handleExpandIconClick">
@ -64,9 +64,12 @@
}, },
render(h) { render(h) {
const parent = this.$parent; const parent = this.$parent;
const node = this.node;
const data = node.data;
const store = node.store;
return ( return (
parent.renderContent parent.renderContent
? parent.renderContent.call(parent._renderProxy, h, { _self: parent.$parent.$vnode.context, node: this.node }) ? parent.renderContent.call(parent._renderProxy, h, { _self: parent.tree.$vnode.context, node, data, store })
: <span class="el-tree-node__label">{ this.node.label }</span> : <span class="el-tree-node__label">{ this.node.label }</span>
); );
} }
@ -75,7 +78,7 @@
data() { data() {
return { return {
$tree: null, tree: null,
expanded: false, expanded: false,
childNodeRendered: false, childNodeRendered: false,
showCheckbox: false, showCheckbox: false,
@ -103,7 +106,7 @@
methods: { methods: {
getNodeKey(node, index) { getNodeKey(node, index) {
const nodeKey = this.$tree.nodeKey; const nodeKey = this.tree.nodeKey;
if (nodeKey && node) { if (nodeKey && node) {
return node.data[nodeKey]; return node.data[nodeKey];
} }
@ -112,14 +115,14 @@
handleSelectChange(checked, indeterminate) { handleSelectChange(checked, indeterminate) {
if (this.oldChecked !== checked && this.oldIndeterminate !== indeterminate) { if (this.oldChecked !== checked && this.oldIndeterminate !== indeterminate) {
this.$tree.$emit('check-change', this.node.data, checked, indeterminate); this.tree.$emit('check-change', this.node.data, checked, indeterminate);
} }
this.oldChecked = checked; this.oldChecked = checked;
this.indeterminate = indeterminate; this.indeterminate = indeterminate;
}, },
handleClick() { handleClick() {
this.$tree.currentNode = this; this.tree.currentNode = this;
}, },
handleExpandIconClick(event) { handleExpandIconClick(event) {
@ -132,18 +135,18 @@
} else { } else {
this.node.expand(); this.node.expand();
} }
this.$tree.$emit('node-click', this.node.data, this.node, this); this.tree.$emit('node-click', this.node.data, this.node, this);
}, },
handleUserClick() { handleUserClick() {
if (this.node.indeterminate) { if (this.node.indeterminate) {
this.node.setChecked(this.node.checked, !this.$tree.checkStrictly); this.node.setChecked(this.node.checked, !this.tree.checkStrictly);
} }
}, },
handleCheckChange(ev) { handleCheckChange(ev) {
if (!this.node.indeterminate) { if (!this.node.indeterminate) {
this.node.setChecked(ev.target.checked, !this.$tree.checkStrictly); this.node.setChecked(ev.target.checked, !this.tree.checkStrictly);
} }
} }
}, },
@ -151,13 +154,13 @@
created() { created() {
const parent = this.$parent; const parent = this.$parent;
if (parent.$isTree) { if (parent.isTree) {
this.$tree = parent; this.tree = parent;
} else { } else {
this.$tree = parent.$tree; this.tree = parent.tree;
} }
const tree = this.$tree; const tree = this.tree;
const props = this.props || {}; const props = this.props || {};
const childrenKey = props['children'] || 'children'; const childrenKey = props['children'] || 'children';

View File

@ -1,19 +1,20 @@
<template> <template>
<div class="el-tree" :class="{ 'el-tree--highlight-current': highlightCurrent }"> <div class="el-tree" :class="{ 'el-tree--highlight-current': highlightCurrent }">
<el-tree-node <el-tree-node
v-for="child in tree.root.childNodes" v-for="child in root.childNodes"
:node="child" :node="child"
:props="props" :props="props"
:key="getNodeKey(child)"
:render-content="renderContent"> :render-content="renderContent">
</el-tree-node> </el-tree-node>
<div class="el-tree__empty-block" v-if="!tree.root.childNodes || tree.root.childNodes.length === 0"> <div class="el-tree__empty-block" v-if="!root.childNodes || root.childNodes.length === 0">
<span class="el-tree__empty-text">{{ emptyText }}</span> <span class="el-tree__empty-text">{{ emptyText }}</span>
</div> </div>
</div> </div>
</template> </template>
<script type="text/ecmascript-6"> <script type="text/ecmascript-6">
import Tree from './model/tree'; import TreeStore from './model/tree-store';
import {t} from 'element-ui/src/locale'; import {t} from 'element-ui/src/locale';
export default { export default {
@ -62,9 +63,9 @@
}, },
created() { created() {
this.$isTree = true; this.isTree = true;
this.tree = new Tree({ this.store = new TreeStore({
key: this.nodeKey, key: this.nodeKey,
data: this.data, data: this.data,
lazy: this.lazy, lazy: this.lazy,
@ -77,11 +78,14 @@
defaultExpandAll: this.defaultExpandAll, defaultExpandAll: this.defaultExpandAll,
filterNodeMethod: this.filterNodeMethod filterNodeMethod: this.filterNodeMethod
}); });
this.root = this.store.root;
}, },
data() { data() {
return { return {
tree: {}, store: null,
root: null,
currentNode: null currentNode: null
}; };
}, },
@ -103,31 +107,38 @@
watch: { watch: {
data(newVal) { data(newVal) {
this.tree.setData(newVal); this.store.setData(newVal);
}, },
defaultCheckedKeys(newVal) { defaultCheckedKeys(newVal) {
this.tree.setDefaultCheckedKey(newVal); this.store.setDefaultCheckedKey(newVal);
} }
}, },
methods: { methods: {
filter(value) { filter(value) {
if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter'); if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
this.tree.filter(value); this.store.filter(value);
},
getNodeKey(node, index) {
const nodeKey = this.nodeKey;
if (nodeKey && node) {
return node.data[nodeKey];
}
return index;
}, },
getCheckedNodes(leafOnly) { getCheckedNodes(leafOnly) {
return this.tree.getCheckedNodes(leafOnly); return this.store.getCheckedNodes(leafOnly);
}, },
getCheckedKeys(leafOnly) { getCheckedKeys(leafOnly) {
return this.tree.getCheckedKeys(leafOnly); return this.store.getCheckedKeys(leafOnly);
}, },
setCheckedNodes(nodes, leafOnly) { setCheckedNodes(nodes, leafOnly) {
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes'); if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
this.tree.setCheckedNodes(nodes, leafOnly); this.store.setCheckedNodes(nodes, leafOnly);
}, },
setCheckedKeys(keys, leafOnly) { setCheckedKeys(keys, leafOnly) {
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes'); if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
this.tree.setCheckedKeys(keys, leafOnly); this.store.setCheckedKeys(keys, leafOnly);
} }
} }
}; };