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 | 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 | 3 | </template> |
4 | 4 | <script> |
5 | 5 | import Emitter from '../../mixins/emitter'; |
... | ... | @@ -22,15 +22,19 @@ |
22 | 22 | disabled: { |
23 | 23 | type: Boolean, |
24 | 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 | 35 | data () { |
28 | 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 | 38 | autoComplete: false |
35 | 39 | }; |
36 | 40 | }, |
... | ... | @@ -41,53 +45,34 @@ |
41 | 45 | { |
42 | 46 | [`${prefixCls}-disabled`]: this.disabled, |
43 | 47 | [`${prefixCls}-selected`]: this.selected && !this.autoComplete, |
44 | - [`${prefixCls}-focus`]: this.isFocus | |
48 | + [`${prefixCls}-focus`]: this.isFocused | |
45 | 49 | } |
46 | 50 | ]; |
47 | 51 | }, |
48 | 52 | showLabel () { |
49 | 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 | 59 | methods: { |
53 | 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 | 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 | 74 | const Select = findComponentUpward(this, 'iSelect'); |
85 | 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 | 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 | 1 | <template> |
2 | 2 | <div |
3 | - tabindex="0" | |
4 | - @keydown.down="handleFocus" | |
5 | 3 | :class="classes" |
6 | - v-clickoutside="handleClose"> | |
4 | + v-click-outside.capture="onClickOutside" | |
5 | + > | |
7 | 6 | <div |
8 | - :class="selectionCls" | |
9 | 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 | 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 | 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 | 48 | </slot> |
37 | 49 | </div> |
38 | 50 | <transition name="transition-drop"> |
... | ... | @@ -42,9 +54,17 @@ |
42 | 54 | :placement="placement" |
43 | 55 | ref="dropdown" |
44 | 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 | 68 | <ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul> |
49 | 69 | </Drop> |
50 | 70 | </transition> |
... | ... | @@ -53,20 +73,32 @@ |
53 | 73 | <script> |
54 | 74 | import Icon from '../icon'; |
55 | 75 | import Drop from './dropdown.vue'; |
56 | - import clickoutside from '../../directives/clickoutside'; | |
76 | + import vClickOutside from 'v-click-outside-x/index'; | |
57 | 77 | import TransferDom from '../../directives/transfer-dom'; |
58 | - import { oneOf, findComponentDownward } from '../../utils/assist'; | |
78 | + import { oneOf } from '../../utils/assist'; | |
59 | 79 | import Emitter from '../../mixins/emitter'; |
60 | 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 | 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 | 97 | export default { |
66 | 98 | name: 'iSelect', |
67 | 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 | 102 | props: { |
71 | 103 | value: { |
72 | 104 | type: [String, Number, Array], |
... | ... | @@ -99,10 +131,6 @@ |
99 | 131 | filterMethod: { |
100 | 132 | type: Function |
101 | 133 | }, |
102 | - remote: { | |
103 | - type: Boolean, | |
104 | - default: false | |
105 | - }, | |
106 | 134 | remoteMethod: { |
107 | 135 | type: Function |
108 | 136 | }, |
... | ... | @@ -147,23 +175,29 @@ |
147 | 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 | 186 | data () { |
187 | + | |
151 | 188 | return { |
152 | 189 | prefixCls: prefixCls, |
190 | + values: this.getInitialValue(), | |
191 | + dropDownWidth: 0, | |
153 | 192 | visible: false, |
154 | - options: [], | |
155 | - optionInstances: [], | |
156 | - selectedSingle: '', // label | |
157 | - selectedMultiple: [], | |
158 | - focusIndex: 0, | |
193 | + focusIndex: -1, | |
194 | + isFocused: false, | |
159 | 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 | 203 | computed: { |
... | ... | @@ -189,58 +223,19 @@ |
189 | 223 | }, |
190 | 224 | selectionCls () { |
191 | 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 | 230 | localeNotFoundText () { |
236 | - if (this.notFoundText === undefined) { | |
231 | + if (typeof this.notFoundText === 'undefined') { | |
237 | 232 | return this.t('i.select.noMatch'); |
238 | 233 | } else { |
239 | 234 | return this.notFoundText; |
240 | 235 | } |
241 | 236 | }, |
242 | 237 | localeLoadingText () { |
243 | - if (this.loadingText === undefined) { | |
238 | + if (typeof this.loadingText === 'undefined') { | |
244 | 239 | return this.t('i.select.loading'); |
245 | 240 | } else { |
246 | 241 | return this.loadingText; |
... | ... | @@ -251,574 +246,358 @@ |
251 | 246 | }, |
252 | 247 | dropVisible () { |
253 | 248 | let status = true; |
254 | - const options = this.$slots.default || []; | |
249 | + const options = this.selectOptions; | |
255 | 250 | if (!this.loading && this.remote && this.query === '' && !options.length) status = false; |
256 | 251 | |
257 | 252 | if (this.autoComplete && !options.length) status = false; |
258 | 253 | |
259 | 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 | 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 | 421 | handleKeydown (e) { |
422 | + if (e.key === 'Backspace'){ | |
423 | + return; // so we don't call preventDefault | |
424 | + } | |
425 | + | |
497 | 426 | if (this.visible) { |
498 | - const keyCode = e.keyCode; | |
427 | + e.preventDefault(); | |
428 | + if (e.key === 'Tab'){ | |
429 | + e.stopPropagation(); | |
430 | + } | |
431 | + | |
499 | 432 | // Esc slide-up |
500 | - if (keyCode === 27) { | |
501 | - e.preventDefault(); | |
433 | + if (e.key === 'Escape') { | |
502 | 434 | this.hideMenu(); |
503 | 435 | } |
504 | 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 | 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 | 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 | 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 | 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 | 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 | 25 | border: 1px solid @border-color-base; |
26 | 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 | 29 | .hover(); |
35 | - .@{select-prefix-cls}-arrow:nth-of-type(1) { | |
30 | + .@{select-prefix-cls}-arrow { | |
36 | 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 | 36 | &-arrow { |
46 | 37 | .inner-arrow(); |
47 | 38 | } |
... | ... | @@ -51,14 +42,9 @@ |
51 | 42 | .active(); |
52 | 43 | } |
53 | 44 | |
54 | - .@{select-prefix-cls}-arrow:nth-of-type(2) { | |
45 | + .@{select-prefix-cls}-arrow { | |
55 | 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 | 52 | .@{select-prefix-cls}-selection { |
67 | 53 | .disabled(); |
68 | 54 | |
69 | - .@{select-prefix-cls}-arrow:nth-of-type(1) { | |
55 | + .@{select-prefix-cls}-arrow { | |
70 | 56 | display: none; |
71 | 57 | } |
72 | 58 | |
... | ... | @@ -74,7 +60,7 @@ |
74 | 60 | border-color: @border-color-base; |
75 | 61 | box-shadow: none; |
76 | 62 | |
77 | - .@{select-prefix-cls}-arrow:nth-of-type(2) { | |
63 | + .@{select-prefix-cls}-arrow { | |
78 | 64 | display: inline-block; |
79 | 65 | } |
80 | 66 | } | ... | ... |