Commit d44420be721153feb22e150d3b05ecccfdc703d7
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 | 1 | <template> |
2 | 2 | <collapse-transition> |
3 | - <ul :class="classes" v-show="visible"> | |
3 | + <ul :class="classes"> | |
4 | 4 | <li> |
5 | 5 | <span :class="arrowClasses" @click="handleExpand"> |
6 | 6 | <Icon type="arrow-right-b"></Icon> |
... | ... | @@ -8,15 +8,15 @@ |
8 | 8 | <Checkbox |
9 | 9 | v-if="showCheckbox" |
10 | 10 | :value="data.checked" |
11 | - :indeterminate="indeterminate" | |
11 | + :indeterminate="data.indeterminate" | |
12 | 12 | :disabled="data.disabled || data.disableCheckbox" |
13 | 13 | @click.native.prevent="handleCheck"></Checkbox> |
14 | 14 | <span :class="titleClasses" v-html="data.title" @click="handleSelect"></span> |
15 | 15 | <Tree-node |
16 | + v-if="data.expand" | |
16 | 17 | v-for="item in data.children" |
17 | 18 | :key="item.nodeKey" |
18 | 19 | :data="item" |
19 | - :visible="data.expand" | |
20 | 20 | :multiple="multiple" |
21 | 21 | :show-checkbox="showCheckbox"> |
22 | 22 | </Tree-node> |
... | ... | @@ -29,7 +29,6 @@ |
29 | 29 | import Icon from '../icon/icon.vue'; |
30 | 30 | import CollapseTransition from '../base/collapse-transition'; |
31 | 31 | import Emitter from '../../mixins/emitter'; |
32 | - import { findComponentsDownward } from '../../utils/assist'; | |
33 | 32 | |
34 | 33 | const prefixCls = 'ivu-tree'; |
35 | 34 | |
... | ... | @@ -51,16 +50,11 @@ |
51 | 50 | showCheckbox: { |
52 | 51 | type: Boolean, |
53 | 52 | default: false |
54 | - }, | |
55 | - visible: { | |
56 | - type: Boolean, | |
57 | - default: false | |
58 | 53 | } |
59 | 54 | }, |
60 | 55 | data () { |
61 | 56 | return { |
62 | - prefixCls: prefixCls, | |
63 | - indeterminate: false | |
57 | + prefixCls: prefixCls | |
64 | 58 | }; |
65 | 59 | }, |
66 | 60 | computed: { |
... | ... | @@ -103,40 +97,16 @@ |
103 | 97 | }, |
104 | 98 | handleSelect () { |
105 | 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 | 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 | 112 | \ No newline at end of file |
113 | +</script> | ... | ... |
src/components/tree/tree.vue
1 | 1 | <template> |
2 | 2 | <div :class="prefixCls"> |
3 | 3 | <Tree-node |
4 | - v-for="item in data" | |
4 | + v-for="item in stateTree" | |
5 | 5 | :key="item.nodeKey" |
6 | 6 | :data="item" |
7 | 7 | visible |
8 | 8 | :multiple="multiple" |
9 | 9 | :show-checkbox="showCheckbox"> |
10 | 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 | 12 | </div> |
13 | 13 | </template> |
14 | 14 | <script> |
15 | 15 | import TreeNode from './node.vue'; |
16 | - import { findComponentsDownward } from '../../utils/assist'; | |
17 | 16 | import Emitter from '../../mixins/emitter'; |
18 | 17 | import Locale from '../../mixins/locale'; |
19 | 18 | |
20 | 19 | const prefixCls = 'ivu-tree'; |
21 | 20 | |
22 | - let key = 1; | |
23 | - | |
24 | 21 | export default { |
25 | 22 | name: 'Tree', |
26 | 23 | mixins: [ Emitter, Locale ], |
... | ... | @@ -46,92 +43,128 @@ |
46 | 43 | }, |
47 | 44 | data () { |
48 | 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 | 58 | computed: { |
53 | 59 | localeEmptyText () { |
54 | - if (this.emptyText === undefined) { | |
60 | + if (typeof this.emptyText === 'undefined') { | |
55 | 61 | return this.t('i.tree.emptyText'); |
56 | 62 | } else { |
57 | 63 | return this.emptyText; |
58 | 64 | } |
59 | - } | |
65 | + }, | |
60 | 66 | }, |
61 | 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 | 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 | 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 | 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 | 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 | 170 | \ No newline at end of file |
171 | +</script> | ... | ... |