Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
ce0ec01f
Unverified
Commit
ce0ec01f
authored
Dec 28, 2020
by
LongCatIsLooong
Committed by
GitHub
Dec 28, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
squash commits (#68166)
parent
780752e8
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
725 additions
and
28 deletions
+725
-28
text_formatter.dart
packages/flutter/lib/src/services/text_formatter.dart
+211
-14
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+134
-14
text_formatter_test.dart
packages/flutter/test/services/text_formatter_test.dart
+231
-0
editable_text_didUpdateWidget_test.dart
...tter/test/widgets/editable_text_didUpdateWidget_test.dart
+149
-0
No files found.
packages/flutter/lib/src/services/text_formatter.dart
View file @
ce0ec01f
...
@@ -11,6 +11,23 @@ import 'package:flutter/foundation.dart';
...
@@ -11,6 +11,23 @@ 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]
...
@@ -57,16 +74,36 @@ enum MaxLengthEnforcement {
...
@@ -57,16 +74,36 @@ 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.
///
///
/// Text modification should only be applied when text is being committed by the
/// An [EditableText] formats its [TextEditingValue] when the user changes the
/// IME and not on text under composition (i.e., only when
/// text, or when its [EditableText.inputFormatters] parameter changes.
/// [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).
///
///
/// See also the [FilteringTextInputFormatter], a subclass that
/// It is often eaiser to achieve the desired effects by combining
/// removes characters that the user tries to enter if they do, or do
/// [TextInputFormatter]s, as opposed to creating a dedicated
/// not, match a given pattern (as applicable).
/// [TextInputFormatter] from the ground up. See [EditableText.inputFormatters]
/// for an example that implements an idempotent US telephone number formatter
/// using composition.
///
///
/// To create custom formatters, extend the [TextInputFormatter] class and
/// If your input formatter is expensive to run, or the document itself is
/// implement the [formatEditUpdate] method.
/// expensive to format, consider overriding [shouldReformat] to avoid unnessary
/// 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}
...
@@ -77,7 +114,11 @@ enum MaxLengthEnforcement {
...
@@ -77,7 +114,11 @@ enum MaxLengthEnforcement {
/// * [FilteringTextInputFormatter], a provided formatter for filtering
/// * [FilteringTextInputFormatter], a provided formatter for filtering
/// characters.
/// characters.
abstract
class
TextInputFormatter
{
abstract
class
TextInputFormatter
{
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
/// Creates a new [TextInputFormatter].
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.
...
@@ -96,14 +137,145 @@ abstract class TextInputFormatter {
...
@@ -96,14 +137,145 @@ 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
);
}
}
/// Function signature expected for creating custom [TextInputFormatter]
/// A [TextInputFormatter] that composes one or more child [TextInputFormatter]s.
/// shorthands via [TextInputFormatter.withFunction].
///
typedef
TextInputFormatFunction
=
TextEditingValue
Function
(
/// Applying this [CompositeTextInputFormatter] is equivalent to applying all
TextEditingValue
oldValue
,
/// its child [TextInputFormatter]s in the given order.
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
{
...
@@ -280,6 +452,14 @@ class FilteringTextInputFormatter extends TextInputFormatter {
...
@@ -280,6 +452,14 @@ 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].
...
@@ -526,6 +706,23 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
...
@@ -526,6 +706,23 @@ 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
(
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
ce0ec01f
...
@@ -34,6 +34,9 @@ import 'ticker_provider.dart';
...
@@ -34,6 +34,9 @@ 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
);
...
@@ -225,7 +228,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
...
@@ -225,7 +228,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
composing
range is cleared.
/// range, the 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
'
);
...
@@ -272,6 +275,49 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
...
@@ -272,6 +275,49 @@ 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].
...
@@ -525,7 +571,7 @@ class EditableText extends StatefulWidget {
...
@@ -525,7 +571,7 @@ class EditableText extends StatefulWidget {
inputFormatters
=
maxLines
==
1
inputFormatters
=
maxLines
==
1
?
<
TextInputFormatter
>[
?
<
TextInputFormatter
>[
FilteringTextInputFormatter
.
singleLineFormatter
,
FilteringTextInputFormatter
.
singleLineFormatter
,
...
inputFormatters
??
const
Iterable
<
TextInputFormatter
>.
empty
()
,
...
?
inputFormatters
,
]
]
:
inputFormatters
,
:
inputFormatters
,
showCursor
=
showCursor
??
!
readOnly
,
showCursor
=
showCursor
??
!
readOnly
,
...
@@ -1058,9 +1104,76 @@ class EditableText extends StatefulWidget {
...
@@ -1058,9 +1104,76 @@ 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 text input changes. When
/// Formatters are run in the provided order when the user changes the text
/// this parameter changes, the new formatters will not be applied until the
/// contained in the widget. They're not applied when the changes are
/// next time the user inserts or deletes text.
/// selection only, or not initiated by the user.
///
/// 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
;
...
@@ -1550,6 +1663,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1550,6 +1663,7 @@ 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
);
...
@@ -1586,11 +1700,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1586,11 +1700,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
);
...
@@ -1636,6 +1750,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1636,6 +1750,11 @@ 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
...
@@ -2225,7 +2344,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2225,7 +2344,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_lastBottomViewInset
=
WidgetsBinding
.
instance
!.
window
.
viewInsets
.
bottom
;
_lastBottomViewInset
=
WidgetsBinding
.
instance
!.
window
.
viewInsets
.
bottom
;
}
}
late
final
_WhitespaceDirectionalityFormatter
_whitespaceFormatter
=
_WhitespaceDirectionalityFormatter
(
textDirection:
_textDirection
);
_WhitespaceDirectionalityFormatter
?
_lastUsedWhitespaceFormatter
;
_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
...
@@ -2241,18 +2366,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2241,18 +2366,13 @@ 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
)
{
value
=
widget
.
inputFormatters
?.
fold
<
TextEditingValue
>(
final
TextEditingValue
formatted
=
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.
// TODO(LongCatIsLooong): The if statement here is for retaining the
value
=
_whitespaceFormatter
.
formatEditUpdate
(
_value
,
formatted
);
// 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
...
...
packages/flutter/test/services/text_formatter_test.dart
View file @
ce0ec01f
...
@@ -628,4 +628,235 @@ void main() {
...
@@ -628,4 +628,235 @@ 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
\n
d4e5f6'
,
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
);
});
});
}
}
packages/flutter/test/widgets/editable_text_didUpdateWidget_test.dart
0 → 100644
View file @
ce0ec01f
// 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
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment