13f4c0759d982ed92fba6d5b3238ee31a4b9e00d65766529a829d00cb8cd3d6e9e9cf35a337e522b2dbeda9483000e33b28edce28833a9a6eabc4a7e4061dc 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <template>
  2. <div
  3. class="el-autocomplete"
  4. v-clickoutside="close"
  5. aria-haspopup="listbox"
  6. role="combobox"
  7. :aria-expanded="suggestionVisible"
  8. :aria-owns="id"
  9. >
  10. <el-input
  11. ref="input"
  12. v-bind="[$props, $attrs]"
  13. @input="handleInput"
  14. @change="handleChange"
  15. @focus="handleFocus"
  16. @blur="handleBlur"
  17. @clear="handleClear"
  18. @keydown.up.native.prevent="highlight(highlightedIndex - 1)"
  19. @keydown.down.native.prevent="highlight(highlightedIndex + 1)"
  20. @keydown.enter.native="handleKeyEnter"
  21. @keydown.native.tab="close"
  22. >
  23. <template slot="prepend" v-if="$slots.prepend">
  24. <slot name="prepend"></slot>
  25. </template>
  26. <template slot="append" v-if="$slots.append">
  27. <slot name="append"></slot>
  28. </template>
  29. <template slot="prefix" v-if="$slots.prefix">
  30. <slot name="prefix"></slot>
  31. </template>
  32. <template slot="suffix" v-if="$slots.suffix">
  33. <slot name="suffix"></slot>
  34. </template>
  35. </el-input>
  36. <el-autocomplete-suggestions
  37. visible-arrow
  38. :class="[popperClass ? popperClass : '']"
  39. :popper-options="popperOptions"
  40. :append-to-body="popperAppendToBody"
  41. ref="suggestions"
  42. :placement="placement"
  43. :id="id">
  44. <li
  45. v-for="(item, index) in suggestions"
  46. :key="index"
  47. :class="{'highlighted': highlightedIndex === index}"
  48. @click="select(item)"
  49. :id="`${id}-item-${index}`"
  50. role="option"
  51. :aria-selected="highlightedIndex === index"
  52. >
  53. <slot :item="item">
  54. {{ item[valueKey] }}
  55. </slot>
  56. </li>
  57. </el-autocomplete-suggestions>
  58. </div>
  59. </template>
  60. <script>
  61. import debounce from 'throttle-debounce/debounce';
  62. import ElInput from 'element-ui/packages/input';
  63. import Clickoutside from 'element-ui/src/utils/clickoutside';
  64. import ElAutocompleteSuggestions from './autocomplete-suggestions.vue';
  65. import Emitter from 'element-ui/src/mixins/emitter';
  66. import Migrating from 'element-ui/src/mixins/migrating';
  67. import { generateId } from 'element-ui/src/utils/util';
  68. import Focus from 'element-ui/src/mixins/focus';
  69. export default {
  70. name: 'ElAutocomplete',
  71. mixins: [Emitter, Focus('input'), Migrating],
  72. inheritAttrs: false,
  73. componentName: 'ElAutocomplete',
  74. components: {
  75. ElInput,
  76. ElAutocompleteSuggestions
  77. },
  78. directives: { Clickoutside },
  79. props: {
  80. valueKey: {
  81. type: String,
  82. default: 'value'
  83. },
  84. popperClass: String,
  85. popperOptions: Object,
  86. placeholder: String,
  87. clearable: {
  88. type: Boolean,
  89. default: false
  90. },
  91. disabled: Boolean,
  92. name: String,
  93. size: String,
  94. value: String,
  95. maxlength: Number,
  96. minlength: Number,
  97. autofocus: Boolean,
  98. fetchSuggestions: Function,
  99. triggerOnFocus: {
  100. type: Boolean,
  101. default: true
  102. },
  103. customItem: String,
  104. selectWhenUnmatched: {
  105. type: Boolean,
  106. default: false
  107. },
  108. prefixIcon: String,
  109. suffixIcon: String,
  110. label: String,
  111. debounce: {
  112. type: Number,
  113. default: 300
  114. },
  115. placement: {
  116. type: String,
  117. default: 'bottom-start'
  118. },
  119. hideLoading: Boolean,
  120. popperAppendToBody: {
  121. type: Boolean,
  122. default: true
  123. },
  124. highlightFirstItem: {
  125. type: Boolean,
  126. default: false
  127. }
  128. },
  129. data() {
  130. return {
  131. activated: false,
  132. suggestions: [],
  133. loading: false,
  134. highlightedIndex: -1,
  135. suggestionDisabled: false
  136. };
  137. },
  138. computed: {
  139. suggestionVisible() {
  140. const suggestions = this.suggestions;
  141. let isValidData = Array.isArray(suggestions) && suggestions.length > 0;
  142. return (isValidData || this.loading) && this.activated;
  143. },
  144. id() {
  145. return `el-autocomplete-${generateId()}`;
  146. }
  147. },
  148. watch: {
  149. suggestionVisible(val) {
  150. let $input = this.getInput();
  151. if ($input) {
  152. this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
  153. }
  154. }
  155. },
  156. methods: {
  157. getMigratingConfig() {
  158. return {
  159. props: {
  160. 'custom-item': 'custom-item is removed, use scoped slot instead.',
  161. 'props': 'props is removed, use value-key instead.'
  162. }
  163. };
  164. },
  165. getData(queryString) {
  166. if (this.suggestionDisabled) {
  167. return;
  168. }
  169. this.loading = true;
  170. this.fetchSuggestions(queryString, (suggestions) => {
  171. this.loading = false;
  172. if (this.suggestionDisabled) {
  173. return;
  174. }
  175. if (Array.isArray(suggestions)) {
  176. this.suggestions = suggestions;
  177. this.highlightedIndex = this.highlightFirstItem ? 0 : -1;
  178. } else {
  179. console.error('[Element Error][Autocomplete]autocomplete suggestions must be an array');
  180. }
  181. });
  182. },
  183. handleInput(value) {
  184. this.$emit('input', value);
  185. this.suggestionDisabled = false;
  186. if (!this.triggerOnFocus && !value) {
  187. this.suggestionDisabled = true;
  188. this.suggestions = [];
  189. return;
  190. }
  191. this.debouncedGetData(value);
  192. },
  193. handleChange(value) {
  194. this.$emit('change', value);
  195. },
  196. handleFocus(event) {
  197. this.activated = true;
  198. this.$emit('focus', event);
  199. if (this.triggerOnFocus) {
  200. this.debouncedGetData(this.value);
  201. }
  202. },
  203. handleBlur(event) {
  204. this.$emit('blur', event);
  205. },
  206. handleClear() {
  207. this.activated = false;
  208. this.$emit('clear');
  209. },
  210. close(e) {
  211. this.activated = false;
  212. },
  213. handleKeyEnter(e) {
  214. if (this.suggestionVisible && this.highlightedIndex >= 0 && this.highlightedIndex < this.suggestions.length) {
  215. e.preventDefault();
  216. this.select(this.suggestions[this.highlightedIndex]);
  217. } else if (this.selectWhenUnmatched) {
  218. this.$emit('select', {value: this.value});
  219. this.$nextTick(_ => {
  220. this.suggestions = [];
  221. this.highlightedIndex = -1;
  222. });
  223. }
  224. },
  225. select(item) {
  226. this.$emit('input', item[this.valueKey]);
  227. this.$emit('select', item);
  228. this.$nextTick(_ => {
  229. this.suggestions = [];
  230. this.highlightedIndex = -1;
  231. });
  232. },
  233. highlight(index) {
  234. if (!this.suggestionVisible || this.loading) { return; }
  235. if (index < 0) {
  236. this.highlightedIndex = -1;
  237. return;
  238. }
  239. if (index >= this.suggestions.length) {
  240. index = this.suggestions.length - 1;
  241. }
  242. const suggestion = this.$refs.suggestions.$el.querySelector('.el-autocomplete-suggestion__wrap');
  243. const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li');
  244. let highlightItem = suggestionList[index];
  245. let scrollTop = suggestion.scrollTop;
  246. let offsetTop = highlightItem.offsetTop;
  247. if (offsetTop + highlightItem.scrollHeight > (scrollTop + suggestion.clientHeight)) {
  248. suggestion.scrollTop += highlightItem.scrollHeight;
  249. }
  250. if (offsetTop < scrollTop) {
  251. suggestion.scrollTop -= highlightItem.scrollHeight;
  252. }
  253. this.highlightedIndex = index;
  254. let $input = this.getInput();
  255. $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
  256. },
  257. getInput() {
  258. return this.$refs.input.getInput();
  259. }
  260. },
  261. mounted() {
  262. this.debouncedGetData = debounce(this.debounce, this.getData);
  263. this.$on('item-click', item => {
  264. this.select(item);
  265. });
  266. let $input = this.getInput();
  267. $input.setAttribute('role', 'textbox');
  268. $input.setAttribute('aria-autocomplete', 'list');
  269. $input.setAttribute('aria-controls', 'id');
  270. $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
  271. },
  272. beforeDestroy() {
  273. this.$refs.suggestions.$destroy();
  274. }
  275. };
  276. </script>