import { AfterViewChecked , AfterViewInit , Component , ElementRef , EventEmitter , Inject , Input , OnChanges , OnDestroy , OnInit , Optional , Output , QueryList , SimpleChanges , ViewChildren } from '@angular/core' ; import { CodeInputComponentConfig , CodeInputComponentConfigToken , defaultComponentConfig } from './code-input.component.config' ; import { Subscription } from 'rxjs' ; enum InputState { ready = 0 , reset = 1 } @ Component ( { selector : 'code-input' , templateUrl : 'code-input.component.html' , styleUrls : [ './code-input.component.scss' ] } ) export class CodeInputComponent implements AfterViewInit , OnInit , OnChanges , OnDestroy , AfterViewChecked , CodeInputComponentConfig { @ ViewChildren ( 'input' ) inputsList ! : QueryList < ElementRef > ; @ Input ( ) codeLength ! : number ; @ Input ( ) inputType ! : string ; @ Input ( ) inputMode ! : string ; @ Input ( ) initialFocusField ? : number ; deprecated @ Input ( ) isNonDigitsCode = false ; @ Input ( ) isCharsCode ! : boolean ; @ Input ( ) isCodeHidden ! : boolean ; @ Input ( ) isPrevFocusableAfterClearing ! : boolean ; @ Input ( ) isFocusingOnLastByClickIfFilled ! : boolean ; @ Input ( ) code ? : string | number ; @ Input ( ) disabled ! : boolean ; @ Input ( ) autocapitalize ? : string ; @ Output ( ) readonly codeChanged = new EventEmitter < string > ( ) ; @ Output ( ) readonly codeCompleted = new EventEmitter < string > ( ) ; public placeholders : number [ ] = [ ] ; private inputs : HTMLInputElement [ ] = [ ] ; private inputsStates : InputState [ ] = [ ] ; private inputsListSubscription ! : Subscription ; private _codeLength ! : number ; private state = { isFocusingAfterAppearingCompleted : false , isInitialFocusFieldEnabled : false } ; constructor ( @ Optional ( ) @ Inject ( CodeInputComponentConfigToken ) config ? ) { Object . assign ( this , defaultComponentConfig ) ; if ( ! config ) { return ; } for ( const prop in config ) { if ( ! config . hasOwnProperty ( prop ) ) { continue ; } if ( ! defaultComponentConfig . hasOwnProperty ( prop ) ) { continue ; } this [ prop ] = config [ prop ] ; } } /**
     * Life cycle
     */ ngOnInit ( ) { this . state . isInitialFocusFieldEnabled = ! this . isEmpty ( this . initialFocusField ) ; this . onCodeLengthChanges ( ) ; } ngAfterViewInit ( ) { this . inputsListSubscription = this . inputsList . changes . subscribe ( this . onInputsListChanges . bind ( this ) ) ; this . onInputsListChanges ( this . inputsList ) ; } ngAfterViewChecked ( ) { this . focusOnInputAfterAppearing ( ) ; } ngOnChanges ( changes ) { if ( changes . code ) { this . onInputCodeChanges ( ) ; } if ( changes . codeLength ) { this . onCodeLengthChanges ( ) ; } } ngOnDestroy ( ) { if ( this . inputsListSubscription ) { this . inputsListSubscription . unsubscribe ( ) ; } } /**
     * Methods
     */ reset ( isChangesEmitting = false ) { this . onInputCodeChanges ( ) ; if ( this . state . isInitialFocusFieldEnabled ) { this . focusOnField ( this . initialFocusField ! ) ; } if ( isChangesEmitting ) { this . emitChanges ( ) ; } } focusOnField ( index ) { if ( index >= this . _codeLength ) { throw new Error ( 'The index of the focusing input box should be less than the codeLength.' ) ; } this . inputs [ index ] . focus ( ) ; } onClick ( e ) { if ( ! this . isFocusingOnLastByClickIfFilled ) { return ; } const target = e . target ; const last = this . inputs [ this . _codeLength - 1 ] ; if ( target === last ) { return ; } const isFilled = this . getCurrentFilledCode ( ) . length >= this . _codeLength ; if ( ! isFilled ) { return ; } setTimeout ( ( ) => last . focus ( ) ) ; } onInput ( e , i ) { const target = e . target ; const value = e . data || target . value ; if ( this . isEmpty ( value ) ) { return ; } if ( ! this . canInputValue ( value ) ) { e . preventDefault ( ) ; e . stopPropagation ( ) ; this . setInputValue ( target , null ) ; this . setStateForInput ( target , InputState . reset ) ; return ; } const values = value . toString ( ) . trim ( ) . split ( '' ) ; for ( let j = 0 ; j < values . length ; j ++ ) { const index = j + i ; if ( index > this . _codeLength - 1 ) { break ; } this . setInputValue ( this . inputs [ index ] , values [ j ] ) ; } this . emitChanges ( ) ; const next = i + values . length ; if ( next > this . _codeLength - 1 ) { target . blur ( ) ; return ; } this . inputs [ next ] . focus ( ) ; } onPaste ( e , i ) { e . preventDefault ( ) ; e . stopPropagation ( ) ; const data = e . clipboardData ? e . clipboardData . getData ( 'text' ) . trim ( ) : undefined ; if ( this . isEmpty ( data ) ) { return ; } const values = data ! . split ( '' ) ; let valIndex = 0 ; for ( let j = i ; j < this . inputs . length ; j ++ ) { if ( valIndex === values . length ) { break ; } const input = this . inputs [ j ] ; const val = values [ valIndex ] ; if ( ! this . canInputValue ( val ) ) { this . setInputValue ( input , null ) ; this . setStateForInput ( input , InputState . reset ) ; return ; } this . setInputValue ( input , val . toString ( ) ) ; valIndex ++ ; } this . inputs [ i ] . blur ( ) ; this . emitChanges ( ) ; } async onKeydown ( e , i ) { const target = e . target ; const isTargetEmpty = this . isEmpty ( target . value ) ; const prev = i - 1 ; const isBackspaceKey = await this . isBackspaceKey ( e ) ; const isDeleteKey = this . isDeleteKey ( e ) ; if ( ! isBackspaceKey && ! isDeleteKey ) { return ; } e . preventDefault ( ) ; this . setInputValue ( target , null ) ; if ( ! isTargetEmpty ) { this . emitChanges ( ) ; } if ( prev < 0 || isDeleteKey ) { return ; } if ( isTargetEmpty || this . isPrevFocusableAfterClearing ) { this . inputs [ prev ] . focus ( ) ; } } private onInputCodeChanges ( ) { if ( ! this . inputs . length ) { return ; } if ( this . isEmpty ( this . code ) ) { this . inputs . forEach ( ( input ) => { this . setInputValue ( input , null ) ; } ) ; return ; } const chars = this . code ! . toString ( ) . trim ( ) . split ( '' ) ; let isAllCharsAreAllowed = true ; for ( const char of chars ) { if ( ! this . canInputValue ( char ) ) { isAllCharsAreAllowed = false ; break ; } } this . inputs . forEach ( ( input , index ) => { const value = isAllCharsAreAllowed ? chars [ index ] : null ; this . setInputValue ( input , value ) ; } ) ; } private onCodeLengthChanges ( ) { if ( ! this . codeLength ) { return ; } this . _codeLength = this . codeLength ; if ( this . _codeLength > this . placeholders . length ) { const numbers = Array ( this . _codeLength - this . placeholders . length ) . fill ( 1 ) ; this . placeholders . splice ( this . placeholders . length - 1 , 0 , ... numbers ) ; } else if ( this . _codeLength < this . placeholders . length ) { this . placeholders . splice ( this . _codeLength ) ; } } private onInputsListChanges ( list ) { if ( list . length > this . inputs . length ) { const inputsToAdd = list . filter ( ( item , index ) => index > this . inputs . length - 1 ) ; this . inputs . splice ( this . inputs . length , 0 , ... inputsToAdd . map ( item => item . nativeElement ) ) ; const states = Array ( inputsToAdd . length ) . fill ( InputState . ready ) ; this . inputsStates . splice ( this . inputsStates . length , 0 , ... states ) ; } else if ( list . length < this . inputs . length ) { this . inputs . splice ( list . length ) ; this . inputsStates . splice ( list . length ) ; } this . onInputCodeChanges ( ) ; } private focusOnInputAfterAppearing ( ) { if ( ! this . state . isInitialFocusFieldEnabled ) { return ; } if ( this . state . isFocusingAfterAppearingCompleted ) { return ; } this . focusOnField ( this . initialFocusField ! ) ; this . state . isFocusingAfterAppearingCompleted = document . activeElement === this . inputs [ this . initialFocusField ! ] ; } private emitChanges ( ) { setTimeout ( ( ) => this . emitCode ( ) , 50 ) ; } private emitCode ( ) { const code = this . getCurrentFilledCode ( ) ; this . codeChanged . emit ( code ) ; if ( code . length >= this . _codeLength ) { this . codeCompleted . emit ( code ) ; } } private getCurrentFilledCode ( ) { let code = '' ; for ( const input of this . inputs ) { if ( ! this . isEmpty ( input . value ) ) { code += input . value ; } } return code ; } private isBackspaceKey ( e ) { const isBackspace = ( e . key && e . key . toLowerCase ( ) === 'backspace' ) || ( e . keyCode && e . keyCode === 8 ) ; if ( isBackspace ) { return Promise . resolve ( true ) ; } if ( ! e . keyCode || e . keyCode !== 229 ) { return Promise . resolve ( false ) ; } return new Promise < boolean > ( ( resolve ) => { setTimeout ( ( ) => { const input = e . target ; const isReset = this . getStateForInput ( input ) === InputState . reset ; if ( isReset ) { this . setStateForInput ( input , InputState . ready ) ; } resolve ( input . selectionStart === 0 && ! isReset ) ; } ) ; } ) ; } private isDeleteKey ( e ) { return ( e . key && e . key . toLowerCase ( ) === 'delete' ) || ( e . keyCode && e . keyCode === 46 ) ; } private setInputValue ( input , value ) { const isEmpty = this . isEmpty ( value ) ; const valueClassCSS = 'has-value' ; const emptyClassCSS = 'empty' ; if ( isEmpty ) { input . value = '' ; input . classList . remove ( valueClassCSS ) ; input . parentElement ! . classList . add ( emptyClassCSS ) ; } else { input . value = value ; input . classList . add ( valueClassCSS ) ; input . parentElement ! . classList . remove ( emptyClassCSS ) ; } } private canInputValue ( value ) { if ( this . isEmpty ( value ) ) { return false ; } const isDigitsValue = /^[0-9]+$/ . test ( value . toString ( ) ) ; return isDigitsValue || ( this . isCharsCode || this . isNonDigitsCode ) ; } private setStateForInput ( input , state ) { const index = this . inputs . indexOf ( input ) ; if ( index < 0 ) { return ; } this . inputsStates [ index ] = state ; } private getStateForInput ( input ) { const index = this . inputs . indexOf ( input ) ; return this . inputsStates [ index ] ; } private isEmpty ( value ) { return value === null || value === undefined || ! value . toString ( ) . length ; } }