Merge pull request #2090 from SergioCrisostomo/refactor-tree

Refactor tree
This commit is contained in:
Aresn 2017-10-24 02:44:19 -05:00 committed by GitHub
commit c89e0e1791
3 changed files with 123 additions and 117 deletions

View file

@ -1,6 +1,6 @@
<template> <template>
<collapse-transition> <collapse-transition>
<ul :class="classes" v-show="visible"> <ul :class="classes">
<li> <li>
<span :class="arrowClasses" @click="handleExpand"> <span :class="arrowClasses" @click="handleExpand">
<Icon type="arrow-right-b"></Icon> <Icon type="arrow-right-b"></Icon>
@ -8,15 +8,15 @@
<Checkbox <Checkbox
v-if="showCheckbox" v-if="showCheckbox"
:value="data.checked" :value="data.checked"
:indeterminate="indeterminate" :indeterminate="data.indeterminate"
:disabled="data.disabled || data.disableCheckbox" :disabled="data.disabled || data.disableCheckbox"
@click.native.prevent="handleCheck"></Checkbox> @click.native.prevent="handleCheck"></Checkbox>
<span :class="titleClasses" v-html="data.title" @click="handleSelect"></span> <span :class="titleClasses" v-html="data.title" @click="handleSelect"></span>
<Tree-node <Tree-node
v-if="data.expand"
v-for="item in data.children" v-for="item in data.children"
:key="item.nodeKey" :key="item.nodeKey"
:data="item" :data="item"
:visible="data.expand"
:multiple="multiple" :multiple="multiple"
:show-checkbox="showCheckbox"> :show-checkbox="showCheckbox">
</Tree-node> </Tree-node>
@ -29,7 +29,6 @@
import Icon from '../icon/icon.vue'; import Icon from '../icon/icon.vue';
import CollapseTransition from '../base/collapse-transition'; import CollapseTransition from '../base/collapse-transition';
import Emitter from '../../mixins/emitter'; import Emitter from '../../mixins/emitter';
import { findComponentsDownward } from '../../utils/assist';
const prefixCls = 'ivu-tree'; const prefixCls = 'ivu-tree';
@ -51,16 +50,11 @@
showCheckbox: { showCheckbox: {
type: Boolean, type: Boolean,
default: false default: false
},
visible: {
type: Boolean,
default: false
} }
}, },
data () { data () {
return { return {
prefixCls: prefixCls, prefixCls: prefixCls
indeterminate: false
}; };
}, },
computed: { computed: {
@ -103,40 +97,16 @@
}, },
handleSelect () { handleSelect () {
if (this.data.disabled) return; if (this.data.disabled) return;
if (this.data.selected) { this.dispatch('Tree', 'on-selected', this.data.nodeKey);
this.data.selected = false;
} else if (this.multiple) {
this.$set(this.data, 'selected', !this.data.selected);
} else {
this.dispatch('Tree', 'selected', this.data);
}
this.dispatch('Tree', 'on-selected');
}, },
handleCheck () { handleCheck () {
if (this.disabled) return; if (this.data.disabled) return;
const checked = !this.data.checked; const changes = {
if (!checked || this.indeterminate) { checked: !this.data.checked && !this.data.indeterminate,
findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = false); nodeKey: this.data.nodeKey
} else { };
findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = true); this.dispatch('Tree', 'on-check', changes);
} }
this.data.checked = checked;
this.dispatch('Tree', 'checked');
this.dispatch('Tree', 'on-checked');
},
setIndeterminate () {
this.indeterminate = this.data.checked ? false : findComponentsDownward(this, 'TreeNode').some(node => node.data.checked);
}
},
created () {
// created node.vue first, mounted tree.vue second
if (!this.data.checked) this.$set(this.data, 'checked', false);
},
mounted () {
this.$on('indeterminate', () => {
this.broadcast('TreeNode', 'indeterminate');
this.setIndeterminate();
});
} }
}; };
</script> </script>

