import { Component , forwardRef , HostBinding , Input , Output , EventEmitter , Renderer2 , ViewChild , ViewChildren , ContentChildren , ContentChild , OnInit , TemplateRef , QueryList , AfterViewInit } from '@angular/core' ; import { AsyncValidatorFn , FormControl , NG_VALUE_ACCESSOR , ValidatorFn } from '@angular/forms' ; import { Observable , debounceTime , filter , map , first } from 'rxjs' ; import { TagInputAccessor } from '../../core/accessor' ; import { TagModel } from '../../core/tag-model' ; import { listen } from '../../core/helpers/listen' ; import * as constants from '../../core/constants' ; import { DragProvider , DraggedTag } from '../../core/providers/drag-provider' ; import { TagInputForm } from '../tag-input-form/tag-input-form.component' ; import { TagComponent } from '../tag/tag.component' ; import { animations } from './animations' ; import { defaults } from '../../defaults' ; import { TagInputDropdown } from '../dropdown/tag-input-dropdown.component' ; const CUSTOM_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => TagInputComponent ) , multi : true } ; @ Component ( { selector : 'tag-input' , providers : [ CUSTOM_ACCESSOR ] , styleUrls : [ './tag-input.style.scss' ] , templateUrl : './tag-input.template.html' , animations } ) export class TagInputComponent extends TagInputAccessor implements OnInit , AfterViewInit { name desc @ Input ( ) public separatorKeys : string [ ] = defaults . tagInput . separatorKeys ; name desc @ Input ( ) public separatorKeyCodes : number [ ] = defaults . tagInput . separatorKeyCodes ; name desc @ Input ( ) public placeholder : string = defaults . tagInput . placeholder ; name desc @ Input ( ) public secondaryPlaceholder : string = defaults . tagInput . secondaryPlaceholder ; name desc @ Input ( ) public maxItems : number = defaults . tagInput . maxItems ; name desc @ Input ( ) public validators : ValidatorFn [ ] = defaults . tagInput . validators ; name desc @ Input ( ) public asyncValidators : AsyncValidatorFn [ ] = defaults . tagInput . asyncValidators ; name @ Input ( ) public onlyFromAutocomplete = defaults . tagInput . onlyFromAutocomplete ; name @ Input ( ) public errorMessages : { [ key ] : string ; } = defaults . tagInput . errorMessages ; name @ Input ( ) public theme : string = defaults . tagInput . theme ; name @ Input ( ) public onTextChangeDebounce = defaults . tagInput . onTextChangeDebounce ; name @ Input ( ) public inputId = defaults . tagInput . inputId ; /**
     * - custom class assigned to the input
     */ @ Input ( ) public inputClass : string = defaults . tagInput . inputClass ; name @ Input ( ) public clearOnBlur : boolean = defaults . tagInput . clearOnBlur ; name @ Input ( ) public hideForm : boolean = defaults . tagInput . hideForm ; name @ Input ( ) public addOnBlur : boolean = defaults . tagInput . addOnBlur ; name @ Input ( ) public addOnPaste : boolean = defaults . tagInput . addOnPaste ; name @ Input ( ) public pasteSplitPattern = defaults . tagInput . pasteSplitPattern ; name @ Input ( ) public blinkIfDupe = defaults . tagInput . blinkIfDupe ; name @ Input ( ) public removable = defaults . tagInput . removable ; name @ Input ( ) public editable : boolean = defaults . tagInput . editable ; name @ Input ( ) public allowDupes = defaults . tagInput . allowDupes ; description name @ Input ( ) public modelAsStrings = defaults . tagInput . modelAsStrings ; name @ Input ( ) public trimTags = defaults . tagInput . trimTags ; name @ Input ( ) public get inputText ( ) : string { return this . inputTextValue ; } name @ Input ( ) public ripple : boolean = defaults . tagInput . ripple ; name desc @ Input ( ) public tabindex : string = defaults . tagInput . tabIndex ; name @ Input ( ) public disable : boolean = defaults . tagInput . disable ; name @ Input ( ) public dragZone : string = defaults . tagInput . dragZone ; name @ Input ( ) public onRemoving = defaults . tagInput . onRemoving ; name @ Input ( ) public onAdding = defaults . tagInput . onAdding ; name @ Input ( ) public animationDuration = defaults . tagInput . animationDuration ; name desc @ Output ( ) public onAdd = new EventEmitter < TagModel > ( ) ; name desc @ Output ( ) public onRemove = new EventEmitter < TagModel > ( ) ; name desc @ Output ( ) public onSelect = new EventEmitter < TagModel > ( ) ; name desc @ Output ( ) public onFocus = new EventEmitter < string > ( ) ; name desc @ Output ( ) public onBlur = new EventEmitter < string > ( ) ; name desc @ Output ( ) public onTextChange = new EventEmitter < TagModel > ( ) ; name @ Output ( ) public onPaste = new EventEmitter < string > ( ) ; name @ Output ( ) public onValidationError = new EventEmitter < TagModel > ( ) ; name @ Output ( ) public onTagEdited = new EventEmitter < TagModel > ( ) ; name @ ContentChild ( TagInputDropdown ) public dropdown : TagInputDropdown ; name desc @ ContentChildren ( TemplateRef , { descendants : false } ) public templates : QueryList < TemplateRef < any > > ; name @ ViewChild ( TagInputForm ) public inputForm : TagInputForm ; name desc public selectedTag : TagModel | undefined ; name public isLoading = false ; name param text public set inputText ( text ) { this . inputTextValue = text ; this . inputTextChange . emit ( text ) ; } name desc @ ViewChildren ( TagComponent ) public tags : QueryList < TagComponent > ; name desc fireEvents private listeners = { [ constants . KEYDOWN ] : < { ( fun ) : any ; } [ ] > [ ] , [ constants . KEYUP ] : < { ( fun ) : any ; } [ ] > [ ] } ; description name @ Output ( ) public inputTextChange : EventEmitter < string > = new EventEmitter ( ) ; description name public inputTextValue = '' ; desc name @ HostBinding ( 'attr.tabindex' ) public get tabindexAttr ( ) : string { return this . tabindex !== '' ? '-1' : '' ; } name public animationMetadata : { value : string ; params : object ; } ; public errors : string [ ] = [ ] ; public isProgressBarVisible$ : Observable < boolean > ; constructor ( private readonly renderer , public readonly dragProvider ) { super ( ) ; } name public ngAfterViewInit ( ) { this . setUpKeypressListeners ( ) ; this . setupSeparatorKeysListener ( ) ; this . setUpInputKeydownListeners ( ) ; if ( this . onTextChange . observers . length ) { this . setUpTextChangeSubscriber ( ) ; } if ( this . clearOnBlur || this . addOnBlur ) { this . setUpOnBlurSubscriber ( ) ; } if ( this . addOnPaste ) { this . setUpOnPasteListener ( ) ; } const statusChanges$ = this . inputForm . form . statusChanges ; statusChanges$ . pipe ( filter ( ( status ) => status !== 'PENDING' ) ) . subscribe ( ( ) => { this . errors = this . inputForm . getErrorMessages ( this . errorMessages ) ; } ) ; this . isProgressBarVisible$ = statusChanges$ . pipe ( map ( ( status ) => { return status === 'PENDING' || this . isLoading ; } ) ) ; if ( this . hideForm ) { this . inputForm . destroy ( ) ; } } name public ngOnInit ( ) { const hasReachedMaxItems = this . maxItems !== undefined && this . items && this . items . length > this . maxItems ; if ( hasReachedMaxItems ) { this . maxItems = this . items . length ; console . warn ( constants . MAX_ITEMS_WARNING ) ; } this . editable = this . onlyFromAutocomplete ? false : this . editable ; this . setAnimationMetadata ( ) ; } name param tag param index public onRemoveRequested ( tag , index ) { return new Promise ( resolve => { const subscribeFn = ( model ) => { this . removeItem ( model , index ) ; resolve ( tag ) ; } ; this . onRemoving ? this . onRemoving ( tag ) . pipe ( first ( ) ) . subscribe ( subscribeFn ) : subscribeFn ( tag ) ; } ) ; } name param fromAutocomplete { boolean } param tag { TagModel } param index param giveupFocus public onAddingRequested ( fromAutocomplete , tag , index ? , giveupFocus ? ) { return new Promise ( ( resolve , reject ) => { const subscribeFn = ( model ) => { return this . addItem ( fromAutocomplete , model , index , giveupFocus ) . then ( resolve ) . catch ( reject ) ; } ; return this . onAdding ? this . onAdding ( tag ) . pipe ( first ( ) ) . subscribe ( subscribeFn , reject ) : subscribeFn ( tag ) ; } ) ; } name param tag { TagModel } public appendTag = ( tag , index = this . items . length ) : void => { const items = this . items ; const model = this . modelAsStrings ? tag [ this . identifyBy ] : tag ; this . items = [ ... items . slice ( 0 , index ) , model , ... items . slice ( index , items . length ) ] ; } ; name param model public createTag = ( model ) : TagModel => { const trim = ( val , key ) : TagModel => { return typeof val === 'string' ? val . trim ( ) : val [ key ] ; } ; return { ... typeof model !== 'string' ? model : { } , [ this . displayBy ] : this . trimTags ? trim ( model , this . displayBy ) : model , [ this . identifyBy ] : this . trimTags ? trim ( model , this . identifyBy ) : model } ; } ; name desc param item param emit public selectItem ( item , emit = true ) { const isReadonly = item && typeof item !== 'string' && item . readonly ; if ( isReadonly || this . selectedTag === item ) { return ; } this . selectedTag = item ; if ( emit ) { this . onSelect . emit ( item ) ; } } name desc param eventName param $event public fireEvents ( eventName , $event ? ) { this . listeners [ eventName ] . forEach ( listener => listener . call ( this , $event ) ) ; } name desc param data public handleKeydown ( data ) { const event = data . event ; const key = event . keyCode || event . which ; const shiftKey = event . shiftKey || false ; switch ( constants . KEY_PRESS_ACTIONS [ key ] ) { case constants . ACTIONS_KEYS . DELETE : if ( this . selectedTag && this . removable ) { const index = this . items . indexOf ( this . selectedTag ) ; this . onRemoveRequested ( this . selectedTag , index ) ; } break ; case constants . ACTIONS_KEYS . SWITCH_PREV : this . moveToTag ( data . model , constants . PREV ) ; break ; case constants . ACTIONS_KEYS . SWITCH_NEXT : this . moveToTag ( data . model , constants . NEXT ) ; break ; case constants . ACTIONS_KEYS . TAB : if ( shiftKey ) { if ( this . isFirstTag ( data . model ) ) { return ; } this . moveToTag ( data . model , constants . PREV ) ; } else { if ( this . isLastTag ( data . model ) && ( this . disable || this . maxItemsReached ) ) { return ; } this . moveToTag ( data . model , constants . NEXT ) ; } break ; default : return ; } event . preventDefault ( ) ; } public async onFormSubmit ( ) { try { await this . onAddingRequested ( false , this . formValue ) ; } catch { return ; } } name param value public setInputValue ( value , emitEvent = true ) { const control = this . getControl ( ) ; control . setValue ( value , { emitEvent } ) ; } name private getControl ( ) { return this . inputForm . value as FormControl ; } name param applyFocus param displayAutocomplete public focus ( applyFocus = false , displayAutocomplete = false ) { if ( this . dragProvider . getState ( 'dragging' ) ) { return ; } this . selectItem ( undefined , false ) ; if ( applyFocus ) { this . inputForm . focus ( ) ; this . onFocus . emit ( this . formValue ) ; } } name public blur ( ) { this . onTouched ( ) ; this . onBlur . emit ( this . formValue ) ; } name public hasErrors ( ) { return ! ! this . inputForm && this . inputForm . hasErrors ( ) ; } name public isInputFocused ( ) { return ! ! this . inputForm && this . inputForm . isInputFocused ( ) ; } name public hasCustomTemplate ( ) { const template = this . templates ? this . templates . first : undefined ; const menuTemplate = this . dropdown && this . dropdown . templates ? this . dropdown . templates . first : undefined ; return Boolean ( template && template !== menuTemplate ) ; } name public get maxItemsReached ( ) : boolean { return this . maxItems !== undefined && this . items . length >= this . maxItems ; } name public get formValue ( ) : string { const form = this . inputForm . value ; return form ? form . value : '' ; } name param event param index public onDragStarted ( event , tag , index ) { event . stopPropagation ( ) ; const item = { zone : this . dragZone , tag , index } as DraggedTag ; this . dragProvider . setSender ( this ) ; this . dragProvider . setDraggedItem ( event , item ) ; this . dragProvider . setState ( { dragging : true , index } ) ; } name param event public onDragOver ( event , index ? ) { this . dragProvider . setState ( { dropping : true } ) ; this . dragProvider . setReceiver ( this ) ; event . preventDefault ( ) ; } name param event param index public onTagDropped ( event , index ? ) { const item = this . dragProvider . getDraggedItem ( event ) ; if ( ! item || item . zone !== this . dragZone ) { return ; } this . dragProvider . onTagDropped ( item . tag , item . index , index ) ; event . preventDefault ( ) ; event . stopPropagation ( ) ; } name public isDropping ( ) { const isReceiver = this . dragProvider . receiver === this ; const isDropping = this . dragProvider . getState ( 'dropping' ) ; return Boolean ( isReceiver && isDropping ) ; } name param changedElement { TagModel } param index { number } public onTagBlurred ( changedElement , index ) { this . items [ index ] = changedElement ; this . blur ( ) ; } name param items public trackBy ( index , item ) { return item [ this . identifyBy ] ; } name param tag public updateEditedTag ( tag ) { this . onTagEdited . emit ( tag ) ; } param tag param isFromAutocomplete public isTagValid = ( tag , fromAutocomplete = false ) : boolean => { const selectedItem = this . dropdown ? this . dropdown . selectedItem : undefined ; const value = this . getItemDisplay ( tag ) . trim ( ) ; if ( selectedItem && ! fromAutocomplete || ! value ) { return false ; } const dupe = this . findDupe ( tag , fromAutocomplete ) ; if ( ! this . allowDupes && dupe && this . blinkIfDupe ) { const model = this . tags . find ( item => { return this . getItemValue ( item . model ) === this . getItemValue ( dupe ) ; } ) ; if ( model ) { model . blink ( ) ; } } const isFromAutocomplete = fromAutocomplete && this . onlyFromAutocomplete ; const assertions = [ ! dupe || this . allowDupes , ! this . maxItemsReached , ( ( isFromAutocomplete ) || ! this . onlyFromAutocomplete ) ] ; return assertions . filter ( Boolean ) . length === assertions . length ; } ; name param item param direction private moveToTag ( item , direction ) { const isLast = this . isLastTag ( item ) ; const isFirst = this . isFirstTag ( item ) ; const stopSwitch = ( direction === constants . NEXT && isLast ) || ( direction === constants . PREV && isFirst ) ; if ( stopSwitch ) { this . focus ( true ) ; return ; } const offset = direction === constants . NEXT ? 1 : - 1 ; const index = this . getTagIndex ( item ) + offset ; const tag = this . getTagAtIndex ( index ) ; return tag . select . call ( tag ) ; } name param item { TagModel } private isFirstTag ( item ) { return this . tags . first . model === item ; } name param item { TagModel } private isLastTag ( item ) { return this . tags . last . model === item ; } name param item private getTagIndex ( item ) { const tags = this . tags . toArray ( ) ; return tags . findIndex ( tag => tag . model === item ) ; } name param index private getTagAtIndex ( index ) { const tags = this . tags . toArray ( ) ; return tags [ index ] ; } name desc param tag { TagModel } param index { number } public removeItem ( tag , index ) { this . items = this . getItemsWithout ( index ) ; if ( this . selectedTag === tag ) { this . selectItem ( undefined , false ) ; } this . focus ( true , false ) ; this . onRemove . emit ( tag ) ; } name desc param fromAutocomplete { boolean } param item { TagModel } param index param giveupFocus private addItem ( fromAutocomplete = false , item , index ? , giveupFocus ? ) { const display = this . getItemDisplay ( item ) ; const tag = this . createTag ( item ) ; if ( fromAutocomplete ) { this . setInputValue ( this . getItemValue ( item , true ) ) ; } return new Promise ( ( resolve , reject ) => { name const reset = ( ) : void => { this . setInputValue ( '' ) ; if ( giveupFocus ) { this . focus ( false , false ) ; } else { this . focus ( true , false ) ; } resolve ( display ) ; } ; const appendItem = ( ) : void => { this . appendTag ( tag , index ) ; this . onAdd . emit ( tag ) ; if ( ! this . dropdown ) { return ; } this . dropdown . hide ( ) ; if ( this . dropdown . showDropdownIfEmpty ) { this . dropdown . show ( ) ; } } ; const status = this . inputForm . form . status ; const isTagValid = this . isTagValid ( tag , fromAutocomplete ) ; const onValidationError = ( ) => { this . onValidationError . emit ( tag ) ; return reject ( ) ; } ; if ( status === 'VALID' && isTagValid ) { appendItem ( ) ; return reset ( ) ; } if ( status === 'INVALID' || ! isTagValid ) { reset ( ) ; return onValidationError ( ) ; } if ( status === 'PENDING' ) { const statusUpdate$ = this . inputForm . form . statusChanges ; return statusUpdate$ . pipe ( filter ( statusUpdate => statusUpdate !== 'PENDING' ) , first ( ) ) . subscribe ( ( statusUpdate ) => { if ( statusUpdate === 'VALID' && isTagValid ) { appendItem ( ) ; return reset ( ) ; } else { reset ( ) ; return onValidationError ( ) ; } } ) ; } } ) ; } name private setupSeparatorKeysListener ( ) { const useSeparatorKeys = this . separatorKeyCodes . length > 0 || this . separatorKeys . length > 0 ; const listener = ( $event ) => { const hasKeyCode = this . separatorKeyCodes . indexOf ( $event . keyCode ) >= 0 ; const hasKey = this . separatorKeys . indexOf ( $event . key ) >= 0 ; const isIMEProcessing = $event . keyCode === 229 ; if ( hasKeyCode || ( hasKey && ! isIMEProcessing ) ) { $event . preventDefault ( ) ; this . onAddingRequested ( false , this . formValue ) . catch ( ( ) => { } ) ; } } ; listen . call ( this , constants . KEYDOWN , listener , useSeparatorKeys ) ; } name private setUpKeypressListeners ( ) { const listener = ( $event ) => { const isCorrectKey = $event . keyCode === 37 || $event . keyCode === 8 ; if ( isCorrectKey && ! this . formValue && this . items . length ) { this . tags . last . select . call ( this . tags . last ) ; } } ; listen . call ( this , constants . KEYDOWN , listener ) ; } name private setUpInputKeydownListeners ( ) { this . inputForm . onKeydown . subscribe ( event => { if ( event . key === 'Backspace' && this . formValue . trim ( ) === '' ) { event . preventDefault ( ) ; } } ) ; } name private setUpOnPasteListener ( ) { const input = this . inputForm . input . nativeElement ; this . renderer . listen ( input , 'paste' , ( event ) => { this . onPasteCallback ( event ) ; event . preventDefault ( ) ; return true ; } ) ; } name private setUpTextChangeSubscriber ( ) { this . inputForm . form . valueChanges . pipe ( debounceTime ( this . onTextChangeDebounce ) ) . subscribe ( ( value ) => { this . onTextChange . emit ( value . item ) ; } ) ; } name private setUpOnBlurSubscriber ( ) { const filterFn = ( ) : boolean => { const isVisible = this . dropdown && this . dropdown . isVisible ; return ! isVisible && ! ! this . formValue ; } ; this . inputForm . onBlur . pipe ( debounceTime ( 100 ) , filter ( filterFn ) ) . subscribe ( ( ) => { const reset = ( ) => this . setInputValue ( '' ) ; if ( this . addOnBlur ) { return this . onAddingRequested ( false , this . formValue , undefined , true ) . then ( reset ) . catch ( reset ) ; } reset ( ) ; } ) ; } name param tag param isFromAutocomplete private findDupe ( tag , isFromAutocomplete ) { const identifyBy = isFromAutocomplete ? this . dropdown . identifyBy : this . identifyBy ; const id = tag [ identifyBy ] ; return this . items . find ( item => this . getItemValue ( item ) === id ) ; } name param data private onPasteCallback = async ( data ) => { interface IEWindow extends Window { clipboardData : DataTransfer ; } const getText = ( ) : string => { const isIE = Boolean ( ( window as IEWindow & typeof globalThis ) . clipboardData ) ; const clipboardData = isIE ? ( ( window as IEWindow & typeof globalThis ) . clipboardData ) : data . clipboardData ; const type = isIE ? 'Text' : 'text/plain' ; return clipboardData === null ? '' : clipboardData . getData ( type ) || '' ; } ; const text = getText ( ) ; const requests = text . split ( this . pasteSplitPattern ) . map ( item => { const tag = this . createTag ( item ) ; this . setInputValue ( tag [ this . displayBy ] ) ; return this . onAddingRequested ( false , tag ) ; } ) ; const resetInput = ( ) => setTimeout ( ( ) => this . setInputValue ( '' ) , 50 ) ; Promise . all ( requests ) . then ( ( ) => { this . onPaste . emit ( text ) ; resetInput ( ) ; } ) . catch ( resetInput ) ; } ; name private setAnimationMetadata ( ) { this . animationMetadata = { value : 'in' , params : { ... this . animationDuration } } ; } }