LCOV - code coverage report
Current view: top level - lib/util - mask_text_input_formatter.dart (source / functions) Hit Total Coverage
Test: Folly Fields Lines: 95 158 60.1 %
Date: 2023-05-11 23:59:26 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:math';
       2             : 
       3             : import 'package:flutter/services.dart';
       4             : 
       5             : ///
       6             : ///
       7             : ///
       8             : class MaskTextInputFormatter implements TextInputFormatter {
       9             :   String _mask = '';
      10             :   List<String> _maskChars = <String>[];
      11             :   Map<String, RegExp> _maskFilter = <String, RegExp>{};
      12             : 
      13             :   int _maskLength = 0;
      14             :   final _TextMatcher _resultTextArray = _TextMatcher();
      15             :   String _resultTextMasked = '';
      16             : 
      17             :   TextEditingValue? _lastResValue;
      18             :   TextEditingValue? _lastNewValue;
      19             : 
      20             :   ///
      21             :   ///
      22             :   ///
      23          19 :   MaskTextInputFormatter({
      24             :     String mask = '',
      25             :     Map<String, RegExp>? filter,
      26             :     String initialText = '',
      27             :   }) {
      28          19 :     updateMask(
      29             :       mask: mask,
      30             :       filter: filter ??
      31          12 :           <String, RegExp>{
      32          12 :             '#': RegExp('[0-9]'),
      33          12 :             'A': RegExp('[^0-9]'),
      34             :           },
      35             :     );
      36             : 
      37          19 :     formatEditUpdate(
      38             :       TextEditingValue.empty,
      39          19 :       TextEditingValue(text: initialText),
      40             :     );
      41             :   }
      42             : 
      43             :   ///
      44             :   ///
      45             :   ///
      46          19 :   TextEditingValue updateMask({
      47             :     String mask = '',
      48             :     Map<String, RegExp>? filter,
      49             :     bool clear = false,
      50             :   }) {
      51          19 :     _mask = mask;
      52             : 
      53          38 :     if (_mask.isEmpty) {
      54             :       clear = true;
      55             :     }
      56             : 
      57             :     if (filter != null) {
      58          19 :       _updateFilter(filter);
      59             :     }
      60             : 
      61          19 :     _calcMaskLength();
      62             : 
      63          19 :     String unmaskedText = clear ? '' : getUnmaskedText();
      64             : 
      65          19 :     _resultTextMasked = '';
      66          38 :     _resultTextArray.clear();
      67          19 :     _lastResValue = null;
      68          19 :     _lastNewValue = null;
      69             : 
      70          19 :     return formatEditUpdate(
      71             :       TextEditingValue.empty,
      72          19 :       TextEditingValue(
      73             :         text: unmaskedText,
      74          38 :         selection: TextSelection.collapsed(offset: unmaskedText.length),
      75             :       ),
      76             :     );
      77             :   }
      78             : 
      79             :   ///
      80             :   ///
      81             :   ///
      82           0 :   String getMask() => _mask;
      83             : 
      84             :   ///
      85             :   ///
      86             :   ///
      87           0 :   String getMaskedText() => _resultTextMasked;
      88             : 
      89             :   ///
      90             :   ///
      91             :   ///
      92          57 :   String getUnmaskedText() => _resultTextArray.toString();
      93             : 
      94             :   ///
      95             :   ///
      96             :   ///
      97           0 :   bool isFill() => _resultTextArray.length == _maskLength;
      98             : 
      99             :   ///
     100             :   ///
     101             :   ///
     102           0 :   String maskText(String text) => MaskTextInputFormatter(
     103           0 :         mask: _mask,
     104           0 :         filter: _maskFilter,
     105             :         initialText: text,
     106           0 :       ).getMaskedText();
     107             : 
     108             :   ///
     109             :   ///
     110             :   ///
     111           0 :   String unmaskText(String text) => MaskTextInputFormatter(
     112           0 :         mask: _mask,
     113           0 :         filter: _maskFilter,
     114             :         initialText: text,
     115           0 :       ).getUnmaskedText();
     116             : 
     117             :   ///
     118             :   ///
     119             :   ///
     120          19 :   @override
     121             :   TextEditingValue formatEditUpdate(
     122             :     TextEditingValue oldValue,
     123             :     TextEditingValue newValue,
     124             :   ) {
     125          38 :     if (_lastResValue == oldValue && newValue == _lastNewValue) {
     126             :       return oldValue;
     127             :     }
     128          19 :     _lastNewValue = newValue;
     129             : 
     130          38 :     return _lastResValue = _format(oldValue, newValue);
     131             :   }
     132             : 
     133             :   ///
     134             :   ///
     135             :   ///
     136          19 :   TextEditingValue _format(
     137             :     TextEditingValue oldValue,
     138             :     TextEditingValue newValue,
     139             :   ) {
     140          19 :     String mask = _mask;
     141             : 
     142          19 :     if (mask.isEmpty) {
     143           0 :       _resultTextMasked = newValue.text;
     144           0 :       _resultTextArray.set(newValue.text);
     145             : 
     146             :       return newValue;
     147             :     }
     148             : 
     149          19 :     String beforeText = oldValue.text;
     150          19 :     String afterText = newValue.text;
     151             : 
     152          19 :     TextSelection beforeSelection = oldValue.selection;
     153             :     int beforeSelectionStart =
     154          19 :         beforeSelection.isValid ? beforeSelection.start : 0;
     155          19 :     int beforeSelectionLength = beforeSelection.isValid
     156           0 :         ? beforeSelection.end - beforeSelection.start
     157             :         : 0;
     158             : 
     159             :     int lengthDifference =
     160          76 :         afterText.length - (beforeText.length - beforeSelectionLength);
     161          19 :     int lengthRemoved = lengthDifference < 0 ? lengthDifference.abs() : 0;
     162          19 :     int lengthAdded = lengthDifference > 0 ? lengthDifference : 0;
     163             : 
     164          38 :     int afterChangeStart = max(0, beforeSelectionStart - lengthRemoved);
     165          38 :     int afterChangeEnd = max(0, afterChangeStart + lengthAdded);
     166             : 
     167          38 :     int beforeReplaceStart = max(0, beforeSelectionStart - lengthRemoved);
     168          19 :     int beforeReplaceLength = beforeSelectionLength + lengthRemoved;
     169             : 
     170          38 :     int beforeResultTextLength = _resultTextArray.length;
     171             : 
     172          38 :     int currentResultTextLength = _resultTextArray.length;
     173             :     int currentResultSelectionStart = 0;
     174             :     int currentResultSelectionLength = 0;
     175             : 
     176             :     for (int pos = 0;
     177          76 :         pos < min(beforeReplaceStart + beforeReplaceLength, mask.length);
     178           0 :         pos++) {
     179           0 :       if (_maskChars.contains(mask[pos]) && currentResultTextLength > 0) {
     180           0 :         currentResultTextLength -= 1;
     181           0 :         if (pos < beforeReplaceStart) {
     182           0 :           currentResultSelectionStart += 1;
     183             :         }
     184           0 :         if (pos >= beforeReplaceStart) {
     185           0 :           currentResultSelectionLength += 1;
     186             :         }
     187             :       }
     188             :     }
     189             : 
     190             :     String replacementText =
     191          19 :         afterText.substring(afterChangeStart, afterChangeEnd);
     192             :     int targetCursorPosition = currentResultSelectionStart;
     193          19 :     if (replacementText.isEmpty) {
     194          38 :       _resultTextArray.removeRange(
     195             :         currentResultSelectionStart,
     196          19 :         currentResultSelectionStart + currentResultSelectionLength,
     197             :       );
     198             :     } else {
     199           0 :       if (currentResultSelectionLength > 0) {
     200           0 :         _resultTextArray.removeRange(
     201             :           currentResultSelectionStart,
     202           0 :           currentResultSelectionStart + currentResultSelectionLength,
     203             :         );
     204             :       }
     205           0 :       _resultTextArray.insert(currentResultSelectionStart, replacementText);
     206           0 :       targetCursorPosition += replacementText.length;
     207             :     }
     208             : 
     209          76 :     if (beforeResultTextLength == 0 && _resultTextArray.length > 1) {
     210           0 :       for (int pos = 0; pos < mask.length; pos++) {
     211           0 :         if (_maskChars.contains(mask[pos]) || _resultTextArray.isEmpty) {
     212             :           break;
     213           0 :         } else if (mask[pos] == _resultTextArray[0]) {
     214           0 :           _resultTextArray.removeAt(0);
     215             :         }
     216             :       }
     217             :     }
     218             : 
     219             :     int curTextPos = 0;
     220             :     int maskPos = 0;
     221          19 :     _resultTextMasked = '';
     222          19 :     int cursorPos = -1;
     223             :     int nonMaskedCount = 0;
     224             : 
     225          38 :     while (maskPos < mask.length) {
     226          19 :       String curMaskChar = mask[maskPos];
     227          38 :       bool isMaskChar = _maskChars.contains(curMaskChar);
     228             : 
     229          57 :       bool curTextInRange = curTextPos < _resultTextArray.length;
     230             : 
     231             :       String? curTextChar;
     232             :       if (isMaskChar && curTextInRange) {
     233             :         while (curTextChar == null && curTextInRange) {
     234           0 :           String potentialTextChar = _resultTextArray[curTextPos];
     235           0 :           if (_maskFilter[curMaskChar]!.hasMatch(potentialTextChar)) {
     236             :             curTextChar = potentialTextChar;
     237             :           } else {
     238           0 :             _resultTextArray.removeAt(curTextPos);
     239           0 :             curTextInRange = curTextPos < _resultTextArray.length;
     240           0 :             if (curTextPos <= targetCursorPosition) {
     241           0 :               targetCursorPosition -= 1;
     242             :             }
     243             :           }
     244             :         }
     245             :       }
     246             : 
     247             :       if (isMaskChar && curTextInRange && curTextChar != null) {
     248           0 :         _resultTextMasked += curTextChar;
     249           0 :         if (curTextPos == targetCursorPosition && cursorPos == -1) {
     250           0 :           cursorPos = maskPos - nonMaskedCount;
     251             :         }
     252             :         nonMaskedCount = 0;
     253           0 :         curTextPos += 1;
     254             :       } else {
     255          19 :         if (curTextPos == targetCursorPosition &&
     256          38 :             cursorPos == -1 &&
     257             :             !curTextInRange) {
     258             :           cursorPos = maskPos;
     259             :         }
     260             : 
     261             :         if (!curTextInRange) {
     262             :           break;
     263             :         } else {
     264           0 :           _resultTextMasked += mask[maskPos];
     265             :         }
     266             : 
     267           0 :         nonMaskedCount++;
     268             :       }
     269             : 
     270           0 :       maskPos += 1;
     271             :     }
     272             : 
     273          19 :     if (nonMaskedCount > 0) {
     274           0 :       _resultTextMasked = _resultTextMasked.substring(
     275             :         0,
     276           0 :         _resultTextMasked.length - nonMaskedCount,
     277             :       );
     278           0 :       cursorPos -= nonMaskedCount;
     279             :     }
     280             : 
     281          76 :     if (_resultTextArray.length > _maskLength) {
     282           0 :       _resultTextArray.removeRange(_maskLength, _resultTextArray.length);
     283             :     }
     284             : 
     285             :     int finalCursorPosition =
     286          19 :         cursorPos < 0 ? _resultTextMasked.length : cursorPos;
     287             : 
     288          19 :     return TextEditingValue(
     289          19 :       text: _resultTextMasked,
     290          19 :       selection: TextSelection(
     291             :         baseOffset: finalCursorPosition,
     292             :         // ignore: no-equal-arguments
     293             :         extentOffset: finalCursorPosition,
     294          38 :         affinity: newValue.selection.affinity,
     295          38 :         isDirectional: newValue.selection.isDirectional,
     296             :       ),
     297             :     );
     298             :   }
     299             : 
     300             :   ///
     301             :   ///
     302             :   ///
     303          19 :   void _calcMaskLength() {
     304          19 :     _maskLength = 0;
     305          19 :     String mask = _mask;
     306          57 :     for (int pos = 0; pos < mask.length; pos++) {
     307          57 :       if (_maskChars.contains(mask[pos])) {
     308          38 :         _maskLength++;
     309             :       }
     310             :     }
     311             :   }
     312             : 
     313             :   ///
     314             :   ///
     315             :   ///
     316          19 :   void _updateFilter(Map<String, RegExp> filter) {
     317          19 :     _maskFilter = filter;
     318          76 :     _maskChars = _maskFilter.keys.toList(growable: false);
     319             :   }
     320             : }
     321             : 
     322             : ///
     323             : ///
     324             : ///
     325             : class _TextMatcher {
     326             :   final List<String> _symbolArray = <String>[];
     327             : 
     328             :   ///
     329             :   ///
     330             :   ///
     331          19 :   int get length =>
     332          38 :       _symbolArray.fold(0, (int prev, String match) => prev + match.length);
     333             : 
     334             :   ///
     335             :   ///
     336             :   ///
     337          57 :   void removeRange(int start, int end) => _symbolArray.removeRange(start, end);
     338             : 
     339             :   ///
     340             :   ///
     341             :   ///
     342           0 :   void insert(int start, String substring) {
     343           0 :     for (int pos = 0; pos < substring.length; pos++) {
     344           0 :       _symbolArray.insert(start + pos, substring[pos]);
     345             :     }
     346             :   }
     347             : 
     348             :   ///
     349             :   ///
     350             :   ///
     351           0 :   bool get isEmpty => _symbolArray.isEmpty;
     352             : 
     353             :   ///
     354             :   ///
     355             :   ///
     356           0 :   void removeAt(int index) => _symbolArray.removeAt(index);
     357             : 
     358             :   ///
     359             :   ///
     360             :   ///
     361           0 :   String operator [](int index) => _symbolArray[index];
     362             : 
     363             :   ///
     364             :   ///
     365             :   ///
     366          57 :   void clear() => _symbolArray.clear();
     367             : 
     368             :   ///
     369             :   ///
     370             :   ///
     371          19 :   @override
     372          38 :   String toString() => _symbolArray.join();
     373             : 
     374             :   ///
     375             :   ///
     376             :   ///
     377           0 :   void set(String text) {
     378           0 :     _symbolArray.clear();
     379           0 :     for (int pos = 0; pos < text.length; pos++) {
     380           0 :       _symbolArray.add(text[pos]);
     381             :     }
     382             :   }
     383             : }
     384             : 
     385             : ///
     386             : ///
     387             : ///
     388             : class UppercaseMask extends MaskTextInputFormatter {
     389             :   ///
     390             :   ///
     391             :   ///
     392           2 :   UppercaseMask({
     393             :     super.mask,
     394             :     super.filter,
     395             :     super.initialText,
     396           4 :   }) : assert(mask.isNotEmpty, 'mask must be not empty.');
     397             : 
     398             :   ///
     399             :   ///
     400             :   ///
     401           2 :   @override
     402             :   TextEditingValue formatEditUpdate(
     403             :     TextEditingValue oldValue,
     404             :     TextEditingValue newValue,
     405             :   ) {
     406           4 :     if (newValue.text.isNotEmpty) {
     407           0 :       newValue = TextEditingValue(
     408           0 :         text: newValue.text.toUpperCase(),
     409           0 :         selection: newValue.selection,
     410           0 :         composing: newValue.composing,
     411             :       );
     412             :     }
     413             : 
     414           2 :     return super.formatEditUpdate(oldValue, newValue);
     415             :   }
     416             : }
     417             : 
     418             : ///
     419             : ///
     420             : ///
     421             : class ChangeMask extends MaskTextInputFormatter {
     422             :   final String firstMask;
     423             :   final String secondMask;
     424             : 
     425             :   ///
     426             :   ///
     427             :   ///
     428           3 :   ChangeMask({
     429             :     required this.firstMask,
     430             :     required this.secondMask,
     431             :     super.filter,
     432             :     super.initialText,
     433           6 :   })  : assert(firstMask.isNotEmpty, 'firstMask must be not empty.'),
     434           6 :         assert(secondMask.isNotEmpty, 'secondMask must be not empty.'),
     435             :         assert(
     436          12 :           firstMask.length < secondMask.length,
     437             :           'firstMask length must be lower than secondMask length.',
     438             :         ),
     439           3 :         super(
     440             :           mask: firstMask,
     441             :         );
     442             : 
     443             :   ///
     444             :   ///
     445             :   ///
     446           3 :   @override
     447             :   TextEditingValue formatEditUpdate(
     448             :     TextEditingValue oldValue,
     449             :     TextEditingValue newValue,
     450             :   ) {
     451           6 :     int oldLength = oldValue.text.length;
     452           6 :     int newLength = newValue.text.length;
     453             : 
     454           9 :     if (oldLength == firstMask.length && newLength == firstMask.length + 1) {
     455           0 :       oldValue = updateMask(mask: secondMask);
     456             :     }
     457             : 
     458          12 :     if (oldLength == firstMask.length + 1 && newLength == firstMask.length) {
     459           0 :       oldValue = updateMask(mask: firstMask);
     460             :     }
     461             : 
     462           3 :     return super.formatEditUpdate(oldValue, newValue);
     463             :   }
     464             : }

Generated by: LCOV version 1.14