parent
337290e66d
commit
4220e00fff
@ -11,23 +11,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'text_editing.dart';
|
import 'text_editing.dart';
|
||||||
import 'text_input.dart';
|
import 'text_input.dart';
|
||||||
|
|
||||||
// Examples can assume:
|
|
||||||
// late int maxLength;
|
|
||||||
|
|
||||||
/// Function signature expected for creating custom [TextInputFormatter]
|
|
||||||
/// shorthands via [TextInputFormatter.withFunction].
|
|
||||||
typedef TextInputFormatFunction = TextEditingValue Function(
|
|
||||||
TextEditingValue oldValue,
|
|
||||||
TextEditingValue newValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Function signature for creating a custom
|
|
||||||
/// [CompositeTextInputFormatter.shouldReformat] implementation.
|
|
||||||
typedef ShouldReformatPredicate = bool Function(
|
|
||||||
TextInputFormatter oldFormatter,
|
|
||||||
CompositeTextInputFormatter newFormatter,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// {@template flutter.services.textFormatter.maxLengthEnforcement}
|
/// {@template flutter.services.textFormatter.maxLengthEnforcement}
|
||||||
/// ### [MaxLengthEnforcement.enforced] versus
|
/// ### [MaxLengthEnforcement.enforced] versus
|
||||||
/// [MaxLengthEnforcement.truncateAfterCompositionEnds]
|
/// [MaxLengthEnforcement.truncateAfterCompositionEnds]
|
||||||
@ -74,36 +57,16 @@ enum MaxLengthEnforcement {
|
|||||||
/// A [TextInputFormatter] can be optionally injected into an [EditableText]
|
/// A [TextInputFormatter] can be optionally injected into an [EditableText]
|
||||||
/// to provide as-you-type validation and formatting of the text being edited.
|
/// to provide as-you-type validation and formatting of the text being edited.
|
||||||
///
|
///
|
||||||
/// An [EditableText] formats its [TextEditingValue] when the user changes the
|
/// Text modification should only be applied when text is being committed by the
|
||||||
/// text, or when its [EditableText.inputFormatters] parameter changes.
|
/// IME and not on text under composition (i.e., only when
|
||||||
/// [EditableText] may repetitively apply the same formatter against the input
|
|
||||||
/// text, therefore a formatter generally should not further modify a
|
|
||||||
/// [TextEditingValue] if the value has already been formatted by the same
|
|
||||||
/// formatter.
|
|
||||||
///
|
|
||||||
/// See also the [FilteringTextInputFormatter], a subclass that removes
|
|
||||||
/// characters that the user tries to enter if they do, or do not, match a given
|
|
||||||
/// pattern (as applicable).
|
|
||||||
///
|
|
||||||
/// ## Writing a Custom [TextInputFormatter].
|
|
||||||
///
|
|
||||||
/// To create custom formatters, extend the [TextInputFormatter] class.
|
|
||||||
/// Generally, text modification should only be applied when text is being
|
|
||||||
/// committed by the IME and not on text under composition (i.e., only when
|
|
||||||
/// [TextEditingValue.composing] is collapsed).
|
/// [TextEditingValue.composing] is collapsed).
|
||||||
///
|
///
|
||||||
/// It is often eaiser to achieve the desired effects by combining
|
/// See also the [FilteringTextInputFormatter], a subclass that
|
||||||
/// [TextInputFormatter]s, as opposed to creating a dedicated
|
/// removes characters that the user tries to enter if they do, or do
|
||||||
/// [TextInputFormatter] from the ground up. See [EditableText.inputFormatters]
|
/// not, match a given pattern (as applicable).
|
||||||
/// for an example that implements an idempotent US telephone number formatter
|
|
||||||
/// using composition.
|
|
||||||
///
|
///
|
||||||
/// If your input formatter is expensive to run, or the document itself is
|
/// To create custom formatters, extend the [TextInputFormatter] class and
|
||||||
/// expensive to format, consider overriding [shouldReformat] to avoid unnessary
|
/// implement the [formatEditUpdate] method.
|
||||||
/// reformats when the [EditableText] widget rebuilds. If you wish to change the
|
|
||||||
/// [shouldReformat] strategy used by an existing formatter, consider wrapping
|
|
||||||
/// it in a [CompositeTextInputFormatter] and providing it with the desired
|
|
||||||
/// reformat strategy in [CompositeTextInputFormatter.shouldReformatPredicate].
|
|
||||||
///
|
///
|
||||||
/// ## Handling emojis and other complex characters
|
/// ## Handling emojis and other complex characters
|
||||||
/// {@macro flutter.widgets.EditableText.onChanged}
|
/// {@macro flutter.widgets.EditableText.onChanged}
|
||||||
@ -114,11 +77,7 @@ enum MaxLengthEnforcement {
|
|||||||
/// * [FilteringTextInputFormatter], a provided formatter for filtering
|
/// * [FilteringTextInputFormatter], a provided formatter for filtering
|
||||||
/// characters.
|
/// characters.
|
||||||
abstract class TextInputFormatter {
|
abstract class TextInputFormatter {
|
||||||
/// Creates a new [TextInputFormatter].
|
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
|
||||||
const TextInputFormatter();
|
|
||||||
|
|
||||||
/// Called when text is being typed or cut/copy/pasted in the [EditableText]
|
|
||||||
/// by the user.
|
|
||||||
///
|
///
|
||||||
/// You can override the resulting text based on the previous text value and
|
/// You can override the resulting text based on the previous text value and
|
||||||
/// the incoming new text value.
|
/// the incoming new text value.
|
||||||
@ -137,145 +96,14 @@ abstract class TextInputFormatter {
|
|||||||
) {
|
) {
|
||||||
return _SimpleTextInputFormatter(formatFunction);
|
return _SimpleTextInputFormatter(formatFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this [TextInputFormatter] can replace another [TextInputFormatter]
|
|
||||||
/// without triggering a reformat.
|
|
||||||
///
|
|
||||||
/// This method is called by the associated [EditableText] when it rebuilds,
|
|
||||||
/// to determine whether it can avoid calling [format]. See also
|
|
||||||
/// [LengthLimitingTextInputFormatter.shouldReformat] for an example that
|
|
||||||
/// skips reformatting whenever possible.
|
|
||||||
///
|
|
||||||
/// An easy way to determine whether [oldFormatter] can be safely replaced
|
|
||||||
/// without having to rerun this [TextInputFormatter], is to manually apply
|
|
||||||
/// [format] to every possible return value of [oldFormatter]'s [format]. If
|
|
||||||
/// none of the return values changes, it's always safe to return false.
|
|
||||||
///
|
|
||||||
/// The default implementation always returns true.
|
|
||||||
bool shouldReformat(TextInputFormatter oldFormatter) => true;
|
|
||||||
|
|
||||||
/// Called by [EditableText] when this formatter is added to its
|
|
||||||
/// [EditableText.inputFormatters].
|
|
||||||
///
|
|
||||||
/// [EditableText] may repetitively apply this method to the same input text,
|
|
||||||
/// thus the implementation of this method should not further modify a
|
|
||||||
/// [TextEditingValue] if the value has already been formatted by the same
|
|
||||||
/// formatter (by this method or [formatEditUpdate]).
|
|
||||||
///
|
|
||||||
/// If the formatting operation is expensive, try avoid unnecessary [format]
|
|
||||||
/// calls by returning `false` in [shouldReformat] as much as possible.
|
|
||||||
TextEditingValue format(TextEditingValue value) => formatEditUpdate(value, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [TextInputFormatter] that composes one or more child [TextInputFormatter]s.
|
/// Function signature expected for creating custom [TextInputFormatter]
|
||||||
///
|
/// shorthands via [TextInputFormatter.withFunction].
|
||||||
/// Applying this [CompositeTextInputFormatter] is equivalent to applying all
|
typedef TextInputFormatFunction = TextEditingValue Function(
|
||||||
/// its child [TextInputFormatter]s in the given order.
|
TextEditingValue oldValue,
|
||||||
///
|
TextEditingValue newValue,
|
||||||
/// Aside from combining the effects of multiple [TextInputFormatter]s,
|
|
||||||
/// [CompositeTextInputFormatter] can also be used to create an ad-hoc formatter
|
|
||||||
/// with a different reformat strategy, without subclassing.
|
|
||||||
///
|
|
||||||
/// {@tool snippet}
|
|
||||||
///
|
|
||||||
/// The following code creates a [LengthLimitingTextInputFormatter] with a
|
|
||||||
/// varying `maxLength`, but when the `TextField` rebuilds with a smaller
|
|
||||||
/// `maxLength` value, the new character limit won't be enforced until the user
|
|
||||||
/// changes the context of the `TextField`.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// TextField(
|
|
||||||
/// inputFormatters: <TextInputFormatter>[
|
|
||||||
/// CompositeTextInputFormatter(
|
|
||||||
/// <TextInputFormatter>[LengthLimitingTextInputFormatter(maxLength)],
|
|
||||||
/// shouldReformatPredicate: CompositeTextInputFormatter.neverReformat,
|
|
||||||
/// )
|
|
||||||
/// ]
|
|
||||||
/// )
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
class CompositeTextInputFormatter implements TextInputFormatter {
|
|
||||||
/// Creates a [CompositeTextInputFormatter] with a list of child `formatters`
|
|
||||||
/// and a reformat strategy.
|
|
||||||
const CompositeTextInputFormatter(this.formatters, {
|
|
||||||
this.shouldReformatPredicate = anyChildNeedsReformat,
|
|
||||||
}) : assert(formatters != null),
|
|
||||||
assert(formatters.length > 0),
|
|
||||||
assert(shouldReformatPredicate != null);
|
|
||||||
|
|
||||||
/// Only skip reformatting if the [oldFormatter] is also a
|
|
||||||
/// [CompositeTextInputFormatter] and none of the child input formatters
|
|
||||||
/// requires reformatting.
|
|
||||||
///
|
|
||||||
/// This is the default [shouldReformat] strategy employed by
|
|
||||||
/// [CompositeTextInputFormatter].
|
|
||||||
static bool anyChildNeedsReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) {
|
|
||||||
if (identical(oldFormatter, newFormatter))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (oldFormatter is! CompositeTextInputFormatter
|
|
||||||
|| newFormatter.formatters.length != oldFormatter.formatters.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Iterator<TextInputFormatter> newChild = newFormatter.formatters.iterator;
|
|
||||||
final Iterator<TextInputFormatter> oldChild = oldFormatter.formatters.iterator;
|
|
||||||
while(newChild.moveNext() && oldChild.moveNext()) {
|
|
||||||
if (newChild.current.shouldReformat(oldChild.current))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [ShouldReformatPredicate] that indicates this [CompositeTextInputFormatter]
|
|
||||||
/// should never perform reformat when replacing another [TextInputFormatter].
|
|
||||||
static bool neverReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) => false;
|
|
||||||
|
|
||||||
/// A [ShouldReformatPredicate] that indicates this [CompositeTextInputFormatter]
|
|
||||||
/// should always reformat when replacing another [TextInputFormatter].
|
|
||||||
static bool alwaysReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) => true;
|
|
||||||
|
|
||||||
/// The list of child formatters that will be run in the provided order.
|
|
||||||
///
|
|
||||||
/// Must not be null or empty.
|
|
||||||
final Iterable<TextInputFormatter> formatters;
|
|
||||||
|
|
||||||
/// The [shouldReformat] strategy this [CompositeTextInputFormatter] employs.
|
|
||||||
///
|
|
||||||
/// This class provides 3 predefined reformat strategies:
|
|
||||||
/// * [neverReformat]: the resulting [CompositeTextInputFormatter] never
|
|
||||||
/// reformats when the [EditableText] it is associated with rebuilds.
|
|
||||||
/// * [alwaysReformat]: the resulting [CompositeTextInputFormatter] always
|
|
||||||
/// reformats the [TextEditingValue] when its [EditableText] rebuilds.
|
|
||||||
/// * [anyChildNeedsReformat]: the resulting [CompositeTextInputFormatter]
|
|
||||||
/// reformats the [TextEditingValue] when its [EditableText] rebuilds,
|
|
||||||
/// unless the old formatter is also a [CompositeTextInputFormatter], has
|
|
||||||
/// the same number of child formatters, and none of the new child input
|
|
||||||
/// formatters requests reformatting.
|
|
||||||
///
|
|
||||||
/// Defaults to [anyChildNeedsReformat].
|
|
||||||
final ShouldReformatPredicate shouldReformatPredicate;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
|
||||||
return formatters.fold<TextEditingValue>(
|
|
||||||
oldValue,
|
|
||||||
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(oldValue, newValue),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReformat(TextInputFormatter oldFormatter) => shouldReformatPredicate(oldFormatter, this);
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue format(TextEditingValue value) {
|
|
||||||
return formatters.fold(
|
|
||||||
value,
|
|
||||||
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.format(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wiring for [TextInputFormatter.withFunction].
|
/// Wiring for [TextInputFormatter.withFunction].
|
||||||
class _SimpleTextInputFormatter extends TextInputFormatter {
|
class _SimpleTextInputFormatter extends TextInputFormatter {
|
||||||
@ -452,14 +280,6 @@ class FilteringTextInputFormatter extends TextInputFormatter {
|
|||||||
|
|
||||||
/// A [TextInputFormatter] that takes in digits `[0-9]` only.
|
/// A [TextInputFormatter] that takes in digits `[0-9]` only.
|
||||||
static final TextInputFormatter digitsOnly = FilteringTextInputFormatter.allow(RegExp(r'[0-9]'));
|
static final TextInputFormatter digitsOnly = FilteringTextInputFormatter.allow(RegExp(r'[0-9]'));
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReformat(TextInputFormatter oldFormatter) {
|
|
||||||
return oldFormatter is! FilteringTextInputFormatter
|
|
||||||
|| allow != oldFormatter.allow
|
|
||||||
|| filterPattern != oldFormatter.filterPattern
|
|
||||||
|| replacementString != oldFormatter.replacementString;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Old name for [FilteringTextInputFormatter.deny].
|
/// Old name for [FilteringTextInputFormatter.deny].
|
||||||
@ -706,23 +526,6 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
return truncate(newValue, maxLength);
|
return truncate(newValue, maxLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReformat(TextInputFormatter oldFormatter) {
|
|
||||||
// With maxLength == null or -1, this formatter is basically an identity
|
|
||||||
// function and imposes no constraints on the user input. Thus it can be
|
|
||||||
// used to update an arbitrary formatter without re-formatting.
|
|
||||||
final int? maxLength = this.maxLength;
|
|
||||||
if (maxLength == null || maxLength == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (oldFormatter is! LengthLimitingTextInputFormatter)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
final int? maxLengthOld = oldFormatter.maxLength;
|
|
||||||
return (maxLengthOld == null || maxLengthOld == -1)
|
|
||||||
|| maxLength < maxLengthOld;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingValue _selectionAwareTextManipulation(
|
TextEditingValue _selectionAwareTextManipulation(
|
||||||
|
@ -34,9 +34,6 @@ import 'ticker_provider.dart';
|
|||||||
export 'package:flutter/rendering.dart' show SelectionChangedCause;
|
export 'package:flutter/rendering.dart' show SelectionChangedCause;
|
||||||
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
||||||
|
|
||||||
// Examples can assume:
|
|
||||||
// late TextInputFormatter usPhoneNumberFormatter;
|
|
||||||
|
|
||||||
/// Signature for the callback that reports when the user changes the selection
|
/// Signature for the callback that reports when the user changes the selection
|
||||||
/// (including the cursor location).
|
/// (including the cursor location).
|
||||||
typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause? cause);
|
typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause? cause);
|
||||||
@ -228,7 +225,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
/// change the controller's [value].
|
/// change the controller's [value].
|
||||||
///
|
///
|
||||||
/// If the new selection if of non-zero length, or is outside the composing
|
/// If the new selection if of non-zero length, or is outside the composing
|
||||||
/// range, the composing range is cleared.
|
/// range, the composing composing range is cleared.
|
||||||
set selection(TextSelection newSelection) {
|
set selection(TextSelection newSelection) {
|
||||||
if (!isSelectionWithinTextBounds(newSelection))
|
if (!isSelectionWithinTextBounds(newSelection))
|
||||||
throw FlutterError('invalid text selection: $newSelection');
|
throw FlutterError('invalid text selection: $newSelection');
|
||||||
@ -275,49 +272,6 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
bool _isSelectionWithinComposingRange(TextSelection selection) {
|
bool _isSelectionWithinComposingRange(TextSelection selection) {
|
||||||
return selection.start >= value.composing.start && selection.end <= value.composing.end;
|
return selection.start >= value.composing.start && selection.end <= value.composing.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextInputFormatter>? _inputFormatters;
|
|
||||||
void _setInputFormatters(List<TextInputFormatter> newValue) {
|
|
||||||
// The setter does not take null values: if currentValue is null that means
|
|
||||||
// this is the first formatter list ever set, and we should not reformat.
|
|
||||||
final List<TextInputFormatter>? currentValue = _inputFormatters;
|
|
||||||
_inputFormatters = newValue;
|
|
||||||
if (newValue == currentValue || currentValue == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Iterator<TextInputFormatter> oldFormatters = currentValue.iterator;
|
|
||||||
final Iterator<TextInputFormatter> newFormatters = newValue.iterator;
|
|
||||||
|
|
||||||
// Determining how many new input formatters need to be rerun:
|
|
||||||
//
|
|
||||||
// * The entire `newValue` list needs to be rerun if it has less formatters
|
|
||||||
// than the current list, or any of the new input formatter requests
|
|
||||||
// reformatting.
|
|
||||||
// * Otherwise, only apply the new input formatters whose index is larger
|
|
||||||
// than newValue.length.
|
|
||||||
bool needsReformat = currentValue.length > newValue.length;
|
|
||||||
while (!needsReformat && oldFormatters.moveNext() && newFormatters.moveNext()) {
|
|
||||||
if (newFormatters.current.shouldReformat(oldFormatters.current)) {
|
|
||||||
needsReformat = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditingValue formatted = value;
|
|
||||||
|
|
||||||
if (needsReformat || oldFormatters.moveNext()) {
|
|
||||||
formatted = newValue.fold(
|
|
||||||
formatted,
|
|
||||||
(TextEditingValue v, TextInputFormatter formatter) => formatter.format(v),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
while (newFormatters.moveNext()) {
|
|
||||||
formatted = newFormatters.current.format(formatted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = formatted;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toolbar configuration for [EditableText].
|
/// Toolbar configuration for [EditableText].
|
||||||
@ -571,7 +525,7 @@ class EditableText extends StatefulWidget {
|
|||||||
inputFormatters = maxLines == 1
|
inputFormatters = maxLines == 1
|
||||||
? <TextInputFormatter>[
|
? <TextInputFormatter>[
|
||||||
FilteringTextInputFormatter.singleLineFormatter,
|
FilteringTextInputFormatter.singleLineFormatter,
|
||||||
...?inputFormatters,
|
...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
|
||||||
]
|
]
|
||||||
: inputFormatters,
|
: inputFormatters,
|
||||||
showCursor = showCursor ?? !readOnly,
|
showCursor = showCursor ?? !readOnly,
|
||||||
@ -1104,76 +1058,9 @@ class EditableText extends StatefulWidget {
|
|||||||
/// {@template flutter.widgets.editableText.inputFormatters}
|
/// {@template flutter.widgets.editableText.inputFormatters}
|
||||||
/// Optional input validation and formatting overrides.
|
/// Optional input validation and formatting overrides.
|
||||||
///
|
///
|
||||||
/// Formatters are run in the provided order when the user changes the text
|
/// Formatters are run in the provided order when the text input changes. When
|
||||||
/// contained in the widget. They're not applied when the changes are
|
/// this parameter changes, the new formatters will not be applied until the
|
||||||
/// selection only, or not initiated by the user.
|
/// next time the user inserts or deletes text.
|
||||||
///
|
|
||||||
/// When this widget rebuilds, each input formatter in the new widget's
|
|
||||||
/// [inputFormatters] list checks the configuration of the input formatter
|
|
||||||
/// from the same location in the old [inputFormatters], to determine if the
|
|
||||||
/// new formatters need to be re-applied to the current [TextEditingValue] of
|
|
||||||
/// this widget.
|
|
||||||
///
|
|
||||||
/// {@tool snippet}
|
|
||||||
///
|
|
||||||
/// The following code uses a combination of 2 [TextInputFormatter]s and a
|
|
||||||
/// `UsPhoneNumberFormatter` (which simply adds parentheses and hypens), to
|
|
||||||
/// turn user input into a valid United States telephone number (for example,
|
|
||||||
/// (123)456-7890).
|
|
||||||
///
|
|
||||||
/// The combined effect of the 3 formatters is idempotent, meaning applying
|
|
||||||
/// them together to an already formatted value is a no-op. The
|
|
||||||
/// `UsPhoneNumberFormatter` is not idempotent, thus should not be used by
|
|
||||||
/// itself.
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// class UsPhoneNumberFormatter extends TextInputFormatter {
|
|
||||||
/// const UsPhoneNumberFormatter();
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// TextEditingValue format(TextEditingValue value) {
|
|
||||||
/// final int inputLength = value.text.length;
|
|
||||||
/// if (inputLength <= 3)
|
|
||||||
/// return value;
|
|
||||||
///
|
|
||||||
/// final StringBuffer newText = StringBuffer();
|
|
||||||
///
|
|
||||||
/// newText.write('(');
|
|
||||||
/// newText.write(value.text.substring(0, 3));
|
|
||||||
/// newText.write(')');
|
|
||||||
/// newText.write(value.text.substring(3, math.min(6, inputLength)));
|
|
||||||
///
|
|
||||||
/// if (inputLength > 6) {
|
|
||||||
/// newText.write('-');
|
|
||||||
/// newText.write(value.text.substring(6));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// final int selectionOffset = value.selection.end <= 3 ? 1 : value.selection.end <= 6 ? 2 : 3;
|
|
||||||
/// return TextEditingValue(
|
|
||||||
/// text: newText.toString(),
|
|
||||||
/// selection: TextSelection.collapsed(offset: value.selection.end + selectionOffset),
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) => format(newValue);
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// bool shouldReformat(TextInputFormatter oldFormatter) => oldFormatter is! UsPhoneNumberFormatter;
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// TextField(
|
|
||||||
/// inputFormatters: <TextInputFormatter>[
|
|
||||||
/// FilteringTextInputFormatter.digitsOnly,
|
|
||||||
/// LengthLimitingTextInputFormatter(10),
|
|
||||||
/// usPhoneNumberFormatter,
|
|
||||||
/// ],
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final List<TextInputFormatter>? inputFormatters;
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
|
||||||
@ -1663,7 +1550,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller._setInputFormatters(widget.inputFormatters ?? const <TextInputFormatter>[]);
|
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
_focusAttachment = widget.focusNode.attach(context);
|
_focusAttachment = widget.focusNode.attach(context);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
@ -1700,11 +1586,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EditableText oldWidget) {
|
void didUpdateWidget(EditableText oldWidget) {
|
||||||
beginBatchEdit();
|
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.controller != oldWidget.controller) {
|
if (widget.controller != oldWidget.controller) {
|
||||||
oldWidget.controller.removeListener(_didChangeTextEditingValue);
|
oldWidget.controller.removeListener(_didChangeTextEditingValue);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
|
_updateRemoteEditingValueIfNeeded();
|
||||||
}
|
}
|
||||||
if (widget.controller.selection != oldWidget.controller.selection) {
|
if (widget.controller.selection != oldWidget.controller.selection) {
|
||||||
_selectionOverlay?.update(_value);
|
_selectionOverlay?.update(_value);
|
||||||
@ -1750,11 +1636,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (widget.selectionEnabled && pasteEnabled && widget.selectionControls?.canPaste(this) == true) {
|
if (widget.selectionEnabled && pasteEnabled && widget.selectionControls?.canPaste(this) == true) {
|
||||||
_clipboardStatus?.update();
|
_clipboardStatus?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.controller._setInputFormatters(
|
|
||||||
widget.inputFormatters ?? const <TextInputFormatter>[]
|
|
||||||
);
|
|
||||||
endBatchEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -2344,13 +2225,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
|
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
_WhitespaceDirectionalityFormatter? _lastUsedWhitespaceFormatter;
|
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
|
||||||
_WhitespaceDirectionalityFormatter get _whitespaceFormatter {
|
|
||||||
final _WhitespaceDirectionalityFormatter? lastUsed = _lastUsedWhitespaceFormatter;
|
|
||||||
if (lastUsed != null && lastUsed._baseDirection == _textDirection)
|
|
||||||
return lastUsed;
|
|
||||||
return _lastUsedWhitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _formatAndSetValue(TextEditingValue value) {
|
void _formatAndSetValue(TextEditingValue value) {
|
||||||
// Only apply input formatters if the text has changed (including uncommited
|
// Only apply input formatters if the text has changed (including uncommited
|
||||||
@ -2366,13 +2241,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final bool selectionChanged = _value.selection != value.selection;
|
final bool selectionChanged = _value.selection != value.selection;
|
||||||
|
|
||||||
if (textChanged) {
|
if (textChanged) {
|
||||||
final TextEditingValue formatted = widget.inputFormatters?.fold<TextEditingValue>(
|
value = widget.inputFormatters?.fold<TextEditingValue>(
|
||||||
value,
|
value,
|
||||||
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
|
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
|
||||||
) ?? value;
|
) ?? value;
|
||||||
|
|
||||||
// Always pass the text through the whitespace directionality formatter to
|
// Always pass the text through the whitespace directionality formatter to
|
||||||
// maintain expected behavior with carets on trailing whitespace.
|
// maintain expected behavior with carets on trailing whitespace.
|
||||||
value = _whitespaceFormatter.formatEditUpdate(_value, formatted);
|
// TODO(LongCatIsLooong): The if statement here is for retaining the
|
||||||
|
// previous behavior. The input formatter logic will be updated in an
|
||||||
|
// upcoming PR.
|
||||||
|
if (widget.inputFormatters?.isNotEmpty ?? false)
|
||||||
|
value = _whitespaceFormatter.formatEditUpdate(_value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put all optional user callback invocations in a batch edit to prevent
|
// Put all optional user callback invocations in a batch edit to prevent
|
||||||
|
@ -628,235 +628,4 @@ void main() {
|
|||||||
// cursor must be now at fourth position (right after the number 9)
|
// cursor must be now at fourth position (right after the number 9)
|
||||||
expect(formatted.selection.baseOffset, equals(4));
|
expect(formatted.selection.baseOffset, equals(4));
|
||||||
});
|
});
|
||||||
|
|
||||||
group('provided formatters implement shouldReformat correctly', () {
|
|
||||||
test('length limiting formatter', () {
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(-1).shouldReformat(LengthLimitingTextInputFormatter(null)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(null).shouldReformat(LengthLimitingTextInputFormatter(-1)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(null).shouldReformat(LengthLimitingTextInputFormatter(null)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(3)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We're relaxing the length constraint. No reformatting needed.
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(-1).shouldReformat(LengthLimitingTextInputFormatter(3)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We're relaxing the length constraint. No reformatting needed.
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(4).shouldReformat(LengthLimitingTextInputFormatter(3)),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(4)),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(null)),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(-1)),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('FliteringTextInputFormatter', () {
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b'),
|
|
||||||
),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'c'),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat(
|
|
||||||
FilteringTextInputFormatter('a', allow: false, replacementString: 'b'),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat(
|
|
||||||
FilteringTextInputFormatter('c', allow: true, replacementString: 'b'),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat(
|
|
||||||
FilteringTextInputFormatter('c', allow: true),
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('provided formatters do not further modify a formatted value', () {
|
|
||||||
// Framework-provided TextInputFormatters must be idempotent in order to be
|
|
||||||
// used alone.
|
|
||||||
void verifyFormatterIdempotency(
|
|
||||||
TextInputFormatter formatter,
|
|
||||||
TextEditingValue input,
|
|
||||||
) {
|
|
||||||
final TextEditingValue formatted = formatter.format(input);
|
|
||||||
expect(formatter.format(formatted), formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
// a1b(2c3
|
|
||||||
// d4)e5f6
|
|
||||||
// where the parentheses are the selection range.
|
|
||||||
testNewValue = const TextEditingValue(
|
|
||||||
text: 'a1b2c3\nd4e5f6',
|
|
||||||
selection: TextSelection(
|
|
||||||
baseOffset: 3,
|
|
||||||
extentOffset: 9,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('FliteringTextInputFormatter with replacementString', () {
|
|
||||||
const TextEditingValue selectedIntoTheWoods = TextEditingValue(
|
|
||||||
text: 'Into the Woods',
|
|
||||||
selection: TextSelection(baseOffset: 11, extentOffset: 14),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final Pattern p in <Pattern>['o', RegExp('o+')]) {
|
|
||||||
verifyFormatterIdempotency(
|
|
||||||
FilteringTextInputFormatter(p, allow: true, replacementString: '*'),
|
|
||||||
selectedIntoTheWoods,
|
|
||||||
);
|
|
||||||
verifyFormatterIdempotency(
|
|
||||||
FilteringTextInputFormatter(p, allow: false, replacementString: '*'),
|
|
||||||
selectedIntoTheWoods,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('single line formatter', () {
|
|
||||||
verifyFormatterIdempotency(
|
|
||||||
FilteringTextInputFormatter.singleLineFormatter,
|
|
||||||
testNewValue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('digits only formatter', () {
|
|
||||||
verifyFormatterIdempotency(
|
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
|
||||||
testNewValue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('length limiting formatter', () {
|
|
||||||
verifyFormatterIdempotency(
|
|
||||||
LengthLimitingTextInputFormatter(5),
|
|
||||||
testNewValue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('CompositeTextInputFormatter', () {
|
|
||||||
test('combine effects, in provided order', () {
|
|
||||||
final CompositeTextInputFormatter formatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(3),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(formatter.format(const TextEditingValue(text: 'aab')).text, 'aab');
|
|
||||||
expect(
|
|
||||||
formatter.formatEditUpdate(const TextEditingValue(text: 'aaa'), const TextEditingValue(text: 'aab')).text,
|
|
||||||
'aaa',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('anyChildNeedsReformat', () {
|
|
||||||
final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(3),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(1),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(newFormatter.shouldReformat(newFormatter), isFalse);
|
|
||||||
expect(oldFormatter.shouldReformat(oldFormatter), isFalse);
|
|
||||||
expect(newFormatter.shouldReformat(oldFormatter), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('neverReformat', () {
|
|
||||||
final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(3),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(1),
|
|
||||||
],
|
|
||||||
shouldReformatPredicate: CompositeTextInputFormatter.neverReformat,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(newFormatter.shouldReformat(newFormatter), isFalse);
|
|
||||||
expect(oldFormatter.shouldReformat(oldFormatter), isFalse);
|
|
||||||
expect(newFormatter.shouldReformat(oldFormatter), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('alwaysReformat', () {
|
|
||||||
final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(3),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
|
|
||||||
<TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
|
|
||||||
LengthLimitingTextInputFormatter(999),
|
|
||||||
],
|
|
||||||
shouldReformatPredicate: CompositeTextInputFormatter.alwaysReformat,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(newFormatter.shouldReformat(newFormatter), isTrue);
|
|
||||||
expect(oldFormatter.shouldReformat(oldFormatter), isFalse);
|
|
||||||
expect(newFormatter.shouldReformat(oldFormatter), isTrue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -1,149 +0,0 @@
|
|||||||
// Copyright 2014 The Flutter 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/widgets.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node');
|
|
||||||
const TextStyle textStyle = TextStyle();
|
|
||||||
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
|
||||||
const Color backgroundColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
|
||||||
late TextEditingController defaultController;
|
|
||||||
|
|
||||||
group('didUpdateWidget', () {
|
|
||||||
final _AppendingFormatter appendingFormatter = _AppendingFormatter();
|
|
||||||
|
|
||||||
Widget build({
|
|
||||||
TextDirection textDirection = TextDirection.ltr,
|
|
||||||
List<TextInputFormatter>? formatters,
|
|
||||||
TextEditingController? controller,
|
|
||||||
}) {
|
|
||||||
return MediaQuery(
|
|
||||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
|
||||||
child: Directionality(
|
|
||||||
textDirection: textDirection,
|
|
||||||
child: EditableText(
|
|
||||||
backgroundCursorColor: backgroundColor,
|
|
||||||
controller: controller ?? defaultController,
|
|
||||||
maxLines: null, // Remove the builtin newline formatter.
|
|
||||||
focusNode: focusNode,
|
|
||||||
style: textStyle,
|
|
||||||
cursorColor: cursorColor,
|
|
||||||
inputFormatters: formatters,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('EditableText only reformats when needed', (WidgetTester tester) async {
|
|
||||||
appendingFormatter.needsReformat = false;
|
|
||||||
defaultController = TextEditingController(text: 'initialText');
|
|
||||||
String previousText = defaultController.text;
|
|
||||||
|
|
||||||
// Initial build, do not apply formatters.
|
|
||||||
await tester.pumpWidget(build());
|
|
||||||
expect(defaultController.text, previousText);
|
|
||||||
|
|
||||||
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
|
|
||||||
LengthLimitingTextInputFormatter(null),
|
|
||||||
appendingFormatter,
|
|
||||||
]));
|
|
||||||
|
|
||||||
expect(defaultController.text, contains(previousText + 'a'));
|
|
||||||
previousText = defaultController.text;
|
|
||||||
|
|
||||||
// Change the first formatter.
|
|
||||||
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
|
|
||||||
LengthLimitingTextInputFormatter(1000),
|
|
||||||
appendingFormatter,
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Reformat since the length formatter changed and it becomes more
|
|
||||||
// strict (null -> 1000).
|
|
||||||
expect(defaultController.text, contains(previousText + 'a'));
|
|
||||||
previousText = defaultController.text;
|
|
||||||
|
|
||||||
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
|
|
||||||
LengthLimitingTextInputFormatter(2000),
|
|
||||||
appendingFormatter,
|
|
||||||
]));
|
|
||||||
|
|
||||||
// No reformat needed since the length formatter relaxed its constraint
|
|
||||||
// (1000 -> 2000).
|
|
||||||
expect(defaultController.text, previousText);
|
|
||||||
|
|
||||||
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
|
|
||||||
appendingFormatter,
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Reformat since we reduced the number of new formatters.
|
|
||||||
expect(defaultController.text, previousText + 'a');
|
|
||||||
previousText = defaultController.text;
|
|
||||||
|
|
||||||
// Now the the appending formatter always requests a reformat when
|
|
||||||
// didUpdateWidget is called.
|
|
||||||
appendingFormatter.needsReformat = true;
|
|
||||||
|
|
||||||
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
|
|
||||||
appendingFormatter,
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Reformat since appendingFormatter now always requests a rerun.
|
|
||||||
expect(defaultController.text, contains(previousText + 'a'));
|
|
||||||
previousText = defaultController.text;
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'Changing the controller along with the formatter does not reformat',
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
// This test verifies that the `shouldReformat` predicate is run against
|
|
||||||
// the previous formatter associated with the *TextEditingController*,
|
|
||||||
// instead of the one associated with the widget, to avoid unnecessary
|
|
||||||
// rebuilds.
|
|
||||||
final TextEditingController controller1 = TextEditingController(text: 'shorttxt');
|
|
||||||
final TextEditingController controller2 = TextEditingController(text: 'looooong text');
|
|
||||||
|
|
||||||
final Widget editableText1 = build(
|
|
||||||
controller: controller1,
|
|
||||||
formatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(controller1.text.length)],
|
|
||||||
);
|
|
||||||
final Widget editableText2 = build(
|
|
||||||
controller: controller2,
|
|
||||||
formatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(controller2.text.length)],
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(Directionality(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: Column(children: <Widget>[editableText1, editableText2]),
|
|
||||||
));
|
|
||||||
|
|
||||||
// The 2 input fields swap places. The input formatters should not rerun.
|
|
||||||
await tester.pumpWidget(Directionality(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: Column(children: <Widget>[editableText2, editableText1]),
|
|
||||||
));
|
|
||||||
|
|
||||||
expect(controller1.text, 'shorttxt');
|
|
||||||
expect(controller2.text, 'looooong text');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// A TextInputFormatter that appends 'a' to the current editing value every time
|
|
||||||
// it runs.
|
|
||||||
class _AppendingFormatter extends TextInputFormatter {
|
|
||||||
bool needsReformat = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
|
||||||
return newValue.copyWith(text: newValue.text + 'a');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReformat(TextInputFormatter oldFormatter) => needsReformat;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user