| <template> | 
|   <div | 
|     class="el-autocomplete" | 
|     v-clickoutside="close" | 
|     aria-haspopup="listbox" | 
|     role="combobox" | 
|     :aria-expanded="suggestionVisible" | 
|     :aria-owns="id" | 
|   > | 
|     <el-input | 
|       ref="input" | 
|       v-bind="[$props, $attrs]" | 
|       @input="handleInput" | 
|       @change="handleChange" | 
|       @focus="handleFocus" | 
|       @blur="handleBlur" | 
|       @clear="handleClear" | 
|       @keydown.up.native.prevent="highlight(highlightedIndex - 1)" | 
|       @keydown.down.native.prevent="highlight(highlightedIndex + 1)" | 
|       @keydown.enter.native="handleKeyEnter" | 
|       @keydown.native.tab="close" | 
|     > | 
|       <template slot="prepend" v-if="$slots.prepend"> | 
|         <slot name="prepend"></slot> | 
|       </template> | 
|       <template slot="append" v-if="$slots.append"> | 
|         <slot name="append"></slot> | 
|       </template> | 
|       <template slot="prefix" v-if="$slots.prefix"> | 
|         <slot name="prefix"></slot> | 
|       </template> | 
|       <template slot="suffix" v-if="$slots.suffix"> | 
|         <slot name="suffix"></slot> | 
|       </template> | 
|     </el-input> | 
|     <el-autocomplete-suggestions | 
|       visible-arrow | 
|       :class="[popperClass ? popperClass : '']" | 
|       :popper-options="popperOptions" | 
|       :append-to-body="popperAppendToBody" | 
|       ref="suggestions" | 
|       :placement="placement" | 
|       :id="id"> | 
|       <li | 
|         v-for="(item, index) in suggestions" | 
|         :key="index" | 
|         :class="{'highlighted': highlightedIndex === index}" | 
|         @click="select(item)" | 
|         :id="`${id}-item-${index}`" | 
|         role="option" | 
|         :aria-selected="highlightedIndex === index" | 
|       > | 
|         <slot :item="item"> | 
|           {{ item[valueKey] }} | 
|         </slot> | 
|       </li> | 
|     </el-autocomplete-suggestions> | 
|   </div> | 
| </template> | 
| <script> | 
|   import debounce from 'throttle-debounce/debounce'; | 
|   import ElInput from 'element-ui/packages/input'; | 
|   import Clickoutside from 'element-ui/src/utils/clickoutside'; | 
|   import ElAutocompleteSuggestions from './autocomplete-suggestions.vue'; | 
|   import Emitter from 'element-ui/src/mixins/emitter'; | 
|   import Migrating from 'element-ui/src/mixins/migrating'; | 
|   import { generateId } from 'element-ui/src/utils/util'; | 
|   import Focus from 'element-ui/src/mixins/focus'; | 
|   | 
|   export default { | 
|     name: 'ElAutocomplete', | 
|   | 
|     mixins: [Emitter, Focus('input'), Migrating], | 
|   | 
|     inheritAttrs: false, | 
|   | 
|     componentName: 'ElAutocomplete', | 
|   | 
|     components: { | 
|       ElInput, | 
|       ElAutocompleteSuggestions | 
|     }, | 
|   | 
|     directives: { Clickoutside }, | 
|   | 
|     props: { | 
|       valueKey: { | 
|         type: String, | 
|         default: 'value' | 
|       }, | 
|       popperClass: String, | 
|       popperOptions: Object, | 
|       placeholder: String, | 
|       clearable: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       disabled: Boolean, | 
|       name: String, | 
|       size: String, | 
|       value: String, | 
|       maxlength: Number, | 
|       minlength: Number, | 
|       autofocus: Boolean, | 
|       fetchSuggestions: Function, | 
|       triggerOnFocus: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       customItem: String, | 
|       selectWhenUnmatched: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       prefixIcon: String, | 
|       suffixIcon: String, | 
|       label: String, | 
|       debounce: { | 
|         type: Number, | 
|         default: 300 | 
|       }, | 
|       placement: { | 
|         type: String, | 
|         default: 'bottom-start' | 
|       }, | 
|       hideLoading: Boolean, | 
|       popperAppendToBody: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       highlightFirstItem: { | 
|         type: Boolean, | 
|         default: false | 
|       } | 
|     }, | 
|     data() { | 
|       return { | 
|         activated: false, | 
|         suggestions: [], | 
|         loading: false, | 
|         highlightedIndex: -1, | 
|         suggestionDisabled: false | 
|       }; | 
|     }, | 
|     computed: { | 
|       suggestionVisible() { | 
|         const suggestions = this.suggestions; | 
|         let isValidData = Array.isArray(suggestions) && suggestions.length > 0; | 
|         return (isValidData || this.loading) && this.activated; | 
|       }, | 
|       id() { | 
|         return `el-autocomplete-${generateId()}`; | 
|       } | 
|     }, | 
|     watch: { | 
|       suggestionVisible(val) { | 
|         let $input = this.getInput(); | 
|         if ($input) { | 
|           this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]); | 
|         } | 
|       } | 
|     }, | 
|     methods: { | 
|       getMigratingConfig() { | 
|         return { | 
|           props: { | 
|             'custom-item': 'custom-item is removed, use scoped slot instead.', | 
|             'props': 'props is removed, use value-key instead.' | 
|           } | 
|         }; | 
|       }, | 
|       getData(queryString) { | 
|         if (this.suggestionDisabled) { | 
|           return; | 
|         } | 
|         this.loading = true; | 
|         this.fetchSuggestions(queryString, (suggestions) => { | 
|           this.loading = false; | 
|           if (this.suggestionDisabled) { | 
|             return; | 
|           } | 
|           if (Array.isArray(suggestions)) { | 
|             this.suggestions = suggestions; | 
|             this.highlightedIndex = this.highlightFirstItem ? 0 : -1; | 
|           } else { | 
|             console.error('[Element Error][Autocomplete]autocomplete suggestions must be an array'); | 
|           } | 
|         }); | 
|       }, | 
|       handleInput(value) { | 
|         this.$emit('input', value); | 
|         this.suggestionDisabled = false; | 
|         if (!this.triggerOnFocus && !value) { | 
|           this.suggestionDisabled = true; | 
|           this.suggestions = []; | 
|           return; | 
|         } | 
|         this.debouncedGetData(value); | 
|       }, | 
|       handleChange(value) { | 
|         this.$emit('change', value); | 
|       }, | 
|       handleFocus(event) { | 
|         this.activated = true; | 
|         this.$emit('focus', event); | 
|         if (this.triggerOnFocus) { | 
|           this.debouncedGetData(this.value); | 
|         } | 
|       }, | 
|       handleBlur(event) { | 
|         this.$emit('blur', event); | 
|       }, | 
|       handleClear() { | 
|         this.activated = false; | 
|         this.$emit('clear'); | 
|       }, | 
|       close(e) { | 
|         this.activated = false; | 
|       }, | 
|       handleKeyEnter(e) { | 
|         if (this.suggestionVisible && this.highlightedIndex >= 0 && this.highlightedIndex < this.suggestions.length) { | 
|           e.preventDefault(); | 
|           this.select(this.suggestions[this.highlightedIndex]); | 
|         } else if (this.selectWhenUnmatched) { | 
|           this.$emit('select', {value: this.value}); | 
|           this.$nextTick(_ => { | 
|             this.suggestions = []; | 
|             this.highlightedIndex = -1; | 
|           }); | 
|         } | 
|       }, | 
|       select(item) { | 
|         this.$emit('input', item[this.valueKey]); | 
|         this.$emit('select', item); | 
|         this.$nextTick(_ => { | 
|           this.suggestions = []; | 
|           this.highlightedIndex = -1; | 
|         }); | 
|       }, | 
|       highlight(index) { | 
|         if (!this.suggestionVisible || this.loading) { return; } | 
|         if (index < 0) { | 
|           this.highlightedIndex = -1; | 
|           return; | 
|         } | 
|         if (index >= this.suggestions.length) { | 
|           index = this.suggestions.length - 1; | 
|         } | 
|         const suggestion = this.$refs.suggestions.$el.querySelector('.el-autocomplete-suggestion__wrap'); | 
|         const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li'); | 
|   | 
|         let highlightItem = suggestionList[index]; | 
|         let scrollTop = suggestion.scrollTop; | 
|         let offsetTop = highlightItem.offsetTop; | 
|   | 
|         if (offsetTop + highlightItem.scrollHeight > (scrollTop + suggestion.clientHeight)) { | 
|           suggestion.scrollTop += highlightItem.scrollHeight; | 
|         } | 
|         if (offsetTop < scrollTop) { | 
|           suggestion.scrollTop -= highlightItem.scrollHeight; | 
|         } | 
|         this.highlightedIndex = index; | 
|         let $input = this.getInput(); | 
|         $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`); | 
|       }, | 
|       getInput() { | 
|         return this.$refs.input.getInput(); | 
|       } | 
|     }, | 
|     mounted() { | 
|       this.debouncedGetData = debounce(this.debounce, this.getData); | 
|       this.$on('item-click', item => { | 
|         this.select(item); | 
|       }); | 
|       let $input = this.getInput(); | 
|       $input.setAttribute('role', 'textbox'); | 
|       $input.setAttribute('aria-autocomplete', 'list'); | 
|       $input.setAttribute('aria-controls', 'id'); | 
|       $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`); | 
|     }, | 
|     beforeDestroy() { | 
|       this.$refs.suggestions.$destroy(); | 
|     } | 
|   }; | 
| </script> |