import { ElementRef , Inject , Injectable , Renderer2 } from '@angular/core' ; import { DOCUMENT } from '@angular/common' ; import { config , IConfig } from './config' ; import { MaskApplierService } from './mask-applier.service' ; @ Injectable ( ) export class MaskService extends MaskApplierService { public override maskExpression : string = '' ; public isNumberValue : boolean = false ; public override placeHolderCharacter : string = '_' ; public maskIsShown : string = '' ; public selStart : number | null = null ; public selEnd : number | null = null ; /**
     * Whether we are currently in writeValue function, in this case when applying the mask we don't want to trigger onChange function,
     * since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
     */ public writingValue : boolean = false ; public maskChanged : boolean = false ; public onChange = ( _ ) => { } ; public constructor ( @ Inject ( DOCUMENT ) private document , @ Inject ( config ) protected override _config , private _elementRef , private _renderer ) { super ( _config ) ; } public override applyMask ( inputValue , maskExpression , position = 0 , justPasted = false , backspaced = false , cb = ( ) => { } ) { if ( ! maskExpression ) { return inputValue ; } this . maskIsShown = this . showMaskTyped ? this . showMaskInInput ( ) : '' ; if ( this . maskExpression === 'IP' && this . showMaskTyped ) { this . maskIsShown = this . showMaskInInput ( inputValue || '#' ) ; } if ( this . maskExpression === 'CPF_CNPJ' && this . showMaskTyped ) { this . maskIsShown = this . showMaskInInput ( inputValue || '#' ) ; } if ( ! inputValue && this . showMaskTyped ) { this . formControlResult ( this . prefix ) ; return this . prefix + this . maskIsShown ; } const getSymbol = ! ! inputValue && typeof this . selStart === 'number' ? inputValue [ this . selStart ] : '' ; let newInputValue = '' ; if ( this . hiddenInput && ! this . writingValue ) { let actualResult = this . actualValue . split ( '' ) ; inputValue !== '' && actualResult . length ? typeof this . selStart === 'number' && typeof this . selEnd === 'number' ? inputValue . length > actualResult . length ? actualResult . splice ( this . selStart , 0 , getSymbol ! ) : inputValue . length < actualResult . length ? actualResult . length - inputValue . length === 1 ? actualResult . splice ( this . selStart - 1 , 1 ) : actualResult . splice ( this . selStart , this . selEnd - this . selStart ) : null : null : ( actualResult = [ ] ) ; if ( this . showMaskTyped ) { inputValue = this . removeMask ( inputValue ) ; } newInputValue = this . actualValue . length && actualResult . length <= inputValue . length ? this . shiftTypedSymbols ( actualResult . join ( '' ) ) : inputValue ; } newInputValue = Boolean ( newInputValue ) && newInputValue . length ? newInputValue : inputValue ; const result = super . applyMask ( newInputValue , maskExpression , position , justPasted , backspaced , cb ) ; this . actualValue = this . getActualValue ( result ) ; if ( this . thousandSeparator === '.' && this . decimalMarker === '.' ) { this . decimalMarker = ',' ; } if ( this . maskExpression . startsWith ( 'separator' ) && this . dropSpecialCharacters === true ) { this . maskSpecialCharacters = this . maskSpecialCharacters . filter ( ( item ) => ! this . _compareOrIncludes ( item , this . decimalMarker , this . thousandSeparator ) ) ; } this . formControlResult ( result ) ; if ( ! this . showMaskTyped ) { if ( this . hiddenInput ) { return result && result . length ? this . hideInput ( result , this . maskExpression ) : result ; } return result ; } const resLen = result . length ; const prefNmask = this . prefix + this . maskIsShown ; if ( this . maskExpression . includes ( 'H' ) ) { const countSkipedSymbol = this . _numberSkipedSymbols ( result ) ; return result + prefNmask . slice ( resLen + countSkipedSymbol ) ; } else if ( this . maskExpression === 'IP' || this . maskExpression === 'CPF_CNPJ' ) { return result + prefNmask ; } return result + prefNmask . slice ( resLen ) ; } private _numberSkipedSymbols ( value ) { const regex = /(^|\D)(\d\D)/g ; let match = regex . exec ( value ) ; let countSkipedSymbol = 0 ; while ( match != null ) { countSkipedSymbol += 1 ; match = regex . exec ( value ) ; } return countSkipedSymbol ; } public applyValueChanges ( position = 0 , justPasted , backspaced , cb = ( ) => { } ) { const formElement = this . _elementRef . nativeElement ; formElement . value = this . applyMask ( formElement . value , this . maskExpression , position , justPasted , backspaced , cb ) ; if ( formElement === this . document . activeElement ) { return ; } this . clearIfNotMatchFn ( ) ; } public hideInput ( inputValue , maskExpression ) { return inputValue . split ( '' ) . map ( ( curr , index ) => { if ( this . maskAvailablePatterns && this . maskAvailablePatterns [ maskExpression [ index ] ! ] && this . maskAvailablePatterns [ maskExpression [ index ] ! ] ?. symbol ) { return this . maskAvailablePatterns [ maskExpression [ index ] ! ] ?. symbol ; } return curr ; } ) . join ( '' ) ; } public getActualValue ( res ) { const compare = res . split ( '' ) . filter ( ( symbol , i ) => this . _checkSymbolMask ( symbol , this . maskExpression [ i ] ! ) || ( this . maskSpecialCharacters . includes ( this . maskExpression [ i ] ! ) && symbol === this . maskExpression [ i ] ) ) ; if ( compare . join ( '' ) === res ) { return compare . join ( '' ) ; } return res ; } public shiftTypedSymbols ( inputValue ) { let symbolToReplace = '' ; const newInputValue = ( inputValue && inputValue . split ( '' ) . map ( ( currSymbol , index ) => { if ( this . maskSpecialCharacters . includes ( inputValue [ index + 1 ] ! ) && inputValue [ index + 1 ] !== this . maskExpression [ index + 1 ] ) { symbolToReplace = currSymbol ; return inputValue [ index + 1 ] ; } if ( symbolToReplace . length ) { const replaceSymbol = symbolToReplace ; symbolToReplace = '' ; return replaceSymbol ; } return currSymbol ; } ) ) || [ ] ; return newInputValue . join ( '' ) ; } public showMaskInInput ( inputVal ? ) { if ( this . showMaskTyped && ! ! this . shownMaskExpression ) { if ( this . maskExpression . length !== this . shownMaskExpression . length ) { throw new Error ( 'Mask expression must match mask placeholder length' ) ; } else { return this . shownMaskExpression ; } } else if ( this . showMaskTyped ) { if ( inputVal ) { if ( this . maskExpression === 'IP' ) { return this . _checkForIp ( inputVal ) ; } if ( this . maskExpression === 'CPF_CNPJ' ) { return this . _checkForCpfCnpj ( inputVal ) ; } } return this . maskExpression . replace ( /\w/g , this . placeHolderCharacter ) ; } return '' ; } public clearIfNotMatchFn ( ) { const formElement = this . _elementRef . nativeElement ; if ( this . clearIfNotMatch && this . prefix . length + this . maskExpression . length + this . suffix . length !== formElement . value . replace ( /_/g , '' ) . length ) { this . formElementProperty = [ 'value' , '' ] ; this . applyMask ( formElement . value , this . maskExpression ) ; } } public set formElementProperty ( [ name , value ] ) { Promise . resolve ( ) . then ( ( ) => this . _renderer . setProperty ( this . _elementRef . nativeElement , name , value ) ) ; } public checkSpecialCharAmount ( mask ) { const chars = mask . split ( '' ) . filter ( ( item ) => this . _findSpecialChar ( item ) ) ; return chars . length ; } public removeMask ( inputValue ) { return this . _removeMask ( this . _removeSuffix ( this . _removePrefix ( inputValue ) ) , this . maskSpecialCharacters . concat ( '_' ) . concat ( this . placeHolderCharacter ) ) ; } private _checkForIp ( inputVal ) { if ( inputVal === '#' ) { return `${ this . placeHolderCharacter }.${ this . placeHolderCharacter }.${ this . placeHolderCharacter }.${ this . placeHolderCharacter }` ; } const arr = [ ] ; for ( let i = 0 ; i < inputVal . length ; i ++ ) { if ( inputVal [ i ] ?. match ( '\\d' ) ) { arr . push ( inputVal [ i ] ! ) ; } } if ( arr . length <= 3 ) { return `${ this . placeHolderCharacter }.${ this . placeHolderCharacter }.${ this . placeHolderCharacter }` ; } if ( arr . length > 3 && arr . length <= 6 ) { return `${ this . placeHolderCharacter }.${ this . placeHolderCharacter }` ; } if ( arr . length > 6 && arr . length <= 9 ) { return this . placeHolderCharacter ; } if ( arr . length > 9 && arr . length <= 12 ) { return '' ; } return '' ; } private _checkForCpfCnpj ( inputVal ) { const cpf = `${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `.${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `.${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `-${ this . placeHolderCharacter }${ this . placeHolderCharacter }` ; const cnpj = `${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `.${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `.${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `/${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }${ this . placeHolderCharacter }` + `-${ this . placeHolderCharacter }${ this . placeHolderCharacter }` ; if ( inputVal === '#' ) { return cpf ; } const arr = [ ] ; for ( let i = 0 ; i < inputVal . length ; i ++ ) { if ( inputVal [ i ] ?. match ( '\\d' ) ) { arr . push ( inputVal [ i ] ! ) ; } } if ( arr . length <= 3 ) { return cpf . slice ( arr . length , cpf . length ) ; } if ( arr . length > 3 && arr . length <= 6 ) { return cpf . slice ( arr . length + 1 , cpf . length ) ; } if ( arr . length > 6 && arr . length <= 9 ) { return cpf . slice ( arr . length + 2 , cpf . length ) ; } if ( arr . length > 9 && arr . length < 11 ) { return cpf . slice ( arr . length + 3 , cpf . length ) ; } if ( arr . length === 11 ) { return '' ; } if ( arr . length === 12 ) { if ( inputVal . length === 17 ) { return cnpj . slice ( 16 , cnpj . length ) ; } return cnpj . slice ( 15 , cnpj . length ) ; } if ( arr . length > 12 && arr . length <= 14 ) { return cnpj . slice ( arr . length + 4 , cnpj . length ) ; } return '' ; } param inputValue private formControlResult ( inputValue ) { if ( this . writingValue || this . maskChanged ) { this . maskChanged = false ; return ; } if ( Array . isArray ( this . dropSpecialCharacters ) ) { this . onChange ( this . _toNumber ( this . _removeMask ( this . _removeSuffix ( this . _removePrefix ( inputValue ) ) , this . dropSpecialCharacters ) ) ) ; } else if ( this . dropSpecialCharacters ) { this . onChange ( this . _toNumber ( this . _checkSymbols ( inputValue ) ) ) ; } else { this . onChange ( this . _removeSuffix ( inputValue ) ) ; } } private _toNumber ( value ) { if ( ! this . isNumberValue || value === '' ) { return value ; } const num = Number ( value ) ; return Number . isNaN ( num ) ? value : num ; } private _removeMask ( value , specialCharactersForRemove ) { return value ? value . replace ( this . _regExpForRemove ( specialCharactersForRemove ) , '' ) : value ; } private _removePrefix ( value ) { if ( ! this . prefix ) { return value ; } return value ? value . replace ( this . prefix , '' ) : value ; } private _removeSuffix ( value ) { if ( ! this . suffix ) { return value ; } return value ? value . replace ( this . suffix , '' ) : value ; } private _retrieveSeparatorValue ( result ) { return this . _removeMask ( this . _removeSuffix ( this . _removePrefix ( result ) ) , this . maskSpecialCharacters ) ; } private _regExpForRemove ( specialCharactersForRemove ) { return new RegExp ( specialCharactersForRemove . map ( ( item ) => `\\${ item }` ) . join ( '|' ) , 'gi' ) ; } private _checkSymbols ( result ) { if ( result === '' ) { return result ; } const separatorPrecision = this . _retrieveSeparatorPrecision ( this . maskExpression ) ; let separatorValue = this . _retrieveSeparatorValue ( result ) ; if ( this . decimalMarker !== '.' && ! Array . isArray ( this . decimalMarker ) ) { separatorValue = separatorValue . replace ( this . decimalMarker , '.' ) ; } if ( ! this . isNumberValue ) { return separatorValue ; } if ( separatorPrecision ) { if ( result === this . decimalMarker ) { return null ; } return this . _checkPrecision ( this . maskExpression , separatorValue ) ; } else { return Number ( separatorValue ) ; } } private _retrieveSeparatorPrecision ( maskExpretion ) { const matcher = maskExpretion . match ( new RegExp ( `^separator\\.([^d]*)` ) ) ; return matcher ? Number ( matcher [ 1 ] ) : null ; } private _checkPrecision ( separatorExpression , separatorValue ) { if ( separatorExpression . indexOf ( '2' ) > 0 ) { return Number ( separatorValue ) . toFixed ( 2 ) ; } return Number ( separatorValue ) ; } }