فهرست منبع

Merge pull request #1401 from Leopoldthecoder/select-refactor

Select: refactor and bug fix
baiyaaaaa 8 سال پیش
والد
کامیت
d3afe22e89

+ 55 - 1
examples/docs/en-US/select.md

@@ -62,6 +62,16 @@
           }]
         }],
         options4: [],
+        options5: [{
+          value: 'HTML',
+          label: 'HTML'
+        }, {
+          value: 'CSS',
+          label: 'CSS'
+        }, {
+          value: 'JavaScript',
+          label: 'JavaScript'
+        }],
         cities: [{
           value: 'Beijing',
           label: 'Beijing'
@@ -87,9 +97,10 @@
         value4: '',
         value5: [],
         value6: '',
-        value7: [],
+        value7: '',
         value8: '',
         value9: [],
+        value10: [],
         loading: false,
         states: ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]
       };
@@ -569,15 +580,58 @@ Enter keywords and search data from server.
 ```
 :::
 
+### Create new items
+Create and select new items that are not included in select options
+:::demo By using the `allow-create` attribute, users can create new items by typing in the input box. Note that for `allow-create` to work, `filterable` must be `true`.
+```html
+<template>
+  <el-select
+    v-model="value10"
+    multiple
+    filterable
+    allow-create
+    placeholder="Choose tags for your article">
+    <el-option
+      v-for="item in options5"
+      :label="item.label"
+      :value="item.value">
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        options5: [{
+          value: 'HTML',
+          label: 'HTML'
+        }, {
+          value: 'CSS',
+          label: 'CSS'
+        }, {
+          value: 'JavaScript',
+          label: 'JavaScript'
+        }],
+        value10: []
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Select Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | multiple | whether multiple-select is activated | boolean | — | false |
 | disabled | whether Select is disabled | boolean | — | false |
 | clearable | whether single select can be cleared | boolean | — | false |
+| multiple-limit | maximum number of options user can select when `multiple` is `true`. No limit when set to 0 | number | — | 0 |
 | name | the name attribute of select input | string | — | — |
 | placeholder | placeholder | string | — | Select |
 | filterable | whether Select is filterable | boolean | — | false |
+| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | boolean | — | false |
 | filter-method | custom filter method | function | — | — |
 | remote | whether options are loaded from server | boolean | — | false |
 | remote-method | custom remote search method | function | — | — |

+ 56 - 4
examples/docs/zh-CN/select.md

@@ -62,6 +62,16 @@
           }]
         }],
         options4: [],
+        options5: [{
+          value: 'HTML',
+          label: 'HTML'
+        }, {
+          value: 'CSS',
+          label: 'CSS'
+        }, {
+          value: 'JavaScript',
+          label: 'JavaScript'
+        }],
         cities: [{
           value: 'Beijing',
           label: '北京'
@@ -87,9 +97,10 @@
         value4: '',
         value5: [],
         value6: '',
-        value7: [],
+        value7: '',
         value8: '',
-        value9: [],
+        value9: '',
+        value10: [],
         loading: false,
         states: ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]
       };
@@ -129,7 +140,6 @@
 ### 基础用法
 
 适用广泛的基础单选
-
 :::demo `v-model`的值为当前被选中的`el-option`的 value 属性值
 ```html
 <template>
@@ -499,7 +509,6 @@
 ### 远程搜索
 
 从服务器搜索数据,输入关键字进行查找
-
 :::demo 为了启用远程搜索,需要将`filterable`和`remote`设置为`true`,同时传入一个`remote-method`。`remote-method`为一个`Function`,它会在输入值发生变化时调用,参数为当前输入值。需要注意的是,如果`el-option`是通过`v-for`指令渲染出来的,此时需要为`el-option`添加`key`属性,且其值需具有唯一性,比如此例中的`item.value`。
 ```html
 <template>
@@ -573,15 +582,58 @@
 ```
 :::
 
+### 创建条目
+可以创建并选中选项中不存在的条目
+:::demo 使用`allow-create`属性即可通过在输入框中输入文字来创建新的条目。注意此时`filterable`必须为真。
+```html
+<template>
+  <el-select
+    v-model="value10"
+    multiple
+    filterable
+    allow-create
+    placeholder="请选择文章标签">
+    <el-option
+      v-for="item in options5"
+      :label="item.label"
+      :value="item.value">
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        options5: [{
+          value: 'HTML',
+          label: 'HTML'
+        }, {
+          value: 'CSS',
+          label: 'CSS'
+        }, {
+          value: 'JavaScript',
+          label: 'JavaScript'
+        }],
+        value10: []
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Select Attributes 
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | multiple | 是否多选 | boolean | — | false |
 | disabled | 是否禁用 | boolean | — | false |
 | clearable | 单选时是否可以清空选项 | boolean | — | false |
