/** * `MaskFormatter` é usado para formatar strings. Seu comportamento * é controlado pelo formato do atributo `mask` que especifica quais * caracteres são válidos e onde devem estar posicionados, intercalando-os * com eventuais caracteres literais expressados no padrão informado. * Sua implementação é inspirada pela implementação em Java do [MaskFormatter](https://docs.oracle.com/javase/7/docs/api/javax/swing/text/MaskFormatter.html). * * Para o padrão da máscara podem ser usados os seguintes caracteres especiais: * * | Caractere | Comportamento | * |:---------:|-------------------------------------------------------------------------------------------------------------| * | # | Qualquer número | * | ' | "Escapa" o caractere que vem na sequência. Útil quando desejamos converter um caractere especial em literal.| * | U | Qualquer letra. Transforma letras minúsculas em maiúsculas. | * | L | Qualquer letra. Transforma letras maiúsculas em minúsculas. | * | A | Qualquer letra ou número. | * | Z | Qualquer letra ou número. Transforma letras minúsculas em maiúsculas. | * | ? | Qualquer letra. Preserva maiúsculas e minúsculas. | * | * | Qualquer caractere. | * * Os demais caracteres presentes no padrão serão tratados como literais, isto é, * serão apenas inseridos naquela posição. * * Quando o valor a ser formatado é menor que a máscara, um 'placeHolder' * será inserido em cada posição ausente, completando a formatação. * Por padrão será usado um espaço em branco como 'placeHolder', mas * esse valor pode ser alterado. * * Por exemplo: * ''' * const formatter: MaskFormatter = new MaskFormatter("###-####"); * formatter.placeholder = '_'; * console.log(formatter.format("123")); * ''' * resultaria na string '123-____'. * * ##Veja mais alguns exemplos: * |Padrão |Máscara |Entrada |Saída | * |----------------|------------------|--------------|-------------------| * |Telefone |(##) ####-#### |3432192515 |(34) 3219-2515 | * |CPF |###.###.###-## |12345678901 |123.456.789-01 | * |CNPJ |ZZ.ZZZ.ZZZ/ZZZZ-##|1a3B5c7D9e1F31|1A.3B5.C7D9/E1F3-31| * |CEP |##.###-### |12345678 |12.345-678 | * |PLACA (veículo) |UUU-#### |abc1234 |ABC-1234 | * |Cor RGB |'#AAAAAA |00000F0 |#0000F0 | * */ export class MaskFormatter { private static DIGIT_KEY: string = "#"; private static LITERAL_KEY: string = "'"; private static UPPERCASE_KEY: string = "U"; private static LOWERCASE_KEY: string = "L"; private static ALPHA_NUMERIC_KEY: string = "A"; private static ALPHA_NUMERIC_UPPERCASE_KEY: string = "Z"; private static CHARACTER_KEY: string = "?"; private static ANYTHING_KEY: string = "*"; public static DEFAULT_MASKS: any = { "cnpj": "ZZ.ZZZ.ZZZ/ZZZZ-##", "cpf": "###.###.###-##", "phone": "(##) ####-####", "cep": "##.###-###", "cor_rgb" : "'#AAAAAA" }; private _mask: string = ''; private _maskChars: Array = new Array(); /** * Determina qual caractere será usado dos caracteres não presentes no valor, * ou seja, aqueles que o usuário ainda não informou. Por padrão usamos um espaço. */ public placeholder: string = ' '; /** * Setter para mask. Trata-se do padrão que se espera ao formatar o texto. */ public set mask(mask: string) { this._mask = mask; this.updateInternalMask(); } /** * Getter para mask. * * @return A última máscara informada. */ public get mask(): string { return this._mask; } constructor(mask: string) { this.mask = mask; } /** * Aplica a máscara quando o input é alterado * * @param value Valor a ser aplicado com a máscara. * @return O valor processado de acordo com o padrão. */ public applyMask(value: string) { if (this.mask === MaskFormatter.DEFAULT_MASKS.cor_rgb) { value = value.replace("#", "") const expectedFormattedValue = this.format(value.replace(/\s/g, "")); if (value === expectedFormattedValue) { return value; } if (value.length >= 6) { const formattedValue = this.format(value); return formattedValue; } else { return value; } } const expectedFormattedValue = this.format(value.replace(/\s/g, "")); if (value === expectedFormattedValue) { return value; } const maskPlaceholders = this.mask.match(/[UAZL#?*']/g) || []; const placeholderCount = maskPlaceholders.length; const validValue = value.split('').filter((char, index) => { const placeholder = maskPlaceholders[index]; if (!placeholder) return false; switch (placeholder) { case MaskFormatter.DIGIT_KEY: return /\d/.test(char); case MaskFormatter.UPPERCASE_KEY: return /[a-zA-Z]/.test(char); case MaskFormatter.LOWERCASE_KEY: return /[a-zA-Z]/.test(char); case MaskFormatter.ALPHA_NUMERIC_KEY: return /[a-zA-Z0-9]/.test(char); case MaskFormatter.ALPHA_NUMERIC_UPPERCASE_KEY: return /[A-Z0-9]/.test(char); case MaskFormatter.CHARACTER_KEY: return /[a-zA-Z]/.test(char); case MaskFormatter.ANYTHING_KEY: return true; case MaskFormatter.LITERAL_KEY: const literalChar = this.mask.charAt(index); return char === literalChar; default: return false; } }); if (validValue.length >= placeholderCount) { const formattedValue = this.format(validValue.join('')); return formattedValue; } else { const partialValue = validValue.join(''); return partialValue; } } /** * Remove a máscara formatando a string retornando sem máscara * * @param value Valor a ser formatado com máscara. * @return O valor processado de acordo com o padrão. */ public removeMask(value: string): string { const maskString = this.mask.replace("#", ""); const maskElements = maskString.split(""); let valueMapped = value maskElements.forEach(maskChar => { valueMapped = valueMapped.replace(maskChar, "") }) return valueMapped; } /** * Formata a string passada baseada na máscara definda pelo atributo mask. * * @param value Valor a ser formatado. * @param trimBefore Executa um trim para remover espaços em branco. * @return O valor processado de acordo com o padrão. */ public format(value: string, trimBefore: boolean = false): string { if(trimBefore){ value = value.trim(); } let result: string = ''; const index: Array = [0]; let counter: number = 0; const maxCounter: number = this._maskChars.length; while (counter < maxCounter) { result = this._maskChars[counter].append(result, value, index); counter++; } return result; } /** * Prepara a formatação internamente de acordo com o padrão. */ private updateInternalMask(): void { this._maskChars.length = 0; if (this.mask != null) { let counter: number = 0; const maxCounter: number = this.mask.length; while (counter < maxCounter) { let maskChar: string = this.mask.charAt(counter); switch (maskChar) { case MaskFormatter.DIGIT_KEY: this._maskChars.push(new MaskFormatter.DigitMaskCharacter(this, maskChar)); break; case MaskFormatter.LITERAL_KEY: if (++counter < maxCounter) { maskChar = this.mask.charAt(counter); this._maskChars.push(new MaskFormatter.LiteralCharacter(this, maskChar)); } break; case MaskFormatter.UPPERCASE_KEY: this._maskChars.push(new MaskFormatter.UpperCaseCharacter(this, maskChar)); break; case MaskFormatter.LOWERCASE_KEY: this._maskChars.push(new MaskFormatter.LowerCaseCharacter(this, maskChar)); break; case MaskFormatter.ALPHA_NUMERIC_KEY: this._maskChars.push(new MaskFormatter.AlphaNumericCharacter(this, maskChar)); break; case MaskFormatter.ALPHA_NUMERIC_UPPERCASE_KEY: this._maskChars.push(new MaskFormatter.AlphaNumericUpperCaseCharacter(this, maskChar)); break; case MaskFormatter.CHARACTER_KEY: this._maskChars.push(new MaskFormatter.CharCharacter(this, maskChar)); break; case MaskFormatter.ANYTHING_KEY: this._maskChars.push(new MaskFormatter.MaskCharacter(this, maskChar)); break; default: this._maskChars.push(new MaskFormatter.LiteralCharacter(this, maskChar)); break; } counter++; } } } // // Classes internas usadas para representar a máscara. // public static MaskCharacter = class { public maskFormatter: MaskFormatter; public type: string; constructor(maskFormatter: MaskFormatter, type: string) { this.maskFormatter = maskFormatter; this.type = type; } /** * Cada subclasse deve sobrescrever retornando true caso represente. * um caractere literal. Por padrão o retorno é false. */ public isLiteral(): boolean { return false; } /** * Retorna se é um texto válido. * * @param aChar - Valor a ser validado. * @returns - Retorna se é um texto válido. */ public isValidCharacter(aChar: string): boolean { if (this.isLiteral()) { return (this.getChar(aChar) == aChar); } aChar = this.getChar(aChar); return true; } /** * Retorna o texto informado. * * @param aChar - Texto a ser retornado. * @returns - Texto informado. */ public getChar(aChar: string): string { return aChar; } /** * Adiciona valor encontrado. * * @param result - String a ser manipulada. * @param formatting - String de formatação a ser adicionada na result. * @param index - Índice que será utilizado para buscar no parâmetro formatting. * * @returns - Valor passado na result incrementado com o valor encontrado na formatting. * * @example * ``` const result = ""; * const formatting = "123456"; * const index = [0]; * const returnedResult = append(result, formatting, index); * ``` * Retorna: * ```returnedResult = "1"; * index = [1]; * ``` */ public append(result: string, formatting: string, index: Array): string { const inString: boolean = index[0] < formatting.length; const aChar: string = inString ? formatting.charAt(index[0]) : ''; if (this.isLiteral()) { const literal: string = this.getChar(aChar); result += literal; if(literal === aChar){ index[0] = index[0] + 1; } } else if (index[0] >= formatting.length) { result += this.maskFormatter.placeholder; index[0] = index[0] + 1; } else if (this.isValidCharacter(aChar)) { result += this.getChar(aChar); index[0] = index[0] + 1; } else { throw new Error(`Valor inválido: "${aChar}". Na posição ${index[0] + 1} espera-se ${this.getFormatMessage()}.`); } return result; } /** * Obtém tipo por extenso. * * @returns O tipo por extenso. * @example * Se o tipo for **ALPHA_NUMERIC_KEY** recebo **uma letra ou um número** */ public getFormatMessage(): string{ let message: string; switch(this.type){ case MaskFormatter.UPPERCASE_KEY: case MaskFormatter.LOWERCASE_KEY: case MaskFormatter.CHARACTER_KEY: message = 'uma letra'; break; case MaskFormatter.DIGIT_KEY: message = 'um número'; break; case MaskFormatter.ALPHA_NUMERIC_KEY: case MaskFormatter.ALPHA_NUMERIC_UPPERCASE_KEY: message = 'uma letra ou um número'; break; default: message = '' } return message; } } private static LiteralCharacter = class extends MaskFormatter.MaskCharacter { private _fixedChar: string; constructor(maskFormatter: MaskFormatter, fixedChar: string) { super(maskFormatter, fixedChar); this._fixedChar = fixedChar; } public isLiteral(): boolean { return true; } public getChar(aChar: string): string { aChar; return this._fixedChar; } } private static DigitMaskCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se o caracter é válido. * * @param aChar - Caracter a ser validado. * @returns - Se o caracter é valido ou não. */ public isValidCharacter(aChar: string): boolean { return (this.isDigit(aChar) && super.isValidCharacter(aChar)); } /** * Valida se é um numéro. * * @param char - String a ser verificada. * @returns - Verdadeiro para caracteres numéricos. * * @example * ```Informado: "8" | Retorno: true ``` * @example * ```Informado: "A" | Retorno: false ``` */ private isDigit(char: string): boolean { return char >= '0' && char <= '9' } } private static UpperCaseCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se a string é uma letra do alfabeto (a-z). * * @param aChar - String passada para validação. * @returns - Verdadeiro se a string passada for uma letra do alfabeto. */ public isValidCharacter(aChar: string): boolean { return (/[a-z]/i.test(aChar) && super.isValidCharacter(aChar)); } /** * Obtém a string passada em maiúscula. * * @param aChar - String passada para validação. * @returns - O caracter passado em letra maiúscula. */ public getChar(aChar: string): string { return aChar.toUpperCase(); } } private static LowerCaseCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se a string é uma letra do alfabeto (a-z). * * @param aChar - String passada para validação. * @returns - Verdadeiro se a string passada for uma letra do alfabeto. */ public isValidCharacter(aChar: string): boolean { return (/[a-z]/i.test(aChar) && super.isValidCharacter(aChar)); } /** * Obtém a string passada em minúscula. * * @param aChar - String passada para validação. * @returns - O caracter passado em letra minúscula. */ public getChar(aChar: string): string { return aChar.toLocaleLowerCase(); } } private static AlphaNumericCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se a string é uma letra do alfabeto (a-z) ou um numero entre zero e nove. * * @param aChar - String passada para validação. * @returns - Verdadeiro se a string passada for uma letra do alfabeto ou um numeral (de 0 a 9). */ public isValidCharacter(aChar: string): boolean { //FIXME: talvez seja problema usar regex aqui... avaliar se existe forma mais barata. return (/[a-z0-9]/i.test(aChar)) && super.isValidCharacter(aChar); } } private static AlphaNumericUpperCaseCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se a string é uma letra do alfabeto (A-Z) ou um numero entre zero e nove. * * @param aChar - String passada para validação. * @returns - Verdadeiro se a string passada for uma letra do alfabeto ou um numeral (de 0 a 9). */ public isValidCharacter(aChar: string): boolean { //FIXME: talvez seja problema usar regex aqui... avaliar se existe forma mais barata. return (/[a-zA-Z0-9]/.test(aChar)) && super.isValidCharacter(aChar); } public getChar(aChar: string): string { return aChar.toLocaleUpperCase(); } } private static CharCharacter = class extends MaskFormatter.MaskCharacter { /** * Valida se a string é uma letra do alfabeto (a-z). * * @param aChar - String passada para validação. * @returns - Verdadeiro se a string passada for uma letra do alfabeto. */ public isValidCharacter(aChar: string): boolean { //FIXME: talvez seja problema usar regex aqui... avaliar se existe forma mais barata return (/[a-z]/i.test(aChar) && super.isValidCharacter(aChar)); } } } export namespace MaskFormatter { export type MaskCharacter = InstanceType; }