import {escapeHtml} from '../services/util.ts'; import {onChildEvent} from '../services/dom'; import {Component} from './component'; import {KeyboardNavigationHandler} from '../services/keyboard-navigation'; const ajaxCache = {}; /** * AutoSuggest */ export class AutoSuggest extends Component { setup() { this.parent = this.$el.parentElement; this.container = this.$el; this.type = this.$opts.type; this.url = this.$opts.url; this.input = this.$refs.input; this.list = this.$refs.list; this.lastPopulated = 0; this.setupListeners(); } setupListeners() { const navHandler = new KeyboardNavigationHandler( this.list, () => { this.input.focus(); setTimeout(() => this.hideSuggestions(), 1); }, event => { event.preventDefault(); const selectionValue = event.target.textContent; if (selectionValue) { this.selectSuggestion(selectionValue); } }, ); navHandler.shareHandlingToEl(this.input); onChildEvent(this.list, '.text-item', 'click', (event, el) => { this.selectSuggestion(el.textContent); }); this.input.addEventListener('input', this.requestSuggestions.bind(this)); this.input.addEventListener('focus', this.requestSuggestions.bind(this)); this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this)); this.input.addEventListener('keydown', event => { if (event.key === 'Tab') { this.hideSuggestions(); } }); } selectSuggestion(value) { this.input.value = value; this.lastPopulated = Date.now(); this.input.focus(); this.input.dispatchEvent(new Event('input', {bubbles: true})); this.input.dispatchEvent(new Event('change', {bubbles: true})); this.hideSuggestions(); } async requestSuggestions() { if (Date.now() - this.lastPopulated < 50) { return; } const nameFilter = this.getNameFilterIfNeeded(); const search = this.input.value.toLowerCase(); const suggestions = await this.loadSuggestions(search, nameFilter); const toShow = suggestions.filter(val => search === '' || val.toLowerCase().startsWith(search)).slice(0, 10); this.displaySuggestions(toShow); } getNameFilterIfNeeded() { if (this.type !== 'value') return null; return this.parent.querySelector('input').value; } /** * @param {String} search * @param {String|null} nameFilter * @returns {Promise} */ async loadSuggestions(search, nameFilter = null) { // Truncate search to prevent over numerous lookups search = search.slice(0, 4); const params = {search, name: nameFilter}; const cacheKey = `${this.url}:${JSON.stringify(params)}`; if (ajaxCache[cacheKey]) { return ajaxCache[cacheKey]; } const resp = await window.$http.get(this.url, params); ajaxCache[cacheKey] = resp.data; return resp.data; } /** * @param {String[]} suggestions */ displaySuggestions(suggestions) { if (suggestions.length === 0) { this.hideSuggestions(); return; } // This used to use