import { firstBy } from 'thenby' import { Component, Input, HostListener, ViewChildren, QueryList, ElementRef } from '@angular/core' // eslint-disable-line @typescript-eslint/no-unused-vars import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import FuzzySearch from 'fuzzy-search' import { SelectorOption } from '../api/selector' /** @hidden */ @Component({ selector: 'selector-modal', templateUrl: './selectorModal.component.pug', styleUrls: ['./selectorModal.component.scss'], }) export class SelectorModalComponent { @Input() options: SelectorOption[] @Input() filteredOptions: SelectorOption[] @Input() filter = '' @Input() name: string @Input() selectedIndex = 0 hasGroups = false @ViewChildren('item') itemChildren: QueryList constructor ( public modalInstance: NgbActiveModal, ) { } ngOnInit (): void { this.onFilterChange() this.hasGroups = this.options.some(x => x.group) } @HostListener('keydown', ['$event']) onKeyUp (event: KeyboardEvent): void { if (event.key === 'Escape') { this.close() } else if (this.filteredOptions.length > 0) { if (event.key === 'PageUp' || event.key === 'ArrowUp' && event.metaKey) { this.selectedIndex -= Math.min(10, Math.max(1, this.selectedIndex)) event.preventDefault() } else if (event.key === 'PageDown' || event.key === 'ArrowDown' && event.metaKey) { this.selectedIndex += Math.min(10, Math.max(1, this.filteredOptions.length - this.selectedIndex - 1)) event.preventDefault() } else if (event.key === 'ArrowUp') { this.selectedIndex-- event.preventDefault() } else if (event.key === 'ArrowDown') { this.selectedIndex++ event.preventDefault() } else if (event.key === 'Enter') { this.selectOption(this.filteredOptions[this.selectedIndex]) } else if (event.key === 'Backspace' && this.canEditSelected()) { event.preventDefault() this.filter = this.filteredOptions[this.selectedIndex].freeInputEquivalent! this.onFilterChange() } this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', }) } } onFilterChange (): void { const f = this.filter.trim().toLowerCase() if (!f) { this.filteredOptions = this.options.slice().sort( firstBy, number>(x => x.weight ?? 0) .thenBy, string>(x => x.group ?? '') .thenBy, string>(x => x.name), ) .filter(x => !x.freeInputPattern) } else { // eslint-disable-next-line @typescript-eslint/restrict-plus-operands this.filteredOptions = new FuzzySearch( this.options, ['name', 'group', 'description'], { sort: true }, ).search(f) this.options.filter(x => x.freeInputPattern).forEach(freeOption => { if (!this.filteredOptions.includes(freeOption)) { this.filteredOptions.push(freeOption) } }) } this.selectedIndex = Math.max(0, this.selectedIndex) this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex) } filterMatches (option: SelectorOption, terms: string[]): boolean { const content = (option.group ?? '') + option.name + (option.description ?? '') return terms.every(term => content.toLowerCase().includes(term)) } getOptionText (option: SelectorOption): string { if (option.freeInputPattern) { return option.freeInputPattern.replace('%s', this.filter) } return option.name } selectOption (option: SelectorOption): void { this.modalInstance.close(option.result) setTimeout(() => option.callback?.(this.filter)) } canEditSelected (): boolean { return !this.filter && !!this.filteredOptions[this.selectedIndex].freeInputEquivalent && this.options.some(x => x.freeInputPattern) } close (): void { this.modalInstance.dismiss() } }