|
@ -3,18 +3,18 @@ import { CommonModule } from '@angular/common'; |
|
|
import { |
|
|
import { |
|
|
ChangeDetectionStrategy, |
|
|
ChangeDetectionStrategy, |
|
|
Component, |
|
|
Component, |
|
|
computed, |
|
|
|
|
|
CUSTOM_ELEMENTS_SCHEMA, |
|
|
CUSTOM_ELEMENTS_SCHEMA, |
|
|
effect, |
|
|
effect, |
|
|
ElementRef, |
|
|
ElementRef, |
|
|
EventEmitter, |
|
|
EventEmitter, |
|
|
Input, |
|
|
Input, |
|
|
|
|
|
OnDestroy, |
|
|
OnInit, |
|
|
OnInit, |
|
|
Output, |
|
|
Output, |
|
|
signal, |
|
|
signal, |
|
|
ViewChild |
|
|
ViewChild |
|
|
} from '@angular/core'; |
|
|
} from '@angular/core'; |
|
|
import { FormsModule } from '@angular/forms'; |
|
|
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; |
|
|
import { |
|
|
import { |
|
|
MatAutocompleteModule, |
|
|
MatAutocompleteModule, |
|
|
MatAutocompleteSelectedEvent |
|
|
MatAutocompleteSelectedEvent |
|
@ -23,6 +23,7 @@ import { MatChipsModule } from '@angular/material/chips'; |
|
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
|
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
|
|
import { MatInputModule } from '@angular/material/input'; |
|
|
import { MatInputModule } from '@angular/material/input'; |
|
|
import { Tag } from '@prisma/client'; |
|
|
import { Tag } from '@prisma/client'; |
|
|
|
|
|
import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; |
|
|
|
|
|
|
|
|
@Component({ |
|
|
@Component({ |
|
|
changeDetection: ChangeDetectionStrategy.OnPush, |
|
|
changeDetection: ChangeDetectionStrategy.OnPush, |
|
@ -32,14 +33,15 @@ import { Tag } from '@prisma/client'; |
|
|
MatAutocompleteModule, |
|
|
MatAutocompleteModule, |
|
|
MatChipsModule, |
|
|
MatChipsModule, |
|
|
MatFormFieldModule, |
|
|
MatFormFieldModule, |
|
|
MatInputModule |
|
|
MatInputModule, |
|
|
|
|
|
ReactiveFormsModule |
|
|
], |
|
|
], |
|
|
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
|
|
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
|
|
selector: 'gf-tags-selector', |
|
|
selector: 'gf-tags-selector', |
|
|
styleUrls: ['./tags-selector.component.scss'], |
|
|
styleUrls: ['./tags-selector.component.scss'], |
|
|
templateUrl: 'tags-selector.component.html' |
|
|
templateUrl: 'tags-selector.component.html' |
|
|
}) |
|
|
}) |
|
|
export class GfTagsSelectorComponent implements OnInit { |
|
|
export class GfTagsSelectorComponent implements OnInit, OnDestroy { |
|
|
@Input() tags: Tag[]; |
|
|
@Input() tags: Tag[]; |
|
|
@Input() tagsAvailable: Tag[]; |
|
|
@Input() tagsAvailable: Tag[]; |
|
|
@Input() withoutHint: boolean; |
|
|
@Input() withoutHint: boolean; |
|
@ -48,12 +50,12 @@ export class GfTagsSelectorComponent implements OnInit { |
|
|
|
|
|
|
|
|
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>; |
|
|
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>; |
|
|
|
|
|
|
|
|
|
|
|
public filteredOptions: Subject<Tag[]> = new BehaviorSubject([]); |
|
|
|
|
|
public readonly tagInputControl = new FormControl(''); |
|
|
public readonly tagsSelected = signal<Tag[]>([]); |
|
|
public readonly tagsSelected = signal<Tag[]>([]); |
|
|
public readonly separatorKeysCodes: number[] = [COMMA, ENTER]; |
|
|
public readonly separatorKeysCodes: number[] = [COMMA, ENTER]; |
|
|
public readonly tagsUnselected = computed(() => { |
|
|
|
|
|
const tags = this.tagsSelected(); |
|
|
private unsubscribeSubject = new Subject<void>(); |
|
|
return tags ? this.filterTags(tags) : this.tagsAvailable.slice(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
public constructor() { |
|
|
public constructor() { |
|
|
effect(() => { |
|
|
effect(() => { |
|
@ -61,10 +63,21 @@ export class GfTagsSelectorComponent implements OnInit { |
|
|
this.tagsChanged.emit(this.tagsSelected()); |
|
|
this.tagsChanged.emit(this.tagsSelected()); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
this.tagInputControl.valueChanges |
|
|
|
|
|
.pipe(takeUntil(this.unsubscribeSubject)) |
|
|
|
|
|
.subscribe((value) => { |
|
|
|
|
|
this.filteredOptions.next(this.filterTags(value)); |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public ngOnInit() { |
|
|
public ngOnInit() { |
|
|
this.tagsSelected.set(this.tags); |
|
|
this.tagsSelected.set(this.tags); |
|
|
|
|
|
this.updateFilters(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public ngOnDestroy() { |
|
|
|
|
|
this.unsubscribeSubject.next(); |
|
|
|
|
|
this.unsubscribeSubject.complete(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public onAddTag(event: MatAutocompleteSelectedEvent) { |
|
|
public onAddTag(event: MatAutocompleteSelectedEvent) { |
|
@ -75,6 +88,7 @@ export class GfTagsSelectorComponent implements OnInit { |
|
|
return [...(tags ?? []), tag]; |
|
|
return [...(tags ?? []), tag]; |
|
|
}); |
|
|
}); |
|
|
this.tagInput.nativeElement.value = ''; |
|
|
this.tagInput.nativeElement.value = ''; |
|
|
|
|
|
this.tagInputControl.setValue(undefined); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public onRemoveTag(tag: Tag) { |
|
|
public onRemoveTag(tag: Tag) { |
|
@ -83,15 +97,23 @@ export class GfTagsSelectorComponent implements OnInit { |
|
|
return id !== tag.id; |
|
|
return id !== tag.id; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
this.updateFilters(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private filterTags(tagsSelected: Tag[]) { |
|
|
private filterTags(query: string = ''): Tag[] { |
|
|
const tagIds = tagsSelected.map(({ id }) => { |
|
|
const tags = this.tagsSelected() ?? []; |
|
|
|
|
|
const tagIds = tags.map(({ id }) => { |
|
|
return id; |
|
|
return id; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
return this.tagsAvailable.filter(({ id }) => { |
|
|
return this.tagsAvailable.filter(({ id, name }) => { |
|
|
return !tagIds.includes(id); |
|
|
return ( |
|
|
|
|
|
!tagIds.includes(id) && name.toLowerCase().includes(query.toLowerCase()) |
|
|
|
|
|
); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private updateFilters() { |
|
|
|
|
|
this.filteredOptions.next(this.filterTags()); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|