Commit c89e0e1791639c344fb4186e148a34a17ce58b8d
Committed by
GitHub
Merge pull request #2090 from SergioCrisostomo/refactor-tree
Refactor tree
Showing
3 changed files
with
123 additions
and
117 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: this.data, | |
| 48 | + flatState: [], | |
| 50 | 49 | }; |
| 51 | 50 | }, |
| 51 | + watch: { | |
| 52 | + data(){ | |
| 53 | + this.stateTree = 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 && parentKey !== 0) 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> | ... | ... |
src/styles/components/tree.less
| ... | ... | @@ -38,7 +38,10 @@ |
| 38 | 38 | } |
| 39 | 39 | &-arrow{ |
| 40 | 40 | cursor: pointer; |
| 41 | - i{ | |
| 41 | + width: 12px; | |
| 42 | + text-align: center; | |
| 43 | + display: inline-block; | |
| 44 | + i { | |
| 42 | 45 | transition: all @transition-time @ease-in-out; |
| 43 | 46 | } |
| 44 | 47 | &-open{ |
| ... | ... | @@ -56,4 +59,4 @@ |
| 56 | 59 | cursor: @cursor-disabled; |
| 57 | 60 | } |
| 58 | 61 | } |
| 59 | -} | |
| 60 | 62 | \ No newline at end of file |
| 63 | +} | ... | ... |