1 import {escapeHtml} from "../services/util";
2 import {onChildEvent} from "../services/dom";
12 this.parent = this.$el.parentElement;
13 this.container = this.$el;
14 this.type = this.$opts.type;
15 this.url = this.$opts.url;
16 this.input = this.$refs.input;
17 this.list = this.$refs.list;
19 this.setupListeners();
23 this.input.addEventListener('input', this.requestSuggestions.bind(this));
24 this.input.addEventListener('focus', this.requestSuggestions.bind(this));
25 this.input.addEventListener('keydown', event => {
26 if (event.key === 'Tab') {
27 this.hideSuggestions();
31 this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
32 this.container.addEventListener('keydown', this.containerKeyDown.bind(this));
34 onChildEvent(this.list, 'button', 'click', (event, el) => {
35 this.selectSuggestion(el.textContent);
37 onChildEvent(this.list, 'button', 'keydown', (event, el) => {
38 if (event.key === 'Enter') {
39 this.selectSuggestion(el.textContent);
45 selectSuggestion(value) {
46 this.input.value = value;
48 this.hideSuggestions();
51 containerKeyDown(event) {
52 if (event.key === 'Enter') event.preventDefault();
53 if (this.list.classList.contains('hidden')) return;
56 if (event.key === 'ArrowDown') {
58 event.preventDefault();
61 else if (event.key === 'ArrowUp') {
62 this.moveFocus(false);
63 event.preventDefault();
66 else if (event.key === 'Escape') {
67 this.hideSuggestions();
68 event.preventDefault();
72 moveFocus(forward = true) {
73 const focusables = Array.from(this.container.querySelectorAll('input,button'));
74 const index = focusables.indexOf(document.activeElement);
75 const newFocus = focusables[index + (forward ? 1 : -1)];
81 async requestSuggestions() {
82 const nameFilter = this.getNameFilterIfNeeded();
83 const search = this.input.value.slice(0, 3);
84 const suggestions = await this.loadSuggestions(search, nameFilter);
85 let toShow = suggestions.slice(0, 6);
86 if (search.length > 0) {
87 toShow = suggestions.filter(val => {
88 return val.toLowerCase().includes(search);
92 this.displaySuggestions(toShow);
95 getNameFilterIfNeeded() {
96 if (this.type !== 'value') return null;
97 return this.parent.querySelector('input').value;
101 * @param {String} search
102 * @param {String|null} nameFilter
103 * @returns {Promise<Object|String|*>}
105 async loadSuggestions(search, nameFilter = null) {
106 const params = {search, name: nameFilter};
107 const cacheKey = `${this.url}:${JSON.stringify(params)}`;
109 if (ajaxCache[cacheKey]) {
110 return ajaxCache[cacheKey];
113 const resp = await window.$http.get(this.url, params);
114 ajaxCache[cacheKey] = resp.data;
119 * @param {String[]} suggestions
121 displaySuggestions(suggestions) {
122 if (suggestions.length === 0) {
123 return this.hideSuggestions();
126 this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
127 this.list.style.display = 'block';
128 for (const button of this.list.querySelectorAll('button')) {
129 button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
134 this.list.style.display = 'none';
137 hideSuggestionsIfFocusedLost(event) {
138 if (!this.container.contains(event.relatedTarget)) {
139 this.hideSuggestions();
144 export default AutoSuggest;