Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,24 @@ final cnpjField = TextFormField(
| PIS/PASEP (NIS/NIT) | PisPasepInputFormatter() | 120.12345.67-2 |
| Placa | PlacaVeiculoInputFormatter() | AAA-1234 |
| Real | RealInputFormatter() | 20.550 |
| Telefone | TelefoneInputFormatter() | (99) 9999-9999 |
| Telefone fixo | TelefoneFixoInputFormatter() | (99) 9999-9999 |
| Celular | CelularInputFormatter() | (99) 99999-9999 |
| Telefone ou celular | TelefoneOuCelularInputFormatter() | Máscara conforme a quantidade de dígitos |
| Validade cartão | ValidadeCartaoInputFormatter() | 12/24 ou 12/2024 |
| Temperatura | TemperaturaInputFormatter() | 27,1 |

Os formatadores aplicam máscaras; não validam os documentos. Para verificar CPF, CNPJ, NUP ou PIS/PASEP, use os validadores em `UtilBrasilFields`.
Os formatadores aplicam máscaras; não substituem validadores. Para verificar CPF, CNPJ, NUP ou PIS/PASEP, use os validadores em `UtilBrasilFields`.

Para telefones, escolha `TelefoneFixoInputFormatter()` quando o campo aceitar somente números fixos e `CelularInputFormatter()` quando aceitar somente celulares. `TelefoneOuCelularInputFormatter()` escolhe a máscara conforme o tamanho da entrada. `TelefoneInputFormatter()` continua disponível com o comportamento automático legado e exige que celulares comecem por `9` após o DDD.

```dart
final celularField = TextFormField(
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
CelularInputFormatter(),
],
);
```

### Modelos

Expand Down
20 changes: 18 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,13 @@ class _FormatterDemo {
}
if (value is RealInputFormatter) return 'RealInputFormatter';
if (value is TelefoneInputFormatter) return 'TelefoneInputFormatter';
if (value is TelefoneFixoInputFormatter) {
return 'TelefoneFixoInputFormatter';
}
if (value is CelularInputFormatter) return 'CelularInputFormatter';
if (value is TelefoneOuCelularInputFormatter) {
return 'TelefoneOuCelularInputFormatter';
}
if (value is TemperaturaInputFormatter) {
return 'TemperaturaInputFormatter';
}
Expand Down Expand Up @@ -951,8 +958,17 @@ List<_FormatterDemo> _createFormatterDemos() => [
'0601064-21.2022.6.00.0000', NUPInputFormatter()),
_demo('cns', 'Documentos e identificadores', 'CNS', '000 1111 2222 3333',
CNSInputFormatter()),
_demo('telefone', 'Contato, tempo e mobilidade', 'Telefone',
'(11) 98765-4321', TelefoneInputFormatter()),
_demo('telefone-fixo', 'Contato, tempo e mobilidade', 'Telefone fixo',
'(11) 3456-7890', TelefoneFixoInputFormatter()),
_demo('celular', 'Contato, tempo e mobilidade', 'Celular',
'(11) 98765-4321', CelularInputFormatter()),
_demo(
'telefone-ou-celular',
'Contato, tempo e mobilidade',
'Telefone ou celular',
'Aceita 10 ou 11 dígitos',
TelefoneOuCelularInputFormatter(),
),
_demo('data', 'Contato, tempo e mobilidade', 'Data', '21/09/2026',
DataInputFormatter()),
_demo('hora', 'Contato, tempo e mobilidade', 'Hora', '14:35',
Expand Down
40 changes: 40 additions & 0 deletions example/test/example_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,46 @@ void main() {
expect(editable.controller.text, '12.345-678');
});

testWidgets('shows and formats fixed, mobile and automatic phones',
(tester) async {
await tester.pumpWidget(const BrasilFieldsApp());
await tester.pumpAndSettle();

await tester.enterText(
find.byKey(const Key('formatter-search')),
'mobilidade',
);
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const PageStorageKey('formatters-page')),
const Offset(0, -500),
);
await tester.pumpAndSettle();

final fixed = find.byKey(const Key('formatter-telefone-fixo'));
final mobile = find.byKey(const Key('formatter-celular'));
final automatic = find.byKey(const Key('formatter-telefone-ou-celular'));
expect(fixed, findsOneWidget);
expect(mobile, findsOneWidget);
expect(automatic, findsOneWidget);

await tester.enterText(fixed, '1134567890');
await tester.enterText(mobile, '11987654321');
await tester.enterText(automatic, '11987654321');
await tester.pump();

String textIn(Finder field) => tester
.widget<EditableText>(
find.descendant(of: field, matching: find.byType(EditableText)),
)
.controller
.text;

expect(textIn(fixed), '(11) 3456-7890');
expect(textIn(mobile), '(11) 98765-4321');
expect(textIn(automatic), '(11) 98765-4321');
});

testWidgets('search filters formatters and alphanumeric input is formatted',
(tester) async {
await tester.pumpWidget(const BrasilFieldsApp());
Expand Down
3 changes: 3 additions & 0 deletions lib/brasil_fields.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ library;

export 'src/formatters/altura_input_formatter.dart';
export 'src/formatters/cartao_bancario_input_formatter.dart';
export 'src/formatters/celular_input_formatter.dart';
export 'src/formatters/centavos_input_formatter.dart';
export 'src/formatters/cep_input_formatter.dart';
export 'src/formatters/cert_nascimento_input_formatter.dart';
Expand All @@ -23,6 +24,8 @@ export 'src/formatters/pis_pasep_input_formatter.dart';
export 'src/formatters/placa_veiculo_formatter.dart';
export 'src/formatters/real_input_formatter.dart';
export 'src/formatters/telefone_input_formatter.dart';
export 'src/formatters/telefone_fixo_input_formatter.dart';
export 'src/formatters/telefone_ou_celular_input_formatter.dart';
export 'src/formatters/temperatura_input_formatter.dart';
export 'src/formatters/validade_cartao_input_formatter.dart';
export 'src/modelos/estados.dart';
Expand Down
10 changes: 10 additions & 0 deletions lib/src/formatters/celular_input_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'telefone_mask_input_formatter.dart';

/// Formata celular com até 11 dígitos: `(XX) XXXXX-XXXX`.
class CelularInputFormatter extends TelefoneMaskInputFormatter {
@override
int get maxLength => 11;

@override
int get digitosAntesDoHifen => 5;
}
10 changes: 10 additions & 0 deletions lib/src/formatters/telefone_fixo_input_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'telefone_mask_input_formatter.dart';

/// Formata telefone fixo com até 10 dígitos: `(XX) XXXX-XXXX`.
class TelefoneFixoInputFormatter extends TelefoneMaskInputFormatter {
@override
int get maxLength => 10;

@override
int get digitosAntesDoHifen => 4;
}
62 changes: 14 additions & 48 deletions lib/src/formatters/telefone_input_formatter.dart
Original file line number Diff line number Diff line change
@@ -1,57 +1,23 @@
import 'package:flutter/services.dart';

/// Formata o valor do campo como telefone fixo ou celular.
import 'telefone_ou_celular_input_formatter.dart';

/// Formata telefone fixo ou celular conforme a quantidade de dígitos.
///
/// Usa a máscara `(XX) XXXX-XXXX` para 10 dígitos e `(XX) 9XXXX-XXXX` para
/// 11 dígitos. Nesse caso, exige `9` como o primeiro dígito do número.
class TelefoneInputFormatter extends TextInputFormatter {
/// Este nome legado mantém o comportamento automático existente. Para escolher
/// o tipo explicitamente, use `TelefoneFixoInputFormatter` ou
/// `CelularInputFormatter`. Para 11 dígitos, também mantém a regra histórica de
/// exigir `9` como primeiro dígito após o DDD.
class TelefoneInputFormatter extends TelefoneOuCelularInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (!newValue.composing.isCollapsed) return newValue;

if (newValue.text.length > 11) return oldValue;

final newValueLength = newValue.text.length;
if (newValueLength == 11) {
if (newValue.text[2] != '9') {
return oldValue;
}
if (newValue.text.length == 11 && newValue.text[2] != '9') {
return oldValue;
}

var posicaoCursor = newValue.selection.end;
var substrIndex = 0;
final valorFinal = StringBuffer();

if (newValueLength >= 1) {
valorFinal.write('(');
if (newValue.selection.end >= 1) posicaoCursor++;
}

if (newValueLength >= 3) {
valorFinal.write('${newValue.text.substring(0, substrIndex = 2)}) ');
if (newValue.selection.end >= 2) posicaoCursor += 2;
}

if (newValue.text.length == 11) {
if (newValueLength >= 8) {
valorFinal.write('${newValue.text.substring(2, substrIndex = 7)}-');
if (newValue.selection.end >= 7) posicaoCursor++;
}
} else {
if (newValueLength >= 7) {
valorFinal.write('${newValue.text.substring(2, substrIndex = 6)}-');
if (newValue.selection.end >= 6) posicaoCursor++;
}
}

if (newValueLength >= substrIndex) {
valorFinal.write(newValue.text.substring(substrIndex));
}

return TextEditingValue(
text: valorFinal.toString(),
selection: TextSelection.collapsed(offset: posicaoCursor),
);
return super.formatEditUpdate(oldValue, newValue);
}
}
65 changes: 65 additions & 0 deletions lib/src/formatters/telefone_mask_input_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/services.dart';

final _digitPattern = RegExp(r'\d');

/// Aplica uma máscara telefônica limitada a uma quantidade de dígitos.
abstract class TelefoneMaskInputFormatter extends TextInputFormatter {
/// Limite de dígitos aceitos pelo formatador.
int get maxLength;

/// Quantidade de dígitos do assinante antes do hífen.
int get digitosAntesDoHifen;

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (!newValue.composing.isCollapsed) return newValue;
if (newValue.text.length > maxLength) return oldValue;

var substrIndex = 0;
final valorFinal = StringBuffer();

if (newValue.text.isNotEmpty) {
valorFinal.write('(');
}

if (newValue.text.length >= 3) {
valorFinal.write('${newValue.text.substring(0, substrIndex = 2)}) ');
}

final limiteHifen = 2 + digitosAntesDoHifen;
if (newValue.text.length > limiteHifen) {
valorFinal.write(
'${newValue.text.substring(2, substrIndex = limiteHifen)}-',
);
}

if (newValue.text.length >= substrIndex) {
valorFinal.write(newValue.text.substring(substrIndex));
}

final textoFormatado = valorFinal.toString();
final selectionEnd = newValue.selection.end.clamp(0, newValue.text.length);
final digitosAntesDoCursor = _digitPattern
.allMatches(newValue.text.substring(0, selectionEnd))
.length;
var digitosPercorridos = 0;
var posicaoCursor = 0;
if (digitosAntesDoCursor > 0) {
for (var i = 0; i < textoFormatado.length; i++) {
if (_digitPattern.hasMatch(textoFormatado[i])) {
digitosPercorridos++;
posicaoCursor = i + 1;
if (digitosPercorridos == digitosAntesDoCursor) break;
}
}
}

return TextEditingValue(
text: textoFormatado,
selection: TextSelection.collapsed(offset: posicaoCursor),
);
}
}
24 changes: 24 additions & 0 deletions lib/src/formatters/telefone_ou_celular_input_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/services.dart';

import 'celular_input_formatter.dart';
import 'telefone_fixo_input_formatter.dart';

/// Formata telefone fixo ou celular conforme a quantidade de dígitos.
///
/// Até 10 dígitos, usa a máscara de telefone fixo. Com 11 dígitos, usa a
/// máscara de celular.
class TelefoneOuCelularInputFormatter extends TextInputFormatter {
final _telefoneFixo = TelefoneFixoInputFormatter();
final _celular = CelularInputFormatter();

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (!newValue.composing.isCollapsed) return newValue;
return newValue.text.length <= _telefoneFixo.maxLength
? _telefoneFixo.formatEditUpdate(oldValue, newValue)
: _celular.formatEditUpdate(oldValue, newValue);
}
}
Loading