mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
committed by
GitHub
9 changed files with 228 additions and 150 deletions
@ -0,0 +1,33 @@ |
|||
<mat-form-field appearance="outline" class="w-100"> |
|||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon> |
|||
<mat-chip-list #chipList aria-label="Search keywords"> |
|||
<mat-chip |
|||
*ngFor="let searchKeyword of searchKeywords" |
|||
class="mx-1 my-0 px-2 py-0" |
|||
matChipRemove |
|||
[removable]="true" |
|||
(removed)="removeKeyword(searchKeyword)" |
|||
> |
|||
{{ searchKeyword | gfSymbol }} |
|||
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon> |
|||
</mat-chip> |
|||
<input |
|||
#searchInput |
|||
name="close-outline" |
|||
[formControl]="searchControl" |
|||
[matAutocomplete]="autocomplete" |
|||
[matChipInputFor]="chipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
[placeholder]="placeholder" |
|||
(matChipInputTokenEnd)="addKeyword($event)" |
|||
/> |
|||
</mat-chip-list> |
|||
<mat-autocomplete |
|||
#autocomplete="matAutocomplete" |
|||
(optionSelected)="keywordSelected($event)" |
|||
> |
|||
<mat-option *ngFor="let filter of filters | async" [value]="filter"> |
|||
{{ filter | gfSymbol }} |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
</mat-form-field> |
@ -0,0 +1,22 @@ |
|||
@import '~apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
::ng-deep { |
|||
.mat-form-field-infix { |
|||
border-top: 0 solid transparent !important; |
|||
} |
|||
} |
|||
|
|||
.mat-chip { |
|||
cursor: pointer; |
|||
min-height: 1.5rem !important; |
|||
} |
|||
} |
|||
|
|||
:host-context(.is-dark-theme) { |
|||
.mat-form-field { |
|||
color: rgba(var(--light-primary-text)); |
|||
} |
|||
} |
@ -0,0 +1,108 @@ |
|||
import { COMMA, ENTER } from '@angular/cdk/keycodes'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
ElementRef, |
|||
EventEmitter, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
Output, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { FormControl } from '@angular/forms'; |
|||
import { |
|||
MatAutocomplete, |
|||
MatAutocompleteSelectedEvent |
|||
} from '@angular/material/autocomplete'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { BehaviorSubject, Observable, Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
selector: 'gf-activities-filter', |
|||
styleUrls: ['./activities-filter.component.scss'], |
|||
templateUrl: './activities-filter.component.html' |
|||
}) |
|||
export class ActivitiesFilterComponent implements OnChanges, OnDestroy { |
|||
@Input() allFilters: string[]; |
|||
@Input() placeholder: string; |
|||
|
|||
@Output() valueChanged = new EventEmitter<string[]>(); |
|||
|
|||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete; |
|||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>; |
|||
|
|||
public filters$: Subject<string[]> = new BehaviorSubject([]); |
|||
public filters: Observable<string[]> = this.filters$.asObservable(); |
|||
public searchControl = new FormControl(); |
|||
public searchKeywords: string[] = []; |
|||
public separatorKeysCodes: number[] = [ENTER, COMMA]; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor() { |
|||
this.searchControl.valueChanges |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((keyword) => { |
|||
if (keyword) { |
|||
const filterValue = keyword.toLowerCase(); |
|||
this.filters$.next( |
|||
this.allFilters.filter( |
|||
(filter) => filter.toLowerCase().indexOf(filterValue) === 0 |
|||
) |
|||
); |
|||
} else { |
|||
this.filters$.next(this.allFilters); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
if (this.allFilters) { |
|||
this.updateFilter(); |
|||
} |
|||
} |
|||
|
|||
public addKeyword({ input, value }: MatChipInputEvent): void { |
|||
if (value?.trim()) { |
|||
this.searchKeywords.push(value.trim()); |
|||
this.updateFilter(); |
|||
} |
|||
|
|||
// Reset the input value
|
|||
if (input) { |
|||
input.value = ''; |
|||
} |
|||
|
|||
this.searchControl.setValue(null); |
|||
} |
|||
|
|||
public keywordSelected(event: MatAutocompleteSelectedEvent): void { |
|||
this.searchKeywords.push(event.option.viewValue); |
|||
this.updateFilter(); |
|||
this.searchInput.nativeElement.value = ''; |
|||
this.searchControl.setValue(null); |
|||
} |
|||
|
|||
public removeKeyword(keyword: string): void { |
|||
const index = this.searchKeywords.indexOf(keyword); |
|||
|
|||
if (index >= 0) { |
|||
this.searchKeywords.splice(index, 1); |
|||
this.updateFilter(); |
|||
} |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private updateFilter() { |
|||
this.filters$.next(this.allFilters); |
|||
|
|||
this.valueChanged.emit(this.searchKeywords); |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { ReactiveFormsModule } from '@angular/forms'; |
|||
import { MatAutocompleteModule } from '@angular/material/autocomplete'; |
|||
import { MatChipsModule } from '@angular/material/chips'; |
|||
import { MatInputModule } from '@angular/material/input'; |
|||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; |
|||
|
|||
import { ActivitiesFilterComponent } from './activities-filter.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ActivitiesFilterComponent], |
|||
exports: [ActivitiesFilterComponent], |
|||
imports: [ |
|||
CommonModule, |
|||
GfSymbolModule, |
|||
MatAutocompleteModule, |
|||
MatChipsModule, |
|||
MatInputModule, |
|||
ReactiveFormsModule |
|||
], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class GfActivitiesFilterModule {} |
Loading…
Reference in new issue