├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── lib │ └── main.dart └── pubspec.yaml ├── lib └── mask_text_input_formatter.dart ├── pubspec.yaml └── test └── mask_text_input_formatter_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | ios/.generated/ 9 | ios/Flutter/Generated.xcconfig 10 | ios/Runner/GeneratedPluginRegistrant.* 11 | 12 | pubspec.lock 13 | .iml 14 | **/.idea/* 15 | .metadata 16 | *.iml 17 | analysis_options.yaml 18 | /example/android/ 19 | /example/ios/ 20 | /example/integration_test/ 21 | /doc/ 22 | /coverage/ 23 | /.fvm/ 24 | /private/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 7 | sources: 8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 9 | packages: 10 | - libstdc++6 11 | - fonts-droid-fallback 12 | before_script: 13 | - git clone https://github.com/flutter/flutter.git -b beta 14 | - ./flutter/bin/flutter doctor 15 | script: 16 | - flutter/bin/flutter pub get 17 | - flutter/bin/flutter test --coverage --coverage-path=coverage.lcov 18 | after_success: 19 | - bash <(curl -s https://codecov.io/bash) 20 | cache: 21 | directories: 22 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.9.0] 2 | Fixed #92 3 | 4 | ## [2.8.0] 5 | Added: `type` parameter to `updateMask` method 6 | Fixed #88 7 | 8 | ## [2.7.0] 9 | Fixed #86 10 | 11 | ## [2.6.0] 12 | Fixed #82 13 | Removed deprecated lints 14 | Added `eager` mask type constructor for shortening 15 | 16 | ## [2.5.0] 17 | Fixed: #70 18 | Added: `newValue` parameter to updateMask method 19 | 20 | ## [2.4.0] 21 | Fix #70 22 | 23 | ## [2.3.0] 24 | Fix #62 25 | 26 | ## [2.2.0] 27 | Fix eager autocompletion #68 28 | 29 | ## [2.1.0] 30 | Added support for eager autocompletion. Thanks to @jyardin 31 | 32 | ## [2.0.2] 33 | Add analysis_options 34 | 35 | ## [2.0.1] 36 | Fix #52 37 | 38 | ## [2.0.0] 39 | Stable version 40 | 41 | ## [1.2.1] / [2.0.0-nullsafety.2] 42 | Fix bug 43 | 44 | ## [1.2.0] / [2.0.0-nullsafety.1] 45 | Fix some bugs 46 | 47 | ## [2.0.0-nullsafety.0] 48 | Migrate to null safety 49 | 50 | ## [1.1.0] 51 | Added: clear function 52 | Added: function to get a current mask 53 | Added: functions to mask/unmask some text 54 | Updated example app 55 | Fixed some bugs 56 | 57 | ## [1.0.7] 58 | Fix clear text bug + test 59 | Some docs added 60 | 61 | ## [1.0.6] 62 | Fix bug 63 | 64 | ## [1.0.5] 65 | Minor changes 66 | 67 | ## [1.0.4] 68 | Minor changes 69 | 70 | ## [1.0.3] 71 | Fix paste 72 | 73 | ## [1.0.2] 74 | Update readme.md 75 | 76 | ## [1.0.1] 77 | Added tests 78 | Fix some bugs 79 | 80 | ## [1.0.0] 81 | Initial version 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sergey Smurov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mask_text_input_formatter 2 | 3 | [![Version](https://img.shields.io/pub/v/mask_text_input_formatter.svg)](https://pub.dev/packages/mask_text_input_formatter) [![Build Status](https://travis-ci.com/siqwin/mask_text_input_formatter.svg?branch=master)](https://travis-ci.com/siqwin/mask_text_input_formatter) [![codecov](https://codecov.io/gh/siqwin/mask_text_input_formatter/branch/master/graph/badge.svg)](https://codecov.io/gh/siqwin/mask_text_input_formatter) ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat) 4 | 5 | The package provides TextInputFormatter for TextField and TextFormField which format the input by a given mask. 6 | 7 | ![logo](https://user-images.githubusercontent.com/49272216/91583922-88393380-e95a-11ea-85d0-07e1bef1a4c1.png) 8 | 9 | ## Example 10 | 11 | Check 'example' folder for code sample 12 | 13 | ![example](https://user-images.githubusercontent.com/49272216/91583806-5de77600-e95a-11ea-8b13-7b2b85883513.gif) 14 | 15 | ## Usage 16 | 17 | 1. Follow the [install guide](https://pub.dev/packages/mask_text_input_formatter/install) 18 | 19 | 2. Import the library: 20 | 21 | ```dart 22 | import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; 23 | ``` 24 | 25 | 3. Create mask formatter: 26 | 27 | ```dart 28 | var maskFormatter = new MaskTextInputFormatter( 29 | mask: '+# (###) ###-##-##', 30 | filter: { "#": RegExp(r'[0-9]') }, 31 | type: MaskAutoCompletionType.lazy 32 | ); 33 | ``` 34 | 35 | 4. Set it to text field: 36 | 37 | ```dart 38 | TextField(inputFormatters: [maskFormatter]) 39 | ``` 40 | 41 | ## Get value 42 | 43 | Get masked text: 44 | 45 | ```dart 46 | print(maskFormatter.getMaskedText()); // -> "+0 (123) 456-78-90" 47 | ``` 48 | 49 | Get unmasked text: 50 | 51 | ```dart 52 | print(maskFormatter.getUnmaskedText()); // -> 01234567890 53 | ``` 54 | 55 | ## Change the mask 56 | 57 | You can use the `updateMask` method to change the mask after the formatter was created: 58 | 59 | ```dart 60 | var textEditingController = TextEditingController(text: "12345678"); 61 | var maskFormatter = new MaskTextInputFormatter(mask: '####-####', filter: { "#": RegExp(r'[0-9]') }); 62 | 63 | TextField(controller: textEditingController, inputFormatters: [maskFormatter]) // -> "1234-5678" 64 | 65 | textEditingController.value = maskFormatter.updateMask(mask: "##-##-##-##"); // -> "12-34-56-78" 66 | ``` 67 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; 4 | 5 | void main() => runApp(const MyApp()); 6 | 7 | class MyApp extends StatelessWidget { 8 | 9 | const MyApp({ Key? key }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return const MaterialApp( 14 | debugShowCheckedModeBanner: false, 15 | home: ExamplePage(), 16 | ); 17 | } 18 | 19 | } 20 | 21 | class ExamplePage extends StatefulWidget { 22 | 23 | const ExamplePage({ Key? key }) : super(key: key); 24 | 25 | @override 26 | ExamplePageState createState() => ExamplePageState(); 27 | 28 | } 29 | 30 | class ExampleMask { 31 | 32 | final TextEditingController textController = TextEditingController(); 33 | final MaskTextInputFormatter formatter; 34 | final FormFieldValidator? validator; 35 | final String hint; 36 | final TextInputType textInputType; 37 | 38 | ExampleMask({ 39 | required this.formatter, 40 | this.validator, 41 | required this.hint, 42 | required this.textInputType 43 | }); 44 | 45 | } 46 | 47 | class ExamplePageState extends State { 48 | 49 | final List examples = [ 50 | ExampleMask( 51 | formatter: MaskTextInputFormatter(mask: "+# (###) ###-##-##"), 52 | hint: "+1 (234) 567-89-01", 53 | textInputType: TextInputType.phone 54 | ), 55 | ExampleMask( 56 | formatter: MaskTextInputFormatter(mask: "+# (###) ###-##-##", type: MaskAutoCompletionType.eager), 57 | hint: "+1 (234) 567-89-01 (eager type)", 58 | textInputType: TextInputType.phone, 59 | ), 60 | ExampleMask( 61 | formatter: MaskTextInputFormatter(mask: "##/##/####"), 62 | hint: "31/12/2020", 63 | textInputType: TextInputType.phone, 64 | validator: (value) { 65 | if (value == null || value.isEmpty) { 66 | return null; 67 | } 68 | final components = value.split("/"); 69 | if (components.length == 3) { 70 | final day = int.tryParse(components[0]); 71 | final month = int.tryParse(components[1]); 72 | final year = int.tryParse(components[2]); 73 | if (day != null && month != null && year != null) { 74 | final date = DateTime(year, month, day); 75 | if (date.year == year && date.month == month && date.day == day) { 76 | return null; 77 | } 78 | } 79 | } 80 | return "wrong date"; 81 | } 82 | ), 83 | ExampleMask( 84 | formatter: MaskTextInputFormatter(mask: "(AA) ####-####"), 85 | hint: "(AB) 1234-5678", 86 | textInputType: TextInputType.text, 87 | ), 88 | ExampleMask( 89 | formatter: MaskTextInputFormatter(mask: "####.AAAAAA/####-####"), 90 | hint: "1234.ABCDEF/2019-2020", 91 | textInputType: TextInputType.text, 92 | ), 93 | ExampleMask( 94 | formatter: SpecialMaskTextInputFormatter(), 95 | hint: "A.1234 or B.123456", 96 | textInputType: TextInputType.text, 97 | ), 98 | ]; 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return Scaffold( 103 | backgroundColor: Colors.grey.shade200, 104 | body: SafeArea( 105 | child: ListView( 106 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), 107 | children: examples.map(buildTextField).toList() 108 | ) 109 | ) 110 | ); 111 | } 112 | 113 | Widget buildTextField(ExampleMask example) { 114 | return Padding( 115 | padding: const EdgeInsets.symmetric(vertical: 8.0), 116 | child: Stack( 117 | children: [ 118 | TextFormField( 119 | controller: example.textController, 120 | inputFormatters: [const UpperCaseTextFormatter(), example.formatter], 121 | autocorrect: false, 122 | keyboardType: example.textInputType, 123 | autovalidateMode: AutovalidateMode.always, 124 | validator: example.validator, 125 | decoration: InputDecoration( 126 | hintText: example.hint, 127 | hintStyle: const TextStyle(color: Colors.grey), 128 | fillColor: Colors.white, 129 | filled: true, 130 | focusedBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.green)), 131 | enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.blue)), 132 | errorBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.red)), 133 | border: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.green)), 134 | errorMaxLines: 1 135 | ) 136 | ), 137 | Positioned( 138 | right: 0, 139 | top: 0, 140 | child: SizedBox( 141 | width: 48, 142 | height: 48, 143 | child: Material( 144 | type: MaterialType.transparency, 145 | child: InkWell( 146 | borderRadius: const BorderRadius.all(Radius.circular(24)), 147 | child: const Icon(Icons.clear, color: Colors.grey, size: 24), 148 | onTap: () => example.textController.clear() 149 | ), 150 | ) 151 | ), 152 | ) 153 | ], 154 | ), 155 | ); 156 | } 157 | } 158 | 159 | class UpperCaseTextFormatter implements TextInputFormatter { 160 | 161 | const UpperCaseTextFormatter(); 162 | 163 | @override 164 | TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { 165 | return TextEditingValue(text: newValue.text.toUpperCase(), selection: newValue.selection); 166 | } 167 | 168 | } 169 | 170 | class SpecialMaskTextInputFormatter extends MaskTextInputFormatter { 171 | 172 | static String maskA = "S.####"; 173 | static String maskB = "S.######"; 174 | 175 | SpecialMaskTextInputFormatter({ 176 | String? initialText 177 | }): super( 178 | mask: maskA, 179 | filter: {"#": RegExp('[0-9]'), "S": RegExp('[AB]')}, 180 | initialText: initialText 181 | ); 182 | 183 | @override 184 | TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { 185 | if (newValue.text.startsWith("A")) { 186 | if (getMask() != maskA) { 187 | updateMask(mask: maskA); 188 | } 189 | } else { 190 | if (getMask() != maskB) { 191 | updateMask(mask: maskB); 192 | } 193 | } 194 | return super.formatEditUpdate(oldValue, newValue); 195 | } 196 | 197 | } 198 | 199 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mask_text_input_formatter_example 2 | description: Mask Text Input Formatter Example 3 | version: 1.0.0 4 | publish_to: none 5 | homepage: https://github.com/siqwin/mask_text_input_formatter 6 | 7 | environment: 8 | sdk: ">=2.12.0 <4.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | mask_text_input_formatter: 14 | path: ../ 15 | 16 | flutter: 17 | uses-material-design: true 18 | 19 | dev_dependencies: 20 | flutter_test: 21 | sdk: flutter 22 | integration_test: 23 | sdk: flutter -------------------------------------------------------------------------------- /lib/mask_text_input_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | enum MaskAutoCompletionType { 6 | lazy, 7 | eager, 8 | } 9 | 10 | class MaskTextInputFormatter implements TextInputFormatter { 11 | 12 | MaskAutoCompletionType _type; 13 | MaskAutoCompletionType get type => _type; 14 | 15 | String? _mask; 16 | List _maskChars = []; 17 | Map? _maskFilter; 18 | 19 | int _maskLength = 0; 20 | final _TextMatcher _resultTextArray = _TextMatcher(); 21 | String _resultTextMasked = ""; 22 | 23 | /// Create the [mask] formatter for TextField 24 | /// 25 | /// The keys of the [filter] assign which character in the mask should be replaced and the values validate the entered character 26 | /// By default `#` match to the number and `A` to the letter 27 | /// 28 | /// Set [type] for autocompletion behavior: 29 | /// - [MaskAutoCompletionType.lazy] (default): autocomplete unfiltered characters once the following filtered character is input. 30 | /// For example, with the mask "#/#" and the sequence of characters "1" then "2", the formatter will output "1", then "1/2" 31 | /// - [MaskAutoCompletionType.eager]: autocomplete unfiltered characters when the previous filtered character is input. 32 | /// For example, with the mask "#/#" and the sequence of characters "1" then "2", the formatter will output "1/", then "1/2" 33 | MaskTextInputFormatter({ 34 | String? mask, 35 | Map? filter, 36 | String? initialText, 37 | MaskAutoCompletionType type = MaskAutoCompletionType.lazy, 38 | }): _type = type { 39 | updateMask( 40 | mask: mask, 41 | filter: filter ?? {"#": RegExp('[0-9]'), "A": RegExp('[^0-9]')}, 42 | newValue: initialText == null ? null : TextEditingValue(text: initialText, selection: TextSelection.collapsed(offset: initialText.length)) 43 | ); 44 | } 45 | 46 | /// Create the eager [mask] formatter for TextField 47 | MaskTextInputFormatter.eager({ 48 | String? mask, 49 | Map? filter, 50 | String? initialText, 51 | }): this( 52 | mask: mask, 53 | filter: filter, 54 | initialText: initialText, 55 | type: MaskAutoCompletionType.eager 56 | ); 57 | 58 | /// Change the mask 59 | TextEditingValue updateMask({ String? mask, Map? filter, MaskAutoCompletionType? type, TextEditingValue? newValue}) { 60 | _mask = mask; 61 | if (filter != null) { 62 | _updateFilter(filter); 63 | } 64 | if (type != null) { 65 | _type = type; 66 | } 67 | _calcMaskLength(); 68 | TextEditingValue? targetValue = newValue; 69 | if (targetValue == null) { 70 | final unmaskedText = getUnmaskedText(); 71 | targetValue = TextEditingValue(text: unmaskedText, selection: TextSelection.collapsed(offset: unmaskedText.length)); 72 | } 73 | clear(); 74 | return formatEditUpdate(TextEditingValue.empty, targetValue); 75 | } 76 | 77 | /// Get current mask 78 | String? getMask() { 79 | return _mask; 80 | } 81 | 82 | /// Get masked text, e.g. "+0 (123) 456-78-90" 83 | String getMaskedText() { 84 | return _resultTextMasked; 85 | } 86 | 87 | /// Get unmasked text, e.g. "01234567890" 88 | String getUnmaskedText() { 89 | return _resultTextArray.toString(); 90 | } 91 | 92 | /// Check if target mask is filled 93 | bool isFill() { 94 | return _resultTextArray.length == _maskLength; 95 | } 96 | 97 | /// Clear masked text of the formatter 98 | /// Note: you need to call this method if you clear the text of the TextField because it doesn't call the formatter when it has empty text 99 | void clear() { 100 | _resultTextMasked = ""; 101 | _resultTextArray.clear(); 102 | } 103 | 104 | /// Mask some text 105 | String maskText(String text) { 106 | return MaskTextInputFormatter(mask: _mask, filter: _maskFilter, initialText: text).getMaskedText(); 107 | } 108 | 109 | /// Unmask some text 110 | String unmaskText(String text) { 111 | return MaskTextInputFormatter(mask: _mask, filter: _maskFilter, initialText: text).getUnmaskedText(); 112 | } 113 | 114 | @override 115 | TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { 116 | final mask = _mask; 117 | 118 | if (mask == null || mask.isEmpty == true) { 119 | _resultTextMasked = newValue.text; 120 | _resultTextArray.set(newValue.text); 121 | return newValue; 122 | } 123 | 124 | if (oldValue.text.isEmpty) { 125 | _resultTextArray.clear(); 126 | } 127 | 128 | final beforeText = oldValue.text; 129 | final afterText = newValue.text; 130 | 131 | final beforeSelection = oldValue.selection; 132 | final afterSelection = newValue.selection; 133 | 134 | var beforeSelectionStart = afterSelection.isValid ? beforeSelection.isValid ? beforeSelection.start : 0 : 0; 135 | 136 | for (var i = 0; i < beforeSelectionStart && i < beforeText.length && i < afterText.length; i++) { 137 | if (beforeText[i] != afterText[i]) { 138 | beforeSelectionStart = i; 139 | break; 140 | } 141 | } 142 | 143 | final beforeSelectionLength = afterSelection.isValid ? beforeSelection.isValid ? beforeSelection.end - beforeSelectionStart : 0 : oldValue.text.length; 144 | 145 | final lengthDifference = afterText.length - (beforeText.length - beforeSelectionLength); 146 | final lengthRemoved = lengthDifference < 0 ? lengthDifference.abs() : 0; 147 | final lengthAdded = lengthDifference > 0 ? lengthDifference : 0; 148 | 149 | final afterChangeStart = max(0, beforeSelectionStart - lengthRemoved); 150 | final afterChangeEnd = max(0, afterChangeStart + lengthAdded); 151 | 152 | final beforeReplaceStart = max(0, beforeSelectionStart - lengthRemoved); 153 | final beforeReplaceLength = beforeSelectionLength + lengthRemoved; 154 | 155 | final beforeResultTextLength = _resultTextArray.length; 156 | 157 | var currentResultTextLength = _resultTextArray.length; 158 | var currentResultSelectionStart = 0; 159 | var currentResultSelectionLength = 0; 160 | 161 | for (var i = 0; i < min(beforeReplaceStart + beforeReplaceLength, mask.length); i++) { 162 | if (_maskChars.contains(mask[i]) && currentResultTextLength > 0) { 163 | currentResultTextLength -= 1; 164 | if (i < beforeReplaceStart) { 165 | currentResultSelectionStart += 1; 166 | } 167 | if (i >= beforeReplaceStart) { 168 | currentResultSelectionLength += 1; 169 | } 170 | } 171 | } 172 | 173 | final replacementText = afterText.substring(afterChangeStart, afterChangeEnd); 174 | var targetCursorPosition = currentResultSelectionStart; 175 | if (replacementText.isEmpty) { 176 | _resultTextArray.removeRange(currentResultSelectionStart, currentResultSelectionStart + currentResultSelectionLength); 177 | } else { 178 | if (currentResultSelectionLength > 0) { 179 | _resultTextArray.removeRange(currentResultSelectionStart, currentResultSelectionStart + currentResultSelectionLength); 180 | currentResultSelectionLength = 0; 181 | } 182 | _resultTextArray.insert(currentResultSelectionStart, replacementText); 183 | targetCursorPosition += replacementText.length; 184 | } 185 | 186 | if (beforeResultTextLength == 0 && _resultTextArray.length > 1) { 187 | var prefixLength = 0; 188 | for (var i = 0; i < mask.length; i++) { 189 | if (_maskChars.contains(mask[i])) { 190 | prefixLength = i; 191 | break; 192 | } 193 | } 194 | if (prefixLength > 0) { 195 | final resultPrefix = _resultTextArray._symbolArray.take(prefixLength).toList(); 196 | final effectivePrefixLength = min(_resultTextArray.length, resultPrefix.length); 197 | for (var j = 0; j < effectivePrefixLength; j++) { 198 | if (mask[j] != resultPrefix[j]) { 199 | _resultTextArray.removeRange(0, j); 200 | break; 201 | } 202 | if (j == effectivePrefixLength - 1) { 203 | _resultTextArray.removeRange(0, effectivePrefixLength); 204 | break; 205 | } 206 | } 207 | } 208 | } 209 | 210 | var curTextPos = 0; 211 | var maskPos = 0; 212 | _resultTextMasked = ""; 213 | var cursorPos = -1; 214 | var nonMaskedCount = 0; 215 | var maskInside = 0; 216 | 217 | while (maskPos < mask.length) { 218 | final curMaskChar = mask[maskPos]; 219 | final isMaskChar = _maskChars.contains(curMaskChar); 220 | 221 | var curTextInRange = curTextPos < _resultTextArray.length; 222 | 223 | String? curTextChar; 224 | if (isMaskChar && curTextInRange) { 225 | if (maskInside > 0) { 226 | _resultTextArray.removeRange(curTextPos - maskInside, curTextPos); 227 | curTextPos -= maskInside; 228 | } 229 | maskInside = 0; 230 | while (curTextChar == null && curTextInRange) { 231 | final potentialTextChar = _resultTextArray[curTextPos]; 232 | if (_maskFilter?[curMaskChar]?.hasMatch(potentialTextChar) ?? false) { 233 | curTextChar = potentialTextChar; 234 | } else { 235 | _resultTextArray.removeAt(curTextPos); 236 | curTextInRange = curTextPos < _resultTextArray.length; 237 | if (curTextPos <= targetCursorPosition) { 238 | targetCursorPosition -= 1; 239 | } 240 | } 241 | } 242 | } else if (!isMaskChar && !curTextInRange && type == MaskAutoCompletionType.eager) { 243 | curTextInRange = true; 244 | } 245 | 246 | if (isMaskChar && curTextInRange && curTextChar != null) { 247 | _resultTextMasked += curTextChar; 248 | if (curTextPos == targetCursorPosition && cursorPos == -1) { 249 | cursorPos = maskPos - nonMaskedCount; 250 | } 251 | nonMaskedCount = 0; 252 | curTextPos += 1; 253 | } else { 254 | if (!curTextInRange) { 255 | if (maskInside > 0) { 256 | curTextPos -= maskInside; 257 | maskInside = 0; 258 | nonMaskedCount = 0; 259 | continue; 260 | } else { 261 | break; 262 | } 263 | } else { 264 | _resultTextMasked += mask[maskPos]; 265 | if (!isMaskChar && curTextPos < _resultTextArray.length && curMaskChar == _resultTextArray[curTextPos]) { 266 | if (type == MaskAutoCompletionType.lazy && lengthAdded <= 1) { 267 | } else { 268 | maskInside++; 269 | curTextPos++; 270 | } 271 | } else if (maskInside > 0) { 272 | curTextPos -= maskInside; 273 | maskInside = 0; 274 | } 275 | } 276 | 277 | if (curTextPos == targetCursorPosition && cursorPos == -1 && !curTextInRange) { 278 | cursorPos = maskPos; 279 | } 280 | 281 | if (type == MaskAutoCompletionType.lazy || lengthRemoved > 0 || currentResultSelectionLength > 0 || beforeReplaceLength > 0) { 282 | nonMaskedCount++; 283 | } 284 | } 285 | 286 | maskPos++; 287 | } 288 | 289 | if (nonMaskedCount > 0) { 290 | _resultTextMasked = _resultTextMasked.substring(0, _resultTextMasked.length - nonMaskedCount); 291 | cursorPos -= nonMaskedCount; 292 | } 293 | 294 | if (_resultTextArray.length > _maskLength) { 295 | _resultTextArray.removeRange(_maskLength, _resultTextArray.length); 296 | } 297 | 298 | final finalCursorPosition = cursorPos < 0 ? _resultTextMasked.length : cursorPos; 299 | 300 | return TextEditingValue( 301 | text: _resultTextMasked, 302 | selection: TextSelection( 303 | baseOffset: finalCursorPosition, 304 | extentOffset: finalCursorPosition, 305 | affinity: newValue.selection.affinity, 306 | isDirectional: newValue.selection.isDirectional 307 | ) 308 | ); 309 | } 310 | 311 | void _calcMaskLength() { 312 | _maskLength = 0; 313 | final mask = _mask; 314 | if (mask != null) { 315 | for (var i = 0; i < mask.length; i++) { 316 | if (_maskChars.contains(mask[i])) { 317 | _maskLength++; 318 | } 319 | } 320 | } 321 | } 322 | 323 | void _updateFilter(Map filter) { 324 | _maskFilter = filter; 325 | _maskChars = _maskFilter?.keys.toList(growable: false) ?? []; 326 | } 327 | } 328 | 329 | class _TextMatcher { 330 | 331 | final List _symbolArray = []; 332 | 333 | int get length => _symbolArray.fold(0, (prev, match) => prev + match.length); 334 | 335 | void removeRange(int start, int end) => _symbolArray.removeRange(start, end); 336 | 337 | void insert(int start, String substring) { 338 | for (var i = 0; i < substring.length; i++) { 339 | _symbolArray.insert(start + i, substring[i]); 340 | } 341 | } 342 | 343 | void removeAt(int index) => _symbolArray.removeAt(index); 344 | 345 | String operator[](int index) => _symbolArray[index]; 346 | 347 | void clear() => _symbolArray.clear(); 348 | 349 | @override 350 | String toString() => _symbolArray.join(); 351 | 352 | void set(String text) { 353 | _symbolArray.clear(); 354 | for (var i = 0; i < text.length; i++) { 355 | _symbolArray.add(text[i]); 356 | } 357 | } 358 | 359 | } 360 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mask_text_input_formatter 2 | description: The package provides TextInputFormatter for TextField and TextFormField which format the input by a given mask. 3 | version: 2.9.0 4 | homepage: https://github.com/siqwin/mask_text_input_formatter 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | dev_dependencies: 14 | flutter_test: 15 | sdk: flutter -------------------------------------------------------------------------------- /test/mask_text_input_formatter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; 6 | 7 | void main() { 8 | 9 | group("Mask Text Input Formatter Tests", () { 10 | 11 | test('Typing 1', () { 12 | const phone = "01234567890"; 13 | const expectResult = "+0 (123) 456-78-90"; 14 | 15 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 16 | var currentTextEditingValue = TextEditingValue.empty; 17 | for (var i = 0; i < phone.length; i++) { 18 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue(text: currentTextEditingValue.text + phone[i], selection: TextSelection.collapsed(offset: currentTextEditingValue.text.length + 1))); 19 | expect(expectResult.startsWith(currentTextEditingValue.text), true); 20 | expect(maskTextInputFormatter.isFill(), i == phone.length - 1); 21 | expect(maskTextInputFormatter.getUnmaskedText(), phone.substring(0, i + 1)); 22 | expect(maskTextInputFormatter.getMaskedText(), currentTextEditingValue.text); 23 | } 24 | }); 25 | 26 | test('Typing 2', () { 27 | const typing = "123"; 28 | const expectResult = "123123"; 29 | 30 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "123###"); 31 | var currentTextEditingValue = TextEditingValue.empty; 32 | for (var i = 0; i < typing.length; i++) { 33 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue(text: currentTextEditingValue.text + typing[i], selection: TextSelection.collapsed(offset: currentTextEditingValue.text.length + 1))); 34 | expect(expectResult.startsWith(currentTextEditingValue.text), true, reason: "$expectResult not starts with ${currentTextEditingValue.text}"); 35 | expect(maskTextInputFormatter.getUnmaskedText(), typing.substring(0, i + 1)); 36 | expect(maskTextInputFormatter.getMaskedText(), currentTextEditingValue.text); 37 | expect(maskTextInputFormatter.isFill(), i == typing.length - 1); 38 | } 39 | }); 40 | 41 | test('Typing 4', () { 42 | const mask = "0######"; 43 | const typing = "000000"; 44 | const expectResult = "0000000"; 45 | 46 | final maskTextInputFormatter = MaskTextInputFormatter(mask: mask); 47 | var currentTextEditingValue = const TextEditingValue(selection: TextSelection.collapsed(offset: 0)); 48 | for (var i = 0; i < typing.length; i++) { 49 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue(text: currentTextEditingValue.text + typing[i], selection: TextSelection.collapsed(offset: currentTextEditingValue.text.length + 1))); 50 | expect(expectResult.startsWith(currentTextEditingValue.text), true, reason: "$expectResult not starts with ${currentTextEditingValue.text}"); 51 | expect(maskTextInputFormatter.isFill(), i == typing.length - 1); 52 | expect(maskTextInputFormatter.getUnmaskedText(), typing.substring(0, i + 1)); 53 | expect(maskTextInputFormatter.getMaskedText(), currentTextEditingValue.text); 54 | } 55 | }); 56 | 57 | test('Typing 5', () { 58 | const mask = "###1###"; 59 | const typing = "111111"; 60 | const expectResult = "1111111"; 61 | 62 | final maskTextInputFormatter = MaskTextInputFormatter(mask: mask); 63 | var currentTextEditingValue = const TextEditingValue(selection: TextSelection.collapsed(offset: 0)); 64 | for (var i = 0; i < typing.length; i++) { 65 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue(text: currentTextEditingValue.text + typing[i], selection: TextSelection.collapsed(offset: currentTextEditingValue.text.length + 1))); 66 | expect(expectResult.startsWith(currentTextEditingValue.text), true, reason: "$expectResult not starts with ${currentTextEditingValue.text}"); 67 | expect(maskTextInputFormatter.isFill(), i == typing.length - 1); 68 | expect(maskTextInputFormatter.getUnmaskedText(), typing.substring(0, i + 1)); 69 | expect(maskTextInputFormatter.getMaskedText(), currentTextEditingValue.text); 70 | } 71 | }); 72 | 73 | test('Typing 3', () { 74 | const typing = "321"; 75 | const expectResult = "123321321"; 76 | 77 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "123###321"); 78 | var currentTextEditingValue = TextEditingValue.empty; 79 | for (var i = 0; i < typing.length; i++) { 80 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue(text: currentTextEditingValue.text + typing[i], selection: TextSelection.collapsed(offset: currentTextEditingValue.text.length + 1))); 81 | expect(expectResult.startsWith(currentTextEditingValue.text), true, reason: "$expectResult not starts with ${currentTextEditingValue.text}"); 82 | expect(maskTextInputFormatter.getMaskedText(), currentTextEditingValue.text); 83 | expect(maskTextInputFormatter.getUnmaskedText(), typing.substring(0, i + 1)); 84 | expect(maskTextInputFormatter.isFill(), i == typing.length - 1); 85 | } 86 | }); 87 | 88 | test('Insert - Start', () { 89 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 90 | final currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567", selection: TextSelection.collapsed(offset: 8))); 91 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-7", selection: TextSelection.collapsed(offset: 14))); 92 | expect(maskTextInputFormatter.isFill(), false); 93 | expect(maskTextInputFormatter.getUnmaskedText(), "01234567"); 94 | expect(maskTextInputFormatter.getMaskedText(), "+0 (123) 456-7"); 95 | }); 96 | 97 | test('Insert - End', () { 98 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 99 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567", selection: TextSelection.collapsed(offset: 8))); 100 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-7890", selection: TextSelection.collapsed(offset: 18))); 101 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-78-90", selection: TextSelection.collapsed(offset: 18))); 102 | expect(maskTextInputFormatter.isFill(), true); 103 | expect(maskTextInputFormatter.getUnmaskedText(), "01234567890"); 104 | expect(maskTextInputFormatter.getMaskedText(), "+0 (123) 456-78-90"); 105 | }); 106 | 107 | test('Insert - Overflow', () { 108 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 109 | final currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567890123456", selection: TextSelection.collapsed(offset: 18))); 110 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-78-90", selection: TextSelection.collapsed(offset: 18))); 111 | expect(maskTextInputFormatter.isFill(), true); 112 | expect(maskTextInputFormatter.getUnmaskedText(), "01234567890"); 113 | expect(maskTextInputFormatter.getMaskedText(), "+0 (123) 456-78-90"); 114 | }); 115 | 116 | test('Insert - Overflow - 2', () { 117 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "###"); 118 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "123", selection: TextSelection.collapsed(offset: 3))); 119 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "1234", selection: TextSelection.collapsed(offset: 4))); 120 | expect(currentTextEditingValue, const TextEditingValue(text: "123", selection: TextSelection.collapsed(offset: 3))); 121 | expect(maskTextInputFormatter.isFill(), true); 122 | expect(maskTextInputFormatter.getUnmaskedText(), "123"); 123 | expect(maskTextInputFormatter.getMaskedText(), "123"); 124 | }); 125 | 126 | test('Insert - Incorrect symbols', () { 127 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 128 | final currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "0 (123) 456-78-90", selection: TextSelection.collapsed(offset: 18))); 129 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-78-90", selection: TextSelection.collapsed(offset: 18))); 130 | expect(maskTextInputFormatter.isFill(), true); 131 | expect(maskTextInputFormatter.getUnmaskedText(), "01234567890"); 132 | expect(maskTextInputFormatter.getMaskedText(), "+0 (123) 456-78-90"); 133 | }); 134 | 135 | test('Insert without prefix', () { 136 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+998 (••) ••• •• ••", filter: {"•": RegExp('[0-9]')}) 137 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "909006053", selection: TextSelection.collapsed(offset: 9))); 138 | expect(maskTextInputFormatter.getUnmaskedText(), "909006053"); 139 | expect(maskTextInputFormatter.getMaskedText(), "+998 (90) 900 60 53"); 140 | }); 141 | 142 | test('Remove - Part - 1', () { 143 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 144 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567890", selection: TextSelection.collapsed(offset: 11))); 145 | currentTextEditingValue = TextEditingValue(text: currentTextEditingValue.text, selection: const TextSelection(baseOffset: 5, extentOffset: 7)); 146 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "+0 (1) 456-78-90", selection: TextSelection.collapsed(offset: 5))); 147 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (145) 678-90", selection: TextSelection.collapsed(offset: 5))); 148 | expect(maskTextInputFormatter.isFill(), false); 149 | expect(maskTextInputFormatter.getUnmaskedText(), "014567890"); 150 | expect(maskTextInputFormatter.getMaskedText(), "+0 (145) 678-90"); 151 | }); 152 | 153 | test('Remove - Part - 2', () { 154 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "(###) ###-##-##"); 155 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "2555555", selection: TextSelection.collapsed(offset: 11))); 156 | currentTextEditingValue = TextEditingValue(text: currentTextEditingValue.text, selection: const TextSelection.collapsed(offset: 11)); 157 | 158 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "(255) 555-", selection: TextSelection.collapsed(offset: 10))); 159 | expect(currentTextEditingValue, const TextEditingValue(text: "(255) 555", selection: TextSelection.collapsed(offset: 9))); 160 | expect(maskTextInputFormatter.getUnmaskedText(), "255555"); 161 | expect(maskTextInputFormatter.getMaskedText(), "(255) 555"); 162 | 163 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "(255) 55", selection: TextSelection.collapsed(offset: 8))); 164 | expect(currentTextEditingValue, const TextEditingValue(text: "(255) 55", selection: TextSelection.collapsed(offset: 8))); 165 | expect(maskTextInputFormatter.getUnmaskedText(), "25555"); 166 | expect(maskTextInputFormatter.getMaskedText(), "(255) 55"); 167 | }); 168 | 169 | test('Remove - Part - 3', () { 170 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 171 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567890", selection: TextSelection.collapsed(offset: 11))); 172 | currentTextEditingValue = TextEditingValue(text: currentTextEditingValue.text, selection: const TextSelection(baseOffset: 5, extentOffset: 10)); 173 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "+0 (156-78-90", selection: TextSelection.collapsed(offset: 5))); 174 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (156) 789-0", selection: TextSelection.collapsed(offset: 5))); 175 | expect(maskTextInputFormatter.isFill(), false); 176 | expect(maskTextInputFormatter.getUnmaskedText(), "01567890"); 177 | expect(maskTextInputFormatter.getMaskedText(), "+0 (156) 789-0"); 178 | }); 179 | 180 | test('Remove - Part - 4', () { 181 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "## ## ##"); 182 | var textEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "123", selection: TextSelection.collapsed(offset: 3))); 183 | textEditingValue = maskTextInputFormatter.formatEditUpdate(textEditingValue, const TextEditingValue(text: "11", selection: TextSelection.collapsed(offset: 2))); 184 | expect(textEditingValue, const TextEditingValue(text: "11", selection: TextSelection.collapsed(offset: 2))); 185 | expect(maskTextInputFormatter.isFill(), false); 186 | expect(maskTextInputFormatter.getUnmaskedText(), "11"); 187 | expect(maskTextInputFormatter.getMaskedText(), "11"); 188 | 189 | textEditingValue = maskTextInputFormatter.formatEditUpdate(textEditingValue, const TextEditingValue(text: "123", selection: TextSelection.collapsed(offset: 3))); 190 | expect(textEditingValue.text, "12 3"); 191 | expect(maskTextInputFormatter.getUnmaskedText(), "123"); 192 | expect(maskTextInputFormatter.getMaskedText(), "12 3"); 193 | 194 | textEditingValue = maskTextInputFormatter.formatEditUpdate(textEditingValue, const TextEditingValue(text: "555555", selection: TextSelection.collapsed(offset: 6))); 195 | expect(textEditingValue.text, "55 55 55"); 196 | expect(maskTextInputFormatter.getUnmaskedText(), "555555"); 197 | expect(maskTextInputFormatter.getMaskedText(), "55 55 55"); 198 | 199 | textEditingValue = maskTextInputFormatter.formatEditUpdate(textEditingValue, const TextEditingValue(text: "555333", selection: TextSelection.collapsed(offset: 6))); 200 | expect(textEditingValue.text, "55 53 33"); 201 | expect(maskTextInputFormatter.getUnmaskedText(), "555333"); 202 | expect(maskTextInputFormatter.getMaskedText(), "55 53 33"); 203 | 204 | textEditingValue = maskTextInputFormatter.formatEditUpdate(textEditingValue, const TextEditingValue(text: "333555", selection: TextSelection.collapsed(offset: 6))); 205 | expect(textEditingValue.text, "33 35 55"); 206 | expect(maskTextInputFormatter.getUnmaskedText(), "333555"); 207 | expect(maskTextInputFormatter.getMaskedText(), "33 35 55"); 208 | }); 209 | 210 | test('Remove - All', () { 211 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 212 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567890", selection: TextSelection.collapsed(offset: 11))); 213 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, TextEditingValue.empty); 214 | expect(maskTextInputFormatter.isFill(), false); 215 | expect(maskTextInputFormatter.getUnmaskedText(), ""); 216 | expect(maskTextInputFormatter.getMaskedText(), ""); 217 | }); 218 | 219 | test('Replace - Part - 1', () { 220 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 221 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "01234567890", selection: TextSelection.collapsed(offset: 11))); 222 | currentTextEditingValue = TextEditingValue(text: currentTextEditingValue.text, selection: const TextSelection(baseOffset: 4, extentOffset: 7)); 223 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "+0 (132) 456-78-90", selection: TextSelection.collapsed(offset: 7))); 224 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (132) 456-78-90", selection: TextSelection.collapsed(offset: 7))); 225 | expect(maskTextInputFormatter.isFill(), true); 226 | expect(maskTextInputFormatter.getUnmaskedText(), "01324567890"); 227 | expect(maskTextInputFormatter.getMaskedText(), "+0 (132) 456-78-90"); 228 | }); 229 | 230 | test('Replace - Part - 2', () { 231 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 232 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "04321567890", selection: TextSelection.collapsed(offset: 11))); 233 | currentTextEditingValue = TextEditingValue(text: currentTextEditingValue.text, selection: const TextSelection(baseOffset: 4, extentOffset: 10)); 234 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "+0 (123456-78-90", selection: TextSelection.collapsed(offset: 10))); 235 | expect(currentTextEditingValue, const TextEditingValue(text: "+0 (123) 456-78-90", selection: TextSelection.collapsed(offset: 10))); 236 | expect(maskTextInputFormatter.isFill(), true); 237 | expect(maskTextInputFormatter.getUnmaskedText(), "01234567890"); 238 | expect(maskTextInputFormatter.getMaskedText(), "+0 (123) 456-78-90"); 239 | }); 240 | 241 | test('Replace - Part - 3', () { 242 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+# (###) ###-##-##"); 243 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "12223334455", selection: TextSelection.collapsed(offset: 11))); 244 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(currentTextEditingValue, const TextEditingValue(text: "54443332211")); 245 | expect(currentTextEditingValue, const TextEditingValue(text: "+5 (444) 333-22-11", selection: TextSelection.collapsed(offset: 18))); 246 | expect(maskTextInputFormatter.isFill(), true); 247 | expect(maskTextInputFormatter.getUnmaskedText(), "54443332211"); 248 | expect(maskTextInputFormatter.getMaskedText(), "+5 (444) 333-22-11"); 249 | }); 250 | 251 | test('Update mask', () { 252 | const firstMask = "####-####"; 253 | const secondMask = "##/##-##/##"; 254 | 255 | final maskTextInputFormatter = MaskTextInputFormatter(mask: firstMask); 256 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "12345678")); 257 | expect(currentTextEditingValue, const TextEditingValue(text: "1234-5678", selection: TextSelection.collapsed(offset: 9))); 258 | expect(maskTextInputFormatter.isFill(), true); 259 | expect(maskTextInputFormatter.getUnmaskedText(), "12345678"); 260 | expect(maskTextInputFormatter.getMaskedText(), "1234-5678"); 261 | 262 | currentTextEditingValue = maskTextInputFormatter.updateMask(mask: secondMask); 263 | expect(currentTextEditingValue, const TextEditingValue(text: "12/34-56/78", selection: TextSelection.collapsed(offset: 11))); 264 | expect(maskTextInputFormatter.isFill(), true); 265 | expect(maskTextInputFormatter.getUnmaskedText(), "12345678"); 266 | expect(maskTextInputFormatter.getMaskedText(), "12/34-56/78"); 267 | }); 268 | 269 | test('Paste - All', () { 270 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-##-##") 271 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "+12345678901", selection: TextSelection.collapsed(offset: 12))); 272 | expect(maskTextInputFormatter.getUnmaskedText(), "2345678901"); 273 | expect(maskTextInputFormatter.getMaskedText(), "+1 (234) 567-89-01"); 274 | }); 275 | 276 | test('Paste - All by mask', () { 277 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-##-##") 278 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "+1 (234) 567-89-01", selection: TextSelection.collapsed(offset: 12))); 279 | expect(maskTextInputFormatter.getUnmaskedText(), "2345678901"); 280 | expect(maskTextInputFormatter.getMaskedText(), "+1 (234) 567-89-01"); 281 | }); 282 | 283 | test('Clear', () { 284 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-##-##") 285 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "+1 (234) 567-89-01", selection: TextSelection.collapsed(offset: 12))); 286 | expect(maskTextInputFormatter.getUnmaskedText(), "2345678901"); 287 | expect(maskTextInputFormatter.getMaskedText(), "+1 (234) 567-89-01"); 288 | maskTextInputFormatter.clear(); 289 | expect(maskTextInputFormatter.getUnmaskedText(), ""); 290 | expect(maskTextInputFormatter.getMaskedText(), ""); 291 | }); 292 | 293 | test('Clear - 2', () { 294 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "###-##-##") 295 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "567-89-01", selection: TextSelection.collapsed(offset: 7))); 296 | expect(maskTextInputFormatter.getMaskedText(), "567-89-01"); 297 | expect(maskTextInputFormatter.getUnmaskedText(), "5678901"); 298 | maskTextInputFormatter.formatEditUpdate(const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0)), const TextEditingValue(text: "5", selection: TextSelection.collapsed(offset: 1))); 299 | expect(maskTextInputFormatter.getMaskedText(), "5"); 300 | expect(maskTextInputFormatter.getUnmaskedText(), "5"); 301 | }); 302 | 303 | test('Format text', () { 304 | const phone = "2345678901"; 305 | const mask = "+1 (###) ###-##-##"; 306 | final maskTextInputFormatter = MaskTextInputFormatter(mask: mask); 307 | expect(mask, maskTextInputFormatter.getMask()); 308 | final masked = maskTextInputFormatter.maskText(phone); 309 | expect(masked, "+1 (234) 567-89-01"); 310 | final unmasked = maskTextInputFormatter.unmaskText(masked); 311 | expect(unmasked, phone); 312 | }); 313 | 314 | test('Disabled mask', () { 315 | const someText = "someText"; 316 | final maskTextInputFormatter = MaskTextInputFormatter(mask: null); 317 | var currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: someText, selection: TextSelection.collapsed(offset: 12))); 318 | expect(currentTextEditingValue.text, someText); 319 | expect(maskTextInputFormatter.getMaskedText(), someText); 320 | expect(maskTextInputFormatter.getUnmaskedText(), someText); 321 | 322 | maskTextInputFormatter.updateMask(mask: ""); 323 | currentTextEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: someText, selection: TextSelection.collapsed(offset: 12))); 324 | expect(currentTextEditingValue.text, someText); 325 | expect(maskTextInputFormatter.getMaskedText(), someText); 326 | expect(maskTextInputFormatter.getUnmaskedText(), someText); 327 | }); 328 | 329 | test('Empty filter', () { 330 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-##-##", filter: {}) 331 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "+1 (234) 567-89-01", selection: TextSelection.collapsed(offset: 12))); 332 | expect(maskTextInputFormatter.getMaskedText(), ""); 333 | expect(maskTextInputFormatter.getUnmaskedText(), ""); 334 | }); 335 | 336 | test('Eager / Lazy autocompletion', () { 337 | const mask = '#/#'; 338 | 339 | final lazyMaskTextInputFormatter = MaskTextInputFormatter(mask: mask, type: MaskAutoCompletionType.lazy); 340 | final eagerMaskTextInputFormatter = MaskTextInputFormatter(mask: mask, type: MaskAutoCompletionType.eager); 341 | 342 | var lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 343 | var eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 344 | 345 | expect(lazyTextEditingValue.text, '1'); 346 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), '1'); 347 | expect(lazyMaskTextInputFormatter.getMaskedText(), '1'); 348 | expect(eagerTextEditingValue.text, '1/'); 349 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '1'); 350 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1/'); 351 | 352 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "12", selection: TextSelection.collapsed(offset: 2))); 353 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "1/2", selection: TextSelection.collapsed(offset: 3))); 354 | 355 | expect(lazyTextEditingValue.text, '1/2'); 356 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), '12'); 357 | expect(lazyMaskTextInputFormatter.getMaskedText(), '1/2'); 358 | expect(eagerTextEditingValue.text, '1/2'); 359 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '12'); 360 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1/2'); 361 | 362 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "1/", selection: TextSelection.collapsed(offset: 2))); 363 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "1/", selection: TextSelection.collapsed(offset: 2))); 364 | 365 | expect(lazyTextEditingValue.text, '1'); 366 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), '1'); 367 | expect(lazyMaskTextInputFormatter.getMaskedText(), '1'); 368 | expect(eagerTextEditingValue.text, '1'); 369 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '1'); 370 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1'); 371 | 372 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0))); 373 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0))); 374 | 375 | expect(lazyTextEditingValue.text, ''); 376 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), ''); 377 | expect(lazyMaskTextInputFormatter.getMaskedText(), ''); 378 | expect(eagerTextEditingValue.text, ''); 379 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), ''); 380 | expect(eagerMaskTextInputFormatter.getMaskedText(), ''); 381 | 382 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "12", selection: TextSelection.collapsed(offset: 2))); 383 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "12", selection: TextSelection.collapsed(offset: 2))); 384 | 385 | expect(lazyTextEditingValue.text, '1/2'); 386 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), '12'); 387 | expect(lazyMaskTextInputFormatter.getMaskedText(), '1/2'); 388 | expect(eagerTextEditingValue.text, '1/2'); 389 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '12'); 390 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1/2'); 391 | 392 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0))); 393 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0))); 394 | 395 | expect(lazyTextEditingValue.text, ''); 396 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), ''); 397 | expect(lazyMaskTextInputFormatter.getMaskedText(), ''); 398 | expect(eagerTextEditingValue.text, ''); 399 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), ''); 400 | expect(eagerMaskTextInputFormatter.getMaskedText(), ''); 401 | 402 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 403 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 404 | 405 | expect(lazyTextEditingValue.text, '1'); 406 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), '1'); 407 | expect(lazyMaskTextInputFormatter.getMaskedText(), '1'); 408 | expect(eagerTextEditingValue.text, '1/'); 409 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '1'); 410 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1/'); 411 | 412 | lazyTextEditingValue = lazyMaskTextInputFormatter.formatEditUpdate(lazyTextEditingValue, const TextEditingValue(text: "", selection: TextSelection.collapsed(offset: 0))); 413 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 414 | 415 | expect(lazyTextEditingValue.text, ''); 416 | expect(lazyMaskTextInputFormatter.getUnmaskedText(), ''); 417 | expect(lazyMaskTextInputFormatter.getMaskedText(), ''); 418 | expect(eagerTextEditingValue.text, '1'); 419 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '1'); 420 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1'); 421 | 422 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate(eagerTextEditingValue, const TextEditingValue(text: "12", selection: TextSelection.collapsed(offset: 2))); 423 | 424 | expect(eagerTextEditingValue.text, '1/2'); 425 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), '12'); 426 | expect(eagerMaskTextInputFormatter.getMaskedText(), '1/2'); 427 | 428 | }); 429 | 430 | test('Eager autocompletion 2', () { 431 | final eagerMaskTextInputFormatter = MaskTextInputFormatter(mask: '####.A', initialText: "1234.A", type: MaskAutoCompletionType.eager, filter: {"#": RegExp('[0-9]'), "A": RegExp('[A|B]')}); 432 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), "1234A"); 433 | expect(eagerMaskTextInputFormatter.getMaskedText(), "1234.A"); 434 | 435 | var eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate( 436 | const TextEditingValue(text: "1234.A", selection: TextSelection(baseOffset: 5, extentOffset: 6)), 437 | const TextEditingValue(text: "1234.", selection: TextSelection(baseOffset: 5, extentOffset: 5)) 438 | ); 439 | expect(eagerTextEditingValue.text, '1234'); 440 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), "1234"); 441 | expect(eagerMaskTextInputFormatter.getMaskedText(), "1234"); 442 | 443 | eagerTextEditingValue = eagerMaskTextInputFormatter.formatEditUpdate( 444 | const TextEditingValue(text: "1234.", selection: TextSelection(baseOffset: 4, extentOffset: 5)), 445 | const TextEditingValue(text: "1234", selection: TextSelection(baseOffset: 4, extentOffset: 4)) 446 | ); 447 | expect(eagerTextEditingValue.text, '1234'); 448 | expect(eagerMaskTextInputFormatter.getUnmaskedText(), "1234"); 449 | expect(eagerMaskTextInputFormatter.getMaskedText(), "1234"); 450 | }); 451 | 452 | test('Paste masked text', () { 453 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "21#######-##") 454 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "214095276-01", selection: TextSelection.collapsed(offset: 12))); 455 | expect(maskTextInputFormatter.getMaskedText(), "214095276-01"); 456 | 457 | final maskTextInputFormatter2 = MaskTextInputFormatter(mask: "#21#########") 458 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "121409527601", selection: TextSelection.collapsed(offset: 12))); 459 | expect(maskTextInputFormatter2.getMaskedText(), "121409527601"); 460 | 461 | final maskTextInputFormatter3 = MaskTextInputFormatter(mask: "#21#########") 462 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))) 463 | ..formatEditUpdate(const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1)), const TextEditingValue(text: "121409527601", selection: TextSelection.collapsed(offset: 12))); 464 | expect(maskTextInputFormatter3.getMaskedText(), "121409527601"); 465 | 466 | final maskTextInputFormatter4 = MaskTextInputFormatter(mask: "#21#########") 467 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))) 468 | ..formatEditUpdate(const TextEditingValue(text: "121", selection: TextSelection.collapsed(offset: 3)), const TextEditingValue(text: "121409527601", selection: TextSelection.collapsed(offset: 12))); 469 | expect(maskTextInputFormatter4.getMaskedText(), "121409527601"); 470 | }); 471 | 472 | test('Update Mask Type', () { 473 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "123-###") 474 | ..formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "321")); 475 | expect(maskTextInputFormatter.getMaskedText(), "123-321"); 476 | 477 | maskTextInputFormatter.updateMask(mask: "###-555", type: MaskAutoCompletionType.eager); 478 | expect(maskTextInputFormatter.getMaskedText(), "321-555"); 479 | expect(maskTextInputFormatter.getUnmaskedText(), "321"); 480 | }); 481 | 482 | test('Empty formatter', () { 483 | final maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-####"); 484 | 485 | expect(maskTextInputFormatter.getMaskedText(), ""); 486 | expect(maskTextInputFormatter.getUnmaskedText(), ""); 487 | 488 | final resultEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, TextEditingValue.empty); 489 | expect(resultEditingValue.text, ""); 490 | expect(maskTextInputFormatter.getMaskedText(), ""); 491 | expect(maskTextInputFormatter.getUnmaskedText(), ""); 492 | }); 493 | 494 | test('Cursor position', () { 495 | var maskTextInputFormatter = MaskTextInputFormatter(mask: "+1 (###) ###-####"); 496 | var resultEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 497 | 498 | expect(maskTextInputFormatter.getMaskedText(), "+1 (1"); 499 | expect(maskTextInputFormatter.getUnmaskedText(), "1"); 500 | expect(resultEditingValue.selection.baseOffset, 5); 501 | 502 | maskTextInputFormatter = MaskTextInputFormatter.eager(mask: "+# (###) ###-####"); 503 | resultEditingValue = maskTextInputFormatter.formatEditUpdate(TextEditingValue.empty, const TextEditingValue(text: "1", selection: TextSelection.collapsed(offset: 1))); 504 | 505 | expect(maskTextInputFormatter.getMaskedText(), "+1 ("); 506 | expect(maskTextInputFormatter.getUnmaskedText(), "1"); 507 | expect(resultEditingValue.selection.baseOffset, 4); 508 | }); 509 | 510 | }); 511 | 512 | } 513 | --------------------------------------------------------------------------------