Create a text validator/formatter mechanism (#9535)
* Add a text formatter interface used by EditingText. Provide some default implementations. * self nits * Handle -1 selection offsets * review notes * simplify regular expression * Add whitelisting formatters. Use a custom phone number formatter in text demo. * review notes * not being able to addAll(null) is pretty annoying * review notes * partial tests * Add tests * didn’t end up needing mockito * move to services
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class TextFormFieldDemo extends StatefulWidget {
|
||||
const TextFormFieldDemo({ Key key }) : super(key: key);
|
||||
@@ -36,6 +37,7 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
bool _formWasEdited = false;
|
||||
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
||||
final GlobalKey<FormFieldState<String>> _passwordFieldKey = new GlobalKey<FormFieldState<String>>();
|
||||
final _UsNumberTextInputFormatter _phoneNumberFormatter = new _UsNumberTextInputFormatter();
|
||||
void _handleSubmitted() {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (!form.validate()) {
|
||||
@@ -59,9 +61,9 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
|
||||
String _validatePhoneNumber(String value) {
|
||||
_formWasEdited = true;
|
||||
final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
|
||||
final RegExp phoneExp = new RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
|
||||
if (!phoneExp.hasMatch(value))
|
||||
return '###-###-#### - Please enter a valid phone number.';
|
||||
return '(###) ###-#### - Please enter a valid US phone number.';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -131,6 +133,12 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
keyboardType: TextInputType.phone,
|
||||
onSaved: (String value) { person.phoneNumber = value; },
|
||||
validator: _validatePhoneNumber,
|
||||
// TextInputFormatters are applied in sequence.
|
||||
inputFormatters: <TextInputFormatter> [
|
||||
WhitelistingTextInputFormatter.digitsOnly,
|
||||
// Fit the validating format.
|
||||
_phoneNumberFormatter,
|
||||
],
|
||||
),
|
||||
new TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
@@ -184,3 +192,40 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format incoming numeric text to fit the format of (###) ###-#### ##...
|
||||
class _UsNumberTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue
|
||||
) {
|
||||
final int newTextLength = newValue.text.length;
|
||||
int selectionIndex = newValue.selection.end;
|
||||
int usedSubstringIndex = 0;
|
||||
final StringBuffer newText = new StringBuffer();
|
||||
if (newTextLength >= 1) {
|
||||
newText.write('(');
|
||||
if (newValue.selection.end >= 1) selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 4) {
|
||||
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
|
||||
if (newValue.selection.end >= 3) selectionIndex += 2;
|
||||
}
|
||||
if (newTextLength >= 7) {
|
||||
newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-');
|
||||
if (newValue.selection.end >= 6) selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 11) {
|
||||
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
|
||||
if (newValue.selection.end >= 10) selectionIndex++;
|
||||
}
|
||||
// Dump the rest.
|
||||
if (newTextLength >= usedSubstringIndex)
|
||||
newText.write(newValue.text.substring(usedSubstringIndex));
|
||||
return new TextEditingValue(
|
||||
text: newText.toString(),
|
||||
selection: new TextSelection.collapsed(offset: selectionIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,6 @@ export 'src/services/system_chrome.dart';
|
||||
export 'src/services/system_navigator.dart';
|
||||
export 'src/services/system_sound.dart';
|
||||
export 'src/services/text_editing.dart';
|
||||
export 'src/services/text_formatter.dart';
|
||||
export 'src/services/text_input.dart';
|
||||
export 'src/services/url_launcher.dart';
|
||||
|
||||
@@ -75,6 +75,7 @@ class TextField extends StatefulWidget {
|
||||
this.maxLines: 1,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.inputFormatters,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
@@ -141,6 +142,10 @@ class TextField extends StatefulWidget {
|
||||
/// field.
|
||||
final ValueChanged<String> onSubmitted;
|
||||
|
||||
/// Optional input validation and formatting overrides. Formatters are run
|
||||
/// in the provided order when the text input changes.
|
||||
final List<TextInputFormatter> inputFormatters;
|
||||
|
||||
@override
|
||||
_TextFieldState createState() => new _TextFieldState();
|
||||
|
||||
@@ -223,6 +228,7 @@ class _TextFieldState extends State<TextField> {
|
||||
selectionControls: materialTextSelectionControls,
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class TextFormField extends FormField<String> {
|
||||
int maxLines: 1,
|
||||
FormFieldSetter<String> onSaved,
|
||||
FormFieldValidator<String> validator,
|
||||
List<TextInputFormatter> inputFormatters,
|
||||
}) : super(
|
||||
key: key,
|
||||
initialValue: controller != null ? controller.value.text : '',
|
||||
@@ -59,6 +60,7 @@ class TextFormField extends FormField<String> {
|
||||
obscureText: obscureText,
|
||||
maxLines: maxLines,
|
||||
onChanged: field.onChanged,
|
||||
inputFormatters: inputFormatters,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -195,4 +195,20 @@ class TextSelection extends TextRange {
|
||||
affinity.hashCode,
|
||||
isDirectional.hashCode
|
||||
);
|
||||
|
||||
/// Creates a new [TextSelection] based on the current selection, with the
|
||||
/// provided parameters overridden.
|
||||
TextSelection copyWith({
|
||||
int baseOffset,
|
||||
int extentOffset,
|
||||
TextAffinity affinity,
|
||||
bool isDirectional,
|
||||
}) {
|
||||
return new TextSelection(
|
||||
baseOffset: baseOffset ?? this.baseOffset,
|
||||
extentOffset: extentOffset ?? this.extentOffset,
|
||||
affinity: affinity ?? this.affinity,
|
||||
isDirectional: isDirectional ?? this.isDirectional,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
194
packages/flutter/lib/src/services/text_formatter.dart
Normal file
194
packages/flutter/lib/src/services/text_formatter.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A [TextInputFormatter] can be optionally injected into an [EditableText]
|
||||
/// to provide as-you-type validation and formatting of the text being edited.
|
||||
///
|
||||
/// Text modification should only be applied when text is being committed by the
|
||||
/// IME and not on text under composition (i.e. when
|
||||
/// [TextEditingValue.composing] is collapsed).
|
||||
///
|
||||
/// Concrete implementations [BlacklistingTextInputFormatter], which removes
|
||||
/// blacklisted characters upon edit commit, and
|
||||
/// [WhitelistingTextInputFormatter], which only allows entries of whitelisted
|
||||
/// characters, are provided.
|
||||
///
|
||||
/// To create custom formatters, extend the [TextInputFormatter] class and
|
||||
/// implement the [formatEditUpdate] method.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableText] on which the formatting apply.
|
||||
/// * [BlacklistingTextInputFormatter], a provided formatter for blacklisting
|
||||
/// characters.
|
||||
/// * [WhitelistingTextInputFormatter], a provided formatter for whitelisting
|
||||
/// characters.
|
||||
abstract class TextInputFormatter {
|
||||
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
|
||||
///
|
||||
/// You can override the resulting text based on the previous text value and
|
||||
/// the incoming new text value.
|
||||
///
|
||||
/// When formatters are chained, `oldValue` reflects the initial value of
|
||||
/// [TextEditingValue] at the beginning of the chain.
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue
|
||||
);
|
||||
|
||||
/// A shorthand to creating a custom [TextInputFormatter] which formats
|
||||
/// incoming text input changes with the given function.
|
||||
static TextInputFormatter withFunction(
|
||||
TextInputFormatFunction formatFunction
|
||||
) {
|
||||
return new _SimpleTextInputFormatter(formatFunction);
|
||||
}
|
||||
}
|
||||
|
||||
/// Function signature expected for creating custom [TextInputFormatter]
|
||||
/// shorthands via [TextInputFormatter.withFunction];
|
||||
typedef TextEditingValue TextInputFormatFunction(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
);
|
||||
|
||||
/// Wiring for [TextInputFormatter.withFunction].
|
||||
class _SimpleTextInputFormatter extends TextInputFormatter {
|
||||
_SimpleTextInputFormatter(this.formatFunction) :
|
||||
assert(formatFunction != null);
|
||||
|
||||
final TextInputFormatFunction formatFunction;
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue
|
||||
) {
|
||||
return formatFunction(oldValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [TextInputFormatter] that prevents the insertion of blacklisted
|
||||
/// characters patterns.
|
||||
///
|
||||
/// Instances of blacklisted characters found in the new [TextEditingValue]s
|
||||
/// will be replaced with the [replacementString] which defaults to ``.
|
||||
///
|
||||
/// Since this formatter only removes characters from the text, it attempts to
|
||||
/// preserve the existing [TextEditingValue.selection] to values it would now
|
||||
/// fall at with the removed characters.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInputFormatter].
|
||||
/// * [WhitelistingTextInputFormatter].
|
||||
class BlacklistingTextInputFormatter extends TextInputFormatter {
|
||||
BlacklistingTextInputFormatter(
|
||||
this.blacklistedPattern,
|
||||
{
|
||||
this.replacementString: '',
|
||||
}
|
||||
) : assert(blacklistedPattern != null);
|
||||
|
||||
/// A [Pattern] to match and replace incoming [TextEditingValue]s.
|
||||
final Pattern blacklistedPattern;
|
||||
|
||||
/// String used to replace found patterns.
|
||||
final String replacementString;
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, // unused.
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
return _selectionAwareTextManipulation(
|
||||
newValue,
|
||||
(String substring) {
|
||||
return substring.replaceAll(blacklistedPattern, replacementString);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A [BlacklistingTextInputFormatter] that forces input to be a single line.
|
||||
static final BlacklistingTextInputFormatter singleLineFormatter
|
||||
= new BlacklistingTextInputFormatter(new RegExp(r'\n'));
|
||||
}
|
||||
|
||||
/// A [TextInputFormatter] that allows only the insertion of whitelisted
|
||||
/// characters patterns.
|
||||
///
|
||||
/// Since this formatter only removes characters from the text, it attempts to
|
||||
/// preserve the existing [TextEditingValue.selection] to values it would now
|
||||
/// fall at with the removed characters.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInputFormatter].
|
||||
/// * [BlacklistingTextInputFormatter].
|
||||
class WhitelistingTextInputFormatter extends TextInputFormatter {
|
||||
WhitelistingTextInputFormatter(this.whitelistedPattern) :
|
||||
assert(whitelistedPattern != null);
|
||||
|
||||
/// A [Pattern] to extract all instances of allowed characters.
|
||||
///
|
||||
/// [RegExp] with multiple groups is not supported.
|
||||
final Pattern whitelistedPattern;
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, // unused.
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
return _selectionAwareTextManipulation(
|
||||
newValue,
|
||||
(String substring) {
|
||||
return whitelistedPattern
|
||||
.allMatches(substring)
|
||||
.map((Match match) => match.group(0))
|
||||
.join();
|
||||
} ,
|
||||
);
|
||||
}
|
||||
|
||||
/// A [WhitelistingTextInputFormatter] that takes in digits `[0-9]` only.
|
||||
static final WhitelistingTextInputFormatter digitsOnly
|
||||
= new WhitelistingTextInputFormatter(new RegExp(r'\d+'));
|
||||
}
|
||||
|
||||
TextEditingValue _selectionAwareTextManipulation(
|
||||
TextEditingValue value,
|
||||
String substringManipulation(String substring),
|
||||
) {
|
||||
final int selectionStartIndex = value.selection.start;
|
||||
final int selectionEndIndex = value.selection.end;
|
||||
String manipulatedText;
|
||||
TextSelection manipulatedSelection;
|
||||
if (selectionStartIndex < 0 || selectionEndIndex < 0) {
|
||||
manipulatedText = substringManipulation(value.text);
|
||||
} else {
|
||||
final String beforeSelection = substringManipulation(
|
||||
value.text.substring(0, selectionStartIndex)
|
||||
);
|
||||
final String inSelection = substringManipulation(
|
||||
value.text.substring(selectionStartIndex, selectionEndIndex)
|
||||
);
|
||||
final String afterSelection = substringManipulation(
|
||||
value.text.substring(selectionEndIndex)
|
||||
);
|
||||
manipulatedText = beforeSelection + inSelection + afterSelection;
|
||||
manipulatedSelection = value.selection.copyWith(
|
||||
baseOffset: beforeSelection.length,
|
||||
extentOffset: beforeSelection.length + inSelection.length,
|
||||
);
|
||||
}
|
||||
return new TextEditingValue(
|
||||
text: manipulatedText,
|
||||
selection: manipulatedSelection ?? const TextSelection.collapsed(offset: -1),
|
||||
composing: manipulatedText == value.text
|
||||
? value.composing
|
||||
: TextRange.empty,
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class EditableText extends StatefulWidget {
|
||||
///
|
||||
/// The [controller], [focusNode], [style], and [cursorColor] arguments must
|
||||
/// not be null.
|
||||
const EditableText({
|
||||
EditableText({
|
||||
Key key,
|
||||
@required this.controller,
|
||||
@required this.focusNode,
|
||||
@@ -133,6 +133,7 @@ class EditableText extends StatefulWidget {
|
||||
this.keyboardType,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
List<TextInputFormatter> inputFormatters,
|
||||
}) : assert(controller != null),
|
||||
assert(focusNode != null),
|
||||
assert(obscureText != null),
|
||||
@@ -140,6 +141,12 @@ class EditableText extends StatefulWidget {
|
||||
assert(cursorColor != null),
|
||||
assert(maxLines != null),
|
||||
assert(autofocus != null),
|
||||
inputFormatters = maxLines == 1
|
||||
? (
|
||||
<TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
|
||||
..addAll(inputFormatters ?? const Iterable<TextInputFormatter>.empty())
|
||||
)
|
||||
: inputFormatters,
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
@@ -197,6 +204,10 @@ class EditableText extends StatefulWidget {
|
||||
/// Called when the user indicates that they are done editing the text in the field.
|
||||
final ValueChanged<String> onSubmitted;
|
||||
|
||||
/// Optional input validation and formatting overrides. Formatters are run
|
||||
/// in the provided order when the text input changes.
|
||||
final List<TextInputFormatter> inputFormatters;
|
||||
|
||||
@override
|
||||
EditableTextState createState() => new EditableTextState();
|
||||
}
|
||||
@@ -266,7 +277,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
if (value.text != _value.text)
|
||||
_hideSelectionOverlayIfNeeded();
|
||||
_lastKnownRemoteTextEditingValue = value;
|
||||
_value = value;
|
||||
_formatAndSetValue(value);
|
||||
if (widget.onChanged != null)
|
||||
widget.onChanged(value.text);
|
||||
}
|
||||
@@ -396,10 +407,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
|
||||
void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
|
||||
assert(!value.composing.isValid); // composing range must be empty while selecting.
|
||||
_value = value;
|
||||
_formatAndSetValue(value);
|
||||
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
|
||||
}
|
||||
|
||||
void _formatAndSetValue(TextEditingValue value) {
|
||||
if (widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
|
||||
for (TextInputFormatter formatter in widget.inputFormatters)
|
||||
value = formatter.formatEditUpdate(_value, value);
|
||||
_value = value;
|
||||
_updateRemoteEditingValueIfNeeded();
|
||||
} else {
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the blinking cursor is actually visible at this precise moment
|
||||
/// (it's hidden half the time, since it blinks).
|
||||
@visibleForTesting
|
||||
|
||||
@@ -972,4 +972,114 @@ void main() {
|
||||
await tester.idle();
|
||||
expect(tester.testTextInput.editingState['text'], equals(''));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Cannot enter new lines onto single line TextField',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController textController = new TextEditingController();
|
||||
|
||||
await tester.pumpWidget(new Material(
|
||||
child: new TextField(controller: textController, decoration: null),
|
||||
));
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'abc\ndef');
|
||||
|
||||
expect(textController.text, 'abcdef');
|
||||
}
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Injected formatters are chained',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController textController = new TextEditingController();
|
||||
|
||||
await tester.pumpWidget(new Material(
|
||||
child: new TextField(
|
||||
controller: textController,
|
||||
decoration: null,
|
||||
inputFormatters: <TextInputFormatter> [
|
||||
new BlacklistingTextInputFormatter(
|
||||
new RegExp(r'[a-z]'),
|
||||
replacementString: '#',
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
|
||||
// The default single line formatter replaces \n with empty string.
|
||||
expect(textController.text, '#一#二#三#四#五#六');
|
||||
}
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Chained formatters are in sequence',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController textController = new TextEditingController();
|
||||
|
||||
await tester.pumpWidget(new Material(
|
||||
child: new TextField(
|
||||
controller: textController,
|
||||
decoration: null,
|
||||
maxLines: 2,
|
||||
inputFormatters: <TextInputFormatter> [
|
||||
new BlacklistingTextInputFormatter(
|
||||
new RegExp(r'[a-z]'),
|
||||
replacementString: '12\n',
|
||||
),
|
||||
new WhitelistingTextInputFormatter(new RegExp(r'\n[0-9]')),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'a1b2c3');
|
||||
// The first formatter turns it into
|
||||
// 12\n112\n212\n3
|
||||
// The second formatter turns it into
|
||||
// \n1\n2\n3
|
||||
// Multiline is allowed since maxLine != 1.
|
||||
expect(textController.text, '\n1\n2\n3');
|
||||
}
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Pasted values are formatted',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController textController = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
controller: textController,
|
||||
decoration: null,
|
||||
inputFormatters: <TextInputFormatter> [
|
||||
WhitelistingTextInputFormatter.digitsOnly,
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
|
||||
expect(textController.text, '123');
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
|
||||
await tester.pumpWidget(builder());
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints =
|
||||
renderEditable.getEndpointsForSelection(textController.selection);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
|
||||
await tester.tap(find.text('PASTE'));
|
||||
await tester.pumpWidget(builder());
|
||||
// Puts 456 before the 2 in 123.
|
||||
expect(textController.text, '145623');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
112
packages/flutter/test/widgets/text_formatter_test.dart
Normal file
112
packages/flutter/test/widgets/text_formatter_test.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
TextEditingValue testOldValue;
|
||||
TextEditingValue testNewValue;
|
||||
|
||||
test('withFunction wraps formatting function', () {
|
||||
testOldValue = const TextEditingValue();
|
||||
testNewValue = const TextEditingValue();
|
||||
|
||||
TextEditingValue calledOldValue;
|
||||
TextEditingValue calledNewValue;
|
||||
|
||||
final TextInputFormatter formatterUnderTest = TextInputFormatter.withFunction(
|
||||
(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
calledOldValue = oldValue;
|
||||
calledNewValue = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
formatterUnderTest.formatEditUpdate(testOldValue, testNewValue);
|
||||
|
||||
expect(calledOldValue, equals(testOldValue));
|
||||
expect(calledNewValue, equals(testNewValue));
|
||||
});
|
||||
|
||||
group('test provided formatters', () {
|
||||
setUp(() {
|
||||
// a1b(2c3
|
||||
// d4)e5f6
|
||||
// where the parentheses are the selection range.
|
||||
testNewValue = const TextEditingValue(
|
||||
text: 'a1b2c3\nd4e5f6',
|
||||
selection: const TextSelection(
|
||||
baseOffset: 3,
|
||||
extentOffset: 9,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('test blacklisting formatter', () {
|
||||
final TextEditingValue actualValue =
|
||||
new BlacklistingTextInputFormatter(new RegExp(r'[a-z]'))
|
||||
.formatEditUpdate(testOldValue, testNewValue);
|
||||
|
||||
// Expecting
|
||||
// 1(23
|
||||
// 4)56
|
||||
expect(actualValue, const TextEditingValue(
|
||||
text: '123\n456',
|
||||
selection: const TextSelection(
|
||||
baseOffset: 1,
|
||||
extentOffset: 5,
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
test('test single line formatter', () {
|
||||
final TextEditingValue actualValue =
|
||||
BlacklistingTextInputFormatter.singleLineFormatter
|
||||
.formatEditUpdate(testOldValue, testNewValue);
|
||||
|
||||
// Expecting
|
||||
// a1b(2c3d4)e5f6
|
||||
expect(actualValue, const TextEditingValue(
|
||||
text: 'a1b2c3d4e5f6',
|
||||
selection: const TextSelection(
|
||||
baseOffset: 3,
|
||||
extentOffset: 8,
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
test('test whitelisting formatter', () {
|
||||
final TextEditingValue actualValue =
|
||||
new WhitelistingTextInputFormatter(new RegExp(r'[a-c]'))
|
||||
.formatEditUpdate(testOldValue, testNewValue);
|
||||
|
||||
// Expecting
|
||||
// ab(c)
|
||||
expect(actualValue, const TextEditingValue(
|
||||
text: 'abc',
|
||||
selection: const TextSelection(
|
||||
baseOffset: 2,
|
||||
extentOffset: 3,
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
test('test digits only formatter', () {
|
||||
final TextEditingValue actualValue =
|
||||
WhitelistingTextInputFormatter.digitsOnly
|
||||
.formatEditUpdate(testOldValue, testNewValue);
|
||||
|
||||
// Expecting
|
||||
// 1(234)56
|
||||
expect(actualValue, const TextEditingValue(
|
||||
text: '123456',
|
||||
selection: const TextSelection(
|
||||
baseOffset: 1,
|
||||
extentOffset: 4,
|
||||
),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user