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
0b1832ba
Unverified
Commit
0b1832ba
authored
Sep 11, 2021
by
LongCatIsLooong
Committed by
GitHub
Sep 11, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Make `FilteringTextInputFormatter`'s filtering Selection/Composing Region agnostic (#89327)
parent
b2550fe5
Changes
3
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
426 additions
and
77 deletions
+426
-77
text_formatter.dart
packages/flutter/lib/src/services/text_formatter.dart
+162
-76
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+29
-0
text_formatter_test.dart
packages/flutter/test/services/text_formatter_test.dart
+235
-1
No files found.
packages/flutter/lib/src/services/text_formatter.dart
View file @
0b1832ba
This diff is collapsed.
Click to expand it.
packages/flutter/lib/src/services/text_input.dart
View file @
0b1832ba
...
...
@@ -741,9 +741,38 @@ class TextEditingValue {
final
String
text
;
/// 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
;
/// 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
;
/// A value that corresponds to the empty string with no selection and no composing range.
...
...
packages/flutter/test/services/text_formatter_test.dart
View file @
0b1832ba
...
...
@@ -64,6 +64,7 @@ void main() {
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
));
expect
(
FilteringTextInputFormatter
(
'o'
,
allow:
true
,
replacementString:
'*'
).
formatEditUpdate
(
testOldValue
,
selectedIntoTheWoods
),
...
...
@@ -79,7 +80,7 @@ void main() {
);
expect
(
FilteringTextInputFormatter
(
RegExp
(
'o+'
),
allow:
false
,
replacementString:
'*'
).
formatEditUpdate
(
testOldValue
,
selectedIntoTheWoods
),
const
TextEditingValue
(
text:
'Int* the W*
*ds'
,
selection:
TextSelection
(
baseOffset:
11
,
extentOffset:
14
)),
const
TextEditingValue
(
text:
'Int* the W*
ds'
,
selection:
TextSelection
(
baseOffset:
11
,
extentOffset:
13
)),
);
});
...
...
@@ -624,4 +625,237 @@ void main() {
// cursor must be now at fourth position (right after the number 9)
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
,
);
});
});
}
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