Picsur/frontend/src/app/components/values-picker/values-picker.component.ts

122 lines
3.5 KiB
TypeScript

import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import Fuse from 'fuse.js';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { BehaviorSubject } from 'rxjs';
import { Required } from '../../models/decorators/required.decorator';
@Component({
selector: 'values-picker',
templateUrl: './values-picker.component.html',
styleUrls: ['./values-picker.component.scss'],
})
export class ValuesPickerComponent implements OnInit, OnChanges {
// Static data
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
// Ui niceties
@Input('name') @Required name = '';
public get nameCap(): string {
return this.name.charAt(0).toUpperCase() + this.name.slice(1);
}
public get nameCapMul(): string {
return `${this.nameCap}s`;
}
// Inputs/oututs
@Input('selection-list') @Required fullSelection: string[] = [];
@Input('control') @Required myControl: FormControl;
@Input('disabled-list') disabledSelection: string[] = [];
@Input('value-mapper')
valueMapper: (value: string) => string = (value) => value;
// Selection
private selectableSubject = new BehaviorSubject<string[]>([]);
private foundSubject = new BehaviorSubject<string[]>([]);
public found = this.foundSubject.asObservable();
public inputControl = new FormControl('');
public ngOnInit(): void {
this.subscribeInputValue();
this.subscribeMyValue();
}
public ngOnChanges(): void {
this.updateSelectable();
}
public isDisabled(value: string): boolean {
return this.disabledSelection.includes(value);
}
// Remove/add
public removeItem(item: string) {
const selected: string[] = this.myControl.value;
const newSelection = selected.filter((s) => s !== item);
this.myControl.setValue(newSelection);
}
public addItemInput(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.addItem(value);
}
public addItemSelect(event: MatAutocompleteSelectedEvent): void {
this.addItem(event.option.value);
}
private addItem(value: string) {
const selectable = this.selectableSubject.getValue();
if (this.isDisabled(value) || !selectable.includes(value)) return;
const selected: string[] = this.myControl.value;
this.myControl.setValue([...selected, value]);
this.inputControl.setValue('');
}
// Update and subscribe
private updateSelectable() {
const selected: string[] = this.myControl.value;
const available = this.fullSelection.filter(
(s) => !this.isDisabled(s) && !selected.includes(s),
);
this.selectableSubject.next(available);
const searchValue = this.inputControl.value;
if (searchValue && available.length > 0) {
const fuse = new Fuse(available.map(this.valueMapper));
const result = fuse
.search(searchValue, {
limit: 10,
})
.map((r) => available[r.refIndex]);
this.foundSubject.next(result);
} else {
this.foundSubject.next(available);
}
}
@AutoUnsubscribe()
private subscribeInputValue() {
return this.inputControl.valueChanges.subscribe(() => {
this.updateSelectable();
});
}
@AutoUnsubscribe()
private subscribeMyValue() {
return this.myControl.valueChanges.subscribe(() => {
this.updateSelectable();
});
}
}