123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- <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>
|