import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, OnInit, OnChanges, SimpleChanges, inject, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CommandPaletteItemConfig } from 'mayvio-ui/commandpalette'; import 'mayvio-ui/commandpalette/css'; @Component({ selector: 'mv-commandpalette', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
`, }) export class CommandPaletteComponent implements OnInit, OnChanges { @Input() isOpen = false; @Input() commands: CommandPaletteItemConfig[] = []; @Input() placeholder = 'Search commands...'; @Input() emptyText = 'No results found.'; @Input() className = ''; @Output() close = new EventEmitter(); @Output() commandSelect = new EventEmitter(); search = ''; selectedIndex = 0; filteredCommands: CommandPaletteItemConfig[] = []; groupedCommands: Record = {}; groups: string[] = []; flattenedList: CommandPaletteItemConfig[] = []; private cdr = inject(ChangeDetectorRef); ngOnInit() { this.updateLists(); } ngOnChanges(changes: SimpleChanges) { if (changes['commands'] || changes['isOpen']) { if (changes['isOpen'] && this.isOpen) { this.search = ''; this.selectedIndex = 0; document.body.style.overflow = 'hidden'; setTimeout(() => { const input = document.querySelector('.mv-commandpalette-input') as HTMLInputElement; if (input) input.focus(); }); } else if (changes['isOpen'] && !this.isOpen) { document.body.style.overflow = ''; } this.updateLists(); } } @HostListener('window:keydown', ['$event']) onGlobalKeyDown(e: KeyboardEvent) { if (this.isOpen && e.key === 'Escape') { this.closePalette(); } } closePalette() { this.close.emit(); } onSearchChange(val: string) { this.search = val; this.selectedIndex = 0; this.updateLists(); } updateLists() { if (!this.search) { this.filteredCommands = this.commands; } else { const q = this.search.toLowerCase(); this.filteredCommands = this.commands.filter((cmd) => cmd.label.toLowerCase().includes(q)); } this.groupedCommands = { '': [] }; this.filteredCommands.forEach((cmd) => { const g = cmd.group || ''; if (!this.groupedCommands[g]) this.groupedCommands[g] = []; this.groupedCommands[g].push(cmd); }); this.groups = Object.keys(this.groupedCommands).sort(); this.flattenedList = []; this.groups.forEach((g) => { this.flattenedList.push(...this.groupedCommands[g]); }); this.cdr.markForCheck(); } onKeyDown(e: KeyboardEvent) { if (this.flattenedList.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); this.selectedIndex = this.selectedIndex < this.flattenedList.length - 1 ? this.selectedIndex + 1 : 0; this.cdr.markForCheck(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.flattenedList.length - 1; this.cdr.markForCheck(); } else if (e.key === 'Enter') { e.preventDefault(); this.selectCommand(this.flattenedList[this.selectedIndex]); } } onMouseEnter(cmd: CommandPaletteItemConfig) { const idx = this.flattenedList.findIndex((c) => c.id === cmd.id); if (idx !== -1) { this.selectedIndex = idx; this.cdr.markForCheck(); } } isSelected(cmd: CommandPaletteItemConfig): boolean { return this.flattenedList[this.selectedIndex]?.id === cmd.id; } selectCommand(cmd: CommandPaletteItemConfig) { this.commandSelect.emit(cmd); this.closePalette(); } }