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 20 : MaskTextInputFormatter({
24 : String mask = '',
25 : Map<String, RegExp>? filter,
26 : String initialText = '',
27 : }) {
28 20 : 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 20 : formatEditUpdate(
38 : TextEditingValue.empty,
39 20 : TextEditingValue(text: initialText),
40 : );
41 : }
42 :
43 : ///
44 : ///
45 : ///
46 20 : TextEditingValue updateMask({
47 : String mask = '',
48 : Map<String, RegExp>? filter,
49 : bool clear = false,
50 : }) {
51 20 : _mask = mask;
52 :
53 40 : if (_mask.isEmpty) {
54 : clear = true;
55 : }
56 :
57 : if (filter != null) {
58 20 : _updateFilter(filter);
59 : }
60 :
61 20 : _calcMaskLength();
62 :
63 20 : String unmaskedText = clear ? '' : getUnmaskedText();
64 :
65 20 : _resultTextMasked = '';
66 40 : _resultTextArray.clear();
67 20 : _lastResValue = null;
68 20 : _lastNewValue = null;
69 :
70 20 : return formatEditUpdate(
71 : TextEditingValue.empty,
72 20 : TextEditingValue(
73 : text: unmaskedText,
74 40 : 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 60 : 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 20 : @override
121 : TextEditingValue formatEditUpdate(
122 : TextEditingValue oldValue,
123 : TextEditingValue newValue,
124 : ) {
125 40 : if (_lastResValue == oldValue && newValue == _lastNewValue) {
126 : return oldValue;
127 : }
128 20 : _lastNewValue = newValue;
129 :
130 40 : return _lastResValue = _format(oldValue, newValue);
131 : }
132 :
133 : ///
134 : ///
135 : ///
136 20 : TextEditingValue _format(
137 : TextEditingValue oldValue,
138 : TextEditingValue newValue,
139 : ) {
140 20 : String mask = _mask;
141 :
142 20 : if (mask.isEmpty) {
143 0 : _resultTextMasked = newValue.text;
144 0 : _resultTextArray.set(newValue.text);
145 :
146 : return newValue;
147 : }
148 :
149 20 : String beforeText = oldValue.text;
150 20 : String afterText = newValue.text;
151 :
152 20 : TextSelection beforeSelection = oldValue.selection;
153 : int beforeSelectionStart =
154 20 : beforeSelection.isValid ? beforeSelection.start : 0;
155 20 : int beforeSelectionLength = beforeSelection.isValid
156 0 : ? beforeSelection.end - beforeSelection.start
157 : : 0;
158 :
159 : int lengthDifference =
160 80 : afterText.length - (beforeText.length - beforeSelectionLength);
161 20 : int lengthRemoved = lengthDifference < 0 ? lengthDifference.abs() : 0;
162 20 : int lengthAdded = lengthDifference > 0 ? lengthDifference : 0;
163 :
164 40 : int afterChangeStart = max(0, beforeSelectionStart - lengthRemoved);
165 40 : int afterChangeEnd = max(0, afterChangeStart + lengthAdded);
166 :
167 40 : int beforeReplaceStart = max(0, beforeSelectionStart - lengthRemoved);
168 20 : int beforeReplaceLength = beforeSelectionLength + lengthRemoved;
169 :
170 40 : int beforeResultTextLength = _resultTextArray.length;
171 :
172 40 : int currentResultTextLength = _resultTextArray.length;
173 : int currentResultSelectionStart = 0;
174 : int currentResultSelectionLength = 0;
175 :
176 : for (int pos = 0;
177 80 : 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 20 : afterText.substring(afterChangeStart, afterChangeEnd);
192 : int targetCursorPosition = currentResultSelectionStart;
193 20 : if (replacementText.isEmpty) {
194 40 : _resultTextArray.removeRange(
195 : currentResultSelectionStart,
196 20 : 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 80 : 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 20 : _resultTextMasked = '';
222 20 : int cursorPos = -1;
223 : int nonMaskedCount = 0;
224 :
225 40 : while (maskPos < mask.length) {
226 20 : String curMaskChar = mask[maskPos];
227 40 : bool isMaskChar = _maskChars.contains(curMaskChar);
228 :
229 60 : 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 20 : if (curTextPos == targetCursorPosition &&
256 40 : 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 20 : if (nonMaskedCount > 0) {
274 0 : _resultTextMasked = _resultTextMasked.substring(
275 : 0,
276 0 : _resultTextMasked.length - nonMaskedCount,
277 : );
278 0 : cursorPos -= nonMaskedCount;
279 : }
280 :
281 80 : if (_resultTextArray.length > _maskLength) {
282 0 : _resultTextArray.removeRange(_maskLength, _resultTextArray.length);
283 : }
284 :
285 : int finalCursorPosition =
286 20 : cursorPos < 0 ? _resultTextMasked.length : cursorPos;
287 :
288 20 : return TextEditingValue(
289 20 : text: _resultTextMasked,
290 20 : selection: TextSelection(
291 : baseOffset: finalCursorPosition,
292 : // ignore: no-equal-arguments
293 : extentOffset: finalCursorPosition,
294 40 : affinity: newValue.selection.affinity,
295 40 : isDirectional: newValue.selection.isDirectional,
296 : ),
297 : );
298 : }
299 :
300 : ///
301 : ///
302 : ///
303 20 : void _calcMaskLength() {
304 20 : _maskLength = 0;
305 20 : String mask = _mask;
306 60 : for (int pos = 0; pos < mask.length; pos++) {
307 60 : if (_maskChars.contains(mask[pos])) {
308 40 : _maskLength++;
309 : }
310 : }
311 : }
312 :
313 : ///
314 : ///
315 : ///
316 20 : void _updateFilter(Map<String, RegExp> filter) {
317 20 : _maskFilter = filter;
318 80 : _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 20 : int get length =>
332 40 : _symbolArray.fold(0, (int prev, String match) => prev + match.length);
333 :
334 : ///
335 : ///
336 : ///
337 60 : 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 60 : void clear() => _symbolArray.clear();
367 :
368 : ///
369 : ///
370 : ///
371 20 : @override
372 40 : 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 : }
|