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