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 | <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> |