View file

@ -1,26 +1,23 @@
<template> <template>
<div :class="prefixCls"> <div :class="prefixCls">
<Tree-node <Tree-node
v-for="item in data" v-for="item in stateTree"
:key="item.nodeKey" :key="item.nodeKey"
:data="item" :data="item"
visible visible
:multiple="multiple" :multiple="multiple"
:show-checkbox="showCheckbox"> :show-checkbox="showCheckbox">
</Tree-node> </Tree-node>
<div :class="[prefixCls + '-empty']" v-if="!data.length">{{ localeEmptyText }}</div> <div :class="[prefixCls + '-empty']" v-if="!stateTree.length">{{ localeEmptyText }}</div>
</div> </div>
</template> </template>
<script> <script>
import TreeNode from './node.vue'; import TreeNode from './node.vue';
import { findComponentsDownward } from '../../utils/assist';
import Emitter from '../../mixins/emitter'; import Emitter from '../../mixins/emitter';
import Locale from '../../mixins/locale'; import Locale from '../../mixins/locale';
const prefixCls = 'ivu-tree'; const prefixCls = 'ivu-tree';
let key = 1;
export default { export default {
name: 'Tree', name: 'Tree',
mixins: [ Emitter, Locale ], mixins: [ Emitter, Locale ],
@ -46,92 +43,128 @@
}, },
data () { data () {
return { return {
prefixCls: prefixCls prefixCls: prefixCls,
stateTree: this.data,
flatState: [],
}; };
}, },
watch: {
data(){
this.stateTree = this.data;
this.flatState = this.compileFlatState();
this.rebuildTree();
}
},
computed: { computed: {
localeEmptyText () { localeEmptyText () {
if (this.emptyText === undefined) { if (typeof this.emptyText === 'undefined') {
return this.t('i.tree.emptyText'); return this.t('i.tree.emptyText');
} else { } else {
return this.emptyText; return this.emptyText;
} }
} },
}, },
methods: { methods: {
getSelectedNodes () { compileFlatState () { // so we have always a relation parent/children of each node
const nodes = findComponentsDownward(this, 'TreeNode'); let keyCounter = 0;
return nodes.filter(node => node.data.selected).map(node => node.data); const flatTree = [];
}, function flattenChildren(node, parent) {
getCheckedNodes () { node.nodeKey = keyCounter++;
const nodes = findComponentsDownward(this, 'TreeNode'); flatTree[node.nodeKey] = { node: node, nodeKey: node.nodeKey };
return nodes.filter(node => node.data.checked).map(node => node.data); if (typeof parent != 'undefined') {
}, flatTree[node.nodeKey].parent = parent.nodeKey;
updateData (isInit = true) { flatTree[parent.nodeKey].children.push(node.nodeKey);
// init checked status
function reverseChecked(data) {
if (!data.nodeKey) data.nodeKey = key++;
if (data.children && data.children.length) {
let checkedLength = 0;
data.children.forEach(node => {
if (node.children) node = reverseChecked(node);
if (node.checked) checkedLength++;
});
if (isInit) {
if (checkedLength >= data.children.length) data.checked = true;
} else {
data.checked = checkedLength >= data.children.length;
}
return data;
} else {
return data;
}
} }
function forwardChecked(data) { if (node.children) {
if (data.children) { flatTree[node.nodeKey].children = [];
data.children.forEach(node => { node.children.forEach(child => flattenChildren(child, node));
if (data.checked) node.checked = true; }
if (node.children) node = forwardChecked(node); }
this.stateTree.forEach(rootNode => {
flattenChildren(rootNode);
}); });
return data; return flatTree;
},
updateTreeUp(nodeKey){
const parentKey = this.flatState[nodeKey].parent;
if (typeof parentKey == 'undefined') return;
const node = this.flatState[nodeKey].node;
const parent = this.flatState[parentKey].node;
if (node.checked == parent.checked && node.indeterminate == parent.indeterminate) return; // no need to update upwards
if (node.checked == true) {
this.$set(parent, 'checked', parent.children.every(node => node.checked));
this.$set(parent, 'indeterminate', !parent.checked);
} else { } else {
return data; this.$set(parent, 'checked', false);
this.$set(parent, 'indeterminate', parent.children.some(node => node.checked || node.indeterminate));
} }
this.updateTreeUp(parentKey);
},
rebuildTree () { // only called when `data` prop changes
const checkedNodes = this.getCheckedNodes();
checkedNodes.forEach(node => {
this.updateTreeDown(node, {checked: true});
// propagate upwards
const parentKey = this.flatState[node.nodeKey].parent;
if (!parentKey && parentKey !== 0) return;
const parent = this.flatState[parentKey].node;
const childHasCheckSetter = typeof node.checked != 'undefined' && node.checked;
if (childHasCheckSetter && parent.checked != node.checked) {
this.updateTreeUp(node.nodeKey); // update tree upwards
} }
this.data.map(node => reverseChecked(node)).map(node => forwardChecked(node)); });
this.broadcast('TreeNode', 'indeterminate'); },
getSelectedNodes () {
/* public API */
return this.flatState.filter(obj => obj.node.selected).map(obj => obj.node);
},
getCheckedNodes () {
/* public API */
return this.flatState.filter(obj => obj.node.checked).map(obj => obj.node);
},
updateTreeDown(node, changes = {}) {
for (let key in changes) {
this.$set(node, key, changes[key]);
} }
if (node.children) {
node.children.forEach(child => {
this.updateTreeDown(child, changes);
});
}
},
handleSelect (nodeKey) {
const node = this.flatState[nodeKey].node;
if (!this.multiple){ // reset selected
const currentSelectedKey = this.flatState.findIndex(obj => obj.node.selected);
if (currentSelectedKey >= 0) this.$set(this.flatState[currentSelectedKey].node, 'selected', false);
}
this.$set(node, 'selected', !node.selected);
this.$emit('on-select-change', this.getSelectedNodes());
},
handleCheck({ checked, nodeKey }) {
const node = this.flatState[nodeKey].node;
this.$set(node, 'checked', checked);
this.$set(node, 'indeterminate', false);
this.updateTreeUp(nodeKey); // propagate up
this.updateTreeDown(node, {checked, indeterminate: false}); // reset `indeterminate` when going down
this.$emit('on-check-change', this.getCheckedNodes());
}
},
created(){
this.flatState = this.compileFlatState();
this.rebuildTree();
}, },
mounted () { mounted () {
this.updateData(); this.$on('on-check', this.handleCheck);
this.$on('selected', ori => { this.$on('on-selected', this.handleSelect);
const nodes = findComponentsDownward(this, 'TreeNode'); this.$on('toggle-expand', node => this.$emit('on-toggle-expand', node));
nodes.forEach(node => {
this.$set(node.data, 'selected', false);
});
this.$set(ori, 'selected', true);
});
this.$on('on-selected', () => {
this.$emit('on-select-change', this.getSelectedNodes());
});
this.$on('checked', () => {
this.updateData(false);
});
this.$on('on-checked', () => {
this.$emit('on-check-change', this.getCheckedNodes());
});
this.$on('toggle-expand', (payload) => {
this.$emit('on-toggle-expand', payload);
});
},
watch: {
data () {
this.$nextTick(() => {
this.updateData();
this.broadcast('TreeNode', 'indeterminate');
});
}
} }
}; };
</script> </script>

View file

@ -38,7 +38,10 @@
} }
&-arrow{ &-arrow{
cursor: pointer; cursor: pointer;
i{ width: 12px;
text-align: center;
display: inline-block;
i {
transition: all @transition-time @ease-in-out; transition: all @transition-time @ease-in-out;
} }
&-open{ &-open{