216 lines
5.8 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|