import { Inject , Injectable } from '@angular/core' ; import { config , IConfig } from './config' ; @ Injectable ( ) export class MaskApplierService { public dropSpecialCharacters : IConfig [ 'dropSpecialCharacters' ] ; public hiddenInput : IConfig [ 'hiddenInput' ] ; public showTemplate ! : IConfig [ 'showTemplate' ] ; public clearIfNotMatch ! : IConfig [ 'clearIfNotMatch' ] ; public maskExpression : string = '' ; public actualValue : string = '' ; public shownMaskExpression : string = '' ; public maskSpecialCharacters ! : IConfig [ 'specialCharacters' ] ; public maskAvailablePatterns ! : IConfig [ 'patterns' ] ; public prefix ! : IConfig [ 'prefix' ] ; public suffix ! : IConfig [ 'suffix' ] ; public thousandSeparator ! : IConfig [ 'thousandSeparator' ] ; public decimalMarker ! : IConfig [ 'decimalMarker' ] ; public customPattern ! : IConfig [ 'patterns' ] ; public ipError ? : boolean ; public cpfCnpjError ? : boolean ; public showMaskTyped ! : IConfig [ 'showMaskTyped' ] ; public placeHolderCharacter ! : IConfig [ 'placeHolderCharacter' ] ; public validation : IConfig [ 'validation' ] ; public separatorLimit : IConfig [ 'separatorLimit' ] ; public allowNegativeNumbers : IConfig [ 'allowNegativeNumbers' ] ; public leadZeroDateTime : IConfig [ 'leadZeroDateTime' ] ; private _shift ! : Set < number > ; public constructor ( @ Inject ( config ) protected _config ) { this . _shift = new Set ( ) ; this . clearIfNotMatch = this . _config . clearIfNotMatch ; this . dropSpecialCharacters = this . _config . dropSpecialCharacters ; this . maskSpecialCharacters = this . _config . specialCharacters ; this . maskAvailablePatterns = this . _config . patterns ; this . prefix = this . _config . prefix ; this . suffix = this . _config . suffix ; this . thousandSeparator = this . _config . thousandSeparator ; this . decimalMarker = this . _config . decimalMarker ; this . hiddenInput = this . _config . hiddenInput ; this . showMaskTyped = this . _config . showMaskTyped ; this . placeHolderCharacter = this . _config . placeHolderCharacter ; this . validation = this . _config . validation ; this . separatorLimit = this . _config . separatorLimit ; this . allowNegativeNumbers = this . _config . allowNegativeNumbers ; this . leadZeroDateTime = this . _config . leadZeroDateTime ; } public applyMaskWithPattern ( inputValue , maskAndPattern ) { const [ mask , customPattern ] = maskAndPattern ; this . customPattern = customPattern ; return this . applyMask ( inputValue , mask ) ; } public applyMask ( inputValue , maskExpression , position = 0 , justPasted = false , backspaced = false , cb = ( ) => { } ) { if ( ! maskExpression || typeof inputValue !== 'string' ) { return '' ; } let cursor = 0 ; let result = '' ; let multi = false ; let backspaceShift = false ; let shift = 1 ; let stepBack = false ; if ( inputValue . slice ( 0 , this . prefix . length ) === this . prefix ) { inputValue = inputValue . slice ( this . prefix . length , inputValue . length ) ; } if ( ! ! this . suffix && inputValue ?. length > 0 ) { inputValue = this . checkAndRemoveSuffix ( inputValue ) ; } const inputArray = inputValue . toString ( ) . split ( '' ) ; if ( maskExpression === 'IP' ) { this . ipError = inputArray . filter ( ( i ) => i === '.' ) . length < 3 && inputArray . length < 7 ; maskExpression = '099.099.099.099' ; } const arr = [ ] ; for ( let i = 0 ; i < inputValue . length ; i ++ ) { if ( inputValue [ i ] ?. match ( '\\d' ) ) { arr . push ( inputValue [ i ] ! ) ; } } if ( maskExpression === 'CPF_CNPJ' ) { this . cpfCnpjError = arr . length !== 11 && arr . length !== 14 ; if ( arr . length > 11 ) { maskExpression = '00.000.000/0000-00' ; } else { maskExpression = '000.000.000-00' ; } } if ( maskExpression . startsWith ( 'percent' ) ) { if ( inputValue . match ( '[a-z]|[A-Z]' ) || inputValue . match ( /[-!$%^&*()_+|~=`{}\[\]:";'<>?,\/.]/ ) ) { inputValue = this . _stripToDecimal ( inputValue ) ; const precision = this . getPrecision ( maskExpression ) ; inputValue = this . checkInputPrecision ( inputValue , precision , this . decimalMarker ) ; } if ( inputValue . indexOf ( '.' ) > 0 && ! this . percentage ( inputValue . substring ( 0 , inputValue . indexOf ( '.' ) ) ) ) { const base = inputValue . substring ( 0 , inputValue . indexOf ( '.' ) - 1 ) ; inputValue = `${ base }${ inputValue . substring ( inputValue . indexOf ( '.' ) , inputValue . length ) }` ; } if ( this . percentage ( inputValue ) ) { result = inputValue ; } else { result = inputValue . substring ( 0 , inputValue . length - 1 ) ; } } else if ( maskExpression . startsWith ( 'separator' ) ) { if ( inputValue . match ( '[wа-яА-Я]' ) || inputValue . match ( '[ЁёА-я]' ) || inputValue . match ( '[a-z]|[A-Z]' ) || inputValue . match ( /[-@#!$%\\^&*()_£¬'+|~=`{}\[\]:";<>.?\/]/ ) || inputValue . match ( '[^A-Za-z0-9,]' ) ) { inputValue = this . _stripToDecimal ( inputValue ) ; } inputValue = inputValue . length > 1 && inputValue [ 0 ] === '0' && ! this . _compareOrIncludes ( inputValue [ 1 ] , this . decimalMarker , this . thousandSeparator ) && ! backspaced ? inputValue . slice ( 1 , inputValue . length ) : inputValue ; if ( backspaced ) { inputValue = this . _compareOrIncludes ( inputValue [ inputValue . length - 1 ] , this . decimalMarker , this . thousandSeparator ) ? inputValue . slice ( 0 , inputValue . length - 1 ) : inputValue ; } const thousandSeparatorCharEscaped = this . _charToRegExpExpression ( this . thousandSeparator ) ; let invalidChars = '@#!$%^&*()_+|~=`{}\\[\\]:\\s,\\.";<>?\\/' . replace ( thousandSeparatorCharEscaped , '' ) ; if ( Array . isArray ( this . decimalMarker ) ) { for ( const marker of this . decimalMarker ) { invalidChars = invalidChars . replace ( this . _charToRegExpExpression ( marker ) , '' ) ; } } else { invalidChars = invalidChars . replace ( this . _charToRegExpExpression ( this . decimalMarker ) , '' ) ; } const invalidCharRegexp = new RegExp ( '[' + invalidChars + ']' ) ; if ( inputValue . match ( invalidCharRegexp ) || ( inputValue . length === 1 && this . _compareOrIncludes ( inputValue , this . decimalMarker , this . thousandSeparator ) ) ) { inputValue = inputValue . substring ( 0 , inputValue . length - 1 ) ; } const precision = this . getPrecision ( maskExpression ) ; inputValue = this . checkInputPrecision ( inputValue , precision , this . decimalMarker ) ; const strForSep = inputValue . replace ( new RegExp ( thousandSeparatorCharEscaped , 'g' ) , '' ) ; result = this . _formatWithSeparators ( strForSep , this . thousandSeparator , this . decimalMarker , precision ) ; const commaShift = result . indexOf ( ',' ) - inputValue . indexOf ( ',' ) ; const shiftStep = result . length - inputValue . length ; if ( shiftStep > 0 && result [ position ] !== ',' ) { backspaceShift = true ; let _shift = 0 ; do { this . _shift . add ( position + _shift ) ; _shift ++ ; } while ( _shift < shiftStep ) ; } else if ( ( commaShift !== 0 && position > 0 && ! ( result . indexOf ( ',' ) >= position && position > 3 ) ) || ( ! ( result . indexOf ( '.' ) >= position && position > 3 ) && shiftStep <= 0 ) ) { this . _shift . clear ( ) ; backspaceShift = true ; shift = shiftStep ; position += shiftStep ; this . _shift . add ( position ) ; } else { this . _shift . clear ( ) ; } } else { for ( let i = 0 , inputSymbol = inputArray [ 0 ] ! ; i < inputArray . length ; i ++ , inputSymbol = inputArray [ i ] ! ) { if ( cursor === maskExpression . length ) { break ; } if ( this . _checkSymbolMask ( inputSymbol , maskExpression [ cursor ] ! ) && maskExpression [ cursor + 1 ] === '?' ) { result += inputSymbol ; cursor += 2 ; } else if ( maskExpression [ cursor + 1 ] === '*' && multi && this . _checkSymbolMask ( inputSymbol , maskExpression [ cursor + 2 ] ! ) ) { result += inputSymbol ; cursor += 3 ; multi = false ; } else if ( this . _checkSymbolMask ( inputSymbol , maskExpression [ cursor ] ! ) && maskExpression [ cursor + 1 ] === '*' ) { result += inputSymbol ; multi = true ; } else if ( maskExpression [ cursor + 1 ] === '?' && this . _checkSymbolMask ( inputSymbol , maskExpression [ cursor + 2 ] ! ) ) { result += inputSymbol ; cursor += 3 ; } else if ( this . _checkSymbolMask ( inputSymbol , maskExpression [ cursor ] ! ) ) { if ( maskExpression [ cursor ] === 'H' ) { if ( Number ( inputSymbol ) > 2 ) { cursor += 1 ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; if ( this . leadZeroDateTime ) { result += '0' ; } continue ; } } if ( maskExpression [ cursor ] === 'h' ) { if ( result === '2' && Number ( inputSymbol ) > 3 ) { cursor += 1 ; i -- ; continue ; } } if ( maskExpression [ cursor ] === 'm' ) { if ( Number ( inputSymbol ) > 5 ) { cursor += 1 ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; if ( this . leadZeroDateTime ) { result += '0' ; } continue ; } } if ( maskExpression [ cursor ] === 's' ) { if ( Number ( inputSymbol ) > 5 ) { cursor += 1 ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; if ( this . leadZeroDateTime ) { result += '0' ; } continue ; } } const daysCount = 31 ; if ( maskExpression [ cursor ] === 'd' ) { if ( ( Number ( inputSymbol ) > 3 && this . leadZeroDateTime ) || Number ( inputValue . slice ( cursor , cursor + 2 ) ) > daysCount || inputValue [ cursor + 1 ] === '/' ) { cursor += 1 ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; if ( this . leadZeroDateTime ) { result += '0' ; } continue ; } } if ( maskExpression [ cursor ] === 'M' ) { const monthsCount = 12 ; const withoutDays = cursor === 0 && ( Number ( inputSymbol ) > 2 || Number ( inputValue . slice ( cursor , cursor + 2 ) ) > monthsCount || inputValue [ cursor + 1 ] === '/' ) ; const day1monthInput = inputValue . slice ( cursor - 3 , cursor - 1 ) . includes ( '/' ) && ( ( inputValue [ cursor - 2 ] === '/' && Number ( inputValue . slice ( cursor - 1 , cursor + 1 ) ) > monthsCount && inputValue [ cursor ] !== '/' ) || inputValue [ cursor ] === '/' || ( inputValue [ cursor - 3 ] === '/' && Number ( inputValue . slice ( cursor - 2 , cursor ) ) > monthsCount && inputValue [ cursor - 1 ] !== '/' ) || inputValue [ cursor - 1 ] === '/' ) ; const day2monthInput = Number ( inputValue . slice ( cursor - 3 , cursor - 1 ) ) <= daysCount && ! inputValue . slice ( cursor - 3 , cursor - 1 ) . includes ( '/' ) && inputValue [ cursor - 1 ] === '/' && ( Number ( inputValue . slice ( cursor , cursor + 2 ) ) > monthsCount || inputValue [ cursor + 1 ] === '/' ) ; const day1monthPaste = Number ( inputValue . slice ( cursor - 3 , cursor - 1 ) ) > daysCount && ! inputValue . slice ( cursor - 3 , cursor - 1 ) . includes ( '/' ) && ! inputValue . slice ( cursor - 2 , cursor ) . includes ( '/' ) && Number ( inputValue . slice ( cursor - 2 , cursor ) ) > monthsCount ; const day2monthPaste = Number ( inputValue . slice ( cursor - 3 , cursor - 1 ) ) <= daysCount && ! inputValue . slice ( cursor - 3 , cursor - 1 ) . includes ( '/' ) && inputValue [ cursor - 1 ] !== '/' && Number ( inputValue . slice ( cursor - 1 , cursor + 1 ) ) > monthsCount ; if ( ( Number ( inputSymbol ) > 1 && this . leadZeroDateTime ) || withoutDays || day1monthInput || day2monthInput || day1monthPaste || day2monthPaste ) { cursor += 1 ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; if ( this . leadZeroDateTime ) { result += '0' ; } continue ; } } result += inputSymbol ; cursor ++ ; } else if ( this . maskSpecialCharacters . indexOf ( maskExpression [ cursor ] ! ) !== - 1 ) { result += maskExpression [ cursor ] ; cursor ++ ; this . _shiftStep ( maskExpression , cursor , inputArray . length ) ; i -- ; } else if ( this . maskSpecialCharacters . indexOf ( inputSymbol ) > - 1 && this . maskAvailablePatterns [ maskExpression [ cursor ] ! ] && this . maskAvailablePatterns [ maskExpression [ cursor ] ! ] ?. optional ) { if ( ! ! inputArray [ cursor ] && maskExpression !== '099.099.099.099' && maskExpression !== '000.000.000-00' && maskExpression !== '00.000.000/0000-00' ) { result += inputArray [ cursor ] ; } cursor ++ ; i -- ; } else if ( this . maskExpression [ cursor + 1 ] === '*' && this . _findSpecialChar ( this . maskExpression [ cursor + 2 ] ! ) && this . _findSpecialChar ( inputSymbol ) === this . maskExpression [ cursor + 2 ] && multi ) { cursor += 3 ; result += inputSymbol ; } else if ( this . maskExpression [ cursor + 1 ] === '?' && this . _findSpecialChar ( this . maskExpression [ cursor + 2 ] ! ) && this . _findSpecialChar ( inputSymbol ) === this . maskExpression [ cursor + 2 ] && multi ) { cursor += 3 ; result += inputSymbol ; } else if ( this . showMaskTyped && this . maskSpecialCharacters . indexOf ( inputSymbol ) < 0 && inputSymbol !== this . placeHolderCharacter ) { stepBack = true ; } } } if ( result . length + 1 === maskExpression . length && this . maskSpecialCharacters . indexOf ( maskExpression [ maskExpression . length - 1 ] ! ) !== - 1 ) { result += maskExpression [ maskExpression . length - 1 ] ; } let newPosition = position + 1 ; while ( this . _shift . has ( newPosition ) ) { shift ++ ; newPosition ++ ; } let actualShift = justPasted ? cursor : this . _shift . has ( position ) ? shift : 0 ; if ( stepBack ) { actualShift -- ; } cb ( actualShift , backspaceShift ) ; if ( shift < 0 ) { this . _shift . clear ( ) ; } let onlySpecial = false ; if ( backspaced ) { onlySpecial = inputArray . every ( ( char ) => this . maskSpecialCharacters . includes ( char ) ) ; } let res = `${ this . prefix }${ onlySpecial ? '' : result }${ this . suffix }` ; if ( result . length === 0 ) { res = `${ this . prefix }${ result }` ; } return res ; } public _findSpecialChar ( inputSymbol ) { return this . maskSpecialCharacters . find ( ( val ) => val === inputSymbol ) ; } protected _checkSymbolMask ( inputSymbol , maskSymbol ) { this . maskAvailablePatterns = this . customPattern ? this . customPattern : this . maskAvailablePatterns ; return ( this . maskAvailablePatterns [ maskSymbol ] ! && this . maskAvailablePatterns [ maskSymbol ] ! . pattern && this . maskAvailablePatterns [ maskSymbol ] ! . pattern . test ( inputSymbol ) ) ; } private _formatWithSeparators = ( str , thousandSeparatorChar , decimalChars , precision ) => { let x = [ ] ; let decimalChar = '' ; if ( Array . isArray ( decimalChars ) ) { const regExp = new RegExp ( decimalChars . map ( ( v ) => ( '[\\^$.|?*+()' . indexOf ( v ) >= 0 ? `\\${ v }` : v ) ) . join ( '|' ) ) ; x = str . split ( regExp ) ; decimalChar = str . match ( regExp ) ?. [ 0 ] ?? '' ; } else { x = str . split ( decimalChars ) ; decimalChar = decimalChars ; } const decimals = x . length > 1 ? `${ decimalChar }${ x [ 1 ] }` : '' ; let res = x [ 0 ] ! ; const separatorLimit = this . separatorLimit . replace ( /\s/g , '' ) ; if ( separatorLimit && + separatorLimit ) { if ( res [ 0 ] === '-' ) { res = `-${ res . slice ( 1 , res . length ) . slice ( 0 , separatorLimit . length ) }` ; } else { res = res . slice ( 0 , separatorLimit . length ) ; } } const rgx = /(\d+)(\d{3})/ ; while ( thousandSeparatorChar && rgx . test ( res ) ) { res = res . replace ( rgx , '$1' + thousandSeparatorChar + '$2' ) ; } if ( precision === undefined ) { return res + decimals ; } else if ( precision === 0 ) { return res ; } return res + decimals . substr ( 0 , precision + 1 ) ; } ; private percentage = ( str ) : boolean => { return Number ( str ) >= 0 && Number ( str ) <= 100 ; } ; private getPrecision = ( maskExpression ) : number => { const x = maskExpression . split ( '.' ) ; if ( x . length > 1 ) { return Number ( x [ x . length - 1 ] ) ; } return Infinity ; } ; private checkAndRemoveSuffix = ( inputValue ) : string => { for ( let i = this . suffix ?. length - 1 ; i >= 0 ; i -- ) { const substr = this . suffix . substr ( i , this . suffix ?. length ) ; if ( inputValue . includes ( substr ) && ( i - 1 < 0 || ! inputValue . includes ( this . suffix . substr ( i - 1 , this . suffix ?. length ) ) ) ) { return inputValue . replace ( substr , '' ) ; } } return inputValue ; } ; private checkInputPrecision = ( inputValue , precision , decimalMarker ) : string => { if ( precision < Infinity ) { if ( Array . isArray ( decimalMarker ) ) { const marker = decimalMarker . find ( ( dm ) => dm !== this . thousandSeparator ) ; decimalMarker = marker ? marker : decimalMarker [ 0 ] ; } const precisionRegEx = new RegExp ( this . _charToRegExpExpression ( decimalMarker ) + `\\d{${ precision }}.*$` ) ; const precisionMatch = inputValue . match ( precisionRegEx ) ; if ( precisionMatch && precisionMatch [ 0 ] ! . length - 1 > precision ) { const diff = precisionMatch [ 0 ] ! . length - 1 - precision ; inputValue = inputValue . substring ( 0 , inputValue . length - diff ) ; } if ( precision === 0 && this . _compareOrIncludes ( inputValue [ inputValue . length - 1 ] , decimalMarker , this . thousandSeparator ) ) { inputValue = inputValue . substring ( 0 , inputValue . length - 1 ) ; } } return inputValue ; } ; private _stripToDecimal ( str ) { return str . split ( '' ) . filter ( ( i , idx ) => { return ( i . match ( '^-?\\d' ) || i . match ( '\\s' ) || i === '.' || i === ',' || ( i === '-' && idx === 0 && this . allowNegativeNumbers ) ) ; } ) . join ( '' ) ; } private _charToRegExpExpression ( char ) { if ( char ) { const charsToEscape = '[\\^$.|?*+()' ; return char === ' ' ? '\\s' : charsToEscape . indexOf ( char ) >= 0 ? `\\${ char }` : char ; } return char ; } private _shiftStep ( maskExpression , cursor , inputLength ) { const shiftStep = /[*?]/g . test ( maskExpression . slice ( 0 , cursor ) ) ? inputLength : cursor ; this . _shift . add ( shiftStep + this . prefix . length || 0 ) ; } protected _compareOrIncludes < T > ( value , comparedValue , excludedValue ) { return Array . isArray ( comparedValue ) ? comparedValue . filter ( ( v ) => v !== excludedValue ) . includes ( value ) : value === comparedValue ; } }