+| multiple-limit | 多选时用户最多可以选择的项目数,为 0 则不限制 | number | — | 0 |
 | name | select input 的 name 属性 | string | — | — |
 | placeholder | 占位符 | string | — | 请选择 |
 | filterable | 是否可搜索 | boolean | — | false |
+| allow-create | 是否允许用户创建新条目,需配合 `filterable` 使用 | boolean | — | false |
 | filter-method | 自定义过滤方法 | function | — | — |
 | remote | 是否为远程搜索 | boolean | — | false |
 | remote-method | 远程搜索方法 | function | — | — |

+ 6 - 8
packages/pagination/src/pagination.js

@@ -140,17 +140,15 @@ export default {
         return (
           <span class="el-pagination__sizes">
             <el-select
-              size="small"
               value={ this.$parent.internalPageSize }
-              on-change={ this.handleChange }
-              width={ 110 }>
+              on-input={ this.handleChange }>
               {
                 this.pageSizes.map(item =>
-                    <el-option
-                      value={ item }
-                      label={ item + ' ' + this.t('el.pagination.pagesize') }>
-                    </el-option>
-                  )
+                  <el-option
+                    value={ item }
+                    label={ item + ' ' + this.t('el.pagination.pagesize') }>
+                  </el-option>
+                )
               }
             </el-select>
           </span>

+ 17 - 18
packages/select/src/option.vue

@@ -4,7 +4,11 @@
     @click.stop="selectOptionClick"
     class="el-select-dropdown__item"
     v-show="visible"
-    :class="{ 'selected': itemSelected, 'is-disabled': disabled || groupDisabled, 'hover': parent.hoverIndex === index }">
+    :class="{
+      'selected': itemSelected,
+      'is-disabled': disabled || groupDisabled || limitReached,
+      'hover': parent.hoverIndex === index
+    }">
     <slot>
       <span>{{ currentLabel }}</span>
     </slot>
@@ -30,6 +34,7 @@
         type: Boolean,
         default: false
       },
