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.toLowerCase();
92 const suggestions = await this.loadSuggestions(search, nameFilter);
94 const toShow = suggestions.filter(val => {
95 return search === '' || val.toLowerCase().startsWith(search);
98 this.displaySuggestions(toShow);
101 getNameFilterIfNeeded() {
102 if (this.type !== 'value') return null;
103 return this.parent.querySelector('input').value;
107 * @param {String} search
108 * @param {String|null} nameFilter
109 * @returns {Promise<Object|String|*>}
111 async loadSuggestions(search, nameFilter = null) {
112 // Truncate search to prevent over numerous lookups
113 search = search.slice(0, 4);
115 const params = {search, name: nameFilter};
116 const cacheKey = `${this.url}:${JSON.stringify(params)}`;
118 if (ajaxCache[cacheKey]) {
119 return ajaxCache[cacheKey];
122 const resp = await window.$http.get(this.url, params);
123 ajaxCache[cacheKey] = resp.data;
128 * @param {String[]} suggestions
130 displaySuggestions(suggestions) {
131 if (suggestions.length === 0) {
132 return this.hideSuggestions();
135 this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
136 this.list.style.display = 'block';
137 for (const button of this.list.querySelectorAll('button')) {
138 button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
143 this.list.style.display = 'none';
146 hideSuggestionsIfFocusedLost(event) {
147 if (!this.container.contains(event.relatedTarget)) {
148 this.hideSuggestions();
153 export default AutoSuggest;