autocomplete.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. >
  45. <li
  46. v-for="(item, index) in suggestions"
  47. :key="index"
  48. :class="{ highlighted: highlightedIndex === index }"
  49. @click="select(item)"
  50. :id="`${id}-item-${index}`"
  51. role="option"
  52. :aria-selected="highlightedIndex === index"
  53. >
  54. <!-- {{ item[valueKey] }} -->
  55. <slot :item="item">
  56. {{ item[valueKey] }}
  57. </slot>
  58. </li>
  59. </el-autocomplete-suggestions>
  60. </div>
  61. </template>
  62. <script>
  63. import debounce from "throttle-debounce/debounce";
  64. import ElInput from "element-ui/packages/input";
  65. import Clickoutside from "element-ui/src/utils/clickoutside";
  66. import ElAutocompleteSuggestions from "./autocomplete-suggestions.vue";
  67. import Emitter from "element-ui/src/mixins/emitter";
  68. import Migrating from "element-ui/src/mixins/migrating";
  69. import { generateId } from "element-ui/src/utils/util";
  70. import Focus from "element-ui/src/mixins/focus";
  71. export default {
  72. name: "ElAutocomplete",
  73. mixins: [Emitter, Focus("input"), Migrating],
  74. inheritAttrs: false,
  75. componentName: "ElAutocomplete",
  76. components: {
  77. ElInput,
  78. ElAutocompleteSuggestions,
  79. },
  80. directives: { Clickoutside },
  81. props: {
  82. valueKey: {
  83. type: String,
  84. default: "value",
  85. },
  86. popperClass: String,
  87. popperOptions: Object,
  88. placeholder: String,
  89. clearable: {
  90. type: Boolean,
  91. default: false,
  92. },
  93. disabled: Boolean,
  94. name: String,
  95. size: String,
  96. value: String,
  97. maxlength: Number,
  98. minlength: Number,
  99. autofocus: Boolean,
  100. fetchSuggestions: Function,
  101. triggerOnFocus: {
  102. type: Boolean,
  103. default: true,
  104. },
  105. customItem: String,
  106. selectWhenUnmatched: {
  107. type: Boolean,
  108. default: false,
  109. },
  110. prefixIcon: String,
  111. suffixIcon: String,
  112. label: String,
  113. debounce: {
  114. type: Number,
  115. default: 300,
  116. },
  117. placement: {
  118. type: String,
  119. default: "bottom-start",
  120. },
  121. hideLoading: Boolean,
  122. popperAppendToBody: {
  123. type: Boolean,
  124. default: true,
  125. },
  126. highlightFirstItem: {
  127. type: Boolean,
  128. default: false,
  129. },
  130. },
  131. data() {
  132. return {
  133. activated: false,
  134. suggestions: [],
  135. loading: false,
  136. highlightedIndex: -1,
  137. suggestionDisabled: false,
  138. };
  139. },
  140. computed: {
  141. suggestionVisible() {
  142. const suggestions = this.suggestions;
  143. let isValidData = Array.isArray(suggestions) && suggestions.length > 0;
  144. return (isValidData || this.loading) && this.activated;
  145. },
  146. id() {
  147. return `el-autocomplete-${generateId()}`;
  148. },
  149. },
  150. watch: {
  151. suggestionVisible(val) {
  152. let $input = this.getInput();
  153. if ($input) {
  154. this.broadcast("ElAutocompleteSuggestions", "visible", [
  155. val,
  156. $input.offsetWidth,
  157. ]);
  158. }
  159. },
  160. },
  161. methods: {
  162. getMigratingConfig() {
  163. return {
  164. props: {
  165. "custom-item": "custom-item is removed, use scoped slot instead.",
  166. props: "props is removed, use value-key instead.",
  167. },
  168. };
  169. },
  170. getData(queryString) {
  171. if (this.suggestionDisabled) {
  172. return;
  173. }
  174. this.loading = true;
  175. this.fetchSuggestions(queryString, (suggestions) => {
  176. this.loading = false;
  177. if (this.suggestionDisabled) {
  178. return;
  179. }
  180. if (Array.isArray(suggestions)) {
  181. this.suggestions = suggestions;
  182. this.highlightedIndex = this.highlightFirstItem ? 0 : -1;
  183. } else {
  184. console.error(
  185. "[Element Error][Autocomplete]autocomplete suggestions must be an array"
  186. );
  187. }
  188. });
  189. },
  190. handleInput(value) {
  191. this.$emit("input", value);
  192. // this.suggestionDisabled = false;
  193. // if (!this.triggerOnFocus && !value) {
  194. // this.suggestionDisabled = true;
  195. // this.suggestions = [];
  196. // return;
  197. // }
  198. // this.debouncedGetData(value);
  199. },
  200. search(value) {
  201. this.activated = true
  202. this.suggestionDisabled = false;
  203. if (!this.triggerOnFocus && !value) {
  204. this.suggestionDisabled = true;
  205. this.suggestions = [];
  206. return;
  207. }
  208. this.debouncedGetData(value);
  209. },
  210. handleChange(value) {
  211. this.$emit("change", value);
  212. },
  213. handleFocus(event) {
  214. // this.activated = true;
  215. this.$emit("focus", event);
  216. // if (this.triggerOnFocus) {
  217. // this.debouncedGetData(this.value);
  218. // }
  219. },
  220. handleBlur(event) {
  221. this.$emit("blur", event);
  222. },
  223. handleClear() {
  224. this.activated = false;
  225. this.$emit("clear");
  226. },
  227. close(e) {
  228. this.activated = false;
  229. },
  230. handleKeyEnter(e) {
  231. // if (
  232. // this.suggestionVisible &&
  233. // this.highlightedIndex >= 0 &&
  234. // this.highlightedIndex < this.suggestions.length
  235. // ) {
  236. // e.preventDefault();
  237. // this.select(this.suggestions[this.highlightedIndex]);
  238. // } else if (this.selectWhenUnmatched) {
  239. // this.$emit("select", { value: this.value });
  240. // this.$nextTick((_) => {
  241. // this.suggestions = [];
  242. // this.highlightedIndex = -1;
  243. // });
  244. // }
  245. },
  246. select(item) {
  247. this.$emit("input", item[this.valueKey]);
  248. this.$emit("select", item);
  249. this.$nextTick((_) => {
  250. this.suggestions = [];
  251. this.highlightedIndex = -1;
  252. });
  253. },
  254. highlight(index) {
  255. if (!this.suggestionVisible || this.loading) {
  256. return;
  257. }
  258. if (index < 0) {
  259. this.highlightedIndex = -1;
  260. return;
  261. }
  262. if (index >= this.suggestions.length) {
  263. index = this.suggestions.length - 1;
  264. }
  265. const suggestion = this.$refs.suggestions.$el.querySelector(
  266. ".el-autocomplete-suggestion__wrap"
  267. );
  268. const suggestionList = suggestion.querySelectorAll(
  269. ".el-autocomplete-suggestion__list li"
  270. );
  271. let highlightItem = suggestionList[index];
  272. let scrollTop = suggestion.scrollTop;
  273. let offsetTop = highlightItem.offsetTop;
  274. if (
  275. offsetTop + highlightItem.scrollHeight >
  276. scrollTop + suggestion.clientHeight
  277. ) {
  278. suggestion.scrollTop += highlightItem.scrollHeight;
  279. }
  280. if (offsetTop < scrollTop) {
  281. suggestion.scrollTop -= highlightItem.scrollHeight;
  282. }
  283. this.highlightedIndex = index;
  284. let $input = this.getInput();
  285. $input.setAttribute(
  286. "aria-activedescendant",
  287. `${this.id}-item-${this.highlightedIndex}`
  288. );
  289. },
  290. getInput() {
  291. return this.$refs.input.getInput();
  292. },
  293. },
  294. mounted() {
  295. this.debouncedGetData = debounce(this.debounce, this.getData);
  296. this.$on("item-click", (item) => {
  297. this.select(item);
  298. });
  299. let $input = this.getInput();
  300. $input.setAttribute("role", "textbox");
  301. $input.setAttribute("aria-autocomplete", "list");
  302. $input.setAttribute("aria-controls", "id");
  303. $input.setAttribute(
  304. "aria-activedescendant",
  305. `${this.id}-item-${this.highlightedIndex}`
  306. );
  307. },
  308. beforeDestroy() {
  309. this.$refs.suggestions.$destroy();
  310. },
  311. };
  312. </script>