+      created: Boolean,
       disabled: {
         type: Boolean,
         default: false
@@ -63,22 +68,20 @@
       },
 
       itemSelected() {
-        if (Object.prototype.toString.call(this.parent.selected) === '[object Object]') {
-          return this === this.parent.selected;
-        } else if (Array.isArray(this.parent.selected)) {
+        if (!this.parent.multiple) {
+          return this.value === this.parent.value;
+        } else {
           return this.parent.value.indexOf(this.value) > -1;
         }
       },
 
-      currentSelected() {
-        return this.selected || (this.parent.multiple ? this.parent.value.indexOf(this.value) > -1 : this.parent.value === this.value);
-      }
-    },
-
-    watch: {
-      currentSelected(val) {
-        if (val === true) {
-          this.dispatch('ElSelect', 'addOptionToValue', this);
+      limitReached() {
+        if (this.parent.multiple) {
+          return !this.itemSelected &&
+            this.parent.value.length >= this.parent.multipleLimit &&
+            this.parent.multipleLimit > 0;
+        } else {
+          return false;
         }
       }
     },
@@ -103,7 +106,7 @@
       queryChange(query) {
         // query 里如果有正则中的特殊字符,需要先将这些字符转义
         let parsedQuery = query.replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, '\\$1');
-        this.visible = new RegExp(parsedQuery, 'i').test(this.currentLabel);
+        this.visible = new RegExp(parsedQuery, 'i').test(this.currentLabel) || this.created;
         if (!this.visible) {
           this.parent.filteredOptionsCount--;
         }
@@ -122,10 +125,6 @@
       this.parent.filteredOptionsCount++;
       this.index = this.parent.options.indexOf(this);
 
-      if (this.currentSelected === true) {
-        this.dispatch('ElSelect', 'addOptionToValue', [this, true]);
-      }
-
       this.$on('queryChange', this.queryChange);
       this.$on('handleGroupDisabled', this.handleGroupDisabled);
       this.$on('resetIndex', this.resetIndex);

+ 161 - 139
packages/select/src/select.vue

@@ -1,19 +1,26 @@
 <template>
   <div
     class="el-select"
-    v-clickoutside="handleClose"
-    :class="{ 'is-multiple': multiple, 'is-small': size === 'small' }">
-    <div class="el-select__tags" v-if="multiple" @click.stop="toggleMenu" ref="tags" :style="{ 'max-width': inputWidth - 32 + 'px' }">
+    v-clickoutside="handleClose">
+    <div
+      class="el-select__tags"
+      v-if="multiple"
+      @click.stop="toggleMenu"
+      ref="tags"
+      :style="{ 'max-width': inputWidth - 32 + 'px' }">
       <transition-group @after-leave="resetInputHeight">
         <el-tag
           v-for="item in selected"
-          :key="item"
+          :key="item.value"
           closable
           :hit="item.hitState"
           type="primary"
           @close="deleteTag($event, item)"
-          close-transition>{{ item.currentLabel }}</el-tag>
+          close-transition>
+          {{ item.currentLabel }}
+        </el-tag>
       </transition-group>
+
       <input
         type="text"
         class="el-select__input"
@@ -40,7 +47,7 @@
       :disabled="disabled"
       :readonly="!filterable || multiple"
       @focus="toggleMenu"
-      @click="toggleMenu"
+      @click="handleIconClick"
       @mousedown.native="handleMouseDown"
       @keyup.native="debouncedOnInputChange"
       @keydown.native.down.prevent="navigateOptions('next')"
@@ -56,10 +63,18 @@
       <el-select-menu
         ref="popper"
         v-show="visible && emptyText !== false">
-        <ul class="el-select-dropdown__list" v-show="options.length > 0 && filteredOptionsCount > 0 && !loading">
+        <ul
+          class="el-select-dropdown__list"
+          :class="{ 'is-empty': !allowCreate && filteredOptionsCount === 0 }"
+          v-show="options.length > 0 && !loading">
+          <el-option
+            :value="query"
+            created
+            v-if="showNewOption">
+          </el-option>
           <slot></slot>
         </ul>
-        <p class="el-select-dropdown__empty" v-if="emptyText">{{ emptyText }}</p>
+        <p class="el-select-dropdown__empty" v-if="emptyText && !allowCreate">{{ emptyText }}</p>
       </el-select-menu>
     </transition>
   </div>
@@ -70,6 +85,7 @@
   import Locale from 'element-ui/src/mixins/locale';
   import ElInput from 'element-ui/packages/input';
   import ElSelectMenu from './select-dropdown.vue';
+  import ElOption from './option.vue';
   import ElTag from 'element-ui/packages/tag';
   import debounce from 'throttle-debounce/debounce';
   import Clickoutside from 'element-ui/src/utils/clickoutside';
@@ -94,22 +110,18 @@
       },
 
       showCloseIcon() {
-        let criteria = this.clearable && this.inputHovering && !this.multiple && this.options.indexOf(this.selected) > -1;
+        let criteria = this.clearable &&
+          this.inputHovering &&
+          !this.multiple &&
+          this.value !== undefined &&
+          this.value !== '';
         if (!this.$el) return false;
-
         this.$nextTick(() => {
           let icon = this.$el.querySelector('.el-input__icon');
           if (icon) {
-            if (criteria) {
-              icon.addEventListener('click', this.deleteSelected);
-              addClass(icon, 'is-show-close');
-            } else {
-              icon.removeEventListener('click', this.deleteSelected);
-              removeClass(icon, 'is-show-close');
-            }
+            criteria ? addClass(icon, 'is-show-close') : removeClass(icon, 'is-show-close');
           }
         });
-
         return criteria;
       },
 
@@ -129,12 +141,19 @@
           }
         }
         return null;
+      },
+
+      showNewOption() {
+        let hasExistingOption = this.options.filter(option => !option.created)
+          .some(option => option.currentLabel === this.query);
+        return this.filterable && this.allowCreate && this.query !== '' && !hasExistingOption;
       }
     },
 
     components: {
       ElInput,
       ElSelectMenu,
+      ElOption,
       ElTag
     },
 
