Commit c9b86944ec1dfe33c572461f2489a74f217194e8
1 parent
aaa96346
Refactor Select!
Showing
5 changed files
with
616 additions
and
641 deletions
Show diff stats
1 | + | ||
2 | +<script> | ||
3 | + const returnArrayFn = () => []; | ||
4 | + | ||
5 | + export default { | ||
6 | + props: { | ||
7 | + options: { | ||
8 | + type: Array, | ||
9 | + default: returnArrayFn | ||
10 | + }, | ||
11 | + slotOptions: { | ||
12 | + type: Array, | ||
13 | + default: returnArrayFn | ||
14 | + }, | ||
15 | + slotUpdateHook: { | ||
16 | + type: Function, | ||
17 | + default: () => {} | ||
18 | + }, | ||
19 | + }, | ||
20 | + functional: true, | ||
21 | + render(h, {props, parent}){ | ||
22 | + // to detect changes in the $slot children/options we do this hack | ||
23 | + // so we can trigger the parents computed properties and have everything reactive | ||
24 | + // although $slot.default is not | ||
25 | + if (props.slotOptions !== parent.$slots.default) props.slotUpdateHook(); | ||
26 | + return props.options; | ||
27 | + } | ||
28 | + }; | ||
29 | +</script> |
src/components/select/option.vue
1 | <template> | 1 | <template> |
2 | - <li :class="classes" @click.stop="select" @mouseout.stop="blur" v-show="!hidden"><slot>{{ showLabel }}</slot></li> | 2 | + <li :class="classes" @click.stop="select"><slot>{{ showLabel }}</slot></li> |
3 | </template> | 3 | </template> |
4 | <script> | 4 | <script> |
5 | import Emitter from '../../mixins/emitter'; | 5 | import Emitter from '../../mixins/emitter'; |
@@ -22,15 +22,19 @@ | @@ -22,15 +22,19 @@ | ||
22 | disabled: { | 22 | disabled: { |
23 | type: Boolean, | 23 | type: Boolean, |
24 | default: false | 24 | default: false |
25 | + }, | ||
26 | + selected: { | ||
27 | + type: Boolean, | ||
28 | + default: false | ||
29 | + }, | ||
30 | + isFocused: { | ||
31 | + type: Boolean, | ||
32 | + default: false | ||
25 | } | 33 | } |
26 | }, | 34 | }, |
27 | data () { | 35 | data () { |
28 | return { | 36 | return { |
29 | - selected: false, | ||
30 | - index: 0, // for up and down to focus | ||
31 | - isFocus: false, | ||
32 | - hidden: false, // for search | ||
33 | - searchLabel: '', // the value is slot,only for search | 37 | + searchLabel: '', // the slot value (textContent) |
34 | autoComplete: false | 38 | autoComplete: false |
35 | }; | 39 | }; |
36 | }, | 40 | }, |
@@ -41,53 +45,34 @@ | @@ -41,53 +45,34 @@ | ||
41 | { | 45 | { |
42 | [`${prefixCls}-disabled`]: this.disabled, | 46 | [`${prefixCls}-disabled`]: this.disabled, |
43 | [`${prefixCls}-selected`]: this.selected && !this.autoComplete, | 47 | [`${prefixCls}-selected`]: this.selected && !this.autoComplete, |
44 | - [`${prefixCls}-focus`]: this.isFocus | 48 | + [`${prefixCls}-focus`]: this.isFocused |
45 | } | 49 | } |
46 | ]; | 50 | ]; |
47 | }, | 51 | }, |
48 | showLabel () { | 52 | showLabel () { |
49 | return (this.label) ? this.label : this.value; | 53 | return (this.label) ? this.label : this.value; |
54 | + }, | ||
55 | + optionLabel(){ | ||
56 | + return (this.$el && this.$el.textContent) || this.label; | ||
50 | } | 57 | } |
51 | }, | 58 | }, |
52 | methods: { | 59 | methods: { |
53 | select () { | 60 | select () { |
54 | - if (this.disabled) { | ||
55 | - return false; | ||
56 | - } | 61 | + if (this.disabled) return false; |
57 | 62 | ||
58 | - this.dispatch('iSelect', 'on-select-selected', this.value); | 63 | + this.dispatch('iSelect', 'on-select-selected', { |
64 | + value: this.value, | ||
65 | + label: this.optionLabel, | ||
66 | + }); | ||
67 | + this.$emit('on-select-selected', { | ||
68 | + value: this.value, | ||
69 | + label: this.optionLabel, | ||
70 | + }); | ||
59 | }, | 71 | }, |
60 | - blur () { | ||
61 | - this.isFocus = false; | ||
62 | - }, | ||
63 | - queryChange (val) { | ||
64 | - const parsedQuery = val.replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, '\\$1'); | ||
65 | - this.hidden = !new RegExp(parsedQuery, 'i').test(this.searchLabel); | ||
66 | - }, | ||
67 | - // 在使用函数防抖后,设置 key 后,不更新组件了,导致SearchLabel 不更新 #1865 | ||
68 | - updateSearchLabel () { | ||
69 | - this.searchLabel = this.$el.textContent; | ||
70 | - }, | ||
71 | - onSelectClose(){ | ||
72 | - this.isFocus = false; | ||
73 | - }, | ||
74 | - onQueryChange(val){ | ||
75 | - this.queryChange(val); | ||
76 | - } | ||
77 | }, | 72 | }, |
78 | mounted () { | 73 | mounted () { |
79 | - this.updateSearchLabel(); | ||
80 | - this.dispatch('iSelect', 'append'); | ||
81 | - this.$on('on-select-close', this.onSelectClose); | ||
82 | - this.$on('on-query-change',this.onQueryChange); | ||
83 | - | ||
84 | const Select = findComponentUpward(this, 'iSelect'); | 74 | const Select = findComponentUpward(this, 'iSelect'); |
85 | if (Select) this.autoComplete = Select.autoComplete; | 75 | if (Select) this.autoComplete = Select.autoComplete; |
86 | }, | 76 | }, |
87 | - beforeDestroy () { | ||
88 | - this.dispatch('iSelect', 'remove'); | ||
89 | - this.$off('on-select-close', this.onSelectClose); | ||
90 | - this.$off('on-query-change',this.onQueryChange); | ||
91 | - } | ||
92 | }; | 77 | }; |
93 | </script> | 78 | </script> |
1 | +<template> | ||
2 | + <div @click="onHeaderClick"> | ||
3 | + <div class="ivu-tag ivu-tag-checked" v-for="item in selectedMultiple"> | ||
4 | + <span class="ivu-tag-text">{{ item.label }}</span> | ||
5 | + <Icon type="ios-close-empty" @click.native.stop="removeTag(item)"></Icon> | ||
6 | + </div> | ||
7 | + <span | ||
8 | + :class="singleDisplayClasses" | ||
9 | + v-show="singleDisplayValue" | ||
10 | + >{{ singleDisplayValue }}</span> | ||
11 | + <input | ||
12 | + :id="inputElementId" | ||
13 | + type="text" | ||
14 | + v-if="filterable" | ||
15 | + v-model="query" | ||
16 | + :disabled="disabled" | ||
17 | + :class="[prefixCls + '-input']" | ||
18 | + :placeholder="showPlaceholder ? localePlaceholder : ''" | ||
19 | + :style="inputStyle" | ||
20 | + autocomplete="off" | ||
21 | + spellcheck="false" | ||
22 | + @keydown="resetInputState" | ||
23 | + @keydown.delete="handleInputDelete" | ||
24 | + @focus="onInputFocus" | ||
25 | + @blur="onInputFocus" | ||
26 | + | ||
27 | + ref="input"> | ||
28 | + <Icon type="ios-close" :class="[prefixCls + '-arrow']" v-if="resetSelect" @click.native.stop="resetSelect"></Icon> | ||
29 | + <Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!resetSelect && !remote && !disabled"></Icon> | ||
30 | + </div> | ||
31 | +</template> | ||
32 | +<script> | ||
33 | + import Icon from '../icon'; | ||
34 | + import Emitter from '../../mixins/emitter'; | ||
35 | + import Locale from '../../mixins/locale'; | ||
36 | + | ||
37 | + const prefixCls = 'ivu-select'; | ||
38 | + | ||
39 | + export default { | ||
40 | + name: 'iSelectHead', | ||
41 | + mixins: [ Emitter, Locale ], | ||
42 | + components: { Icon }, | ||
43 | + props: { | ||
44 | + disabled: { | ||
45 | + type: Boolean, | ||
46 | + default: false | ||
47 | + }, | ||
48 | + filterable: { | ||
49 | + type: Boolean, | ||
50 | + default: false | ||
51 | + }, | ||
52 | + multiple: { | ||
53 | + type: Boolean, | ||
54 | + default: false | ||
55 | + }, | ||
56 | + remote: { | ||
57 | + type: Boolean, | ||
58 | + default: false | ||
59 | + }, | ||
60 | + initialLabel: { | ||
61 | + type: String, | ||
62 | + }, | ||
63 | + values: { | ||
64 | + type: Array, | ||
65 | + default: () => [] | ||
66 | + }, | ||
67 | + clearable: { | ||
68 | + type: [Function, Boolean], | ||
69 | + default: false, | ||
70 | + }, | ||
71 | + inputElementId: { | ||
72 | + type: String | ||
73 | + }, | ||
74 | + placeholder: { | ||
75 | + type: String | ||
76 | + }, | ||
77 | + queryProp: { | ||
78 | + type: String, | ||
79 | + default: '' | ||
80 | + } | ||
81 | + }, | ||
82 | + data () { | ||
83 | + return { | ||
84 | + prefixCls: prefixCls, | ||
85 | + query: '', | ||
86 | + inputLength: 20, | ||
87 | + remoteInitialLabel: this.initialLabel, | ||
88 | + preventRemoteCall: false, | ||
89 | + }; | ||
90 | + }, | ||
91 | + computed: { | ||
92 | + singleDisplayClasses(){ | ||
93 | + const {filterable, multiple, showPlaceholder} = this; | ||
94 | + return [{ | ||
95 | + [prefixCls + '-placeholder']: showPlaceholder && !filterable, | ||
96 | + [prefixCls + '-selected-value']: !showPlaceholder && !multiple && !filterable, | ||
97 | + }]; | ||
98 | + }, | ||
99 | + singleDisplayValue(){ | ||
100 | + if ((this.multiple && this.values.length > 0) || this.filterable) return ''; | ||
101 | + return `${this.selectedSingle}` || this.localePlaceholder; | ||
102 | + }, | ||
103 | + showPlaceholder () { | ||
104 | + let status = false; | ||
105 | + if (!this.multiple) { | ||
106 | + const value = this.values[0]; | ||
107 | + if (typeof value === 'undefined' || String(value).trim() === ''){ | ||
108 | + status = !this.remoteInitialLabel; | ||
109 | + } | ||
110 | + } else { | ||
111 | + if (!this.values.length > 0) { | ||
112 | + status = true; | ||
113 | + } | ||
114 | + } | ||
115 | + return status; | ||
116 | + }, | ||
117 | + resetSelect(){ | ||
118 | + return !this.showPlaceholder && this.clearable; | ||
119 | + }, | ||
120 | + inputStyle () { | ||
121 | + let style = {}; | ||
122 | + | ||
123 | + if (this.multiple) { | ||
124 | + if (this.showPlaceholder) { | ||
125 | + style.width = '100%'; | ||
126 | + } else { | ||
127 | + style.width = `${this.inputLength}px`; | ||
128 | + } | ||
129 | + } | ||
130 | + | ||
131 | + return style; | ||
132 | + }, | ||
133 | + localePlaceholder () { | ||
134 | + if (this.placeholder === undefined) { | ||
135 | + return this.t('i.select.placeholder'); | ||
136 | + } else { | ||
137 | + return this.placeholder; | ||
138 | + } | ||
139 | + }, | ||
140 | + selectedSingle(){ | ||
141 | + const selected = this.values[0]; | ||
142 | + return selected ? selected.label : (this.remoteInitialLabel || ''); | ||
143 | + }, | ||
144 | + selectedMultiple(){ | ||
145 | + return this.multiple ? this.values : []; | ||
146 | + } | ||
147 | + }, | ||
148 | + methods: { | ||
149 | + onInputFocus(e){ | ||
150 | + this.$emit(e.type === 'focus' ? 'on-input-focus' : 'on-input-blur'); | ||
151 | + }, | ||
152 | + removeTag (value) { | ||
153 | + if (this.disabled) return false; | ||
154 | + this.dispatch('iSelect', 'on-select-selected', value); | ||
155 | + }, | ||
156 | + resetInputState () { | ||
157 | + this.inputLength = this.$refs.input.value.length * 12 + 20; | ||
158 | + }, | ||
159 | + handleInputDelete () { | ||
160 | + if (this.multiple && this.selectedMultiple.length && this.query === '') { | ||
161 | + this.removeTag(this.selectedMultiple[this.selectedMultiple.length - 1]); | ||
162 | + } | ||
163 | + }, | ||
164 | + onHeaderClick(e){ | ||
165 | + if (this.filterable && e.target === this.$el){ | ||
166 | + this.$refs.input.focus(); | ||
167 | + } | ||
168 | + } | ||
169 | + }, | ||
170 | + watch: { | ||
171 | + values ([value]) { | ||
172 | + if (!this.filterable) return; | ||
173 | + this.preventRemoteCall = true; | ||
174 | + if (this.multiple){ | ||
175 | + this.query = ''; | ||
176 | + this.preventRemoteCall = false; // this should be after the query change setter above | ||
177 | + return; | ||
178 | + } | ||
179 | + // #982 | ||
180 | + if (typeof value === 'undefined' || value === '' || value === null) this.query = ''; | ||
181 | + else this.query = value.label; | ||
182 | + }, | ||
183 | + query (val) { | ||
184 | + if (this.preventRemoteCall) { | ||
185 | + this.preventRemoteCall = false; | ||
186 | + return; | ||
187 | + } | ||
188 | + | ||
189 | + this.$emit('on-query-change', val); | ||
190 | + }, | ||
191 | + queryProp(query){ | ||
192 | + if (query !== this.query) this.query = query; | ||
193 | + }, | ||
194 | + } | ||
195 | + }; | ||
196 | +</script> |
src/components/select/select.vue
1 | <template> | 1 | <template> |
2 | <div | 2 | <div |
3 | - tabindex="0" | ||
4 | - @keydown.down="handleFocus" | ||
5 | :class="classes" | 3 | :class="classes" |
6 | - v-clickoutside="handleClose"> | 4 | + v-click-outside.capture="onClickOutside" |
5 | + > | ||
7 | <div | 6 | <div |
8 | - :class="selectionCls" | ||
9 | ref="reference" | 7 | ref="reference" |
10 | - @click="toggleMenu"> | 8 | + |
9 | + :class="selectionCls" | ||
10 | + :tabindex="selectTabindex" | ||
11 | + | ||
12 | + @blur="toggleHeaderFocus" | ||
13 | + @focus="toggleHeaderFocus" | ||
14 | + | ||
15 | + @click="toggleMenu" | ||
16 | + @keydown.esc="handleKeydown" | ||
17 | + @keydown.enter="handleKeydown" | ||
18 | + @keydown.up="handleKeydown" | ||
19 | + @keydown.down="handleKeydown" | ||
20 | + @keydown.tab="handleKeydown" | ||
21 | + @keydown.delete="handleKeydown" | ||
22 | + | ||
23 | + | ||
24 | + @mouseenter="hasMouseHoverHead = true" | ||
25 | + @mouseleave="hasMouseHoverHead = false" | ||
26 | + | ||
27 | + > | ||
11 | <slot name="input"> | 28 | <slot name="input"> |
12 | - <input type="hidden" :name="name" :value="model"> | ||
13 | - <div class="ivu-tag ivu-tag-checked" v-for="(item, index) in selectedMultiple"> | ||
14 | - <span class="ivu-tag-text">{{ item.label }}</span> | ||
15 | - <Icon type="ios-close-empty" @click.native.stop="removeTag(index)"></Icon> | ||
16 | - </div> | ||
17 | - <span :class="[prefixCls + '-placeholder']" v-show="showPlaceholder && !filterable">{{ localePlaceholder }}</span> | ||
18 | - <span :class="[prefixCls + '-selected-value']" v-show="!showPlaceholder && !multiple && !filterable">{{ selectedSingle }}</span> | ||
19 | - <input | ||
20 | - :id="elementId" | ||
21 | - type="text" | ||
22 | - v-if="filterable" | ||
23 | - v-model="query" | 29 | + <input type="hidden" :name="name" :value="publicValue"> |
30 | + <select-head | ||
31 | + :filterable="filterable" | ||
32 | + :multiple="multiple" | ||
33 | + :values="values" | ||
34 | + :clearable="canBeCleared" | ||
24 | :disabled="disabled" | 35 | :disabled="disabled" |
25 | - :class="[prefixCls + '-input']" | ||
26 | - :placeholder="showPlaceholder ? localePlaceholder : ''" | ||
27 | - :style="inputStyle" | ||
28 | - autocomplete="off" | ||
29 | - spellcheck="false" | ||
30 | - @blur="handleBlur" | ||
31 | - @keydown="resetInputState" | ||
32 | - @keydown.delete="handleInputDelete" | ||
33 | - ref="input"> | ||
34 | - <Icon type="ios-close" :class="[prefixCls + '-arrow']" v-show="showCloseIcon" @click.native.stop="clearSingleSelect"></Icon> | ||
35 | - <Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!remote"></Icon> | 36 | + :remote="remote" |
37 | + :input-element-id="elementId" | ||
38 | + :initial-label="initialLabel" | ||
39 | + :placeholder="placeholder" | ||
40 | + :query-prop="query" | ||
41 | + | ||
42 | + @on-query-change="onQueryChange" | ||
43 | + @on-input-focus="isFocused = true" | ||
44 | + @on-input-blur="isFocused = false" | ||
45 | + | ||
46 | + ref="selectHead" | ||
47 | + /> | ||
36 | </slot> | 48 | </slot> |
37 | </div> | 49 | </div> |
38 | <transition name="transition-drop"> | 50 | <transition name="transition-drop"> |
@@ -42,9 +54,17 @@ | @@ -42,9 +54,17 @@ | ||
42 | :placement="placement" | 54 | :placement="placement" |
43 | ref="dropdown" | 55 | ref="dropdown" |
44 | :data-transfer="transfer" | 56 | :data-transfer="transfer" |
45 | - v-transfer-dom> | ||
46 | - <ul v-show="notFoundShow" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul> | ||
47 | - <ul v-show="(!notFound && !remote) || (remote && !loading && !notFound)" :class="[prefixCls + '-dropdown-list']"><slot></slot></ul> | 57 | + v-transfer-dom |
58 | + > | ||
59 | + <ul v-show="showNotFoundLabel" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul> | ||
60 | + <ul :class="prefixCls + '-dropdown-list'"> | ||
61 | + <functional-options | ||
62 | + v-if="(!remote) || (remote && !loading)" | ||
63 | + :options="selectOptions" | ||
64 | + :slot-update-hook="updateSlotOptions" | ||
65 | + :slot-options="slotOptions" | ||
66 | + ></functional-options> | ||
67 | + </ul> | ||
48 | <ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul> | 68 | <ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul> |
49 | </Drop> | 69 | </Drop> |
50 | </transition> | 70 | </transition> |
@@ -53,20 +73,32 @@ | @@ -53,20 +73,32 @@ | ||
53 | <script> | 73 | <script> |
54 | import Icon from '../icon'; | 74 | import Icon from '../icon'; |
55 | import Drop from './dropdown.vue'; | 75 | import Drop from './dropdown.vue'; |
56 | - import clickoutside from '../../directives/clickoutside'; | 76 | + import vClickOutside from 'v-click-outside-x/index'; |
57 | import TransferDom from '../../directives/transfer-dom'; | 77 | import TransferDom from '../../directives/transfer-dom'; |
58 | - import { oneOf, findComponentDownward } from '../../utils/assist'; | 78 | + import { oneOf } from '../../utils/assist'; |
59 | import Emitter from '../../mixins/emitter'; | 79 | import Emitter from '../../mixins/emitter'; |
60 | import Locale from '../../mixins/locale'; | 80 | import Locale from '../../mixins/locale'; |
61 | - import { debounce } from './utils'; | 81 | + import SelectHead from './select-head.vue'; |
82 | + import FunctionalOptions from './functional-options.vue'; | ||
62 | 83 | ||
63 | const prefixCls = 'ivu-select'; | 84 | const prefixCls = 'ivu-select'; |
85 | + const optionGroupRegexp = /option\-?group/i; | ||
86 | + | ||
87 | + const findChild = (instance, checkFn) => { | ||
88 | + let match = checkFn(instance); | ||
89 | + if (match) return instance; | ||
90 | + for (let i = 0, l = instance.$children.length; i < l; i++){ | ||
91 | + const child = instance.$children[i]; | ||
92 | + match = findChild(child, checkFn); | ||
93 | + if (match) return match; | ||
94 | + } | ||
95 | + }; | ||
64 | 96 | ||
65 | export default { | 97 | export default { |
66 | name: 'iSelect', | 98 | name: 'iSelect', |
67 | mixins: [ Emitter, Locale ], | 99 | mixins: [ Emitter, Locale ], |
68 | - components: { Icon, Drop }, | ||
69 | - directives: { clickoutside, TransferDom }, | 100 | + components: { FunctionalOptions, Drop, Icon, SelectHead }, |
101 | + directives: { clickOutside: vClickOutside.directive, TransferDom }, | ||
70 | props: { | 102 | props: { |
71 | value: { | 103 | value: { |
72 | type: [String, Number, Array], | 104 | type: [String, Number, Array], |
@@ -99,10 +131,6 @@ | @@ -99,10 +131,6 @@ | ||
99 | filterMethod: { | 131 | filterMethod: { |
100 | type: Function | 132 | type: Function |
101 | }, | 133 | }, |
102 | - remote: { | ||
103 | - type: Boolean, | ||
104 | - default: false | ||
105 | - }, | ||
106 | remoteMethod: { | 134 | remoteMethod: { |
107 | type: Function | 135 | type: Function |
108 | }, | 136 | }, |
@@ -147,23 +175,29 @@ | @@ -147,23 +175,29 @@ | ||
147 | type: String | 175 | type: String |
148 | } | 176 | } |
149 | }, | 177 | }, |
178 | + mounted(){ | ||
179 | + this.$on('on-select-selected', this.onOptionClick); | ||
180 | + | ||
181 | + // set the initial values if there are any | ||
182 | + if (this.values.length > 0 && !this.remote){ | ||
183 | + this.values = this.values.map(this.getOptionData); | ||
184 | + } | ||
185 | + }, | ||
150 | data () { | 186 | data () { |
187 | + | ||
151 | return { | 188 | return { |
152 | prefixCls: prefixCls, | 189 | prefixCls: prefixCls, |
190 | + values: this.getInitialValue(), | ||
191 | + dropDownWidth: 0, | ||
153 | visible: false, | 192 | visible: false, |
154 | - options: [], | ||
155 | - optionInstances: [], | ||
156 | - selectedSingle: '', // label | ||
157 | - selectedMultiple: [], | ||
158 | - focusIndex: 0, | 193 | + focusIndex: -1, |
194 | + isFocused: false, | ||
159 | query: '', | 195 | query: '', |
160 | - lastQuery: '', | ||
161 | - selectToChangeQuery: false, // when select an option, set this first and set query, because query is watching, it will emit event | ||
162 | - inputLength: 20, | ||
163 | - notFound: false, | ||
164 | - slotChangeDuration: false, // if slot change duration and in multiple, set true and after slot change, set false | ||
165 | - model: this.value, | ||
166 | - currentLabel: this.label | 196 | + initialLabel: this.label, |
197 | + hasMouseHoverHead: false, | ||
198 | + slotOptions: this.$slots.default, | ||
199 | + caretPosition: -1, | ||
200 | + lastRemoteQuery: '', | ||
167 | }; | 201 | }; |
168 | }, | 202 | }, |
169 | computed: { | 203 | computed: { |
@@ -189,58 +223,19 @@ | @@ -189,58 +223,19 @@ | ||
189 | }, | 223 | }, |
190 | selectionCls () { | 224 | selectionCls () { |
191 | return { | 225 | return { |
192 | - [`${prefixCls}-selection`]: !this.autoComplete | 226 | + [`${prefixCls}-selection`]: !this.autoComplete, |
227 | + [`${prefixCls}-selection-focused`]: this.isFocused | ||
193 | }; | 228 | }; |
194 | }, | 229 | }, |
195 | - showPlaceholder () { | ||
196 | - let status = false; | ||
197 | - | ||
198 | - if ((typeof this.model) === 'string') { | ||
199 | - if (this.model === '') { | ||
200 | - status = true; | ||
201 | - } | ||
202 | - } else if (Array.isArray(this.model)) { | ||
203 | - if (!this.model.length) { | ||
204 | - status = true; | ||
205 | - } | ||
206 | - } else if( this.model === null){ | ||
207 | - status = true; | ||
208 | - } | ||
209 | - | ||
210 | - return status; | ||
211 | - }, | ||
212 | - showCloseIcon () { | ||
213 | - return !this.multiple && this.clearable && !this.showPlaceholder; | ||
214 | - }, | ||
215 | - inputStyle () { | ||
216 | - let style = {}; | ||
217 | - | ||
218 | - if (this.multiple) { | ||
219 | - if (this.showPlaceholder) { | ||
220 | - style.width = '100%'; | ||
221 | - } else { | ||
222 | - style.width = `${this.inputLength}px`; | ||
223 | - } | ||
224 | - } | ||
225 | - | ||
226 | - return style; | ||
227 | - }, | ||
228 | - localePlaceholder () { | ||
229 | - if (this.placeholder === undefined) { | ||
230 | - return this.t('i.select.placeholder'); | ||
231 | - } else { | ||
232 | - return this.placeholder; | ||
233 | - } | ||
234 | - }, | ||
235 | localeNotFoundText () { | 230 | localeNotFoundText () { |
236 | - if (this.notFoundText === undefined) { | 231 | + if (typeof this.notFoundText === 'undefined') { |
237 | return this.t('i.select.noMatch'); | 232 | return this.t('i.select.noMatch'); |
238 | } else { | 233 | } else { |
239 | return this.notFoundText; | 234 | return this.notFoundText; |
240 | } | 235 | } |
241 | }, | 236 | }, |
242 | localeLoadingText () { | 237 | localeLoadingText () { |
243 | - if (this.loadingText === undefined) { | 238 | + if (typeof this.loadingText === 'undefined') { |
244 | return this.t('i.select.loading'); | 239 | return this.t('i.select.loading'); |
245 | } else { | 240 | } else { |
246 | return this.loadingText; | 241 | return this.loadingText; |
@@ -251,574 +246,358 @@ | @@ -251,574 +246,358 @@ | ||
251 | }, | 246 | }, |
252 | dropVisible () { | 247 | dropVisible () { |
253 | let status = true; | 248 | let status = true; |
254 | - const options = this.$slots.default || []; | 249 | + const options = this.selectOptions; |
255 | if (!this.loading && this.remote && this.query === '' && !options.length) status = false; | 250 | if (!this.loading && this.remote && this.query === '' && !options.length) status = false; |
256 | 251 | ||
257 | if (this.autoComplete && !options.length) status = false; | 252 | if (this.autoComplete && !options.length) status = false; |
258 | 253 | ||
259 | return this.visible && status; | 254 | return this.visible && status; |
260 | }, | 255 | }, |
261 | - notFoundShow () { | ||
262 | - const options = this.$slots.default || []; | ||
263 | - return (this.notFound && !this.remote) || (this.remote && !this.loading && !options.length); | ||
264 | - } | ||
265 | - }, | ||
266 | - methods: { | ||
267 | - // open when focus on Select and press `down` key | ||
268 | - handleFocus () { | ||
269 | - if (!this.visible) this.toggleMenu(); | ||
270 | - }, | ||
271 | - toggleMenu () { | ||
272 | - if (this.disabled || this.autoComplete) { | ||
273 | - return false; | ||
274 | - } | ||
275 | - this.visible = !this.visible; | 256 | + showNotFoundLabel () { |
257 | + const {loading, remote, selectOptions} = this; | ||
258 | + return selectOptions.length === 0 && (!remote || (remote && !loading)); | ||
276 | }, | 259 | }, |
277 | - hideMenu () { | ||
278 | - this.visible = false; | ||
279 | - this.focusIndex = 0; | ||
280 | - this.broadcast('iOption', 'on-select-close'); | ||
281 | - }, | ||
282 | - // find option component | ||
283 | - findChild (cb) { | ||
284 | - const find = function (child) { | ||
285 | - const name = child.$options.componentName; | ||
286 | - | ||
287 | - if (name) { | ||
288 | - cb(child); | ||
289 | - } else if (child.$children.length) { | ||
290 | - child.$children.forEach((innerChild) => { | ||
291 | - find(innerChild, cb); | ||
292 | - }); | ||
293 | - } | ||
294 | - }; | ||
295 | - | ||
296 | - if (this.optionInstances.length) { | ||
297 | - this.optionInstances.forEach((child) => { | ||
298 | - find(child); | ||
299 | - }); | 260 | + publicValue(){ |
261 | + if (this.labelInValue){ | ||
262 | + return this.multiple ? this.values : this.values[0]; | ||
300 | } else { | 263 | } else { |
301 | - this.$children.forEach((child) => { | ||
302 | - find(child); | ||
303 | - }); | 264 | + return this.multiple ? this.values.map(option => option.value) : (this.values[0] || {}).value; |
304 | } | 265 | } |
305 | }, | 266 | }, |
306 | - updateOptions (slot = false) { | ||
307 | - let options = []; | ||
308 | - let index = 1; | ||
309 | - | ||
310 | - this.findChild((child) => { | ||
311 | - options.push({ | ||
312 | - value: child.value, | ||
313 | - label: (child.label === undefined) ? child.$el.textContent : child.label | ||
314 | - }); | ||
315 | - child.index = index++; | 267 | + canBeCleared(){ |
268 | + const uiStateMatch = this.hasMouseHoverHead || this.active; | ||
269 | + const qualifiesForClear = !this.multiple && this.clearable; | ||
270 | + return uiStateMatch && qualifiesForClear && this.reset; // we return a function | ||
271 | + }, | ||
272 | + selectOptions() { | ||
273 | + const selectOptions = []; | ||
274 | + let optionCounter = -1; | ||
275 | + const currentIndex = this.focusIndex; | ||
276 | + const selectedValues = this.values.map(({value}) => value); | ||
277 | + for (let option of (this.slotOptions || [])) { | ||
316 | 278 | ||
317 | - this.optionInstances.push(child); | ||
318 | - }); | 279 | + if (!option.componentOptions) continue; |
319 | 280 | ||
320 | - this.options = options; | 281 | + if (option.componentOptions.tag.match(optionGroupRegexp)){ |
282 | + let children = option.componentOptions.children; | ||
321 | 283 | ||
322 | - if (!this.remote) { | ||
323 | - this.updateSingleSelected(true, slot); | ||
324 | - this.updateMultipleSelected(true, slot); | ||
325 | - } | ||
326 | - }, | ||
327 | - updateSingleSelected (init = false, slot = false) { | ||
328 | - const type = typeof this.model; | 284 | + // remove filtered children |
285 | + if (this.filterable){ | ||
286 | + children = children.filter( | ||
287 | + ({componentOptions}) => this.validateOption(componentOptions) | ||
288 | + ); | ||
289 | + } | ||
329 | 290 | ||
330 | - if (type === 'string' || type === 'number') { | ||
331 | - let findModel = false; | 291 | + option.componentOptions.children = children.map(opt => { |
292 | + optionCounter = optionCounter + 1; | ||
293 | + return this.processOption(opt, selectedValues, optionCounter === currentIndex); | ||
294 | + }); | ||
332 | 295 | ||
333 | - for (let i = 0; i < this.options.length; i++) { | ||
334 | - if (this.model === this.options[i].value) { | ||
335 | - this.selectedSingle = this.options[i].label; | ||
336 | - findModel = true; | ||
337 | - break; | ||
338 | - } | ||
339 | - } | 296 | + // keep the group if it still has children |
297 | + if (option.componentOptions.children.length > 0) selectOptions.push({...option}); | ||
298 | + } else { | ||
299 | + // ignore option if not passing filter | ||
300 | + const optionPassesFilter = this.filterable ? this.validateOption(option.componentOptions) : option; | ||
301 | + if (!optionPassesFilter) continue; | ||
340 | 302 | ||
341 | - if (slot && !findModel) { | ||
342 | - this.model = ''; | ||
343 | - this.query = ''; | 303 | + optionCounter = optionCounter + 1; |
304 | + selectOptions.push(this.processOption(option, selectedValues, optionCounter === currentIndex)); | ||
344 | } | 305 | } |
345 | } | 306 | } |
346 | 307 | ||
347 | - this.toggleSingleSelected(this.model, init); | 308 | + return selectOptions; |
348 | }, | 309 | }, |
349 | - clearSingleSelect () { | ||
350 | - if (this.showCloseIcon) { | ||
351 | - this.findChild((child) => { | ||
352 | - child.selected = false; | ||
353 | - }); | ||
354 | - this.model = ''; | ||
355 | - | ||
356 | - if (this.filterable) { | ||
357 | - this.query = ''; | ||
358 | - } | 310 | + flatOptions(){ |
311 | + return this.selectOptions.reduce((options, option) => { | ||
312 | + const isOptionGroup = option.componentOptions.tag.match(optionGroupRegexp); | ||
313 | + if (isOptionGroup) return options.concat(option.componentOptions.children || []); | ||
314 | + else return options.concat(option); | ||
315 | + }, []); | ||
316 | + }, | ||
317 | + selectTabindex(){ | ||
318 | + return this.disabled || this.filterable ? -1 : 0; | ||
319 | + }, | ||
320 | + remote(){ | ||
321 | + return typeof this.remoteMethod === 'function'; | ||
322 | + } | ||
323 | + }, | ||
324 | + methods: { | ||
325 | + setQuery(query){ // PUBLIC API | ||
326 | + if (query) { | ||
327 | + this.onQueryChange(query); | ||
328 | + return; | ||
329 | + } | ||
330 | + if (query === null) { | ||
331 | + this.onQueryChange(''); | ||
332 | + this.values = []; | ||
359 | } | 333 | } |
360 | }, | 334 | }, |
361 | - updateMultipleSelected (init = false, slot = false) { | ||
362 | - if (this.multiple && Array.isArray(this.model)) { | ||
363 | - let selected = this.remote ? this.selectedMultiple : []; | ||
364 | - | ||
365 | - for (let i = 0; i < this.model.length; i++) { | ||
366 | - const model = this.model[i]; | ||
367 | - | ||
368 | - for (let j = 0; j < this.options.length; j++) { | ||
369 | - const option = this.options[j]; | 335 | + clearSingleSelect(){ // PUBLIC API |
336 | + if (this.clearable) this.values = []; | ||
337 | + }, | ||
338 | + getOptionData(value){ | ||
339 | + const option = this.flatOptions.find(({componentOptions}) => componentOptions.propsData.value === value); | ||
340 | + const textContent = option.componentOptions.children.reduce((str, child) => str + (child.text || ''), ''); | ||
341 | + const label = option.componentOptions.propsData.label || textContent || ''; | ||
342 | + return { | ||
343 | + value: value, | ||
344 | + label: label, | ||
345 | + }; | ||
346 | + }, | ||
347 | + getInitialValue(){ | ||
348 | + const {multiple, value} = this; | ||
349 | + let initialValue = Array.isArray(value) ? value : [value]; | ||
350 | + if (!multiple && (typeof initialValue[0] === 'undefined' || String(initialValue[0]).trim() === '')) initialValue = []; | ||
351 | + return initialValue; | ||
352 | + }, | ||
353 | + processOption(option, values, isFocused){ | ||
354 | + if (!option.componentOptions) return option; | ||
355 | + const optionValue = option.componentOptions.propsData.value; | ||
356 | + const disabled = option.componentOptions.propsData.disabled; | ||
357 | + const isSelected = values.includes(optionValue); | ||
358 | + | ||
359 | + const propsData = { | ||
360 | + ...option.componentOptions.propsData, | ||
361 | + selected: isSelected, | ||
362 | + isFocused: isFocused, | ||
363 | + disabled: typeof disabled === 'undefined' ? false : disabled !== false, | ||
364 | + }; | ||
370 | 365 | ||
371 | - if (model === option.value) { | ||
372 | - selected.push({ | ||
373 | - value: option.value, | ||
374 | - label: option.label | ||
375 | - }); | ||
376 | - } | ||
377 | - } | 366 | + return { |
367 | + ...option, | ||
368 | + componentOptions: { | ||
369 | + ...option.componentOptions, | ||
370 | + propsData: propsData | ||
378 | } | 371 | } |
372 | + }; | ||
373 | + }, | ||
379 | 374 | ||
380 | - const selectedArray = []; | ||
381 | - const selectedObject = {}; | ||
382 | - | ||
383 | - selected.forEach(item => { | ||
384 | - if (!selectedObject[item.value]) { | ||
385 | - selectedArray.push(item); | ||
386 | - selectedObject[item.value] = 1; | ||
387 | - } | ||
388 | - }); | ||
389 | - | ||
390 | - // #2066 | ||
391 | - this.selectedMultiple = this.remote ? this.model.length ? selectedArray : [] : selected; | ||
392 | - | ||
393 | - if (slot) { | ||
394 | - let selectedModel = []; | ||
395 | - | ||
396 | - for (let i = 0; i < selected.length; i++) { | ||
397 | - selectedModel.push(selected[i].value); | ||
398 | - } | ||
399 | - | ||
400 | - // if slot change and remove a selected option, emit user | ||
401 | - if (this.model.length === selectedModel.length) { | ||
402 | - this.slotChangeDuration = true; | ||
403 | - } | ||
404 | - | ||
405 | - this.model = selectedModel; | ||
406 | - } | ||
407 | - } | ||
408 | - this.toggleMultipleSelected(this.model, init); | 375 | + validateOption({elm, propsData}){ |
376 | + const value = propsData.value; | ||
377 | + const label = propsData.label || ''; | ||
378 | + const textContent = elm && elm.textContent || ''; | ||
379 | + const stringValues = JSON.stringify([value, label, textContent]); | ||
380 | + return stringValues.toLowerCase().includes(this.query.toLowerCase()); | ||
409 | }, | 381 | }, |
410 | - removeTag (index) { | ||
411 | - if (this.disabled) { | ||
412 | - return false; | ||
413 | - } | ||
414 | 382 | ||
415 | - if (this.remote) { | ||
416 | - const tag = this.model[index]; | ||
417 | - this.selectedMultiple = this.selectedMultiple.filter(item => item.value !== tag); | 383 | + toggleMenu (e, force) { |
384 | + if (this.disabled || this.autoComplete) { | ||
385 | + return false; | ||
418 | } | 386 | } |
387 | + this.focusIndex = -1; | ||
419 | 388 | ||
420 | - this.model.splice(index, 1); | ||
421 | - | ||
422 | - if (this.filterable && this.visible) { | ||
423 | - this.$refs.input.focus(); | 389 | + this.visible = typeof force !== 'undefined' ? force : !this.visible; |
390 | + if (this.visible){ | ||
391 | + this.dropDownWidth = this.$el.getBoundingClientRect().width; | ||
424 | } | 392 | } |
425 | - | ||
426 | - this.broadcast('Drop', 'on-update-popper'); | ||
427 | }, | 393 | }, |
428 | - // to select option for single | ||
429 | - toggleSingleSelected (value, init = false) { | ||
430 | - if (!this.multiple) { | ||
431 | - let label = ''; | ||
432 | - | ||
433 | - this.findChild((child) => { | ||
434 | - if (child.value === value) { | ||
435 | - child.selected = true; | ||
436 | - label = (child.label === undefined) ? child.$el.innerHTML : child.label; | ||
437 | - } else { | ||
438 | - child.selected = false; | ||
439 | - } | ||
440 | - }); | ||
441 | - | ||
442 | - this.hideMenu(); | ||
443 | - | ||
444 | - if (!init) { | ||
445 | - if (this.labelInValue) { | ||
446 | - this.$emit('on-change', { | ||
447 | - value: value, | ||
448 | - label: label | ||
449 | - }); | ||
450 | - this.dispatch('FormItem', 'on-form-change', { | ||
451 | - value: value, | ||
452 | - label: label | ||
453 | - }); | ||
454 | - } else { | ||
455 | - this.$emit('on-change', value); | ||
456 | - this.dispatch('FormItem', 'on-form-change', value); | ||
457 | - } | ||
458 | - } | ||
459 | - } | 394 | + hideMenu () { |
395 | + this.toggleMenu(null, false); | ||
460 | }, | 396 | }, |
461 | - // to select option for multiple | ||
462 | - toggleMultipleSelected (value, init = false) { | ||
463 | - if (this.multiple) { | ||
464 | - let hybridValue = []; | ||
465 | - for (let i = 0; i < value.length; i++) { | ||
466 | - hybridValue.push({ | ||
467 | - value: value[i] | 397 | + onClickOutside(event){ |
398 | + if (this.visible) { | ||
399 | + | ||
400 | + if (this.filterable) { | ||
401 | + const input = this.$refs.selectHead.$refs.input; | ||
402 | + this.caretPosition = input.selectionStart; | ||
403 | + this.$nextTick(() => { | ||
404 | + const caretPosition = this.caretPosition === -1 ? input.value.length : this.caretPosition; | ||
405 | + input.setSelectionRange(caretPosition, caretPosition); | ||
468 | }); | 406 | }); |
469 | } | 407 | } |
470 | 408 | ||
471 | - this.findChild((child) => { | ||
472 | - const index = value.indexOf(child.value); | ||
473 | - | ||
474 | - if (index >= 0) { | ||
475 | - child.selected = true; | ||
476 | - hybridValue[index].label = (child.label === undefined) ? child.$el.innerHTML : child.label; | ||
477 | - } else { | ||
478 | - child.selected = false; | ||
479 | - } | ||
480 | - }); | ||
481 | - | ||
482 | - if (!init) { | ||
483 | - if (this.labelInValue) { | ||
484 | - this.$emit('on-change', hybridValue); | ||
485 | - this.dispatch('FormItem', 'on-form-change', hybridValue); | ||
486 | - } else { | ||
487 | - this.$emit('on-change', value); | ||
488 | - this.dispatch('FormItem', 'on-form-change', value); | ||
489 | - } | ||
490 | - } | 409 | + event.stopPropagation(); |
410 | + event.preventDefault(); | ||
411 | + this.hideMenu(); | ||
412 | + this.isFocused = true; | ||
413 | + } else { | ||
414 | + this.caretPosition = -1; | ||
415 | + this.isFocused = false; | ||
491 | } | 416 | } |
492 | }, | 417 | }, |
493 | - handleClose () { | ||
494 | - this.hideMenu(); | 418 | + reset(){ |
419 | + this.values = []; | ||
495 | }, | 420 | }, |
496 | handleKeydown (e) { | 421 | handleKeydown (e) { |
422 | + if (e.key === 'Backspace'){ | ||
423 | + return; // so we don't call preventDefault | ||
424 | + } | ||
425 | + | ||
497 | if (this.visible) { | 426 | if (this.visible) { |
498 | - const keyCode = e.keyCode; | 427 | + e.preventDefault(); |
428 | + if (e.key === 'Tab'){ | ||
429 | + e.stopPropagation(); | ||
430 | + } | ||
431 | + | ||
499 | // Esc slide-up | 432 | // Esc slide-up |
500 | - if (keyCode === 27) { | ||
501 | - e.preventDefault(); | 433 | + if (e.key === 'Escape') { |
502 | this.hideMenu(); | 434 | this.hideMenu(); |
503 | } | 435 | } |
504 | // next | 436 | // next |
505 | - if (keyCode === 40) { | ||
506 | - e.preventDefault(); | ||
507 | - this.navigateOptions('next'); | 437 | + if (e.key === 'ArrowUp') { |
438 | + this.navigateOptions(-1); | ||
508 | } | 439 | } |
509 | // prev | 440 | // prev |
510 | - if (keyCode === 38) { | ||
511 | - e.preventDefault(); | ||
512 | - this.navigateOptions('prev'); | 441 | + if (e.key === 'ArrowDown') { |
442 | + this.navigateOptions(1); | ||
513 | } | 443 | } |
514 | // enter | 444 | // enter |
515 | - if (keyCode === 13) { | ||
516 | - e.preventDefault(); | ||
517 | - | ||
518 | - this.findChild((child) => { | ||
519 | - if (child.isFocus) { | ||
520 | - child.select(); | ||
521 | - } | ||
522 | - }); | 445 | + if (e.key === 'Enter' && this.focusIndex > -1) { |
446 | + const optionComponent = this.flatOptions[this.focusIndex]; | ||
447 | + const option = this.getOptionData(optionComponent.componentOptions.propsData.value); | ||
448 | + this.onOptionClick(option); | ||
523 | } | 449 | } |
524 | - } | ||
525 | - }, | ||
526 | - navigateOptions (direction) { | ||
527 | - if (direction === 'next') { | ||
528 | - const next = this.focusIndex + 1; | ||
529 | - this.focusIndex = (this.focusIndex === this.options.length) ? 1 : next; | ||
530 | - } else if (direction === 'prev') { | ||
531 | - const prev = this.focusIndex - 1; | ||
532 | - this.focusIndex = (this.focusIndex <= 1) ? this.options.length : prev; | 450 | + } else { |
451 | + const keysThatCanOpenSelect = ['ArrowUp', 'ArrowDown']; | ||
452 | + if (keysThatCanOpenSelect.includes(e.key)) this.toggleMenu(null, true); | ||
533 | } | 453 | } |
534 | 454 | ||
535 | - let child_status = { | ||
536 | - disabled: false, | ||
537 | - hidden: false | ||
538 | - }; | ||
539 | 455 | ||
540 | - let find_deep = false; // can next find allowed | 456 | + }, |
457 | + navigateOptions(direction){ | ||
458 | + const optionsLength = this.flatOptions.length - 1; | ||
541 | 459 | ||
542 | - this.findChild((child) => { | ||
543 | - if (child.index === this.focusIndex) { | ||
544 | - child_status.disabled = child.disabled; | ||
545 | - child_status.hidden = child.hidden; | 460 | + let index = this.focusIndex + direction; |
461 | + if (index < 0) index = optionsLength; | ||
462 | + if (index > optionsLength) index = 0; | ||
546 | 463 | ||
547 | - if (!child.disabled && !child.hidden) { | ||
548 | - child.isFocus = true; | ||
549 | - } | ||
550 | - } else { | ||
551 | - child.isFocus = false; | 464 | + // find nearest option in case of disabled options in between |
465 | + if (direction > 0){ | ||
466 | + let nearestActiveOption = -1; | ||
467 | + for (let i = 0; i < this.flatOptions.length; i++){ | ||
468 | + const optionIsActive = !this.flatOptions[i].componentOptions.propsData.disabled; | ||
469 | + if (optionIsActive) nearestActiveOption = i; | ||
470 | + if (nearestActiveOption >= index) break; | ||
552 | } | 471 | } |
553 | - | ||
554 | - if (!child.hidden && !child.disabled) { | ||
555 | - find_deep = true; | 472 | + index = nearestActiveOption; |
473 | + } else { | ||
474 | + let nearestActiveOption = this.flatOptions.length; | ||
475 | + for (let i = optionsLength; i >= 0; i--){ | ||
476 | + const optionIsActive = !this.flatOptions[i].componentOptions.propsData.disabled; | ||
477 | + if (optionIsActive) nearestActiveOption = i; | ||
478 | + if (nearestActiveOption <= index) break; | ||
556 | } | 479 | } |
557 | - }); | ||
558 | - | ||
559 | - this.resetScrollTop(); | ||
560 | - | ||
561 | - if ((child_status.disabled || child_status.hidden) && find_deep) { | ||
562 | - this.navigateOptions(direction); | 480 | + index = nearestActiveOption; |
563 | } | 481 | } |
564 | - }, | ||
565 | - resetScrollTop () { | ||
566 | - const index = this.focusIndex - 1; | ||
567 | - if (!this.optionInstances.length) return; | ||
568 | - let bottomOverflowDistance = this.optionInstances[index].$el.getBoundingClientRect().bottom - this.$refs.dropdown.$el.getBoundingClientRect().bottom; | ||
569 | - let topOverflowDistance = this.optionInstances[index].$el.getBoundingClientRect().top - this.$refs.dropdown.$el.getBoundingClientRect().top; | ||
570 | 482 | ||
571 | - if (bottomOverflowDistance > 0) { | ||
572 | - this.$refs.dropdown.$el.scrollTop += bottomOverflowDistance; | ||
573 | - } | ||
574 | - if (topOverflowDistance < 0) { | ||
575 | - this.$refs.dropdown.$el.scrollTop += topOverflowDistance; | ||
576 | - } | 483 | + this.focusIndex = index; |
577 | }, | 484 | }, |
578 | - handleBlur () { | ||
579 | - setTimeout(() => { | ||
580 | - if (this.autoComplete) return; | ||
581 | - const model = this.model; | 485 | + onOptionClick(option) { |
486 | + if (this.multiple){ | ||
487 | + | ||
488 | + // keep the query for remote select | ||
489 | + if (this.remote) this.lastRemoteQuery = this.lastRemoteQuery || this.query; | ||
490 | + else this.lastRemoteQuery = ''; | ||
582 | 491 | ||
583 | - if (this.multiple) { | ||
584 | - this.query = ''; | 492 | + const valueIsSelected = this.values.find(({value}) => value === option.value); |
493 | + if (valueIsSelected){ | ||
494 | + this.values = this.values.filter(({value}) => value !== option.value); | ||
585 | } else { | 495 | } else { |
586 | - if (model !== '') { | ||
587 | - this.findChild((child) => { | ||
588 | - if (child.value === model) { | ||
589 | - this.query = child.label === undefined ? child.searchLabel : child.label; | ||
590 | - } | ||
591 | - }); | ||
592 | - // 如果删除了搜索词,下拉列表也清空了,所以强制调用一次remoteMethod | ||
593 | - if (this.remote && this.query !== this.lastQuery) { | ||
594 | - this.$nextTick(() => { | ||
595 | - this.query = this.lastQuery; | ||
596 | - }); | ||
597 | - } | ||
598 | - } else { | ||
599 | - this.query = ''; | ||
600 | - } | 496 | + this.values = this.values.concat(option); |
601 | } | 497 | } |
602 | - }, 300); | ||
603 | - }, | ||
604 | - resetInputState () { | ||
605 | - this.inputLength = this.$refs.input.value.length * 12 + 20; | ||
606 | - }, | ||
607 | - handleInputDelete () { | ||
608 | - if (this.multiple && this.model.length && this.query === '') { | ||
609 | - this.removeTag(this.model.length - 1); | 498 | + |
499 | + this.isFocused = true; // so we put back focus after clicking with mouse on option elements | ||
500 | + } else { | ||
501 | + this.values = [option]; | ||
502 | + this.lastRemoteQuery = ''; | ||
503 | + this.hideMenu(); | ||
504 | + } | ||
505 | + | ||
506 | + if (this.filterable){ | ||
507 | + const inputField = this.$refs.selectHead.$refs.input; | ||
508 | + this.$nextTick(() => inputField.focus()); | ||
610 | } | 509 | } |
611 | }, | 510 | }, |
612 | - // use when slot changed | ||
613 | - slotChange () { | ||
614 | - this.options = []; | ||
615 | - this.optionInstances = []; | ||
616 | - }, | ||
617 | - setQuery (query) { | ||
618 | - if (!this.filterable) return; | 511 | + onQueryChange(query) { |
619 | this.query = query; | 512 | this.query = query; |
513 | + if (this.query.length > 0) this.visible = true; | ||
620 | }, | 514 | }, |
621 | - modelToQuery() { | ||
622 | - if (!this.multiple && this.filterable && this.model !== undefined) { | ||
623 | - this.findChild((child) => { | ||
624 | - if (this.model === child.value) { | ||
625 | - if (child.label) { | ||
626 | - this.query = child.label; | ||
627 | - } else if (child.searchLabel) { | ||
628 | - this.query = child.searchLabel; | ||
629 | - } else { | ||
630 | - this.query = child.value; | ||
631 | - } | ||
632 | - } | ||
633 | - }); | ||
634 | - } | ||
635 | - }, | ||
636 | - broadcastQuery (val) { | ||
637 | - if (findComponentDownward(this, 'OptionGroup')) { | ||
638 | - this.broadcast('OptionGroup', 'on-query-change', val); | ||
639 | - this.broadcast('iOption', 'on-query-change', val); | ||
640 | - } else { | ||
641 | - this.broadcast('iOption', 'on-query-change', val); | 515 | + toggleHeaderFocus({type}){ |
516 | + if (this.disabled) { | ||
517 | + return; | ||
642 | } | 518 | } |
519 | + this.isFocused = type === 'focus'; | ||
643 | }, | 520 | }, |
644 | - debouncedAppendRemove(){ | ||
645 | - return debounce(function(){ | ||
646 | - if (!this.remote) { | ||
647 | - this.modelToQuery(); | ||
648 | - this.$nextTick(() => this.broadcastQuery('')); | ||
649 | - } else { | ||
650 | - this.findChild((child) => { | ||
651 | - child.updateSearchLabel(); // #1865 | ||
652 | - child.selected = this.multiple ? this.model.indexOf(child.value) > -1 : this.model === child.value; | ||
653 | - }); | ||
654 | - } | ||
655 | - this.slotChange(); | ||
656 | - this.updateOptions(true); | ||
657 | - }); | ||
658 | - }, | ||
659 | - // 处理 remote 初始值 | ||
660 | - updateLabel () { | ||
661 | - if (this.remote) { | ||
662 | - if (!this.multiple && this.model !== '') { | ||
663 | - this.selectToChangeQuery = true; | ||
664 | - if (this.currentLabel === '') this.currentLabel = this.model; | ||
665 | - this.lastQuery = this.currentLabel; | ||
666 | - this.query = this.currentLabel; | ||
667 | - } else if (this.multiple && this.model.length) { | ||
668 | - if (this.currentLabel.length !== this.model.length) this.currentLabel = this.model; | ||
669 | - this.selectedMultiple = this.model.map((item, index) => { | ||
670 | - return { | ||
671 | - value: item, | ||
672 | - label: this.currentLabel[index] | ||
673 | - }; | ||
674 | - }); | ||
675 | - } else if (this.multiple && !this.model.length) { | ||
676 | - this.selectedMultiple = []; | ||
677 | - } | ||
678 | - } | 521 | + updateSlotOptions(){ |
522 | + this.slotOptions = this.$slots.default; | ||
679 | } | 523 | } |
680 | }, | 524 | }, |
681 | - mounted () { | ||
682 | - this.modelToQuery(); | ||
683 | - // 处理 remote 初始值 | ||
684 | - this.updateLabel(); | ||
685 | - this.$nextTick(() => { | ||
686 | - this.broadcastQuery(''); | ||
687 | - }); | ||
688 | - | ||
689 | - this.updateOptions(); | ||
690 | - document.addEventListener('keydown', this.handleKeydown); | ||
691 | - | ||
692 | - this.$on('append', this.debouncedAppendRemove()); | ||
693 | - this.$on('remove', this.debouncedAppendRemove()); | ||
694 | - | ||
695 | - this.$on('on-select-selected', (value) => { | ||
696 | - if (this.model === value) { | ||
697 | - if (this.autoComplete) this.$emit('on-change', value); | ||
698 | - this.hideMenu(); | ||
699 | - } else { | ||
700 | - if (this.multiple) { | ||
701 | - const index = this.model.indexOf(value); | ||
702 | - if (index >= 0) { | ||
703 | - this.removeTag(index); | ||
704 | - } else { | ||
705 | - this.model.push(value); | ||
706 | - this.broadcast('Drop', 'on-update-popper'); | ||
707 | - } | ||
708 | - | ||
709 | - if (this.filterable) { | ||
710 | - // remote&filterable&multiple时,一次点多项,不应该设置true,因为无法置为false,下次的搜索会失效 | ||
711 | - if (this.query !== '') this.selectToChangeQuery = true; | ||
712 | - this.query = ''; | ||
713 | - this.$refs.input.focus(); | ||
714 | - } | ||
715 | - } else { | ||
716 | - this.model = value; | ||
717 | - | ||
718 | - if (this.filterable) { | ||
719 | - this.findChild((child) => { | ||
720 | - if (child.value === value) { | ||
721 | - if (this.query !== '') this.selectToChangeQuery = true; | ||
722 | - this.lastQuery = this.query = child.label === undefined ? child.searchLabel : child.label; | ||
723 | - } | ||
724 | - }); | ||
725 | - } | ||
726 | - } | ||
727 | - } | ||
728 | - }); | ||
729 | - }, | ||
730 | - beforeDestroy () { | ||
731 | - document.removeEventListener('keydown', this.handleKeydown); | ||
732 | - }, | ||
733 | watch: { | 525 | watch: { |
734 | - value (val) { | ||
735 | - this.model = val; | ||
736 | - // #982 | ||
737 | - if (val === '' || val === null) this.query = ''; | ||
738 | - }, | ||
739 | - label (val) { | ||
740 | - this.currentLabel = val; | ||
741 | - this.updateLabel(); | ||
742 | - }, | ||
743 | - model () { | ||
744 | - this.$emit('input', this.model); | ||
745 | - this.modelToQuery(); | ||
746 | - if (this.multiple) { | ||
747 | - if (this.slotChangeDuration) { | ||
748 | - this.slotChangeDuration = false; | ||
749 | - } else { | ||
750 | - this.updateMultipleSelected(); | ||
751 | - } | ||
752 | - } else { | ||
753 | - this.updateSingleSelected(); | 526 | + value(value){ |
527 | + const {getInitialValue, getOptionData, publicValue} = this; | ||
528 | + | ||
529 | + if (value === '') this.values = []; | ||
530 | + else if (JSON.stringify(value) !== JSON.stringify(publicValue)) { | ||
531 | + this.$nextTick(() => this.values = getInitialValue().map(getOptionData)); | ||
754 | } | 532 | } |
755 | - // #957 | ||
756 | - if (!this.visible && this.filterable) { | ||
757 | - this.$nextTick(() => { | ||
758 | - this.broadcastQuery(''); | ||
759 | - }); | 533 | + }, |
534 | + values(now, before){ | ||
535 | + const newValue = JSON.stringify(now); | ||
536 | + const oldValue = JSON.stringify(before); | ||
537 | + const shouldEmitInput = newValue !== oldValue; | ||
538 | + | ||
539 | + if (shouldEmitInput) { | ||
540 | + // v-model is always just the value, event with labelInValue === true | ||
541 | + const vModelValue = this.labelInValue ? | ||
542 | + (this.multiple ? this.publicValue.map(({value}) => value) | ||
543 | + : | ||
544 | + this.publicValue.value) : this.publicValue; | ||
545 | + this.$emit('input', vModelValue); // to update v-model | ||
546 | + this.$emit('on-change', this.publicValue); | ||
547 | + this.dispatch('FormItem', 'on-form-change', this.publicValue); | ||
760 | } | 548 | } |
761 | }, | 549 | }, |
762 | - visible (val) { | ||
763 | - if (val) { | ||
764 | - if (this.filterable) { | ||
765 | - if (this.multiple) { | ||
766 | - this.$refs.input.focus(); | ||
767 | - } else { | ||
768 | - if (!this.autoComplete) this.$refs.input.select(); | ||
769 | - } | ||
770 | - if (this.remote) { | ||
771 | - this.findChild(child => { | ||
772 | - child.selected = this.multiple ? this.model.indexOf(child.value) > -1 : this.model === child.value; | ||
773 | - }); | ||
774 | - // remote下,设置了默认值,第一次打开时,搜索一次 | ||
775 | - const options = this.$slots.default || []; | ||
776 | - if (this.query !== '' && !options.length) { | ||
777 | - this.remoteMethod(this.query); | ||
778 | - } | ||
779 | - } | ||
780 | - } | ||
781 | - this.broadcast('Drop', 'on-update-popper'); | ||
782 | - } else { | ||
783 | - if (this.filterable) { | ||
784 | - if (!this.autoComplete) this.$refs.input.blur(); | ||
785 | - // #566 reset options visible | ||
786 | - setTimeout(() => { | ||
787 | - this.broadcastQuery(''); | ||
788 | - }, 300); | 550 | + query (query) { |
551 | + this.$emit('on-query-change', query); | ||
552 | + const {remoteMethod, lastRemoteQuery} = this; | ||
553 | + const hasValidQuery = query !== '' && (query !== lastRemoteQuery || !lastRemoteQuery); | ||
554 | + const shouldCallRemoteMethod = remoteMethod && hasValidQuery; | ||
555 | + | ||
556 | + if (shouldCallRemoteMethod){ | ||
557 | + this.focusIndex = -1; | ||
558 | + const promise = this.remoteMethod(query); | ||
559 | + this.initialLabel = ''; | ||
560 | + if (promise && promise.then){ | ||
561 | + promise.then(options => { | ||
562 | + if (options) this.options = options; | ||
563 | + }); | ||
789 | } | 564 | } |
790 | - this.broadcast('Drop', 'on-destroy-popper'); | ||
791 | } | 565 | } |
566 | + if (query !== '' && this.remote) this.lastRemoteQuery = query; | ||
792 | }, | 567 | }, |
793 | - query (val) { | ||
794 | - if (this.remote && this.remoteMethod) { | ||
795 | - if (!this.selectToChangeQuery) { | ||
796 | - this.$emit('on-query-change', val); | ||
797 | - this.remoteMethod(val); | ||
798 | - } | ||
799 | - this.focusIndex = 0; | ||
800 | - this.findChild(child => { | ||
801 | - child.isFocus = false; | ||
802 | - }); | ||
803 | - } else { | ||
804 | - if (!this.selectToChangeQuery) { | ||
805 | - this.$emit('on-query-change', val); | ||
806 | - } | ||
807 | - this.broadcastQuery(val); | 568 | + loading(state){ |
569 | + if (state === false){ | ||
570 | + this.updateSlotOptions(); | ||
571 | + } | ||
572 | + }, | ||
573 | + isFocused(focused){ | ||
574 | + const {selectHead, reference} = this.$refs; | ||
575 | + const el = this.filterable ? selectHead.$el.querySelector('input') : reference; | ||
576 | + el[this.isFocused ? 'focus' : 'blur'](); | ||
808 | 577 | ||
809 | - let is_hidden = true; | 578 | + // restore query value in filterable single selects |
579 | + const [selectedOption] = this.values; | ||
580 | + if (selectedOption && this.filterable && !this.multiple && !focused){ | ||
581 | + const selectedLabel = selectedOption.label || selectedOption.value; | ||
582 | + if (this.query !== selectedLabel) this.query = selectedLabel; | ||
583 | + } | ||
584 | + }, | ||
585 | + focusIndex(index){ | ||
586 | + if (index < 0) return; | ||
587 | + // update scroll | ||
588 | + const optionValue = this.flatOptions[index].componentOptions.propsData.value; | ||
589 | + const optionInstance = findChild(this, ({$options}) => { | ||
590 | + return $options.componentName === 'select-item' && $options.propsData.value === optionValue; | ||
591 | + }); | ||
810 | 592 | ||
811 | - this.$nextTick(() => { | ||
812 | - this.findChild((child) => { | ||
813 | - if (!child.hidden) { | ||
814 | - is_hidden = false; | ||
815 | - } | ||
816 | - }); | ||
817 | - this.notFound = is_hidden; | ||
818 | - }); | 593 | + let bottomOverflowDistance = optionInstance.$el.getBoundingClientRect().bottom - this.$refs.dropdown.$el.getBoundingClientRect().bottom; |
594 | + let topOverflowDistance = optionInstance.$el.getBoundingClientRect().top - this.$refs.dropdown.$el.getBoundingClientRect().top; | ||
595 | + if (bottomOverflowDistance > 0) { | ||
596 | + this.$refs.dropdown.$el.scrollTop += bottomOverflowDistance; | ||
597 | + } | ||
598 | + if (topOverflowDistance < 0) { | ||
599 | + this.$refs.dropdown.$el.scrollTop += topOverflowDistance; | ||
819 | } | 600 | } |
820 | - this.selectToChangeQuery = false; | ||
821 | - this.broadcast('Drop', 'on-update-popper'); | ||
822 | } | 601 | } |
823 | } | 602 | } |
824 | }; | 603 | }; |
src/styles/components/select.less
@@ -25,23 +25,14 @@ | @@ -25,23 +25,14 @@ | ||
25 | border: 1px solid @border-color-base; | 25 | border: 1px solid @border-color-base; |
26 | transition: all @transition-time @ease-in-out; | 26 | transition: all @transition-time @ease-in-out; |
27 | 27 | ||
28 | - .@{select-prefix-cls}-arrow:nth-of-type(1) { | ||
29 | - display: none; | ||
30 | - cursor: pointer; | ||
31 | - } | ||
32 | - | ||
33 | - &:hover { | 28 | + &:hover, &-focused { |
34 | .hover(); | 29 | .hover(); |
35 | - .@{select-prefix-cls}-arrow:nth-of-type(1) { | 30 | + .@{select-prefix-cls}-arrow { |
36 | display: inline-block; | 31 | display: inline-block; |
37 | } | 32 | } |
38 | } | 33 | } |
39 | } | 34 | } |
40 | 35 | ||
41 | - &-show-clear &-selection:hover .@{select-prefix-cls}-arrow:nth-of-type(2){ | ||
42 | - display: none; | ||
43 | - } | ||
44 | - | ||
45 | &-arrow { | 36 | &-arrow { |
46 | .inner-arrow(); | 37 | .inner-arrow(); |
47 | } | 38 | } |
@@ -51,14 +42,9 @@ | @@ -51,14 +42,9 @@ | ||
51 | .active(); | 42 | .active(); |
52 | } | 43 | } |
53 | 44 | ||
54 | - .@{select-prefix-cls}-arrow:nth-of-type(2) { | 45 | + .@{select-prefix-cls}-arrow { |
55 | transform: rotate(180deg); | 46 | transform: rotate(180deg); |
56 | - } | ||
57 | - } | ||
58 | - &:focus{ | ||
59 | - outline: 0; | ||
60 | - .@{select-prefix-cls}-selection{ | ||
61 | - .active(); | 47 | + display: inline-block; |
62 | } | 48 | } |
63 | } | 49 | } |
64 | 50 | ||
@@ -66,7 +52,7 @@ | @@ -66,7 +52,7 @@ | ||
66 | .@{select-prefix-cls}-selection { | 52 | .@{select-prefix-cls}-selection { |
67 | .disabled(); | 53 | .disabled(); |
68 | 54 | ||
69 | - .@{select-prefix-cls}-arrow:nth-of-type(1) { | 55 | + .@{select-prefix-cls}-arrow { |
70 | display: none; | 56 | display: none; |
71 | } | 57 | } |
72 | 58 | ||
@@ -74,7 +60,7 @@ | @@ -74,7 +60,7 @@ | ||
74 | border-color: @border-color-base; | 60 | border-color: @border-color-base; |
75 | box-shadow: none; | 61 | box-shadow: none; |
76 | 62 | ||
77 | - .@{select-prefix-cls}-arrow:nth-of-type(2) { | 63 | + .@{select-prefix-cls}-arrow { |
78 | display: inline-block; | 64 | display: inline-block; |
79 | } | 65 | } |
80 | } | 66 | } |