ObsiViewer/src/components/search-input-with-assistant/search-input-with-assistant.component.ts

216 lines
5.8 KiB
TypeScript

import {
Component,
Input,
Output,
EventEmitter,
signal,
ViewChild,
ElementRef,
ChangeDetectionStrategy,
AfterViewInit
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SearchQueryAssistantComponent } from '../search-query-assistant/search-query-assistant.component';
import { inject } from '@angular/core';
import { SearchHistoryService } from '../../core/search/search-history.service';
/**
* Search input with integrated query assistant
* Wraps a text input and manages the search assistant popover
*/
@Component({
selector: 'app-search-input-with-assistant',
standalone: true,
imports: [CommonModule, FormsModule, SearchQueryAssistantComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="relative">
<div class="relative">
<svg
*ngIf="showSearchIcon"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
#searchInput
type="text"
[placeholder]="placeholder"
[value]="value"
(input)="onInputChange($event)"
(focus)="onFocus()"
(blur)="onBlur()"
(keydown.enter)="onEnter()"
[class]="inputClass"
[attr.aria-label]="placeholder"
/>
<button
*ngIf="value"
(click)="clear()"
class="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors"
aria-label="Clear search"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<app-search-query-assistant
#assistant
[context]="context"
[currentQuery]="value"
[anchorElement]="anchorElement"
(queryChange)="onQueryChange($event)"
(querySubmit)="onQuerySubmit($event)"
/>
</div>
`,
styles: [`
:host {
display: block;
}
`]
})
export class SearchInputWithAssistantComponent implements AfterViewInit {
@Input() placeholder: string = 'Search...';
@Input() value: string = '';
@Input() context: string = 'default';
@Input() showSearchIcon: boolean = true;
@Input() inputClass: string = 'w-full px-3 py-2 text-sm border border-border rounded-md bg-bg-primary text-text-main placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent';
@Output() valueChange = new EventEmitter<string>();
@Output() submit = new EventEmitter<string>();
@ViewChild('searchInput') searchInputRef?: ElementRef<HTMLInputElement>;
@ViewChild('assistant') assistantRef?: SearchQueryAssistantComponent;
anchorElement: HTMLElement | null = null;
private historyService = inject(SearchHistoryService);
constructor(private hostElement: ElementRef<HTMLElement>) {}
ngAfterViewInit(): void {
this.anchorElement = this.hostElement.nativeElement;
}
/**
* Handle input changes
*/
onInputChange(event: Event): void {
const target = event.target as HTMLInputElement;
this.value = target.value;
this.valueChange.emit(this.value);
// Update suggestions in assistant
if (this.assistantRef) {
this.assistantRef.currentQuery = this.value;
this.assistantRef.updateOptions();
this.assistantRef.updateSuggestions();
}
}
/**
* Handle focus event - open assistant
*/
onFocus(): void {
if (this.assistantRef) {
this.assistantRef.open();
}
}
/**
* Handle blur event
*/
onBlur(): void {
// Delay close to allow clicking on popover items
setTimeout(() => {
if (this.assistantRef && !this.assistantRef.popoverRef) {
// Only close if not interacting with popover
}
}, 200);
}
/**
* Handle Enter key
*/
onEnter(): void {
// Save to history and submit
if (this.value.trim()) {
this.historyService.add(this.context, this.value);
}
this.submit.emit(this.value);
if (this.assistantRef) {
this.assistantRef.refreshHistoryView();
this.assistantRef.close();
}
}
/**
* Handle query change from assistant
*/
onQueryChange(query: string): void {
this.value = query;
this.valueChange.emit(query);
if (this.searchInputRef) {
this.searchInputRef.nativeElement.value = query;
}
if (this.assistantRef) {
this.assistantRef.currentQuery = query;
this.assistantRef.updateOptions();
this.assistantRef.updateSuggestions();
}
}
/**
* Handle query submit from assistant
*/
onQuerySubmit(query: string): void {
this.value = query;
if (query.trim()) {
this.historyService.add(this.context, query);
}
this.submit.emit(query);
if (this.searchInputRef) {
this.searchInputRef.nativeElement.value = query;
this.searchInputRef.nativeElement.focus();
}
if (this.assistantRef) {
this.assistantRef.currentQuery = query;
this.assistantRef.refreshHistoryView();
this.assistantRef.close();
}
}
/**
* Clear the input
*/
clear(): void {
this.value = '';
this.valueChange.emit('');
if (this.searchInputRef) {
this.searchInputRef.nativeElement.value = '';
this.searchInputRef.nativeElement.focus();
}
}
/**
* Focus the input
*/
focus(): void {
if (this.searchInputRef) {
this.searchInputRef.nativeElement.focus();
}
}
}