@@ -143,15 +162,19 @@
     props: {
       name: String,
       value: {},
-      size: String,
       disabled: Boolean,
       clearable: Boolean,
       filterable: Boolean,
+      allowCreate: Boolean,
       loading: Boolean,
       remote: Boolean,
       remoteMethod: Function,
       filterMethod: Function,
       multiple: Boolean,
+      multipleLimit: {
+        type: Number,
+        default: 0
+      },
       placeholder: {
         type: String,
         default() {
@@ -163,22 +186,21 @@
     data() {
       return {
         options: [],
-        selected: {},
+        selected: this.multiple ? [] : {},
         isSelect: true,
         inputLength: 20,
         inputWidth: 0,
-        valueChangeBySelected: false,
         cachedPlaceHolder: '',
         optionsCount: 0,
         filteredOptionsCount: 0,
         dropdownUl: null,
         visible: false,
         selectedLabel: '',
-        selectInit: false,
         hoverIndex: -1,
         query: '',
         voidRemoteQuery: false,
         bottomOverflowBeforeHidden: 0,
+        topOverflowBeforeHidden: 0,
         optionsAllDisabled: false,
         inputHovering: false,
         currentPlaceholder: ''
@@ -191,79 +213,25 @@
       },
 
       value(val) {
-        if (this.valueChangeBySelected) {
-          this.valueChangeBySelected = false;
-          return;
-        }
-        this.$nextTick(() => {
-          if (this.multiple && Array.isArray(val)) {
-            this.$nextTick(() => {
-              this.resetInputHeight();
-            });
-            this.selectedInit = true;
-            this.selected = [];
-            this.currentPlaceholder = this.cachedPlaceHolder;
-            val.forEach(item => {
-              let option = this.options.filter(option => option.value === item)[0];
-              if (option) {
-                this.$emit('addOptionToValue', option);
-              }
-            });
-          }
-          if (!this.multiple) {
-            let option = this.options.filter(option => option.value === val)[0];
-            if (option) {
-              this.$emit('addOptionToValue', option);
-            } else {
-              this.selected = {};
-              this.selectedLabel = '';
-            }
-          }
-          this.resetHoverIndex();
-        });
-      },
-
-      selected(val, oldVal) {
         if (this.multiple) {
-          if (this.selected.length > 0) {
+          this.resetInputHeight();
+          if (val.length > 0) {
             this.currentPlaceholder = '';
           } else {
             this.currentPlaceholder = this.cachedPlaceHolder;
           }
-          this.$nextTick(() => {
-            this.resetInputHeight();
-          });
-          if (this.selectedInit) {
-            this.selectedInit = false;
-            return;
-          }
-          this.valueChangeBySelected = true;
-          const result = val.map(item => item.value);
-
-          this.$emit('input', result);
-          this.$emit('change', result);
-          this.dispatch('ElFormItem', 'el.form.change', val);
-          if (this.filterable) {
-            this.query = '';
-            this.hoverIndex = -1;
-            this.$refs.input.focus();
-            this.inputLength = 20;
-          }
-        } else {
-          if (this.selectedInit) {
-            this.selectedInit = false;
-            return;
-          }
-          if (val.value === oldVal.value) return;
-          this.$emit('input', val.value);
-          this.$emit('change', val.value);
         }
+        this.selected = this.getSelected();
+        if (this.filterable && !this.multiple) {
+          this.inputLength = 20;
+        }
+        this.$emit('change', val);
+        this.dispatch('ElFormItem', 'el.form.change', val);
       },
 
       query(val) {
-        this.$nextTick(() => {
-          this.broadcast('ElSelectDropdown', 'updatePopper');
-        });
+        this.broadcast('ElSelectDropdown', 'updatePopper');
+        this.hoverIndex = -1;
         if (this.multiple && this.filterable) {
           this.resetInputHeight();
         }
@@ -283,27 +251,22 @@
       visible(val) {
         if (!val) {
           this.$refs.reference.$el.querySelector('input').blur();
-          if (this.$el.querySelector('.el-input__icon')) {
-            removeClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
-          }
+          this.handleIconHide();
           this.broadcast('ElSelectDropdown', 'destroyPopper');
           if (this.$refs.input) {
             this.$refs.input.blur();
           }
+          this.query = '';
+          this.selectedLabel = '';
           this.resetHoverIndex();
           if (!this.multiple) {
-            if (this.dropdownUl && this.selected.$el) {
-              this.bottomOverflowBeforeHidden = this.selected.$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
-            }
+            this.getOverflows();
             if (this.selected && this.selected.value) {
               this.selectedLabel = this.selected.currentLabel;
             }
           }
         } else {
-          let icon = this.$el.querySelector('.el-input__icon');
-          if (icon && !hasClass(icon, 'el-icon-circle-close')) {
-            addClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
-          }
+          this.handleIconShow();
           this.broadcast('ElSelectDropdown', 'updatePopper');
           if (this.filterable) {
             this.query = this.selectedLabel;
@@ -318,19 +281,88 @@
             this.dropdownUl = [].filter.call(dropdownChildNodes, item => item.tagName === 'UL')[0];
           }
           if (!this.multiple && this.dropdownUl) {
-            if (this.bottomOverflowBeforeHidden > 0) {
-              this.dropdownUl.scrollTop += this.bottomOverflowBeforeHidden;
-            }
+            this.setOverflow();
           }
         }
       },
 
       options(val) {
         this.optionsAllDisabled = val.length === val.filter(item => item.disabled === true).length;
+        if (this.multiple) {
+          this.resetInputHeight();
+        }
       }
     },
 
     methods: {
+      handleIconHide() {
+        let icon = this.$el.querySelector('.el-input__icon');
+        if (icon) {
+          removeClass(icon, 'is-reverse');
+        }
+      },
+
+      handleIconShow() {
+        let icon = this.$el.querySelector('.el-input__icon');
+        if (icon && !hasClass(icon, 'el-icon-circle-close')) {
+          addClass(icon, 'is-reverse');
+        }
+      },
+
+      getOverflows() {
+        if (this.dropdownUl && this.selected && this.selected.$el) {
+          let selectedRect = this.selected.$el.getBoundingClientRect();
+          let popperRect = this.$refs.popper.$el.getBoundingClientRect();
+          this.bottomOverflowBeforeHidden = selectedRect.bottom - popperRect.bottom;
+          this.topOverflowBeforeHidden = selectedRect.top - popperRect.top;
+        }
+      },
+
+      setOverflow() {
+        if (this.bottomOverflowBeforeHidden > 0) {
+          this.$nextTick(() => {
+            this.dropdownUl.scrollTop += this.bottomOverflowBeforeHidden;
+          });
+        } else if (this.topOverflowBeforeHidden < 0) {
+          this.$nextTick(() => {
+            this.dropdownUl.scrollTop += this.topOverflowBeforeHidden;
+          });
+        }
+      },
+
+      getSelected() {
+        if (!this.multiple) {
+          let option = this.options.filter(option => option.value === this.value)[0] ||
+              { value: this.value, currentLabel: this.value };
+          this.selectedLabel = option.currentLabel;
+          return option;
+        }
+        let result = [];
+        if (Array.isArray(this.value)) {
+          this.value.forEach(value => {
+            let option = this.options.filter(option => option.value === value)[0];
+            if (option) {
+              result.push(option);
+            } else {
+              result.push({
+                value: this.value,
+                currentLabel: value,
+                hitState: false
+              });
+            }
+          });
+        }
+        return result;
+      },
+
+      handleIconClick(event) {
+        if (this.iconClass === 'circle-close') {
+          this.deleteSelected(event);
+        } else {
+          this.toggleMenu();
+        }
+      },
+
       handleMouseDown(event) {
         if (event.target.tagName !== 'INPUT') return;
         if (this.visible) {
@@ -363,22 +395,7 @@
 
       deletePrevTag(e) {
         if (e.target.value.length <= 0 && !this.toggleLastOptionHitState()) {
-          this.selected.pop();
-        }
-      },
-
-      addOptionToValue(option, init) {
-        if (this.multiple) {
-          if (this.selected.indexOf(option) === -1 && (this.remote ? this.value.indexOf(option.value) === -1 : true)) {
-            this.selectedInit = !!init;
-            this.selected.push(option);
-            this.resetHoverIndex();
-          }
-        } else {
-          this.selectedInit = !!init;
-          this.selected = option;
-          this.selectedLabel = option.currentLabel;
-          this.hoverIndex = option.index;
+          this.value.pop();
         }
       },
 
@@ -418,20 +435,24 @@
 
       handleOptionSelect(option) {
         if (!this.multiple) {
-          this.selected = option;
-          this.selectedLabel = option.currentLabel;
+          this.$emit('input', option.value);
           this.visible = false;
         } else {
           let optionIndex = -1;
-          this.selected.forEach((item, index) => {
-            if (item === option || item.currentValue === option.currentValue) {
+          this.value.forEach((item, index) => {
+            if (item === option.value) {
               optionIndex = index;
             }
           });
           if (optionIndex > -1) {
-            this.selected.splice(optionIndex, 1);
-          } else {
-            this.selected.push(option);
+            this.value.splice(optionIndex, 1);
+          } else if (this.multipleLimit <= 0 || this.value.length < this.multipleLimit) {
+            this.value.push(option.value);
+          }
+          if (option.created) {
+            this.query = '';
+            this.inputLength = 20;
+            this.$refs.input.focus();
           }
         }
       },
@@ -450,6 +471,7 @@
           this.visible = true;
           return;
         }
+        if (this.options.length === 0 || this.filteredOptionsCount === 0) return;
         if (!this.optionsAllDisabled) {
           if (direction === 'next') {
             this.hoverIndex++;
@@ -479,8 +501,10 @@
       },
 
       resetScrollTop() {
-        let bottomOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
-        let topOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().top - this.$refs.popper.$el.getBoundingClientRect().top;
+        let bottomOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().bottom -
+          this.$refs.popper.$el.getBoundingClientRect().bottom;
+        let topOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().top -
+          this.$refs.popper.$el.getBoundingClientRect().top;
         if (bottomOverflowDistance > 0) {
           this.dropdownUl.scrollTop += bottomOverflowDistance;
         }
@@ -497,8 +521,6 @@
 
       deleteSelected(event) {
         event.stopPropagation();
-        this.selected = {};
-        this.selectedLabel = '';
         this.$emit('input', '');
         this.$emit('change', '');
         this.visible = false;
@@ -507,13 +529,13 @@
       deleteTag(event, tag) {
         let index = this.selected.indexOf(tag);
         if (index > -1) {
-          this.selected.splice(index, 1);
+          this.value.splice(index, 1);
         }
         event.stopPropagation();
       },
 
       onInputChange() {
-        if (this.filterable && this.selectedLabel !== this.value) {
+        if (this.filterable) {
           this.query = this.selectedLabel;
         }
       },
@@ -535,9 +557,11 @@
 
     created() {
       this.cachedPlaceHolder = this.currentPlaceholder = this.placeholder;
-      if (this.multiple) {
-        this.selectedInit = true;
-        this.selected = [];
+      if (this.multiple && !Array.isArray(this.value)) {
+        this.$emit('input', []);
+      }
+      if (!this.multiple && (!this.value || Array.isArray(this.value))) {
+        this.$emit('input', '');
       }
       if (this.remote) {
         this.voidRemoteQuery = true;
@@ -547,20 +571,18 @@
         this.onInputChange();
       });
 
-      this.$on('addOptionToValue', this.addOptionToValue);
       this.$on('handleOptionClick', this.handleOptionSelect);
       this.$on('onOptionDestroy', this.onOptionDestroy);
     },
 
     mounted() {
+      if (this.multiple && Array.isArray(this.value) && this.value.length > 0) {
+        this.currentPlaceholder = '';
+      }
       addResizeListener(this.$el, this.resetInputWidth);
-      if (this.remote && this.multiple && Array.isArray(this.value)) {
-        this.selected = this.options.reduce((prev, curr) => {
-          return this.value.indexOf(curr.value) > -1 ? prev.concat(curr) : prev;
-        }, []);
-        this.$nextTick(() => {
-          this.resetInputHeight();
-        });
+      this.selected = this.getSelected();
+      if (this.remote && this.multiple) {
+        this.resetInputHeight();
       }
       this.$nextTick(() => {
         if (this.$refs.reference.$el) {

+ 2 - 0
packages/theme-default/src/pagination.css

@@ -23,6 +23,8 @@
       width: 110px;
       input {
         padding-right: 25px;
+        border-radius: var(--border-radius-small);
+        height: 28px;
       }
     }
 

+ 4 - 0
packages/theme-default/src/select-dropdown.css

@@ -51,5 +51,9 @@
     max-height: var(--select-dropdown-max-height);
     box-sizing: border-box;
     overflow-y: auto;
+
+    @when empty {
+      padding: 0;
+    }
   }
 }

+ 0 - 7
packages/theme-default/src/select.css

@@ -12,13 +12,6 @@
     display: block;
     position: relative;
 
-    @when small {
-      & input {
-        border-radius: var(--border-radius-small);
-        height: 28px;
-      }
-    }
-
     &:hover {
       .el-input__inner {
         border-color: var(--select-border-color-hover);

+ 53 - 15
test/unit/specs/select.spec.js

@@ -3,9 +3,10 @@ import Select from 'packages/select';
 
 describe('Select', () => {
   const getSelectVm = (configs = {}, options) => {
-    ['multiple', 'clearable', 'filterable', 'remote'].forEach(config => {
+    ['multiple', 'clearable', 'filterable', 'allowCreate', 'remote'].forEach(config => {
       configs[config] = configs[config] || false;
     });
+    configs.multipleLimit = configs.multipleLimit || 0;
     if (!options) {
       options = [{
         value: '选项1',
@@ -35,8 +36,10 @@ describe('Select', () => {
           <el-select
             v-model="value"
             :multiple="multiple"
+            :multiple-limit="multipleLimit"
             :clearable="clearable"
             :filterable="filterable"
+            :allow-create="allowCreate"
             :filterMethod="filterMethod"
             :remote="remote"
             :loading="loading"
@@ -55,8 +58,10 @@ describe('Select', () => {
         return {
           options,
           multiple: configs.multiple,
+          multipleLimit: configs.multipleLimit,
           clearable: configs.clearable,
           filterable: configs.filterable,
+          allowCreate: configs.allowCreate,
           loading: false,
           filterMethod: configs.filterMethod && configs.filterMethod(this),
           remote: configs.remote,
@@ -349,6 +354,23 @@ describe('Select', () => {
     }, 100);
   });
 
+  it('allow create', done => {
+    vm = getSelectVm({ filterable: true, allowCreate: true });
+    const select = vm.$children[0];
+    select.selectedLabel = 'new';
+    select.onInputChange();
+    select.visible = true;
+    setTimeout(() => {
+      const options = document.querySelectorAll('.el-select-dropdown__item span');
+      const target = [].filter.call(options, option => option.innerText === 'new');
+      target[0].click();
+      setTimeout(() => {
+        expect(select.value.indexOf('new') > -1).to.true;
+        done();
+      }, 50);
+    }, 50);
+  });
+
   it('multiple select', done => {
     vm = getSelectVm({ multiple: true });
     const options = vm.$el.querySelectorAll('.el-select-dropdown__item');
@@ -368,6 +390,20 @@ describe('Select', () => {
     }, 100);
   });
 
+  it('multiple limit', done => {
+    vm = getSelectVm({ multiple: true, multipleLimit: 1 });
+    const options = vm.$el.querySelectorAll('.el-select-dropdown__item');
+    options[1].click();
+    setTimeout(() => {
+      expect(vm.value.indexOf('选项2') > -1).to.true;
+      options[3].click();
+      setTimeout(() => {
+        expect(vm.value.indexOf('选项4')).to.equal(-1);
+        done();
+      }, 50);
+    }, 50);
+  });
+
   it('multiple remote search', done => {
     const remoteMethod = vm => {
       return query => {
@@ -387,21 +423,23 @@ describe('Select', () => {
       remoteMethod
     });
     const select = vm.$children[0];
-    select.query = '面';
-    setTimeout(() => {
-      expect(select.filteredOptionsCount).to.equal(1);
-      select.query = '';
-      select.options[0].$el.click();
-      vm.$nextTick(() => {
-        expect(vm.value[0]).to.equal('选项4');
-        select.deletePrevTag({ target: select.$refs.input });
-        select.deletePrevTag({ target: select.$refs.input });
-        select.resetInputState({ keyCode: 1 });
+    vm.$nextTick(() => {
+      select.query = '面';
+      setTimeout(() => {
+        expect(select.filteredOptionsCount).to.equal(1);
+        select.query = '';
+        select.options[0].$el.click();
         vm.$nextTick(() => {
-          expect(vm.value.length).to.equal(0);
-          done();
+          expect(vm.value[0]).to.equal('选项4');
+          select.deletePrevTag({ target: select.$refs.input });
+          select.deletePrevTag({ target: select.$refs.input });
+          select.resetInputState({ keyCode: 1 });
+          vm.$nextTick(() => {
+            expect(vm.value.length).to.equal(0);
+            done();
+          });
         });
-      });
-    }, 250);
+      }, 250);
+    });
   });
 });