11import { IFormatterParserFn } from './struct/formatter-parser-function' ;
22import { IFormatterParserResult } from './struct/formatter-parser-result' ;
3+ import { IConformToMaskConfig } from './struct/transform-functions/conform-to-mask-config' ;
34
4- export class FormatterParser {
5-
6- static toCapitalized : IFormatterParserFn = ( value : any ) : IFormatterParserResult => {
75
8- let transformedValue = value ;
9-
10- if ( typeof value === 'string' || value instanceof String ) {
11- transformedValue = transformedValue
12- . toLowerCase ( )
13- . split ( ' ' )
14- . map ( val => val . charAt ( 0 ) . toUpperCase ( ) + val . slice ( 1 ) )
15- . join ( ' ' ) ;
16- }
6+ const emptyString = '' ;
7+ const emptyArray = [ ] ;
8+ const defaultPlaceholderChar = '_' ;
9+ const convertMaskToPlaceholder = function ( mask : ( string | RegExp ) [ ] = emptyArray , placeholderChar : string = defaultPlaceholderChar ) : string {
10+ if ( mask . indexOf ( placeholderChar ) !== - 1 ) {
11+ throw new Error (
12+ 'Placeholder character must not be used as part of the mask. Please specify a character ' +
13+ 'that is not present in your mask as your placeholder character.\n\n' +
14+ `The placeholder character that was received is: ${ JSON . stringify ( placeholderChar ) } \n\n` +
15+ `The mask that was received is: ${ JSON . stringify ( mask ) } `
16+ ) ;
17+ }
1718
18- return {
19- name : 'toCapitalized' ,
20- result : transformedValue ,
21- previous : value
22- } ;
19+ return mask . map ( ( char ) => {
20+ return ( char instanceof RegExp ) ? placeholderChar : char ;
21+ } ) . join ( '' ) ;
22+ } ;
2323
24- }
24+ export class FormatterParser {
2525
2626 static toUpperCase : IFormatterParserFn = ( value : any ) : IFormatterParserResult => {
2727 let transformedValue = value ;
@@ -49,6 +49,26 @@ export class FormatterParser {
4949 } ;
5050 }
5151
52+ static toCapitalized : IFormatterParserFn = ( value : any ) : IFormatterParserResult => {
53+
54+ let transformedValue = value ;
55+
56+ if ( typeof value === 'string' || value instanceof String ) {
57+ transformedValue = transformedValue
58+ . toLowerCase ( )
59+ . split ( ' ' )
60+ . map ( val => val . charAt ( 0 ) . toUpperCase ( ) + val . slice ( 1 ) )
61+ . join ( ' ' ) ;
62+ }
63+
64+ return {
65+ name : 'toCapitalized' ,
66+ result : transformedValue ,
67+ previous : value
68+ } ;
69+
70+ }
71+
5272 static replaceString ( searchValue : RegExp , replaceValue : string ) : IFormatterParserFn {
5373
5474 return ( value : any ) => {
@@ -71,5 +91,247 @@ export class FormatterParser {
7191
7292 }
7393
94+ static conformToMask ( mask : ( string | RegExp ) [ ] | Function = emptyArray , config : IConformToMaskConfig ) {
95+
96+ config = config || { } as IConformToMaskConfig ;
97+ // These configurations tell us how to conform the mask
98+ const {
99+ guide = true ,
100+ previousConformedValue = emptyString ,
101+ placeholderChar = defaultPlaceholderChar ,
102+ // placeholder = convertMaskToPlaceholder(mask, placeholderChar),
103+ currentCaretPosition,
104+ keepCharPositions
105+ } = config ;
106+
107+ return ( rawValue : any ) => {
108+
109+ if ( typeof mask === 'function' ) {
110+ mask = mask ( rawValue ) as ( string | RegExp ) [ ] ;
111+ }
112+ const placeholder = convertMaskToPlaceholder ( mask , placeholderChar ) ;
113+
114+
115+ // The configs below indicate that the user wants the algorithm to work in *no guide* mode
116+ const suppressGuide = guide === false && previousConformedValue !== undefined ;
117+
118+ // Calculate lengths once for performance
119+ const rawValueLength = rawValue . length ;
120+ const previousConformedValueLength = previousConformedValue . length ;
121+ const placeholderLength = placeholder . length ;
122+ const maskLength = mask . length ;
123+
124+ // This tells us the number of edited characters and the direction in which they were edited (+/-)
125+ const editDistance = rawValueLength - previousConformedValueLength ;
126+
127+ // In *no guide* mode, we need to know if the user is trying to add a character or not
128+ const isAddition = editDistance > 0 ;
129+
130+ // Tells us the index of the first change. For (438) 394-4938 to (38) 394-4938, that would be 1
131+ const indexOfFirstChange = currentCaretPosition + ( isAddition ? - editDistance : 0 ) ;
132+
133+ // We're also gonna need the index of last change, which we can derive as follows...
134+ const indexOfLastChange = indexOfFirstChange + Math . abs ( editDistance ) ;
135+
136+ // If `conformToMask` is configured to keep character positions, that is, for mask 111, previous value
137+ // _2_ and raw value 3_2_, the new conformed value should be 32_, not 3_2 (default behavior). That's in the case of
138+ // addition. And in the case of deletion, previous value _23, raw value _3, the new conformed string should be
139+ // __3, not _3_ (default behavior)
140+ //
141+ // The next block of logic handles keeping character positions for the case of deletion. (Keeping
142+ // character positions for the case of addition is further down since it is handled differently.)
143+ // To do this, we want to compensate for all characters that were deleted
144+ if ( keepCharPositions === true && ! isAddition ) {
145+ // We will be storing the new placeholder characters in this variable.
146+ let compensatingPlaceholderChars = emptyString ;
147+
148+ // For every character that was deleted from a placeholder position, we add a placeholder char
149+ for ( let i = indexOfFirstChange ; i < indexOfLastChange ; i ++ ) {
150+ if ( placeholder [ i ] === placeholderChar ) {
151+ compensatingPlaceholderChars += placeholderChar ;
152+ }
153+ }
154+
155+ // Now we trick our algorithm by modifying the raw value to make it contain additional placeholder characters
156+ // That way when the we start laying the characters again on the mask, it will keep the non-deleted characters
157+ // in their positions.
158+ rawValue = (
159+ rawValue . slice ( 0 , indexOfFirstChange ) +
160+ compensatingPlaceholderChars +
161+ rawValue . slice ( indexOfFirstChange , rawValueLength )
162+ ) ;
163+ }
164+
165+ // Convert `rawValue` string to an array, and mark characters based on whether they are newly added or have
166+ // existed in the previous conformed value. Identifying new and old characters is needed for `conformToMask`
167+ // to work if it is configured to keep character positions.
168+ const rawValueArr = rawValue
169+ . split ( emptyString )
170+ . map ( ( char , i ) => ( { char, isNew : i >= indexOfFirstChange && i < indexOfLastChange } ) ) ;
171+
172+ // The loop below removes masking characters from user input. For example, for mask
173+ // `00 (111)`, the placeholder would be `00 (___)`. If user input is `00 (234)`, the loop below
174+ // would remove all characters but `234` from the `rawValueArr`. The rest of the algorithm
175+ // then would lay `234` on top of the available placeholder positions in the mask.
176+ for ( let i = rawValueLength - 1 ; i >= 0 ; i -- ) {
177+ const { char} = rawValueArr [ i ] ;
178+
179+ if ( char !== placeholderChar ) {
180+ const shouldOffset = i >= indexOfFirstChange && previousConformedValueLength === maskLength ;
181+
182+ if ( char === placeholder [ ( shouldOffset ) ? i - editDistance : i ] ) {
183+ rawValueArr . splice ( i , 1 ) ;
184+ }
185+ }
186+ }
187+
188+ // This is the variable that we will be filling with characters as we figure them out
189+ // in the algorithm below
190+ let conformedValue = emptyString ;
191+ let someCharsRejected = false ;
192+
193+ // Ok, so first we loop through the placeholder looking for placeholder characters to fill up.
194+ placeholderLoop: for ( let i = 0 ; i < placeholderLength ; i ++ ) {
195+ const charInPlaceholder = placeholder [ i ] ;
196+
197+ // We see one. Let's find out what we can put in it.
198+ if ( charInPlaceholder === placeholderChar ) {
199+ // But before that, do we actually have any user characters that need a place?
200+ if ( rawValueArr . length > 0 ) {
201+ // We will keep chipping away at user input until either we run out of characters
202+ // or we find at least one character that we can map.
203+ while ( rawValueArr . length > 0 ) {
204+ // Let's retrieve the first user character in the queue of characters we have left
205+ const { char : rawValueChar , isNew} = rawValueArr . shift ( ) ;
206+
207+ // If the character we got from the user input is a placeholder character (which happens
208+ // regularly because user input could be something like (540) 90_-____, which includes
209+ // a bunch of `_` which are placeholder characters) and we are not in *no guide* mode,
210+ // then we map this placeholder character to the current spot in the placeholder
211+ if ( rawValueChar === placeholderChar && suppressGuide !== true ) {
212+ conformedValue += placeholderChar ;
213+
214+ // And we go to find the next placeholder character that needs filling
215+ continue placeholderLoop;
216+
217+ // Else if, the character we got from the user input is not a placeholder, let's see
218+ // if the current position in the mask can accept it.
219+ } else if ( ( < RegExp > mask [ i ] ) . test ( rawValueChar ) ) {
220+ // we map the character differently based on whether we are keeping character positions or not.
221+ // If any of the conditions below are met, we simply map the raw value character to the
222+ // placeholder position.
223+ if (
224+ keepCharPositions !== true ||
225+ isNew === false ||
226+ previousConformedValue === emptyString ||
227+ guide === false ||
228+ ! isAddition
229+ ) {
230+ conformedValue += rawValueChar ;
231+ } else {
232+ // We enter this block of code if we are trying to keep character positions and none of the conditions
233+ // above is met. In this case, we need to see if there's an available spot for the raw value character
234+ // to be mapped to. If we couldn't find a spot, we will discard the character.
235+ //
236+ // For example, for mask `1111`, previous conformed value `_2__`, raw value `942_2__`. We can map the
237+ // `9`, to the first available placeholder position, but then, there are no more spots available for the
238+ // `4` and `2`. So, we discard them and end up with a conformed value of `92__`.
239+ const rawValueArrLength = rawValueArr . length ;
240+ let indexOfNextAvailablePlaceholderChar = null ;
241+
242+ // Let's loop through the remaining raw value characters. We are looking for either a suitable spot, ie,
243+ // a placeholder character or a non-suitable spot, ie, a non-placeholder character that is not new.
244+ // If we see a suitable spot first, we store its position and exit the loop. If we see a non-suitable
245+ // spot first, we exit the loop and our `indexOfNextAvailablePlaceholderChar` will stay as `null`.
246+ for ( let j = 0 ; j < rawValueArrLength ; j ++ ) {
247+ const charData = rawValueArr [ j ] ;
248+
249+ if ( charData . char !== placeholderChar && charData . isNew === false ) {
250+ break ;
251+ }
252+
253+ if ( charData . char === placeholderChar ) {
254+ indexOfNextAvailablePlaceholderChar = j ;
255+ break ;
256+ }
257+ }
258+
259+ // If `indexOfNextAvailablePlaceholderChar` is not `null`, that means the character is not blocked.
260+ // We can map it. And to keep the character positions, we remove the placeholder character
261+ // from the remaining characters
262+ if ( indexOfNextAvailablePlaceholderChar !== null ) {
263+ conformedValue += rawValueChar ;
264+ rawValueArr . splice ( indexOfNextAvailablePlaceholderChar , 1 ) ;
265+
266+ // If `indexOfNextAvailablePlaceholderChar` is `null`, that means the character is blocked. We have to
267+ // discard it.
268+ } else {
269+ i -- ;
270+ }
271+ }
272+
273+ // Since we've mapped this placeholder position. We move on to the next one.
274+ continue placeholderLoop;
275+ } else {
276+ someCharsRejected = true ;
277+ }
278+ }
279+ }
280+
281+ // We reach this point when we've mapped all the user input characters to placeholder
282+ // positions in the mask. In *guide* mode, we append the left over characters in the
283+ // placeholder to the `conformedString`, but in *no guide* mode, we don't wanna do that.
284+ //
285+ // That is, for mask `(111)` and user input `2`, we want to return `(2`, not `(2__)`.
286+ if ( suppressGuide === false ) {
287+ conformedValue += placeholder . substr ( i , placeholderLength ) ;
288+ }
289+
290+ // And we break
291+ break ;
292+
293+ // Else, the charInPlaceholder is not a placeholderChar. That is, we cannot fill it
294+ // with user input. So we just map it to the final output
295+ } else {
296+ conformedValue += charInPlaceholder ;
297+ }
298+ }
299+
300+ // The following logic is needed to deal with the case of deletion in *no guide* mode.
301+ //
302+ // Consider the silly mask `(111) /// 1`. What if user tries to delete the last placeholder
303+ // position? Something like `(589) /// `. We want to conform that to `(589`. Not `(589) /// `.
304+ // That's why the logic below finds the last filled placeholder character, and removes everything
305+ // from that point on.
306+ if ( suppressGuide && isAddition === false ) {
307+ let indexOfLastFilledPlaceholderChar = null ;
308+
309+ // Find the last filled placeholder position and substring from there
310+ for ( let i = 0 ; i < conformedValue . length ; i ++ ) {
311+ if ( placeholder [ i ] === placeholderChar ) {
312+ indexOfLastFilledPlaceholderChar = i ;
313+ }
314+ }
315+
316+ if ( indexOfLastFilledPlaceholderChar !== null ) {
317+ // We substring from the beginning until the position after the last filled placeholder char.
318+ conformedValue = conformedValue . substr ( 0 , indexOfLastFilledPlaceholderChar + 1 ) ;
319+ } else {
320+ // If we couldn't find `indexOfLastFilledPlaceholderChar` that means the user deleted
321+ // the first character in the mask. So we return an empty string.
322+ conformedValue = emptyString ;
323+ }
324+ }
325+
326+ return {
327+ name : 'conformToMask' ,
328+ result : conformedValue ,
329+ previous : rawValue ,
330+ meta : { someCharsRejected}
331+ } ;
332+
333+ }
334+ }
335+
74336}
75337
0 commit comments