Commit d44420be721153feb22e150d3b05ecccfdc703d7

Authored by Sergio Crisostomo
1 parent 94d177cc

refactor and make indeterminate accessible outside tree

Showing 2 changed files with 118 additions and 115 deletions   Show diff stats
src/components/tree/node.vue
1 <template> 1 <template>
2 <collapse-transition> 2 <collapse-transition>
3 - <ul :class="classes" v-show="visible"> 3 + <ul :class="classes">
4 <li> 4 <li>
5 <span :class="arrowClasses" @click="handleExpand"> 5 <span :class="arrowClasses" @click="handleExpand">
6 <Icon type="arrow-right-b"></Icon> 6 <Icon type="arrow-right-b"></Icon>
@@ -8,15 +8,15 @@ @@ -8,15 +8,15 @@
8 <Checkbox 8 <Checkbox
9 v-if="showCheckbox" 9 v-if="showCheckbox"
10 :value="data.checked" 10 :value="data.checked"
11 - :indeterminate="indeterminate" 11 + :indeterminate="data.indeterminate"
12 :disabled="data.disabled || data.disableCheckbox" 12 :disabled="data.disabled || data.disableCheckbox"
13 @click.native.prevent="handleCheck"></Checkbox> 13 @click.native.prevent="handleCheck"></Checkbox>
14 <span :class="titleClasses" v-html="data.title" @click="handleSelect"></span> 14 <span :class="titleClasses" v-html="data.title" @click="handleSelect"></span>
15 <Tree-node 15 <Tree-node
  16 + v-if="data.expand"
16 v-for="item in data.children" 17 v-for="item in data.children"
17 :key="item.nodeKey" 18 :key="item.nodeKey"
18 :data="item" 19 :data="item"
19 - :visible="data.expand"  
20 :multiple="multiple" 20 :multiple="multiple"
21 :show-checkbox="showCheckbox"> 21 :show-checkbox="showCheckbox">
22 </Tree-node> 22 </Tree-node>
@@ -29,7 +29,6 @@ @@ -29,7 +29,6 @@
29 import Icon from '../icon/icon.vue'; 29 import Icon from '../icon/icon.vue';
30 import CollapseTransition from '../base/collapse-transition'; 30 import CollapseTransition from '../base/collapse-transition';
31 import Emitter from '../../mixins/emitter'; 31 import Emitter from '../../mixins/emitter';
32 - import { findComponentsDownward } from '../../utils/assist';  
33 32
34 const prefixCls = 'ivu-tree'; 33 const prefixCls = 'ivu-tree';
35 34
@@ -51,16 +50,11 @@ @@ -51,16 +50,11 @@
51 showCheckbox: { 50 showCheckbox: {
52 type: Boolean, 51 type: Boolean,
53 default: false 52 default: false
54 - },  
55 - visible: {  
56 - type: Boolean,  
57 - default: false  
58 } 53 }
59 }, 54 },
60 data () { 55 data () {
61 return { 56 return {
62 - prefixCls: prefixCls,  
63 - indeterminate: false 57 + prefixCls: prefixCls
64 }; 58 };
65 }, 59 },
66 computed: { 60 computed: {
@@ -103,40 +97,16 @@ @@ -103,40 +97,16 @@
103 }, 97 },
104 handleSelect () { 98 handleSelect () {
105 if (this.data.disabled) return; 99 if (this.data.disabled) return;
106 - if (this.data.selected) {  
107 - this.data.selected = false;  
108 - } else if (this.multiple) {  
109 - this.$set(this.data, 'selected', !this.data.selected);  
110 - } else {  
111 - this.dispatch('Tree', 'selected', this.data);  
112 - }  
113 - this.dispatch('Tree', 'on-selected'); 100 + this.dispatch('Tree', 'on-selected', this.data.nodeKey);
114 }, 101 },
115 handleCheck () { 102 handleCheck () {
116 - if (this.disabled) return;  
117 - const checked = !this.data.checked;  
118 - if (!checked || this.indeterminate) {  
119 - findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = false);  
120 - } else {  
121 - findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = true);  
122 - }  
123 - this.data.checked = checked;  
124 - this.dispatch('Tree', 'checked');  
125 - this.dispatch('Tree', 'on-checked');  
126 - },  
127 - setIndeterminate () {  
128 - this.indeterminate = this.data.checked ? false : findComponentsDownward(this, 'TreeNode').some(node => node.data.checked); 103 + if (this.data.disabled) return;
  104 + const changes = {
  105 + checked: !this.data.checked && !this.data.indeterminate,
  106 + nodeKey: this.data.nodeKey
  107 + };
  108 + this.dispatch('Tree', 'on-check', changes);
129 } 109 }
130 - },  
131 - created () {  
132 - // created node.vue first, mounted tree.vue second  
133 - if (!this.data.checked) this.$set(this.data, 'checked', false);  
134 - },  
135 - mounted () {  
136 - this.$on('indeterminate', () => {  
137 - this.broadcast('TreeNode', 'indeterminate');  
138 - this.setIndeterminate();  
139 - });  
140 } 110 }
141 }; 111 };
142 -</script>  
143 \ No newline at end of file 112 \ No newline at end of file
  113 +</script>
