mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* feat(ui): create gf-tags-selector component * feat(ui): implement gf-tags-selector in activity dialog * feat(ui): implement gf-tags-selector in holding detail dialog * Update changelogpull/4272/head^2
committed by
GitHub
10 changed files with 191 additions and 171 deletions
@ -0,0 +1 @@ |
|||||
|
export * from './tags-selector.component'; |
@ -0,0 +1,32 @@ |
|||||
|
<mat-form-field appearance="outline" class="w-100 without-hint"> |
||||
|
<mat-label i18n>Tags</mat-label> |
||||
|
<mat-chip-grid #tagsChipList> |
||||
|
@for (tag of tagsSelected(); track tag.id) { |
||||
|
<mat-chip-row |
||||
|
matChipRemove |
||||
|
[removable]="true" |
||||
|
(removed)="onRemoveTag(tag)" |
||||
|
> |
||||
|
{{ tag.name }} |
||||
|
<ion-icon matChipTrailingIcon name="close-outline" /> |
||||
|
</mat-chip-row> |
||||
|
} |
||||
|
<input |
||||
|
#tagInput |
||||
|
[formControl]="tagInputControl" |
||||
|
[matAutocomplete]="autocompleteTags" |
||||
|
[matChipInputFor]="tagsChipList" |
||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
||||
|
/> |
||||
|
</mat-chip-grid> |
||||
|
<mat-autocomplete |
||||
|
#autocompleteTags="matAutocomplete" |
||||
|
(optionSelected)="onAddTag($event)" |
||||
|
> |
||||
|
@for (tag of filteredOptions | async; track tag.id) { |
||||
|
<mat-option [value]="tag.id"> |
||||
|
{{ tag.name }} |
||||
|
</mat-option> |
||||
|
} |
||||
|
</mat-autocomplete> |
||||
|
</mat-form-field> |
@ -0,0 +1,3 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
} |
@ -0,0 +1,123 @@ |
|||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
CUSTOM_ELEMENTS_SCHEMA, |
||||
|
ElementRef, |
||||
|
EventEmitter, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
signal, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; |
||||
|
import { |
||||
|
MatAutocompleteModule, |
||||
|
MatAutocompleteSelectedEvent |
||||
|
} from '@angular/material/autocomplete'; |
||||
|
import { MatChipsModule } from '@angular/material/chips'; |
||||
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
||||
|
import { MatInputModule } from '@angular/material/input'; |
||||
|
import { Tag } from '@prisma/client'; |
||||
|
import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
FormsModule, |
||||
|
MatAutocompleteModule, |
||||
|
MatChipsModule, |
||||
|
MatFormFieldModule, |
||||
|
MatInputModule, |
||||
|
ReactiveFormsModule |
||||
|
], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
||||
|
selector: 'gf-tags-selector', |
||||
|
styleUrls: ['./tags-selector.component.scss'], |
||||
|
templateUrl: 'tags-selector.component.html' |
||||
|
}) |
||||
|
export class GfTagsSelectorComponent implements OnInit, OnChanges, OnDestroy { |
||||
|
@Input() tags: Tag[]; |
||||
|
@Input() tagsAvailable: Tag[]; |
||||
|
|
||||
|
@Output() tagsChanged = new EventEmitter<Tag[]>(); |
||||
|
|
||||
|
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>; |
||||
|
|
||||
|
public filteredOptions: Subject<Tag[]> = new BehaviorSubject([]); |
||||
|
public readonly separatorKeysCodes: number[] = [COMMA, ENTER]; |
||||
|
public readonly tagInputControl = new FormControl(''); |
||||
|
public readonly tagsSelected = signal<Tag[]>([]); |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
public constructor() { |
||||
|
this.tagInputControl.valueChanges |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((value) => { |
||||
|
this.filteredOptions.next(this.filterTags(value)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.tagsSelected.set(this.tags); |
||||
|
this.updateFilters(); |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.tagsSelected.set(this.tags); |
||||
|
this.updateFilters(); |
||||
|
} |
||||
|
|
||||
|
public onAddTag(event: MatAutocompleteSelectedEvent) { |
||||
|
const tag = this.tagsAvailable.find(({ id }) => { |
||||
|
return id === event.option.value; |
||||
|
}); |
||||
|
|
||||
|
this.tagsSelected.update((tags) => { |
||||
|
return [...(tags ?? []), tag]; |
||||
|
}); |
||||
|
|
||||
|
this.tagsChanged.emit(this.tagsSelected()); |
||||
|
this.tagInput.nativeElement.value = ''; |
||||
|
this.tagInputControl.setValue(undefined); |
||||
|
} |
||||
|
|
||||
|
public onRemoveTag(tag: Tag) { |
||||
|
this.tagsSelected.update((tags) => { |
||||
|
return tags.filter(({ id }) => { |
||||
|
return id !== tag.id; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
this.tagsChanged.emit(this.tagsSelected()); |
||||
|
this.updateFilters(); |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
private filterTags(query: string = ''): Tag[] { |
||||
|
const tags = this.tagsSelected() ?? []; |
||||
|
const tagIds = tags.map(({ id }) => { |
||||
|
return id; |
||||
|
}); |
||||
|
|
||||
|
return this.tagsAvailable.filter(({ id, name }) => { |
||||
|
return ( |
||||
|
!tagIds.includes(id) && name.toLowerCase().includes(query.toLowerCase()) |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateFilters() { |
||||
|
this.filteredOptions.next(this.filterTags()); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue