From d7e26fb51fe62466f879f72de1de9271226c565c Mon Sep 17 00:00:00 2001 From: Clodoaldo Ribeiro Date: Wed, 23 Jul 2025 17:26:20 -0300 Subject: [PATCH 1/3] CnpjAlfanumericoInputFormatter - fix in check digits --- .../cnpj_alfanumerico_input_formatter.dart | 95 +++++++++++-------- ...npj_alfanumerico_input_formatter_test.dart | 6 +- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart b/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart index f1944dd..24dc659 100644 --- a/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart +++ b/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart @@ -1,57 +1,72 @@ -import 'package:brasil_fields/src/interfaces/compoundable_formatter.dart'; import 'package:flutter/services.dart'; +import 'package:brasil_fields/src/interfaces/compoundable_formatter.dart'; -/// Formata o valor do campo com a mascara de CNPJ `XX.XXX.XXX/XXXX-XX` -/// -/// Deve ser usado num TextInput que recebe letras e números: -/// ```dart -/// TextField( -/// inputFormatters: [ -/// FilteringTextInputFormatter.allow(RegExp('[0-9a-zA-Z]')), -/// CnpjAlfanumericoInputFormatter(), -/// ], -/// ), -/// ``` class CnpjAlfanumericoInputFormatter extends TextInputFormatter implements CompoundableFormatter { - // Define o tamanho máximo do campo. @override int get maxLength => 14; + static final _alphanumericRegex = RegExp(r'[A-Z0-9]'); + static final _digitRegex = RegExp(r'\d'); + @override TextEditingValue formatEditUpdate( - TextEditingValue oldValue, TextEditingValue newValue) { - final newValueLength = newValue.text.length; - - if (newValueLength > maxLength) return oldValue; + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final rawText = newValue.text.toUpperCase(); - var selectionIndex = newValue.selection.end; - var substrIndex = 0; - final newText = StringBuffer(); + // Filtra o texto: primeiras 12 posições aceitam letras e números, últimas 2 só números + final filtered = StringBuffer(); + int rawCursorPosition = 0; - if (newValueLength >= 3) { - newText.write('${newValue.text.substring(0, substrIndex = 2)}.'); - if (newValue.selection.end >= 2) selectionIndex++; - } - if (newValueLength >= 6) { - newText.write('${newValue.text.substring(2, substrIndex = 5)}.'); - if (newValue.selection.end >= 5) selectionIndex++; - } - if (newValueLength >= 9) { - newText.write('${newValue.text.substring(5, substrIndex = 8)}/'); - if (newValue.selection.end >= 8) selectionIndex++; - } - if (newValueLength >= 13) { - newText.write('${newValue.text.substring(8, substrIndex = 12)}-'); - if (newValue.selection.end >= 12) selectionIndex++; - } - if (newValueLength >= substrIndex) { - newText.write(newValue.text.substring(substrIndex)); + for (int i = 0, cursor = 0; i < rawText.length && cursor < maxLength; i++) { + final char = rawText[i]; + if (cursor < 12) { + if (_isAlphanumeric(char)) { + filtered.write(char); + cursor++; + if (i < newValue.selection.baseOffset) rawCursorPosition++; + } + } else { + if (_isDigit(char)) { + filtered.write(char); + cursor++; + if (i < newValue.selection.baseOffset) rawCursorPosition++; + } + } } + final cleanText = filtered.toString(); + final formatted = _applyMask(cleanText); + + // Calcula nova posição do cursor com base nos caracteres e a máscara + int newCursorPosition = rawCursorPosition; + if (newCursorPosition >= 2) newCursorPosition++; + if (newCursorPosition >= 5) newCursorPosition++; + if (newCursorPosition >= 8) newCursorPosition++; + if (newCursorPosition >= 12) newCursorPosition++; + return TextEditingValue( - text: newText.toString().toUpperCase(), - selection: TextSelection.collapsed(offset: selectionIndex), + text: formatted, + selection: TextSelection.collapsed( + offset: newCursorPosition.clamp(0, formatted.length), + ), ); } + + String _applyMask(String input) { + final buffer = StringBuffer(); + for (int i = 0; i < input.length; i++) { + if (i == 2 || i == 5) buffer.write('.'); + if (i == 8) buffer.write('/'); + if (i == 12) buffer.write('-'); + buffer.write(input[i]); + } + return buffer.toString(); + } + + bool _isAlphanumeric(String char) => _alphanumericRegex.hasMatch(char); + + bool _isDigit(String char) => _digitRegex.hasMatch(char); } diff --git a/test/src/formatters/cnpj_alfanumerico_input_formatter_test.dart b/test/src/formatters/cnpj_alfanumerico_input_formatter_test.dart index ec05093..5b237d7 100644 --- a/test/src/formatters/cnpj_alfanumerico_input_formatter_test.dart +++ b/test/src/formatters/cnpj_alfanumerico_input_formatter_test.dart @@ -19,12 +19,14 @@ void main() { test( 'limite 14 digitos alfanuméricos', - () => expect(evaluate('', 'ABBBBBBBBBBBB99'), ''), + () => expect(evaluate('', 'ABBBBBBBBBBBB99'), 'AB.BBB.BBB/BBBB-99'), ); test('backspace alfanumérico', () { expect( - evaluate('AA.BBB.CCC/DDDD-99', 'AABBBCCCDDDD9'), 'AA.BBB.CCC/DDDD-9'); + evaluate('AA.BBB.CCC/DDDD-99', 'AABBBCCCDDDD9'), + 'AA.BBB.CCC/DDDD-9', + ); expect(evaluate('AA.BBB.CCC/DDDD-9', 'AABBBCCCDDDD'), 'AA.BBB.CCC/DDDD'); expect(evaluate('AA.BBB.CCC/DDDD', 'AABBBCCCDDD'), 'AA.BBB.CCC/DDD'); expect(evaluate('AA.BBB.CCC/DDD', 'AABBBCCCDD'), 'AA.BBB.CCC/DD'); From 1dba758a7fe7161aaa9ead75882480dc6072967a Mon Sep 17 00:00:00 2001 From: Clodoaldo Ribeiro Date: Wed, 23 Jul 2025 17:37:00 -0300 Subject: [PATCH 2/3] CnpjAlfanumericoInputFormatter - Add doc --- .../cnpj_alfanumerico_input_formatter.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart b/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart index 24dc659..6992c90 100644 --- a/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart +++ b/lib/src/formatters/cnpj_alfanumerico_input_formatter.dart @@ -3,6 +3,20 @@ import 'package:brasil_fields/src/interfaces/compoundable_formatter.dart'; class CnpjAlfanumericoInputFormatter extends TextInputFormatter implements CompoundableFormatter { + /// Um [TextInputFormatter] personalizado para entrada de CNPJ com 14 caracteres, + /// onde os 12 primeiros aceitam letras maiúsculas e números (alfanuméricos), + /// e os 2 últimos aceitam apenas números. + /// + /// O valor digitado é automaticamente convertido para maiúsculas e formatado com a máscara: + /// `XX.XXX.XXX/XXXX-XX`, respeitando a posição dos separadores. + /// + /// Exemplo de entrada válida: `AB12CDE3F45678` + /// Resultado formatado: `AB.12C.DE3/F456-78` + /// + /// Caracteres inválidos são ignorados durante a digitação. + /// A posição do cursor é preservada com base na interação do usuário. + const CnpjAlfanumericoInputFormatter(); + @override int get maxLength => 14; From be2c944dc0a42248107bf0a5c45ba9c4cd54c99d Mon Sep 17 00:00:00 2001 From: Clodoaldo Ribeiro Date: Wed, 23 Jul 2025 17:51:37 -0300 Subject: [PATCH 3/3] example - Add const --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a7c6f1f..3bdcec1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -52,7 +52,7 @@ class MyApp extends StatelessWidget { label: 'CNPJ', formatter: CnpjInputFormatter(), ), - AlphanumericTextField( + const AlphanumericTextField( label: 'Novo CNPJ', formatter: CnpjAlfanumericoInputFormatter(), ),