UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

78 lines (70 loc) 2.92 kB
import type { Injector } from '@furystack/inject' import { debounce, EventHub, ObservableValue } from '@furystack/utils' import type { SuggestionResult } from './suggestion-result.js' export class SuggestManager<T> extends EventHub<{ onSelectSuggestion: T }> implements Disposable { public isOpened = new ObservableValue(false) public isLoading = new ObservableValue(false) public term = new ObservableValue('') public selectedIndex = new ObservableValue(0) public currentSuggestions = new ObservableValue<Array<{ entry: T; suggestion: SuggestionResult }>>([]) public keyPressListener = ((ev: KeyboardEvent) => { if (ev.key === 'Escape') { this.isOpened.setValue(false) } }).bind(this) public element?: HTMLElement public clickOutsideListener = ((ev: MouseEvent) => { if ( this.element && this.isOpened.getValue() && (ev.target as HTMLElement).closest(this.element.tagName) !== this.element ) { this.isOpened.setValue(false) } }).bind(this) public [Symbol.dispose]() { window.removeEventListener('keyup', this.keyPressListener, true) window.removeEventListener('click', this.clickOutsideListener, true) this.isOpened[Symbol.dispose]() this.isLoading[Symbol.dispose]() this.term[Symbol.dispose]() this.selectedIndex[Symbol.dispose]() this.currentSuggestions[Symbol.dispose]() super[Symbol.dispose]() } public selectSuggestion(index: number = this.selectedIndex.getValue()) { const selectedSuggestion = this.currentSuggestions.getValue()[index] this.isOpened.setValue(false) this.emit('onSelectSuggestion', selectedSuggestion.entry) } private lastGetSuggestionOptions?: { injector: Injector; term: string } public getSuggestion = debounce(async (options: { injector: Injector; term: string }) => { try { if (this.lastGetSuggestionOptions?.term === options.term) { return } const lastSelectedEntry = JSON.stringify(this.currentSuggestions.getValue()[this.selectedIndex.getValue()]?.entry) this.isLoading.setValue(true) this.lastGetSuggestionOptions = options const newEntries = await this.getEntries(options.term) this.isOpened.setValue(true) this.currentSuggestions.setValue(newEntries.map((e) => ({ entry: e, suggestion: this.getSuggestionEntry(e) }))) this.selectedIndex.setValue( Math.max( 0, this.currentSuggestions.getValue().findIndex((e) => JSON.stringify(e.entry) === lastSelectedEntry), ), ) } finally { this.isLoading.setValue(false) } }, 250) constructor( private readonly getEntries: (term: string) => Promise<T[]>, private readonly getSuggestionEntry: (entry: T) => SuggestionResult, ) { super() window.addEventListener('keyup', this.keyPressListener, true) window.addEventListener('click', this.clickOutsideListener, true) } }