Revert "Make FilteringTextInputFormatter
's filtering Selection/Composing Region agnostic (#89327)" (#90089)
This commit is contained in:
parent
b129fffa23
commit
5d9afaa0f7
@ -121,94 +121,6 @@ class _SimpleTextInputFormatter extends TextInputFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mutable, half-open range [`base`, `extent`) within a string.
|
|
||||||
class _MutableTextRange {
|
|
||||||
_MutableTextRange(this.base, this.extent);
|
|
||||||
|
|
||||||
static _MutableTextRange? fromComposingRange(TextRange range) {
|
|
||||||
return range.isValid && !range.isCollapsed
|
|
||||||
? _MutableTextRange(range.start, range.end)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static _MutableTextRange? fromTextSelection(TextSelection selection) {
|
|
||||||
return selection.isValid
|
|
||||||
? _MutableTextRange(selection.baseOffset, selection.extentOffset)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The start index of the range, inclusive.
|
|
||||||
///
|
|
||||||
/// The value of [base] should always be greater than or equal to 0, and can
|
|
||||||
/// be larger than, smaller than, or equal to [extent].
|
|
||||||
int base;
|
|
||||||
|
|
||||||
/// The end index of the range, exclusive.
|
|
||||||
///
|
|
||||||
/// The value of [extent] should always be greater than or equal to 0, and can
|
|
||||||
/// be larger than, smaller than, or equal to [base].
|
|
||||||
int extent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The intermediate state of a [FilteringTextInputFormatter] when it's
|
|
||||||
// formatting a new user input.
|
|
||||||
class _TextEditingValueAccumulator {
|
|
||||||
_TextEditingValueAccumulator(this.inputValue)
|
|
||||||
: selection = _MutableTextRange.fromTextSelection(inputValue.selection),
|
|
||||||
composingRegion = _MutableTextRange.fromComposingRange(inputValue.composing);
|
|
||||||
|
|
||||||
// The original string that was sent to the [FilteringTextInputFormatter] as
|
|
||||||
// input.
|
|
||||||
final TextEditingValue inputValue;
|
|
||||||
|
|
||||||
/// The [StringBuffer] that contains the string which has already been
|
|
||||||
/// formatted.
|
|
||||||
///
|
|
||||||
/// In a [FilteringTextInputFormatter], typically the replacement string,
|
|
||||||
/// instead of the original string within the given range, is written to this
|
|
||||||
/// [StringBuffer].
|
|
||||||
final StringBuffer stringBuffer = StringBuffer();
|
|
||||||
|
|
||||||
/// The updated selection, as well as the original selection from the input
|
|
||||||
/// [TextEditingValue] of the [FilteringTextInputFormatter].
|
|
||||||
///
|
|
||||||
/// This parameter will be null if the input [TextEditingValue.selection] is
|
|
||||||
/// invalid.
|
|
||||||
final _MutableTextRange? selection;
|
|
||||||
|
|
||||||
/// The updated composing region, as well as the original composing region
|
|
||||||
/// from the input [TextEditingValue] of the [FilteringTextInputFormatter].
|
|
||||||
///
|
|
||||||
/// This parameter will be null if the input [TextEditingValue.composing] is
|
|
||||||
/// invalid or collapsed.
|
|
||||||
final _MutableTextRange? composingRegion;
|
|
||||||
|
|
||||||
// Whether this state object has reached its end-of-life.
|
|
||||||
bool debugFinalized = false;
|
|
||||||
|
|
||||||
TextEditingValue finalize() {
|
|
||||||
debugFinalized = true;
|
|
||||||
final _MutableTextRange? selection = this.selection;
|
|
||||||
final _MutableTextRange? composingRegion = this.composingRegion;
|
|
||||||
return TextEditingValue(
|
|
||||||
text: stringBuffer.toString(),
|
|
||||||
composing: composingRegion == null || composingRegion.base == composingRegion.extent
|
|
||||||
? TextRange.empty
|
|
||||||
: TextRange(start: composingRegion.base, end: composingRegion.extent),
|
|
||||||
selection: selection == null
|
|
||||||
? const TextSelection.collapsed(offset: -1)
|
|
||||||
: TextSelection(
|
|
||||||
baseOffset: selection.base,
|
|
||||||
extentOffset: selection.extent,
|
|
||||||
// Try to preserve the selection affinity and isDirectional. This
|
|
||||||
// may not make sense if the selection has changed.
|
|
||||||
affinity: inputValue.selection.affinity,
|
|
||||||
isDirectional: inputValue.selection.isDirectional,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [TextInputFormatter] that prevents the insertion of characters
|
/// A [TextInputFormatter] that prevents the insertion of characters
|
||||||
/// matching (or not matching) a particular pattern.
|
/// matching (or not matching) a particular pattern.
|
||||||
///
|
///
|
||||||
@ -247,26 +159,33 @@ class FilteringTextInputFormatter extends TextInputFormatter {
|
|||||||
/// The [filterPattern] and [replacementString] arguments
|
/// The [filterPattern] and [replacementString] arguments
|
||||||
/// must not be null.
|
/// must not be null.
|
||||||
FilteringTextInputFormatter.allow(
|
FilteringTextInputFormatter.allow(
|
||||||
Pattern filterPattern, {
|
this.filterPattern, {
|
||||||
String replacementString = '',
|
this.replacementString = '',
|
||||||
}) : this(filterPattern, allow: true, replacementString: replacementString);
|
}) : assert(filterPattern != null),
|
||||||
|
assert(replacementString != null),
|
||||||
|
allow = true;
|
||||||
|
|
||||||
/// Creates a formatter that blocks characters matching a pattern.
|
/// Creates a formatter that blocks characters matching a pattern.
|
||||||
///
|
///
|
||||||
/// The [filterPattern] and [replacementString] arguments
|
/// The [filterPattern] and [replacementString] arguments
|
||||||
/// must not be null.
|
/// must not be null.
|
||||||
FilteringTextInputFormatter.deny(
|
FilteringTextInputFormatter.deny(
|
||||||
Pattern filterPattern, {
|
this.filterPattern, {
|
||||||
String replacementString = '',
|
this.replacementString = '',
|
||||||
}) : this(filterPattern, allow: false, replacementString: replacementString);
|
}) : assert(filterPattern != null),
|
||||||
|
assert(replacementString != null),
|
||||||
|
allow = false;
|
||||||
|
|
||||||
/// A [Pattern] to match or replace in incoming [TextEditingValue]s.
|
/// A [Pattern] to match and replace in incoming [TextEditingValue]s.
|
||||||
///
|
///
|
||||||
/// The behavior of the pattern depends on the [allow] property. If
|
/// The behavior of the pattern depends on the [allow] property. If
|
||||||
/// it is true, then this is an allow list, specifying a pattern that
|
/// it is true, then this is an allow list, specifying a pattern that
|
||||||
/// characters must match to be accepted. Otherwise, it is a deny list,
|
/// characters must match to be accepted. Otherwise, it is a deny list,
|
||||||
/// specifying a pattern that characters must not match to be accepted.
|
/// specifying a pattern that characters must not match to be accepted.
|
||||||
///
|
///
|
||||||
|
/// In general, the pattern should only match one character at a
|
||||||
|
/// time. See the discussion at [replacementString].
|
||||||
|
///
|
||||||
/// {@tool snippet}
|
/// {@tool snippet}
|
||||||
/// Typically the pattern is a regular expression, as in:
|
/// Typically the pattern is a regular expression, as in:
|
||||||
///
|
///
|
||||||
@ -327,18 +246,16 @@ class FilteringTextInputFormatter extends TextInputFormatter {
|
|||||||
/// string) because both of the "o"s would be matched simultaneously
|
/// string) because both of the "o"s would be matched simultaneously
|
||||||
/// by the pattern.
|
/// by the pattern.
|
||||||
///
|
///
|
||||||
/// The filter may adjust the selection and the composing region of the text
|
/// Additionally, each segment of the string before, during, and
|
||||||
/// after applying the text replacement, such that they still cover the same
|
/// after the current selection in the [TextEditingValue] is handled
|
||||||
/// text. For instance, if the pattern was `o+` and the last character "s" was
|
/// separately. This means that, in the case of the "Into the Woods"
|
||||||
/// selected: "Into The Wood|s|", then the result will be "Into The W*d|s|",
|
/// example above, if the selection ended between the two "o"s in
|
||||||
/// with the selection still around the same character "s" despite that it is
|
/// "Woods", even if the pattern was `RegExp('o+')`, the result
|
||||||
/// now the 12th character.
|
/// would be "Int* the W**ds", since the two "o"s would be handled
|
||||||
|
/// in separate passes.
|
||||||
///
|
///
|
||||||
/// In the case where one end point of the selection (or the composing region)
|
/// See also [String.splitMapJoin], which is used to implement this
|
||||||
/// is strictly inside the banned pattern (for example, "Into The |Wo|ods"),
|
/// behavior in both cases.
|
||||||
/// that endpoint will be moved to the end of the replacement string (it will
|
|
||||||
/// become "Into The |W*|ds" if the pattern was `o+` and the original text and
|
|
||||||
/// selection were "Into The |Wo|ods").
|
|
||||||
final String replacementString;
|
final String replacementString;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -346,61 +263,16 @@ class FilteringTextInputFormatter extends TextInputFormatter {
|
|||||||
TextEditingValue oldValue, // unused.
|
TextEditingValue oldValue, // unused.
|
||||||
TextEditingValue newValue,
|
TextEditingValue newValue,
|
||||||
) {
|
) {
|
||||||
final _TextEditingValueAccumulator formatState = _TextEditingValueAccumulator(newValue);
|
return _selectionAwareTextManipulation(
|
||||||
assert(!formatState.debugFinalized);
|
newValue,
|
||||||
|
(String substring) {
|
||||||
final Iterable<Match> matches = filterPattern.allMatches(newValue.text);
|
return substring.splitMapJoin(
|
||||||
Match? previousMatch;
|
filterPattern,
|
||||||
for (final Match match in matches) {
|
onMatch: !allow ? (Match match) => replacementString : null,
|
||||||
assert(match.end > match.start);
|
onNonMatch: allow ? (String nonMatch) => nonMatch.isNotEmpty ? replacementString : '' : null,
|
||||||
// Compute the non-match region between this `Match` and the previous
|
);
|
||||||
// `Match`. Depending on the value of `allow`, either the match region or
|
},
|
||||||
// the non-match region is the banned pattern.
|
);
|
||||||
//
|
|
||||||
// The non-matching region.
|
|
||||||
_processRegion(allow, previousMatch?.end ?? 0, match.start, formatState);
|
|
||||||
assert(!formatState.debugFinalized);
|
|
||||||
// The matched region.
|
|
||||||
_processRegion(!allow, match.start, match.end, formatState);
|
|
||||||
assert(!formatState.debugFinalized);
|
|
||||||
|
|
||||||
previousMatch = match;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the last non-matching region between the last match region and the
|
|
||||||
// end of the text.
|
|
||||||
_processRegion(allow, previousMatch?.end ?? 0, newValue.text.length, formatState);
|
|
||||||
assert(!formatState.debugFinalized);
|
|
||||||
return formatState.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _processRegion(bool isBannedRegion, int regionStart, int regionEnd, _TextEditingValueAccumulator state) {
|
|
||||||
final String replacementString = isBannedRegion
|
|
||||||
? (regionStart == regionEnd ? '' : this.replacementString)
|
|
||||||
: state.inputValue.text.substring(regionStart, regionEnd);
|
|
||||||
|
|
||||||
state.stringBuffer.write(replacementString);
|
|
||||||
|
|
||||||
if (replacementString.length == regionEnd - regionStart) {
|
|
||||||
// We don't have to adjust the indices if the replaced string and the
|
|
||||||
// replacement string have the same length.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int adjustIndex(int originalIndex) {
|
|
||||||
// Add to the index the length of the replacement if needed.
|
|
||||||
// The condition of the ternary operator is chosen such that the index
|
|
||||||
// will be placed **after** the replacement text, if the index was
|
|
||||||
// strictly inside the banned pattern.
|
|
||||||
final int replacedLength = regionStart >= originalIndex ? 0 : replacementString.length;
|
|
||||||
// Subtract from the index the length of the banned pattern before the index.
|
|
||||||
return replacedLength - (originalIndex.clamp(regionStart, regionEnd) - regionStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.selection?.base += adjustIndex(state.inputValue.selection.baseOffset);
|
|
||||||
state.selection?.extent += adjustIndex(state.inputValue.selection.extentOffset);
|
|
||||||
state.composingRegion?.base += adjustIndex(state.inputValue.composing.start);
|
|
||||||
state.composingRegion?.extent += adjustIndex(state.inputValue.composing.end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [TextInputFormatter] that forces input to be a single line.
|
/// A [TextInputFormatter] that forces input to be a single line.
|
||||||
@ -655,3 +527,45 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextEditingValue _selectionAwareTextManipulation(
|
||||||
|
TextEditingValue value,
|
||||||
|
String Function(String substring) substringManipulation,
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
if (value.selection.baseOffset > value.selection.extentOffset) {
|
||||||
|
manipulatedSelection = value.selection.copyWith(
|
||||||
|
baseOffset: beforeSelection.length + inSelection.length,
|
||||||
|
extentOffset: beforeSelection.length,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
manipulatedSelection = value.selection.copyWith(
|
||||||
|
baseOffset: beforeSelection.length,
|
||||||
|
extentOffset: beforeSelection.length + inSelection.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TextEditingValue(
|
||||||
|
text: manipulatedText,
|
||||||
|
selection: manipulatedSelection ?? const TextSelection.collapsed(offset: -1),
|
||||||
|
composing: manipulatedText == value.text
|
||||||
|
? value.composing
|
||||||
|
: TextRange.empty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -741,38 +741,9 @@ class TextEditingValue {
|
|||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
/// The range of text that is currently selected.
|
/// The range of text that is currently selected.
|
||||||
///
|
|
||||||
/// When [selection] is a [TextSelection] that has the same non-negative
|
|
||||||
/// `baseOffset` and `extentOffset`, the [selection] property represents the
|
|
||||||
/// caret position.
|
|
||||||
///
|
|
||||||
/// If the current [selection] has a negative `baseOffset` or `extentOffset`,
|
|
||||||
/// then the text currently does not have a selection or a caret location, and
|
|
||||||
/// most text editing operations that rely on the current selection (for
|
|
||||||
/// instance, insert a character at the caret location) will do nothing.
|
|
||||||
final TextSelection selection;
|
final TextSelection selection;
|
||||||
|
|
||||||
/// The range of text that is still being composed.
|
/// The range of text that is still being composed.
|
||||||
///
|
|
||||||
/// Composing regions are created by input methods (IMEs) to indicate the text
|
|
||||||
/// within a certain range is provisional. For instance, the Android Gboard
|
|
||||||
/// app's English keyboard puts the current word under the caret into a
|
|
||||||
/// composing region to indicate the word is subject to autocorrect or
|
|
||||||
/// prediction changes.
|
|
||||||
///
|
|
||||||
/// Composing regions can also be used for performing multistage input, which
|
|
||||||
/// is typically used by IMEs designed for phoetic keyboard to enter
|
|
||||||
/// ideographic symbols. As an example, many CJK keyboards require the user to
|
|
||||||
/// enter a latin alphabet sequence and then convert it to CJK characters. On
|
|
||||||
/// iOS, the default software keyboards do not have a dedicated view to show
|
|
||||||
/// the unfinished latin sequence, so it's displayed directly in the text
|
|
||||||
/// field, inside of a composing region.
|
|
||||||
///
|
|
||||||
/// The composing region should typically only be changed by the IME, or the
|
|
||||||
/// user via interacting with the IME.
|
|
||||||
///
|
|
||||||
/// If the range represented by this property is [TextRange.empty], then the
|
|
||||||
/// text is not currently being composed.
|
|
||||||
final TextRange composing;
|
final TextRange composing;
|
||||||
|
|
||||||
/// A value that corresponds to the empty string with no selection and no composing range.
|
/// A value that corresponds to the empty string with no selection and no composing range.
|
||||||
|
@ -64,7 +64,6 @@ void main() {
|
|||||||
const TextEditingValue(text: 'Int* the W*ds'),
|
const TextEditingValue(text: 'Int* the W*ds'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// "Into the Wo|ods|"
|
|
||||||
const TextEditingValue selectedIntoTheWoods = TextEditingValue(text: 'Into the Woods', selection: TextSelection(baseOffset: 11, extentOffset: 14));
|
const TextEditingValue selectedIntoTheWoods = TextEditingValue(text: 'Into the Woods', selection: TextSelection(baseOffset: 11, extentOffset: 14));
|
||||||
expect(
|
expect(
|
||||||
FilteringTextInputFormatter('o', allow: true, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
|
FilteringTextInputFormatter('o', allow: true, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
|
||||||
@ -80,7 +79,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
FilteringTextInputFormatter(RegExp('o+'), allow: false, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
|
FilteringTextInputFormatter(RegExp('o+'), allow: false, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
|
||||||
const TextEditingValue(text: 'Int* the W*ds', selection: TextSelection(baseOffset: 11, extentOffset: 13)),
|
const TextEditingValue(text: 'Int* the W**ds', selection: TextSelection(baseOffset: 11, extentOffset: 14)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -625,237 +624,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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('FilteringTextInputFormatter should filter independent of selection', () {
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/80842.
|
|
||||||
|
|
||||||
final TextInputFormatter formatter = FilteringTextInputFormatter.deny('abc', replacementString: '*');
|
|
||||||
|
|
||||||
const TextEditingValue oldValue = TextEditingValue.empty;
|
|
||||||
const TextEditingValue newValue = TextEditingValue(text: 'abcabcabc');
|
|
||||||
|
|
||||||
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
|
|
||||||
|
|
||||||
for (int i = 0; i < newValue.text.length; i += 1) {
|
|
||||||
final String text = formatter.formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(selection: TextSelection.collapsed(offset: i)),
|
|
||||||
).text;
|
|
||||||
expect(filteredText, text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('FilteringTextInputFormatter should filter independent of composingRegion', () {
|
|
||||||
final TextInputFormatter formatter = FilteringTextInputFormatter.deny('abc', replacementString: '*');
|
|
||||||
|
|
||||||
const TextEditingValue oldValue = TextEditingValue.empty;
|
|
||||||
const TextEditingValue newValue = TextEditingValue(text: 'abcabcabc');
|
|
||||||
|
|
||||||
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
|
|
||||||
|
|
||||||
for (int i = 0; i < newValue.text.length; i += 1) {
|
|
||||||
final String text = formatter.formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(composing: TextRange.collapsed(i)),
|
|
||||||
).text;
|
|
||||||
expect(filteredText, text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
group('FilteringTextInputFormatter region', () {
|
|
||||||
const TextEditingValue oldValue = TextEditingValue.empty;
|
|
||||||
|
|
||||||
test('Preserves selection region', () {
|
|
||||||
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
|
|
||||||
|
|
||||||
// AAA | BBB | CCC => AAA | **** | CCC
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 7, extentOffset: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA | BBB CCC | => AAA | **** CCC |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 9, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 10, extentOffset: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA BBB | CCC | => AAA **** | CCC |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 9, extentOffset: 6),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 10, extentOffset: 7),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAAB | B | BCCC => AAA***|CCC
|
|
||||||
// Same length replacement, keep the selection at where it is.
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '***').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 5, extentOffset: 4),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 5, extentOffset: 4),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA | BBB | CCC => AAA | CCC
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 3, extentOffset: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 3, extentOffset: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
// The unfortunate case, we don't know for sure where to put the selection
|
|
||||||
// so put it after the replacement string.
|
|
||||||
// AAAB|B|BCCC => AAA****|CCC
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 5, extentOffset: 4),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 7, extentOffset: 7),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Preserves selection region, allow', () {
|
|
||||||
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
|
|
||||||
|
|
||||||
// AAA | BBB | CCC => **** | BBB | ****
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 7, extentOffset: 4),
|
|
||||||
);
|
|
||||||
|
|
||||||
// | AAABBBCCC | => | ****BBB**** |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 9, extentOffset: 0),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 11, extentOffset: 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAABBB | CCC | => ****BBB | **** |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
selection: const TextSelection(baseOffset: 9, extentOffset: 6),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 11, extentOffset: 7),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Overlapping matches: AAA | BBBBB | CCC => | BBB |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.allow('BBB', replacementString: '').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
const TextEditingValue(
|
|
||||||
text: 'AAABBBBBCCC',
|
|
||||||
selection: TextSelection(baseOffset: 8, extentOffset: 3),
|
|
||||||
),
|
|
||||||
).selection,
|
|
||||||
const TextSelection(baseOffset: 3, extentOffset: 0),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Preserves composing region', () {
|
|
||||||
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
|
|
||||||
|
|
||||||
// AAA | BBB | CCC => AAA | **** | CCC
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
composing: const TextRange(start: 3, end: 6),
|
|
||||||
),
|
|
||||||
).composing,
|
|
||||||
const TextRange(start: 3, end: 7),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA | BBB CCC | => AAA | **** CCC |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
composing: const TextRange(start: 3, end: 9),
|
|
||||||
),
|
|
||||||
).composing,
|
|
||||||
const TextRange(start: 3, end: 10),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA BBB | CCC | => AAA **** | CCC |
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
composing: const TextRange(start: 6, end: 9),
|
|
||||||
),
|
|
||||||
).composing,
|
|
||||||
const TextRange(start: 7, end: 10),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAAB | B | BCCC => AAA*** | CCC
|
|
||||||
// Same length replacement, don't move the composing region.
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '***').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
composing: const TextRange(start: 4, end: 5),
|
|
||||||
),
|
|
||||||
).composing,
|
|
||||||
const TextRange(start: 4, end: 5),
|
|
||||||
);
|
|
||||||
|
|
||||||
// AAA | BBB | CCC => | AAA CCC
|
|
||||||
expect(
|
|
||||||
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
|
|
||||||
oldValue,
|
|
||||||
newValue.copyWith(
|
|
||||||
composing: const TextRange(start: 3, end: 6),
|
|
||||||
),
|
|
||||||
).composing,
|
|
||||||
TextRange.empty,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user