Commit c9b86944ec1dfe33c572461f2489a74f217194e8

Authored by Sergio Crisostomo
1 parent aaa96346

Refactor Select!

src/components/select/functional-options.vue 0 → 100644
  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>
src/components/select/select-head.vue 0 → 100644
  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 }