import { Component , ElementRef , EventEmitter , Input , OnChanges , Output , SimpleChanges , ViewChild , } from "@angular/core" ; import { COMMA , ENTER } from "@angular/cdk/keycodes" ; import { Entity } from "../../../entity/model/entity" ; import { BehaviorSubject , Observable } from "rxjs" ; import { FormControl } from "@angular/forms" ; import { filter , map } from "rxjs/operators" ; import { MatChipInputEvent } from "@angular/material/chips" ; import { MatAutocompleteTrigger } from "@angular/material/autocomplete" ; import { UntilDestroy , untilDestroyed } from "@ngneat/until-destroy" ; import { DynamicEntityService } from "../../../entity/dynamic-entity.service" ; @ Component ( { selector : "app-entity-select" , templateUrl : "./entity-select.component.html" , styleUrls : [ "./entity-select.component.scss" ] , } ) @ UntilDestroy ( ) export class EntitySelectComponent < E extends Entity > implements OnChanges { readonly separatorKeysCodes : number [ ] = [ ENTER , COMMA ] ; readonly loadingPlaceholder = $localize `:A placeholder for the input element when select options are not loaded yet:loading...` ; param type throws @ Input ( ) set entityType ( type ) { this . loading . next ( true ) ; this . dynamicEntityService . loadType < E > ( type ) . then ( ( entities ) => { this . allEntities = entities ; this . loading . next ( false ) ; this . formControl . setValue ( null ) ; } ) ; } /**
     * The (initial) selection. Can be used in combination with  selectionChange * to enable two-way binding to an array of strings corresponding to the id's of the entities.
     *  param sel @ Input ( ) set selection ( sel ) { if ( ! Array . isArray ( sel ) ) { this . selectedEntities = [ ] ; return ; } this . loading . pipe ( untilDestroyed ( this ) , filter ( ( isLoading ) => ! isLoading ) ) . subscribe ( ( _ ) => { this . selectedEntities = this . allEntities . filter ( ( e ) => sel . find ( ( s ) => s === e . getId ( ) ) ) ; } ) ; } /** Underlying data-array */ selectedEntities : E [ ] = [ ] ; /**
     * called whenever the selection changes.
     * This happens when a new entity is being added or an existing
     * one is removed
     */ @ Output ( ) selectionChange = new EventEmitter < string [ ] > ( ) ; /**
     * The label is what is seen above the list. For example when used
     * in the note-details-view, this is "Children"
     */ @ Input ( ) label : string ; /**
     * The placeholder is what is seen when someone clicks into the input-
     * field and adds new entities.
     * In the note-details-view, this is "Add children..."
     * The placeholder is only displayed if `loading === false`
     */ @ Input ( ) placeholder : string ; /**
     * Whether or not single chips (entity-views) are selectable.
     * This currently has no specific meaning and defaults to <code>false</code>
     */ @ Input ( ) selectable = false ; /**
     * Whether or not single chips (entity-views) are removable.
     * If this is the case, they can be deleted.
     */ @ Input ( ) removable = true ; /**
     * Whether or not to show entities in the list.
     * Entities can still be selected using the autocomplete,
     * and  selection as well as  selectionChange will
     * still work as expected
      @ Input ( ) showEntities : boolean = true ; param disabled @ Input ( ) set disabled ( disabled ) { if ( disabled ) { this . formControl . disable ( ) ; } else { this . formControl . enable ( ) ; } } /**
     * true when this is loading and false when it's ready.
     * This subject's state reflects the actual loading resp. the 'readiness'-
     * state of this component. Will trigger once loading is done
     */ loading = new BehaviorSubject ( true ) ; inputPlaceholder = this . loadingPlaceholder ; allEntities : E [ ] = [ ] ; filteredEntities : Observable < E [ ] > ; formControl = new FormControl ( ) ; @ ViewChild ( "inputField" ) inputField : ElementRef < HTMLInputElement > ; @ ViewChild ( MatAutocompleteTrigger ) autocomplete : MatAutocompleteTrigger ; constructor ( private dynamicEntityService ) { this . filteredEntities = this . formControl . valueChanges . pipe ( untilDestroyed ( this ) , filter ( ( value ) => value === null || typeof value === "string" ) , map ( ( searchText ? ) => this . filter ( searchText ) ) ) ; this . loading . pipe ( untilDestroyed ( this ) ) . subscribe ( ( isLoading ) => { this . inputPlaceholder = isLoading ? this . loadingPlaceholder : this . placeholder ; } ) ; } ngOnChanges ( changes ) { if ( changes . hasOwnProperty ( "additionalFilter" ) ) { this . formControl . setValue ( this . formControl . value ) ; } } /**
     * The accessor used for filtering and when selecting a new
     * entity.
     * <br> Per default, this filters for the name. If the entity
     * has no name, this filters for the entity's id.
     */ @ Input ( ) accessor : ( e : Entity ) => string = ( e ) => e [ "name" ] || e . getId ( ) ; @ Input ( ) additionalFilter : ( e : E ) => boolean = ( _ ) => true ; param entity selectEntity ( entity ) { this . selectedEntities . push ( entity ) ; this . emitChange ( ) ; this . inputField . nativeElement . value = "" ; this . formControl . setValue ( null ) ; setTimeout ( ( ) => this . autocomplete . openPanel ( ) ) ; } /**
     * called when a key code from  separatorKeysCodes * is recorded and the user has entered a new entity-name (resp.
     * whatever the accessor defines)
     *  param event select ( event ) { const value = event . value ; if ( value ) { const entity = this . allEntities . find ( ( e ) => this . accessor ( e ) === value . trim ( ) ) ; if ( entity ) { this . selectEntity ( entity ) ; } } } /**
     * filters a subset of all entities with a given search-text.
     * Entities that do not match the  additionalFilter -predicate
     * or are already included won't be in this subset.
     * If the search-text is <code>null</code> or <code>undefined</code>,
     * this will return all entities (with the aforementioned additional filters).
     *  param value private filter ( value ? ) { let filteredEntities = this . allEntities . filter ( ( e ) => this . additionalFilter ( e ) && ! this . isSelected ( e ) ) ; if ( value ) { const filterValue = value . toLowerCase ( ) ; filteredEntities = filteredEntities . filter ( ( entity ) => this . accessor ( entity ) . toLowerCase ( ) . includes ( filterValue ) ) ; } return filteredEntities ; } param entity unselectEntity ( entity ) { const index = this . selectedEntities . findIndex ( ( e ) => e . getId ( ) === entity . getId ( ) ) ; if ( index !== - 1 ) { this . selectedEntities . splice ( index , 1 ) ; this . emitChange ( ) ; this . formControl . updateValueAndValidity ( ) ; } } private emitChange ( ) { this . selectionChange . emit ( this . selectedEntities . map ( ( e ) => e . getId ( ) ) ) ; } private isSelected ( entity ) { return this . selectedEntities . some ( ( e ) => e . getId ( ) === entity . getId ( ) ) ; } }