import { ControlValueAccessor , FormControl , NG_VALIDATORS , NG_VALUE_ACCESSOR , ValidationErrors , Validator , } from '@angular/forms' ; import { Directive , forwardRef , HostListener , Inject , Input , OnChanges , SimpleChanges , } from '@angular/core' ; import { DOCUMENT } from '@angular/common' ; import { CustomKeyboardEvent } from './custom-keyboard-event' ; import { config , IConfig , timeMasks , withoutValidation } from './config' ; import { MaskService } from './mask.service' ; @ Directive ( { selector : 'input[mask], textarea[mask]' , providers : [ { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => MaskDirective ) , multi : true , } , { provide : NG_VALIDATORS , useExisting : forwardRef ( ( ) => MaskDirective ) , multi : true , } , MaskService , ] , } ) export class MaskDirective implements ControlValueAccessor , OnChanges , Validator { @ Input ( 'mask' ) public maskExpression : string = '' ; @ Input ( ) public specialCharacters : IConfig [ 'specialCharacters' ] = [ ] ; @ Input ( ) public patterns : IConfig [ 'patterns' ] = { } ; @ Input ( ) public prefix : IConfig [ 'prefix' ] = '' ; @ Input ( ) public suffix : IConfig [ 'suffix' ] = '' ; @ Input ( ) public thousandSeparator : IConfig [ 'thousandSeparator' ] = ' ' ; @ Input ( ) public decimalMarker : IConfig [ 'decimalMarker' ] = '.' ; @ Input ( ) public dropSpecialCharacters : IConfig [ 'dropSpecialCharacters' ] | null = null ; @ Input ( ) public hiddenInput : IConfig [ 'hiddenInput' ] | null = null ; @ Input ( ) public showMaskTyped : IConfig [ 'showMaskTyped' ] | null = null ; @ Input ( ) public placeHolderCharacter : IConfig [ 'placeHolderCharacter' ] | null = null ; @ Input ( ) public shownMaskExpression : IConfig [ 'shownMaskExpression' ] | null = null ; @ Input ( ) public showTemplate : IConfig [ 'showTemplate' ] | null = null ; @ Input ( ) public clearIfNotMatch : IConfig [ 'clearIfNotMatch' ] | null = null ; @ Input ( ) public validation : IConfig [ 'validation' ] | null = null ; @ Input ( ) public separatorLimit : IConfig [ 'separatorLimit' ] | null = null ; @ Input ( ) public allowNegativeNumbers : IConfig [ 'allowNegativeNumbers' ] | null = null ; @ Input ( ) public leadZeroDateTime : IConfig [ 'leadZeroDateTime' ] | null = null ; private _maskValue : string = '' ; private _inputValue ! : string ; private _position : number | null = null ; private _start ! : number ; private _end ! : number ; private _code ! : string ; private _maskExpressionArray : string [ ] = [ ] ; private _justPasted : boolean = false ; public constructor ( @ Inject ( DOCUMENT ) private document , public _maskService , @ Inject ( config ) protected _config ) { } public onChange = ( _ ) => { } ; public onTouch = ( ) => { } ; public ngOnChanges ( changes ) { const { maskExpression , specialCharacters , patterns , prefix , suffix , thousandSeparator , decimalMarker , dropSpecialCharacters , hiddenInput , showMaskTyped , placeHolderCharacter , shownMaskExpression , showTemplate , clearIfNotMatch , validation , separatorLimit , allowNegativeNumbers , leadZeroDateTime , } = changes ; if ( maskExpression ) { if ( maskExpression . currentValue !== maskExpression . previousValue && ! maskExpression . firstChange ) { this . _maskService . maskChanged = true ; } if ( maskExpression . currentValue && maskExpression . currentValue . split ( '||' ) . length > 1 ) { this . _maskExpressionArray = maskExpression . currentValue . split ( '||' ) . sort ( ( a , b ) => { return a . length - b . length ; } ) ; this . _maskValue = this . _maskExpressionArray [ 0 ] ! ; this . maskExpression = this . _maskExpressionArray [ 0 ] ! ; this . _maskService . maskExpression = this . _maskExpressionArray [ 0 ] ! ; } else { this . _maskExpressionArray = [ ] ; this . _maskValue = maskExpression . currentValue || '' ; this . _maskService . maskExpression = this . _maskValue ; } } if ( specialCharacters ) { if ( ! specialCharacters . currentValue || ! Array . isArray ( specialCharacters . currentValue ) ) { return ; } else { this . _maskService . maskSpecialCharacters = specialCharacters . currentValue || [ ] ; } } if ( patterns && patterns . currentValue ) { this . _maskService . maskAvailablePatterns = patterns . currentValue ; } if ( prefix ) { this . _maskService . prefix = prefix . currentValue ; } if ( suffix ) { this . _maskService . suffix = suffix . currentValue ; } if ( thousandSeparator ) { this . _maskService . thousandSeparator = thousandSeparator . currentValue ; } if ( decimalMarker ) { this . _maskService . decimalMarker = decimalMarker . currentValue ; } if ( dropSpecialCharacters ) { this . _maskService . dropSpecialCharacters = dropSpecialCharacters . currentValue ; } if ( hiddenInput ) { this . _maskService . hiddenInput = hiddenInput . currentValue ; } if ( showMaskTyped ) { this . _maskService . showMaskTyped = showMaskTyped . currentValue ; } if ( placeHolderCharacter ) { this . _maskService . placeHolderCharacter = placeHolderCharacter . currentValue ; } if ( shownMaskExpression ) { this . _maskService . shownMaskExpression = shownMaskExpression . currentValue ; } if ( showTemplate ) { this . _maskService . showTemplate = showTemplate . currentValue ; } if ( clearIfNotMatch ) { this . _maskService . clearIfNotMatch = clearIfNotMatch . currentValue ; } if ( validation ) { this . _maskService . validation = validation . currentValue ; } if ( separatorLimit ) { this . _maskService . separatorLimit = separatorLimit . currentValue ; } if ( allowNegativeNumbers ) { this . _maskService . allowNegativeNumbers = allowNegativeNumbers . currentValue ; if ( this . _maskService . allowNegativeNumbers ) { this . _maskService . maskSpecialCharacters = this . _maskService . maskSpecialCharacters . filter ( ( c ) => c !== '-' ) ; } } if ( leadZeroDateTime ) { this . _maskService . leadZeroDateTime = leadZeroDateTime . currentValue ; } this . _applyMask ( ) ; } public validate ( { value } ) { if ( ! this . _maskService . validation || ! this . _maskValue ) { return null ; } if ( this . _maskService . ipError ) { return this . _createValidationError ( value ) ; } if ( this . _maskService . cpfCnpjError ) { return this . _createValidationError ( value ) ; } if ( this . _maskValue . startsWith ( 'separator' ) ) { return null ; } if ( withoutValidation . includes ( this . _maskValue ) ) { return null ; } if ( this . _maskService . clearIfNotMatch ) { return null ; } if ( timeMasks . includes ( this . _maskValue ) ) { return this . _validateTime ( value ) ; } if ( value && value . toString ( ) . length >= 1 ) { let counterOfOpt = 0 ; for ( const key in this . _maskService . maskAvailablePatterns ) { if ( this . _maskService . maskAvailablePatterns [ key ] ! . optional ) { if ( this . _maskValue . indexOf ( key ) !== this . _maskValue . lastIndexOf ( key ) ) { const opt = this . _maskValue . split ( '' ) . filter ( ( i ) => i === key ) . join ( '' ) ; counterOfOpt += opt . length ; } else if ( this . _maskValue . indexOf ( key ) !== - 1 ) { counterOfOpt ++ ; } if ( this . _maskValue . indexOf ( key ) !== - 1 && value . toString ( ) . length >= this . _maskValue . indexOf ( key ) ) { return null ; } if ( counterOfOpt === this . _maskValue . length ) { return null ; } } } if ( this . _maskValue . indexOf ( '{' ) === 1 && value . toString ( ) . length === this . _maskValue . length + Number ( this . _maskValue . split ( '{' ) [ 1 ] ! . split ( '}' ) [ 0 ] ) - 4 ) { return null ; } if ( this . _maskValue . indexOf ( '*' ) === 1 || this . _maskValue . indexOf ( '?' ) === 1 ) { return null ; } else if ( ( this . _maskValue . indexOf ( '*' ) > 1 && value . toString ( ) . length < this . _maskValue . indexOf ( '*' ) ) || ( this . _maskValue . indexOf ( '?' ) > 1 && value . toString ( ) . length < this . _maskValue . indexOf ( '?' ) ) || this . _maskValue . indexOf ( '{' ) === 1 ) { return this . _createValidationError ( value ) ; } if ( this . _maskValue . indexOf ( '*' ) === - 1 || this . _maskValue . indexOf ( '?' ) === - 1 ) { const length = this . _maskService . dropSpecialCharacters ? this . _maskValue . length - this . _maskService . checkSpecialCharAmount ( this . _maskValue ) - counterOfOpt : this . _maskValue . length - counterOfOpt ; if ( value . toString ( ) . length < length ) { return this . _createValidationError ( value ) ; } } } return null ; } @ HostListener ( 'paste' ) public onPaste ( ) { this . _justPasted = true ; } @ HostListener ( 'ngModelChange' , [ '$event' ] ) public onModelChange ( value ) { if ( ! value && this . _maskService . actualValue ) { this . _maskService . actualValue = this . _maskService . getActualValue ( '' ) ; } } @ HostListener ( 'input' , [ '$event' ] ) public onInput ( e ) { const el = e . target as HTMLInputElement ; this . _inputValue = el . value ; this . _setMask ( ) ; if ( ! this . _maskValue ) { this . onChange ( el . value ) ; return ; } const position = el . selectionStart === 1 ? ( el . selectionStart as number ) + this . _maskService . prefix . length : ( el . selectionStart as number ) ; let caretShift = 0 ; let backspaceShift = false ; this . _maskService . applyValueChanges ( position , this . _justPasted , this . _code === 'Backspace' || this . _code === 'Delete' , ( shift , _backspaceShift ) => { this . _justPasted = false ; caretShift = shift ; backspaceShift = _backspaceShift ; } ) ; if ( this . document . activeElement !== el ) { return ; } this . _position = this . _position === 1 && this . _inputValue . length === 1 ? null : this . _position ; let positionToApply = this . _position ? this . _inputValue . length + position + caretShift : position + ( this . _code === 'Backspace' && ! backspaceShift ? 0 : caretShift ) ; if ( positionToApply > this . _getActualInputLength ( ) ) { positionToApply = this . _getActualInputLength ( ) ; } if ( positionToApply < 0 ) { positionToApply = 0 ; } el . setSelectionRange ( positionToApply , positionToApply ) ; this . _position = null ; } @ HostListener ( 'blur' ) public onBlur ( ) { if ( this . _maskValue ) { this . _maskService . clearIfNotMatchFn ( ) ; } this . onTouch ( ) ; } @ HostListener ( 'click' , [ '$event' ] ) public onClick ( e ) { if ( ! this . _maskValue ) { return ; } const el = e . target as HTMLInputElement ; const posStart = 0 ; const posEnd = 0 ; if ( el !== null && el . selectionStart !== null && el . selectionStart === el . selectionEnd && el . selectionStart > this . _maskService . prefix . length && ( e as any ) . keyCode !== 38 ) { if ( this . _maskService . showMaskTyped ) { this . _maskService . maskIsShown = this . _maskService . showMaskInInput ( ) ; if ( el . setSelectionRange && this . _maskService . prefix + this . _maskService . maskIsShown === el . value ) { el . focus ( ) ; el . setSelectionRange ( posStart , posEnd ) ; } else { if ( el . selectionStart > this . _maskService . actualValue . length ) { el . setSelectionRange ( this . _maskService . actualValue . length , this . _maskService . actualValue . length ) ; } } } } const nextValue = ! el . value || el . value === this . _maskService . prefix ? this . _maskService . prefix + this . _maskService . maskIsShown : el . value ; /** Fix of cursor position jumping to end in most browsers no matter where cursor is inserted onFocus */ if ( el . value !== nextValue ) { el . value = nextValue ; } /** fix of cursor position with prefix when mouse click occur */ if ( ( ( el . selectionStart as number ) || ( el . selectionEnd as number ) ) <= this . _maskService . prefix . length ) { el . selectionStart = this . _maskService . prefix . length ; return ; } /** select only inserted text */ if ( ( el . selectionEnd as number ) > this . _getActualInputLength ( ) ) { el . selectionEnd = this . _getActualInputLength ( ) ; } } @ HostListener ( 'keydown' , [ '$event' ] ) public onKeyDown ( e ) { if ( ! this . _maskValue ) { return ; } this . _code = e . code ? e . code : e . key ; const el = e . target as HTMLInputElement ; this . _inputValue = el . value ; this . _setMask ( ) ; if ( e . keyCode === 38 ) { e . preventDefault ( ) ; } if ( e . keyCode === 37 || e . keyCode === 8 || e . keyCode === 46 ) { if ( e . keyCode === 8 && el . value . length === 0 ) { el . selectionStart = el . selectionEnd ; } if ( e . keyCode === 8 && ( el . selectionStart as number ) !== 0 ) { this . specialCharacters = this . specialCharacters ?. length ? this . specialCharacters : this . _config . specialCharacters ; if ( this . prefix . length > 1 && ( el . selectionStart as number ) <= this . prefix . length ) { el . setSelectionRange ( this . prefix . length , el . selectionEnd ) ; } else { if ( this . _inputValue . length !== ( el . selectionStart as number ) && ( el . selectionStart as number ) !== 1 ) { while ( this . specialCharacters . includes ( this . _inputValue [ ( el . selectionStart as number ) - 1 ] ! . toString ( ) ) && ( ( this . prefix . length >= 1 && ( el . selectionStart as number ) > this . prefix . length ) || this . prefix . length === 0 ) ) { el . setSelectionRange ( ( el . selectionStart as number ) - 1 , el . selectionEnd ) ; } } } } this . checkSelectionOnDeletion ( el ) ; if ( this . _maskService . prefix . length && ( el . selectionStart as number ) <= this . _maskService . prefix . length && ( el . selectionEnd as number ) <= this . _maskService . prefix . length ) { e . preventDefault ( ) ; } const cursorStart = el . selectionStart ; if ( e . keyCode === 8 && ! el . readOnly && cursorStart === 0 && el . selectionEnd === el . value . length && el . value . length !== 0 ) { this . _position = this . _maskService . prefix ? this . _maskService . prefix . length : 0 ; this . _maskService . applyMask ( this . _maskService . prefix , this . _maskService . maskExpression , this . _position ) ; } } if ( ! ! this . suffix && this . suffix . length > 1 && this . _inputValue . length - this . suffix . length < ( el . selectionStart as number ) ) { el . setSelectionRange ( this . _inputValue . length - this . suffix . length , this . _inputValue . length ) ; } else if ( ( e . keyCode === 65 && e . ctrlKey ) || ( e . keyCode === 65 && e . metaKey ) ) { el . setSelectionRange ( 0 , this . _getActualInputLength ( ) ) ; e . preventDefault ( ) ; } this . _maskService . selStart = el . selectionStart ; this . _maskService . selEnd = el . selectionEnd ; } /** It writes the value in the input */ public async writeValue ( inputValue ) { if ( typeof inputValue === 'object' && inputValue !== null && 'value' in inputValue ) { if ( 'disable' in inputValue ) { this . setDisabledState ( Boolean ( inputValue . disable ) ) ; } inputValue = inputValue . value ; } if ( typeof inputValue === 'number' ) { inputValue = String ( inputValue ) ; if ( ! Array . isArray ( this . decimalMarker ) ) { inputValue = this . decimalMarker !== '.' ? inputValue . replace ( '.' , this . decimalMarker ) : inputValue ; } this . _maskService . isNumberValue = true ; } if ( typeof inputValue !== 'string' ) { inputValue = '' ; } this . _inputValue = inputValue ; this . _setMask ( ) ; if ( ( inputValue && this . _maskService . maskExpression ) || ( this . _maskService . maskExpression && ( this . _maskService . prefix || this . _maskService . showMaskTyped ) ) ) { this . _maskService . writingValue = true ; this . _maskService . formElementProperty = [ 'value' , this . _maskService . applyMask ( inputValue , this . _maskService . maskExpression ) , ] ; this . _maskService . writingValue = false ; } else { this . _maskService . formElementProperty = [ 'value' , inputValue ] ; } this . _inputValue = inputValue ; } public registerOnChange ( fn ) { this . onChange = fn ; this . _maskService . onChange = this . onChange ; } public registerOnTouched ( fn ) { this . onTouch = fn ; } public checkSelectionOnDeletion ( el ) { el . selectionStart = Math . min ( Math . max ( this . prefix . length , el . selectionStart as number ) , this . _inputValue . length - this . suffix . length ) ; el . selectionEnd = Math . min ( Math . max ( this . prefix . length , el . selectionEnd as number ) , this . _inputValue . length - this . suffix . length ) ; } /** It disables the input element */ public setDisabledState ( isDisabled ) { this . _maskService . formElementProperty = [ 'disabled' , isDisabled ] ; } private _repeatPatternSymbols ( maskExp ) { return ( ( maskExp . match ( /{[0-9]+}/ ) && maskExp . split ( '' ) . reduce ( ( accum , currval , index ) : string => { this . _start = currval === '{' ? index : this . _start ; if ( currval !== '}' ) { return this . _maskService . _findSpecialChar ( currval ) ? accum + currval : accum ; } this . _end = index ; const repeatNumber = Number ( maskExp . slice ( this . _start + 1 , this . _end ) ) ; const replaceWith = new Array ( repeatNumber + 1 ) . join ( maskExp [ this . _start - 1 ] ) ; return accum + replaceWith ; } , '' ) ) || maskExp ) ; } private _applyMask ( ) { this . _maskService . maskExpression = this . _repeatPatternSymbols ( this . _maskValue || '' ) ; this . _maskService . formElementProperty = [ 'value' , this . _maskService . applyMask ( this . _inputValue , this . _maskService . maskExpression ) , ] ; } private _validateTime ( value ) { const rowMaskLen = this . _maskValue . split ( '' ) . filter ( ( s ) => s !== ':' ) . length ; if ( ! value ) { return null ; } if ( ( + value [ value . length - 1 ] ! === 0 && value . length < rowMaskLen ) || value . length <= rowMaskLen - 2 ) { return this . _createValidationError ( value ) ; } return null ; } private _getActualInputLength ( ) { return ( this . _maskService . actualValue . length || this . _maskService . actualValue . length + this . _maskService . prefix . length ) ; } private _createValidationError ( actualValue ) { return { mask : { requiredMask : this . _maskValue , actualValue , } , } ; } private _setMask ( ) { if ( this . _maskExpressionArray . length > 0 ) { this . _maskExpressionArray . some ( ( mask ) : boolean | void => { const test = this . _maskService . removeMask ( this . _inputValue ) ?. length <= this . _maskService . removeMask ( mask ) ?. length ; if ( this . _inputValue && test ) { this . _maskValue = mask ; this . maskExpression = mask ; this . _maskService . maskExpression = mask ; return test ; } else { this . _maskValue = this . _maskExpressionArray [ this . _maskExpressionArray . length - 1 ] ! ; this . maskExpression = this . _maskExpressionArray [ this . _maskExpressionArray . length - 1 ] ! ; this . _maskService . maskExpression = this . _maskExpressionArray [ this . _maskExpressionArray . length - 1 ] ! ; } } ) ; } } }