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: `
{{ emptyText }}
0">
{{ group }}
{{ cmd.label }}
{{ key }}
`,
})
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();
}
}