src/components/tree/tree.vue
1 <template> 1 <template>
2 <div :class="prefixCls"> 2 <div :class="prefixCls">
3 <Tree-node 3 <Tree-node
4 - v-for="item in data" 4 + v-for="item in stateTree"
5 :key="item.nodeKey" 5 :key="item.nodeKey"
6 :data="item" 6 :data="item"
7 visible 7 visible
8 :multiple="multiple" 8 :multiple="multiple"
9 :show-checkbox="showCheckbox"> 9 :show-checkbox="showCheckbox">
10 </Tree-node> 10 </Tree-node>
11 - <div :class="[prefixCls + '-empty']" v-if="!data.length">{{ localeEmptyText }}</div> 11 + <div :class="[prefixCls + '-empty']" v-if="!stateTree.length">{{ localeEmptyText }}</div>
12 </div> 12 </div>
13 </template> 13 </template>
14 <script> 14 <script>
15 import TreeNode from './node.vue'; 15 import TreeNode from './node.vue';
16 - import { findComponentsDownward } from '../../utils/assist';  
17 import Emitter from '../../mixins/emitter'; 16 import Emitter from '../../mixins/emitter';
18 import Locale from '../../mixins/locale'; 17 import Locale from '../../mixins/locale';
19 18
20 const prefixCls = 'ivu-tree'; 19 const prefixCls = 'ivu-tree';
21 20
22 - let key = 1;  
23 -  
24 export default { 21 export default {
25 name: 'Tree', 22 name: 'Tree',
26 mixins: [ Emitter, Locale ], 23 mixins: [ Emitter, Locale ],
@@ -46,92 +43,128 @@ @@ -46,92 +43,128 @@
46 }, 43 },
47 data () { 44 data () {
48 return { 45 return {
49 - prefixCls: prefixCls 46 + prefixCls: prefixCls,
  47 + stateTree: JSON.parse(JSON.stringify(this.data)),
  48 + flatState: [],
50 }; 49 };
51 }, 50 },
  51 + watch: {
  52 + data(){
  53 + this.stateTree = JSON.parse(JSON.stringify(this.data));
  54 + this.flatState = this.compileFlatState();
  55 + this.rebuildTree();
  56 + }
  57 + },
