From 37058a5c09e608f2678fb6818f8c2937177ede14 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sat, 23 Mar 2019 10:35:51 +0800 Subject: [PATCH 1/5] [iOS] Fixed maxLength of TextInput based on glyph count --- .../Text/TextInput/RCTBaseTextInputView.m | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 58b987aac83932..cd39c89c1aa7c2 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -20,6 +20,25 @@ #import "RCTTextAttributes.h" #import "RCTTextSelection.h" +@interface NSString (RCTUtility) + +@property (nonatomic, readonly) NSUInteger reactLengthOfGlyphs; + +@end + +@implementation NSString (RCTUtility) + +- (NSUInteger)reactLengthOfGlyphs +{ + __block NSUInteger lengthOfGlyphs = 0; + [self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + lengthOfGlyphs++; + }]; + return lengthOfGlyphs; +} + +@end + @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; __weak RCTEventDispatcher *_eventDispatcher; @@ -373,13 +392,24 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin } if (_maxLength) { - NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 0); - - if (text.length > allowedLength) { + NSString *backedTextInputViewText = backedTextInputView.attributedText.string; + __block NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); + NSUInteger textGlyphsLength = text.reactLengthOfGlyphs; + if (textGlyphsLength > allowedLength) { // If we typed/pasted more than one character, limit the text inputted. - if (text.length > 1) { + if (textGlyphsLength > 1) { + __block NSUInteger allowedIndex = 0; + // We truncated the text based on glyphs. + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + if (allowedLength == 0) { + *stop = YES; + return; + } + allowedIndex = substringRange.location + substringRange.length; + allowedLength--; + }]; // Truncate the input string so the result is exactly maxLength - NSString *limitedString = [text substringToIndex:allowedLength]; + NSString *limitedString = [text substringToIndex:allowedIndex]; NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; [newAttributedText replaceCharactersInRange:range withString:limitedString]; backedTextInputView.attributedText = newAttributedText; From 6e78b3ee8068a99b853c59e46e6713438ef4379b Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 27 Mar 2019 10:42:32 +0800 Subject: [PATCH 2/5] Restruct the code for manageable --- Libraries/Text/NSString+RCTUtility.h | 16 ++++ Libraries/Text/NSString+RCTUtility.m | 21 +++++ .../Text/TextInput/RCTBaseTextInputView.m | 79 +++++++++---------- 3 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 Libraries/Text/NSString+RCTUtility.h create mode 100644 Libraries/Text/NSString+RCTUtility.m diff --git a/Libraries/Text/NSString+RCTUtility.h b/Libraries/Text/NSString+RCTUtility.h new file mode 100644 index 00000000000000..ea47ada5c598e2 --- /dev/null +++ b/Libraries/Text/NSString+RCTUtility.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (RCTUtility) +@property (nonatomic, readonly) NSUInteger reactLengthOfGlyphs; +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Text/NSString+RCTUtility.m b/Libraries/Text/NSString+RCTUtility.m new file mode 100644 index 00000000000000..ec1e9561d9d703 --- /dev/null +++ b/Libraries/Text/NSString+RCTUtility.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSString+RCTUtility.h" + +@implementation NSString (RCTUtility) + +- (NSUInteger)reactLengthOfGlyphs +{ + __block NSUInteger lengthOfGlyphs = 0; + [self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + lengthOfGlyphs++; + }]; + return lengthOfGlyphs; +} + +@end diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index cd39c89c1aa7c2..354b7656c097a8 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -19,25 +19,7 @@ #import "RCTInputAccessoryViewContent.h" #import "RCTTextAttributes.h" #import "RCTTextSelection.h" - -@interface NSString (RCTUtility) - -@property (nonatomic, readonly) NSUInteger reactLengthOfGlyphs; - -@end - -@implementation NSString (RCTUtility) - -- (NSUInteger)reactLengthOfGlyphs -{ - __block NSUInteger lengthOfGlyphs = 0; - [self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ - lengthOfGlyphs++; - }]; - return lengthOfGlyphs; -} - -@end +#import "NSString+RCTUtility.h" @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; @@ -379,6 +361,38 @@ - (void)textInputDidReturn // Does nothing. } +- (void)trimInputTextInRange:(NSRange)range + replacementText:(NSString *)text + allowedLength:(NSInteger)length +{ + __block NSUInteger allowedIndex = 0; + __block NSInteger allowedLength = length; + + id backedTextInputView = self.backedTextInputView; + + // We truncated the text based on glyphs. + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + if (allowedLength == 0) { + *stop = YES; + return; + } + allowedIndex = substringRange.location + substringRange.length; + allowedLength--; + }]; + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedIndex]; + NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; + [newAttributedText replaceCharactersInRange:range withString:limitedString]; + backedTextInputView.attributedText = newAttributedText; + _predictedText = newAttributedText.string; + + // Collapse selection at end of insert to match normal paste behavior. + UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:(range.location + limitedString.length)]; + [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] + notifyDelegate:YES]; +} + - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { id backedTextInputView = self.backedTextInputView; @@ -393,34 +407,13 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin if (_maxLength) { NSString *backedTextInputViewText = backedTextInputView.attributedText.string; - __block NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); + // Get allowedLength based on glyphs + NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); NSUInteger textGlyphsLength = text.reactLengthOfGlyphs; if (textGlyphsLength > allowedLength) { // If we typed/pasted more than one character, limit the text inputted. if (textGlyphsLength > 1) { - __block NSUInteger allowedIndex = 0; - // We truncated the text based on glyphs. - [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ - if (allowedLength == 0) { - *stop = YES; - return; - } - allowedIndex = substringRange.location + substringRange.length; - allowedLength--; - }]; - // Truncate the input string so the result is exactly maxLength - NSString *limitedString = [text substringToIndex:allowedIndex]; - NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; - [newAttributedText replaceCharactersInRange:range withString:limitedString]; - backedTextInputView.attributedText = newAttributedText; - _predictedText = newAttributedText.string; - - // Collapse selection at end of insert to match normal paste behavior. - UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument - offset:(range.location + allowedLength)]; - [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] - notifyDelegate:YES]; - + [self trimInputTextInRange:range replacementText:text allowedLength:allowedLength]; [self textInputDidChange]; } From 8be1baecaccc003f2125c1ff1b08e61bf368e1de Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 27 Mar 2019 10:44:44 +0800 Subject: [PATCH 3/5] Add files to project --- Libraries/Text/RCTText.xcodeproj/project.pbxproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index d3205e5ee16eb6..2899c77ba3db56 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 0E8D88DB224B1515000FE6B8 /* NSString+RCTUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D88DA224B1515000FE6B8 /* NSString+RCTUtility.m */; }; + 0E8D88DC224B153B000FE6B8 /* NSString+RCTUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D88DA224B1515000FE6B8 /* NSString+RCTUtility.m */; }; + 0E8D88DD224B154A000FE6B8 /* NSString+RCTUtility.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 0E8D88D9224B1515000FE6B8 /* NSString+RCTUtility.h */; }; + 0E8D88DE224B1573000FE6B8 /* NSString+RCTUtility.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 0E8D88D9224B1515000FE6B8 /* NSString+RCTUtility.h */; }; 5956B130200FEBAA008D9D16 /* RCTRawTextShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B0FD200FEBA9008D9D16 /* RCTRawTextShadowView.m */; }; 5956B131200FEBAA008D9D16 /* RCTRawTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B0FE200FEBA9008D9D16 /* RCTRawTextViewManager.m */; }; 5956B132200FEBAA008D9D16 /* RCTSinglelineTextInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B101200FEBA9008D9D16 /* RCTSinglelineTextInputView.m */; }; @@ -139,6 +143,7 @@ 5956B171200FF324008D9D16 /* RCTBaseTextInputView.h in Copy Headers */, 5956B172200FF324008D9D16 /* RCTBaseTextInputViewManager.h in Copy Headers */, 5956B173200FF324008D9D16 /* RCTTextSelection.h in Copy Headers */, + 0E8D88DD224B154A000FE6B8 /* NSString+RCTUtility.h in Copy Headers */, 5956B174200FF324008D9D16 /* RCTSinglelineTextInputView.h in Copy Headers */, 5956B175200FF324008D9D16 /* RCTSinglelineTextInputViewManager.h in Copy Headers */, 5956B176200FF324008D9D16 /* RCTUITextField.h in Copy Headers */, @@ -157,6 +162,7 @@ 5970936A20845F0600D163A7 /* RCTTextTransform.h in Copy Headers */, 5956B179200FF338008D9D16 /* RCTBaseTextShadowView.h in Copy Headers */, 5956B17A200FF338008D9D16 /* RCTBaseTextViewManager.h in Copy Headers */, + 0E8D88DE224B1573000FE6B8 /* NSString+RCTUtility.h in Copy Headers */, 5956B17B200FF338008D9D16 /* RCTRawTextShadowView.h in Copy Headers */, 5956B17C200FF338008D9D16 /* RCTRawTextViewManager.h in Copy Headers */, 5956B17D200FF338008D9D16 /* RCTConvert+Text.h in Copy Headers */, @@ -187,6 +193,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0E8D88D9224B1515000FE6B8 /* NSString+RCTUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+RCTUtility.h"; sourceTree = ""; }; + 0E8D88DA224B1515000FE6B8 /* NSString+RCTUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+RCTUtility.m"; sourceTree = ""; }; 2D2A287B1D9B048500D4039D /* libRCTText-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTText-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; }; 5956B0F9200FEBA9008D9D16 /* RCTConvert+Text.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+Text.h"; sourceTree = ""; }; @@ -260,6 +268,8 @@ 5956B11A200FEBA9008D9D16 /* RCTTextAttributes.h */, 5956B120200FEBA9008D9D16 /* RCTTextAttributes.m */, 5970936820845DDE00D163A7 /* RCTTextTransform.h */, + 0E8D88D9224B1515000FE6B8 /* NSString+RCTUtility.h */, + 0E8D88DA224B1515000FE6B8 /* NSString+RCTUtility.m */, 5956B121200FEBAA008D9D16 /* Text */, 5956B0FF200FEBA9008D9D16 /* TextInput */, 5956B12A200FEBAA008D9D16 /* VirtualText */, @@ -457,6 +467,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0E8D88DC224B153B000FE6B8 /* NSString+RCTUtility.m in Sources */, 5956B192200FF35C008D9D16 /* RCTBaseTextShadowView.m in Sources */, 5956B193200FF35C008D9D16 /* RCTBaseTextViewManager.m in Sources */, 5956B194200FF35C008D9D16 /* RCTRawTextShadowView.m in Sources */, @@ -487,6 +498,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0E8D88DB224B1515000FE6B8 /* NSString+RCTUtility.m in Sources */, 5956B13D200FEBAA008D9D16 /* RCTBaseTextShadowView.m in Sources */, 5956B140200FEBAA008D9D16 /* RCTTextShadowView.m in Sources */, 5956B131200FEBAA008D9D16 /* RCTRawTextViewManager.m in Sources */, From 3ec1deab5852d6efe7bdaa304e27bb34eb580793 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 27 Mar 2019 10:50:10 +0800 Subject: [PATCH 4/5] Reposition method --- .../Text/TextInput/RCTBaseTextInputView.m | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 354b7656c097a8..a5f79a3bbcd80e 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -361,38 +361,6 @@ - (void)textInputDidReturn // Does nothing. } -- (void)trimInputTextInRange:(NSRange)range - replacementText:(NSString *)text - allowedLength:(NSInteger)length -{ - __block NSUInteger allowedIndex = 0; - __block NSInteger allowedLength = length; - - id backedTextInputView = self.backedTextInputView; - - // We truncated the text based on glyphs. - [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ - if (allowedLength == 0) { - *stop = YES; - return; - } - allowedIndex = substringRange.location + substringRange.length; - allowedLength--; - }]; - // Truncate the input string so the result is exactly maxLength - NSString *limitedString = [text substringToIndex:allowedIndex]; - NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; - [newAttributedText replaceCharactersInRange:range withString:limitedString]; - backedTextInputView.attributedText = newAttributedText; - _predictedText = newAttributedText.string; - - // Collapse selection at end of insert to match normal paste behavior. - UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument - offset:(range.location + limitedString.length)]; - [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] - notifyDelegate:YES]; -} - - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { id backedTextInputView = self.backedTextInputView; @@ -642,6 +610,38 @@ - (void)handleInputAccessoryDoneButton #pragma mark - Helpers +- (void)trimInputTextInRange:(NSRange)range + replacementText:(NSString *)text + allowedLength:(NSInteger)length +{ + __block NSUInteger allowedIndex = 0; + __block NSInteger allowedLength = length; + + id backedTextInputView = self.backedTextInputView; + + // We truncated the text based on glyphs. + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + if (allowedLength == 0) { + *stop = YES; + return; + } + allowedIndex = substringRange.location + substringRange.length; + allowedLength--; + }]; + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedIndex]; + NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; + [newAttributedText replaceCharactersInRange:range withString:limitedString]; + backedTextInputView.attributedText = newAttributedText; + _predictedText = newAttributedText.string; + + // Collapse selection at end of insert to match normal paste behavior. + UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:(range.location + limitedString.length)]; + [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] + notifyDelegate:YES]; +} + static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange) { NSInteger firstMismatch = -1; From f94bb9e2680bc6776d016029e353b1520ba1d159 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 9 May 2019 23:15:43 +0800 Subject: [PATCH 5/5] Try separate maxLength handler to sublcass --- .../Multiline/RCTMultilineTextInputView.m | 55 +++++++++++++++++++ .../Text/TextInput/RCTBaseTextInputView.h | 1 + .../Text/TextInput/RCTBaseTextInputView.m | 52 +++--------------- .../Singleline/RCTSinglelineTextInputView.m | 55 +++++++++++++++++++ 4 files changed, 118 insertions(+), 45 deletions(-) diff --git a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m index dd6b489020926a..faf5d118b265ae 100644 --- a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m +++ b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m @@ -10,6 +10,7 @@ #import #import "RCTUITextView.h" +#import "NSString+RCTUtility.h" @implementation RCTMultilineTextInputView { @@ -45,6 +46,60 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return _backedTextInputView; } +- (BOOL)handleTextInputLengthLimitInRange:(NSRange)range + replacementText:(NSString *)text +{ + id backedTextInputView = self.backedTextInputView; + if (self.maxLength) { + NSString *backedTextInputViewText = backedTextInputView.attributedText.string; + // Get allowedLength based on glyphs + NSInteger allowedLength = MAX(self.maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); + NSUInteger textGlyphsLength = text.reactLengthOfGlyphs; + if (textGlyphsLength > allowedLength) { + // If we typed/pasted more than one character, limit the text inputted. + if (textGlyphsLength > 1) { + [self trimInputTextInRange:range replacementText:text allowedLength:allowedLength]; + [self textInputDidChange]; + } + + return YES; + } + } + return NO; +} + +- (void)trimInputTextInRange:(NSRange)range + replacementText:(NSString *)text + allowedLength:(NSInteger)length +{ + __block NSUInteger allowedIndex = 0; + __block NSInteger allowedLength = length; + + id backedTextInputView = self.backedTextInputView; + + // We truncated the text based on glyphs. + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + if (allowedLength == 0) { + *stop = YES; + return; + } + allowedIndex = substringRange.location + substringRange.length; + allowedLength--; + }]; + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedIndex]; + NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; + [newAttributedText replaceCharactersInRange:range withString:limitedString]; + backedTextInputView.attributedText = newAttributedText; + self.predictedText = newAttributedText.string; + + // Collapse selection at end of insert to match normal paste behavior. + UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:(range.location + limitedString.length)]; + [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] + notifyDelegate:YES]; +} + #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.h b/Libraries/Text/TextInput/RCTBaseTextInputView.h index 5b62f90251492e..a18096794429ed 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.h +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.h @@ -49,6 +49,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSAttributedString *attributedText; @property (nonatomic, copy) NSString *inputAccessoryViewID; @property (nonatomic, assign) UIKeyboardType keyboardType; +@property (nonatomic, copy, nullable) NSString *predictedText; @end diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index d107d402ab9d60..ce64dd6d0aa9d0 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -19,13 +19,11 @@ #import "RCTInputAccessoryViewContent.h" #import "RCTTextAttributes.h" #import "RCTTextSelection.h" -#import "NSString+RCTUtility.h" @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; __weak RCTEventDispatcher *_eventDispatcher; BOOL _hasInputAccesoryView; - NSString *_Nullable _predictedText; NSInteger _nativeEventCount; } @@ -373,20 +371,9 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin eventCount:_nativeEventCount]; } - if (_maxLength) { - NSString *backedTextInputViewText = backedTextInputView.attributedText.string; - // Get allowedLength based on glyphs - NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); - NSUInteger textGlyphsLength = text.reactLengthOfGlyphs; - if (textGlyphsLength > allowedLength) { - // If we typed/pasted more than one character, limit the text inputted. - if (textGlyphsLength > 1) { - [self trimInputTextInRange:range replacementText:text allowedLength:allowedLength]; - [self textInputDidChange]; - } - - return NO; - } + BOOL isLimited = [self handleTextInputLengthLimitInRange:range replacementText:text]; + if (isLimited) { + return NO; } NSString *previousText = backedTextInputView.attributedText.string ?: @""; @@ -610,36 +597,11 @@ - (void)handleInputAccessoryDoneButton #pragma mark - Helpers -- (void)trimInputTextInRange:(NSRange)range - replacementText:(NSString *)text - allowedLength:(NSInteger)length +- (BOOL)handleTextInputLengthLimitInRange:(NSRange)range + replacementText:(NSString *)text { - __block NSUInteger allowedIndex = 0; - __block NSInteger allowedLength = length; - - id backedTextInputView = self.backedTextInputView; - - // We truncated the text based on glyphs. - [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ - if (allowedLength == 0) { - *stop = YES; - return; - } - allowedIndex = substringRange.location + substringRange.length; - allowedLength--; - }]; - // Truncate the input string so the result is exactly maxLength - NSString *limitedString = [text substringToIndex:allowedIndex]; - NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; - [newAttributedText replaceCharactersInRange:range withString:limitedString]; - backedTextInputView.attributedText = newAttributedText; - _predictedText = newAttributedText.string; - - // Collapse selection at end of insert to match normal paste behavior. - UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument - offset:(range.location + limitedString.length)]; - [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] - notifyDelegate:YES]; + RCTAssert(NO, @"-[RCTBaseTextInputView handleTextInputLengthLimitInRange:replacementText:] must be implemented in subclass."); + return YES; } static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange) diff --git a/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m b/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m index 4292a04fb69031..68819854387329 100644 --- a/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m +++ b/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputView.m @@ -10,6 +10,7 @@ #import #import "RCTUITextField.h" +#import "NSString+RCTUtility.h" @implementation RCTSinglelineTextInputView { @@ -37,4 +38,58 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return _backedTextInputView; } +- (BOOL)handleTextInputLengthLimitInRange:(NSRange)range + replacementText:(NSString *)text +{ + id backedTextInputView = self.backedTextInputView; + if (self.maxLength) { + NSString *backedTextInputViewText = backedTextInputView.attributedText.string; + // Get allowedLength based on glyphs + NSInteger allowedLength = MAX(self.maxLength.integerValue - (NSInteger)backedTextInputViewText.reactLengthOfGlyphs + (NSInteger)[backedTextInputViewText substringWithRange:range].reactLengthOfGlyphs, 0); + NSUInteger textGlyphsLength = text.reactLengthOfGlyphs; + if (textGlyphsLength > allowedLength) { + // If we typed/pasted more than one character, limit the text inputted. + if (textGlyphsLength > 1) { + [self trimInputTextInRange:range replacementText:text allowedLength:allowedLength]; + [self textInputDidChange]; + } + + return YES; + } + } + return NO; +} + +- (void)trimInputTextInRange:(NSRange)range + replacementText:(NSString *)text + allowedLength:(NSInteger)length +{ + __block NSUInteger allowedIndex = 0; + __block NSInteger allowedLength = length; + + id backedTextInputView = self.backedTextInputView; + + // We truncated the text based on glyphs. + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop){ + if (allowedLength == 0) { + *stop = YES; + return; + } + allowedIndex = substringRange.location + substringRange.length; + allowedLength--; + }]; + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedIndex]; + NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; + [newAttributedText replaceCharactersInRange:range withString:limitedString]; + backedTextInputView.attributedText = newAttributedText; + self.predictedText = newAttributedText.string; + + // Collapse selection at end of insert to match normal paste behavior. + UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument + offset:(range.location + limitedString.length)]; + [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd] + notifyDelegate:YES]; +} + @end