Skip to content

Commit 5dba567

Browse files
committed
fix(added conformToMask): added new built in transform conformToMask
1 parent b3922b9 commit 5dba567

File tree

2 files changed

+288
-18
lines changed

2 files changed

+288
-18
lines changed

src/formatterParser.ts

Lines changed: 280 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
import { IFormatterParserFn } from './struct/formatter-parser-function';
22
import { 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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface IConformToMaskConfig {
2+
guide?: boolean;
3+
previousConformedValue?: string;
4+
placeholderChar?: string;
5+
placeholder?: string;
6+
currentCaretPosition?: number;
7+
keepCharPositions?: boolean;
8+
}

0 commit comments

Comments
 (0)