52 computed: { 58 computed: {
53 localeEmptyText () { 59 localeEmptyText () {
54 - if (this.emptyText === undefined) { 60 + if (typeof this.emptyText === 'undefined') {
55 return this.t('i.tree.emptyText'); 61 return this.t('i.tree.emptyText');
56 } else { 62 } else {
57 return this.emptyText; 63 return this.emptyText;
58 } 64 }
59 - } 65 + },
60 }, 66 },
61 methods: { 67 methods: {
  68 + compileFlatState () { // so we have always a relation parent/children of each node
  69 + let keyCounter = 0;
  70 + const flatTree = [];
  71 + function flattenChildren(node, parent) {
  72 + node.nodeKey = keyCounter++;
  73 + flatTree[node.nodeKey] = { node: node, nodeKey: node.nodeKey };
  74 + if (typeof parent != 'undefined') {
  75 + flatTree[node.nodeKey].parent = parent.nodeKey;
  76 + flatTree[parent.nodeKey].children.push(node.nodeKey);
  77 + }
  78 +
  79 + if (node.children) {
  80 + flatTree[node.nodeKey].children = [];
  81 + node.children.forEach(child => flattenChildren(child, node));
  82 + }
  83 + }
  84 + this.stateTree.forEach(rootNode => {
  85 + flattenChildren(rootNode);
  86 + });
  87 + return flatTree;
  88 + },
  89 + updateTreeUp(nodeKey){
  90 + const parentKey = this.flatState[nodeKey].parent;
  91 + if (typeof parentKey == 'undefined') return;
  92 +
  93 + const node = this.flatState[nodeKey].node;
  94 + const parent = this.flatState[parentKey].node;
  95 + if (node.checked == parent.checked && node.indeterminate == parent.indeterminate) return; // no need to update upwards
  96 +
  97 + if (node.checked == true) {
  98 + this.$set(parent, 'checked', parent.children.every(node => node.checked));
  99 + this.$set(parent, 'indeterminate', !parent.checked);
  100 + } else {
  101 + this.$set(parent, 'checked', false);
  102 + this.$set(parent, 'indeterminate', parent.children.some(node => node.checked || node.indeterminate));
  103 + }
  104 + this.updateTreeUp(parentKey);
  105 + },
  106 + rebuildTree () { // only called when `data` prop changes
  107 + const checkedNodes = this.getCheckedNodes();
  108 + checkedNodes.forEach(node => {
  109 + this.updateTreeDown(node, {checked: true});
  110 + // propagate upwards
  111 + const parentKey = this.flatState[node.nodeKey].parent;
  112 + if (!parentKey) return;
  113 + const parent = this.flatState[parentKey].node;
  114 + const childHasCheckSetter = typeof node.checked != 'undefined' && node.checked;
  115 + if (childHasCheckSetter && parent.checked != node.checked) {
  116 + this.updateTreeUp(node.nodeKey); // update tree upwards
  117 + }
  118 + });
  119 + },
  120 +
62 getSelectedNodes () { 121 getSelectedNodes () {
63 - const nodes = findComponentsDownward(this, 'TreeNode');  
64 - return nodes.filter(node => node.data.selected).map(node => node.data); 122 + /* public API */
  123 + return this.flatState.filter(obj => obj.node.selected).map(obj => obj.node);
65 }, 124 },
66 getCheckedNodes () { 125 getCheckedNodes () {
67 - const nodes = findComponentsDownward(this, 'TreeNode');  
68 - return nodes.filter(node => node.data.checked).map(node => node.data); 126 + /* public API */
  127 + return this.flatState.filter(obj => obj.node.checked).map(obj => obj.node);
69 }, 128 },
70 - updateData (isInit = true) {  
71 - // init checked status  
72 - function reverseChecked(data) {  
73 - if (!data.nodeKey) data.nodeKey = key++;  
74 - if (data.children && data.children.length) {  
75 - let checkedLength = 0;  
76 - data.children.forEach(node => {  
77 - if (node.children) node = reverseChecked(node);  
78 - if (node.checked) checkedLength++;  
79 - });  
80 - if (isInit) {  
81 - if (checkedLength >= data.children.length) data.checked = true;  
82 - } else {  
83 - data.checked = checkedLength >= data.children.length;  
84 - }  
85 - return data;  
86 - } else {  
87 - return data;  
88 - } 129 + updateTreeDown(node, changes = {}) {
  130 + for (let key in changes) {
  131 + this.$set(node, key, changes[key]);
89 } 132 }
90 -  
91 - function forwardChecked(data) {  
92 - if (data.children) {  
93 - data.children.forEach(node => {  
94 - if (data.checked) node.checked = true;  
95 - if (node.children) node = forwardChecked(node);  
96 - });  
97 - return data;  
98 - } else {  
99 - return data;  
100 - } 133 + if (node.children) {
  134 + node.children.forEach(child => {
  135 + this.updateTreeDown(child, changes);
  136 + });
101 } 137 }
102 - this.data.map(node => reverseChecked(node)).map(node => forwardChecked(node));  
103 - this.broadcast('TreeNode', 'indeterminate');  
104 - }  
105 - },  
106 - mounted () {  
107 - this.updateData();  
108 - this.$on('selected', ori => {  
109 - const nodes = findComponentsDownward(this, 'TreeNode');  
110 - nodes.forEach(node => {  
111 - this.$set(node.data, 'selected', false);  
112 - });  
113 - this.$set(ori, 'selected', true);  
114 - });  
115 - this.$on('on-selected', () => { 138 + },
  139 + handleSelect (nodeKey) {
  140 + const node = this.flatState[nodeKey].node;
  141 + if (!this.multiple){ // reset selected
  142 + const currentSelectedKey = this.flatState.findIndex(obj => obj.node.selected);
  143 + if (currentSelectedKey >= 0) this.$set(this.flatState[currentSelectedKey].node, 'selected', false);
  144 + }
  145 + this.$set(node, 'selected', !node.selected);
  146 +
116 this.$emit('on-select-change', this.getSelectedNodes()); 147 this.$emit('on-select-change', this.getSelectedNodes());
117 - });  
118 - this.$on('checked', () => {  
119 - this.updateData(false);  
120 - });  
121 - this.$on('on-checked', () => { 148 + },
  149 + handleCheck({ checked, nodeKey }) {
  150 + const node = this.flatState[nodeKey].node;
  151 + this.$set(node, 'checked', checked);
  152 + this.$set(node, 'indeterminate', false);
  153 +
  154 + this.updateTreeUp(nodeKey); // propagate up
  155 + this.updateTreeDown(node, {checked, indeterminate: false}); // reset `indeterminate` when going down
  156 +
122 this.$emit('on-check-change', this.getCheckedNodes()); 157 this.$emit('on-check-change', this.getCheckedNodes());
123 - });  
124 - this.$on('toggle-expand', (payload) => {  
125 - this.$emit('on-toggle-expand', payload);  
126 - });  
127 - },  
128 - watch: {  
129 - data () {  
130 - this.$nextTick(() => {  
131 - this.updateData();  
132 - this.broadcast('TreeNode', 'indeterminate');  
133 - });  
134 } 158 }
  159 + },
  160 + created(){
  161 + this.flatState = this.compileFlatState();
  162 + this.rebuildTree();
  163 + },
  164 + mounted () {
  165 + this.$on('on-check', this.handleCheck);
  166 + this.$on('on-selected', this.handleSelect);
  167 + this.$on('toggle-expand', node => this.$emit('on-toggle-expand', node));
135 } 168 }
136 }; 169 };
137 -</script>  
138 \ No newline at end of file 170 \ No newline at end of file
  171 +</script>