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