import { Injectable } from '@angular/core'; const FLAGS = new Set('aACL09#&?'); const REGEX = new Map([ ['C', /(?!^$)/u], // Non-empty ['&', /[^\p{Separator}]/u], // Non-whitespace ['a', /[\p{Letter}\d\p{Separator}]/u], // Alphanumeric & whitespace ['A', /[\p{Letter}\d]/u], // Alphanumeric ['?', /[\p{Letter}\p{Separator}]/u], // Alpha & whitespace ['L', /\p{Letter}/u], // Alpha ['0', /\d/], // Numeric ['9', /[\d\p{Separator}]/u], // Numeric & whitespace ['#', /[\d\-+]/], // Numeric and sign ]); /** @hidden */ export interface MaskOptions { format: string; promptChar: string; } /** @hidden */ export interface Replaced { value: string; end: number; } interface ParsedMask { literals: Map, mask: string } const replaceCharAt = (string: string, idx: number, char: string) => `${string.substring(0, idx)}${char}${string.substring(idx + 1)}`; export function parseMask(format: string): ParsedMask { const literals = new Map(); let mask = format; for (let i = 0, j = 0; i < format.length; i++, j++) { const [current, next] = [format.charAt(i), format.charAt(i + 1)]; if (current === '\\' && FLAGS.has(next)) { mask = replaceCharAt(mask, j, ''); literals.set(j, next); i++; } else { if (!FLAGS.has(current)) { literals.set(j, current); } } } return { literals, mask }; } /** @hidden */ @Injectable({ providedIn: 'root' }) export class MaskParsingService { public applyMask(inputVal: string, maskOptions: MaskOptions, pos = 0): string { let outputVal = ''; let value = ''; const { literals, mask } = parseMask(maskOptions.format); const literalKeys: number[] = Array.from(literals.keys()); const nonLiteralIndices: number[] = this.getNonLiteralIndices(mask, literalKeys); const literalValues: string[] = Array.from(literals.values()); if (inputVal != null) { value = inputVal.toString(); } for (const _maskSym of mask) { outputVal += maskOptions.promptChar; } literals.forEach((val: string, key: number) => { outputVal = replaceCharAt(outputVal, key, val); }); if (!value) { return outputVal; } const nonLiteralValues: string[] = this.getNonLiteralValues(value, literalValues); for (let i = 0; i < nonLiteralValues.length; i++) { const char = nonLiteralValues[i]; const isCharValid = this.validateCharOnPosition(char, nonLiteralIndices[i], mask); if (!isCharValid && char !== maskOptions.promptChar) { nonLiteralValues[i] = maskOptions.promptChar; } } if (nonLiteralValues.length > nonLiteralIndices.length) { nonLiteralValues.splice(nonLiteralIndices.length); } for (const nonLiteralValue of nonLiteralValues) { const char = nonLiteralValue; outputVal = replaceCharAt(outputVal, nonLiteralIndices[pos++], char); } return outputVal; } public parseValueFromMask(maskedValue: string, maskOptions: MaskOptions): string { let outputVal = ''; const literalValues: string[] = Array.from(parseMask(maskOptions.format).literals.values()); for (const val of maskedValue) { if (literalValues.indexOf(val) === -1) { if (val !== maskOptions.promptChar) { outputVal += val; } } } return outputVal; } public replaceInMask(maskedValue: string, value: string, maskOptions: MaskOptions, start: number, end: number): Replaced { const { literals, mask } = parseMask(maskOptions.format); const literalsPositions = Array.from(literals.keys()); value = this.replaceIMENumbers(value); const chars = Array.from(value); let cursor = start; end = Math.min(end, maskedValue.length); const initialMaskedValue = maskedValue; for (let i = start; i < end || (chars.length && i < maskedValue.length); i++) { if (literalsPositions.indexOf(i) !== -1) { if (chars[0] === maskedValue[i]) { cursor = i + 1; chars.shift(); } continue; } if (chars[0] && !this.validateCharOnPosition(chars[0], i, mask) && chars[0] !== maskOptions.promptChar) { break; } let char = maskOptions.promptChar; if (chars.length) { cursor = i + 1; char = chars.shift(); } maskedValue = replaceCharAt(maskedValue, i, char); } if (value.length <= 1) { let isDelete = false; cursor = start; for (let i = 0; i < literalsPositions.length; i++) { if (value === '') { // on `delete` the cursor should move forward cursor = Math.max(cursor, end); isDelete = true; } else if (cursor === literalsPositions[i]) { cursor = literalsPositions[i] + 1; } } if (!isDelete && initialMaskedValue !== maskedValue) { cursor++; } } return { value: maskedValue, end: cursor }; } /** Validates only non literal positions. */ private validateCharOnPosition(inputChar: string, position: number, mask: string): boolean { const regex = REGEX.get(mask.charAt(position)); return regex ? regex.test(inputChar) : false; } private getNonLiteralIndices(mask: string, literalKeys: number[]): number[] { const nonLiteralsIndices: number[] = []; for (let i = 0; i < mask.length; i++) { if (literalKeys.indexOf(i) === -1) { nonLiteralsIndices.push(i); } } return nonLiteralsIndices; } private getNonLiteralValues(value: string, literalValues: string[]): string[] { const nonLiteralValues: string[] = []; for (const val of value) { if (literalValues.indexOf(val) === -1) { nonLiteralValues.push(val); } } return nonLiteralValues; } private replaceIMENumbers(value: string): string { return value.replace(/[0123456789]/g, (num) => ({ '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0' }[num])); } }