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.lastPopulated = 0;
20 this.setupListeners();
24 this.input.addEventListener('input', this.requestSuggestions.bind(this));
25 this.input.addEventListener('focus', this.requestSuggestions.bind(this));
26 this.input.addEventListener('keydown', event => {
27 if (event.key === 'Tab') {
28 this.hideSuggestions();
32 this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
33 this.container.addEventListener('keydown', this.containerKeyDown.bind(this));
35 onChildEvent(this.list, 'button', 'click', (event, el) => {
36 this.selectSuggestion(el.textContent);
38 onChildEvent(this.list, 'button', 'keydown', (event, el) => {
39 if (event.key === 'Enter') {
40 this.selectSuggestion(el.textContent);
46 selectSuggestion(value) {
47 this.input.value = value;
48 this.lastPopulated = Date.now();
50 this.input.dispatchEvent(new Event('input', {bubbles: true}));
51 this.input.dispatchEvent(new Event('change', {bubbles: true}));
52 this.hideSuggestions();
55 containerKeyDown(event) {
56 if (event.key === 'Enter') event.preventDefault();
57 if (this.list.classList.contains('hidden')) return;
60 if (event.key === 'ArrowDown') {
62 event.preventDefault();
65 else if (event.key === 'ArrowUp') {
66 this.moveFocus(false);
67 event.preventDefault();
70 else if (event.key === 'Escape') {
71 this.hideSuggestions();
72 event.preventDefault();
76 moveFocus(forward = true) {
77 const focusables = Array.from(this.container.querySelectorAll('input,button'));
78 const index = focusables.indexOf(document.activeElement);
79 const newFocus = focusables[index + (forward ? 1 : -1)];
85 async requestSuggestions() {
86 if (Date.now() - this.lastPopulated < 50) {
90 const nameFilter = this.getNameFilterIfNeeded();
91 const search = this.input.value.slice(0, 3).toLowerCase();
92 const suggestions = await this.loadSuggestions(search, nameFilter);
93 let toShow = suggestions.slice(0, 6);
94 if (search.length > 0) {
95 toShow = suggestions.filter(val => {
96 return val.toLowerCase().includes(search);
100 this.displaySuggestions(toShow);
103 getNameFilterIfNeeded() {
104 if (this.type !== 'value') return null;
105 return this.parent.querySelector('input').value;
109 * @param {String} search
110 * @param {String|null} nameFilter
111 * @returns {Promise<Object|String|*>}
113 async loadSuggestions(search, nameFilter = null) {
114 const params = {search, name: nameFilter};
115 const cacheKey = `${this.url}:${JSON.stringify(params)}`;
117 if (ajaxCache[cacheKey]) {
118 return ajaxCache[cacheKey];
121 const resp = await window.$http.get(this.url, params);
122 ajaxCache[cacheKey] = resp.data;
127 * @param {String[]} suggestions
129 displaySuggestions(suggestions) {
130 if (suggestions.length === 0) {
131 return this.hideSuggestions();
134 this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
135 this.list.style.display = 'block';
136 for (const button of this.list.querySelectorAll('button')) {
137 button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
142 this.list.style.display = 'none';
145 hideSuggestionsIfFocusedLost(event) {
146 if (!this.container.contains(event.relatedTarget)) {
147 this.hideSuggestions();
152 export default AutoSuggest;