diff --git a/Libraries/Text/RCTBackedTextInputDelegate.h b/Libraries/Text/RCTBackedTextInputDelegate.h new file mode 100644 index 00000000000..bb9be6938a3 --- /dev/null +++ b/Libraries/Text/RCTBackedTextInputDelegate.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol RCTBackedTextInputViewProtocol; + +@protocol RCTBackedTextInputDelegate + +- (BOOL)textInputShouldBeginEditing; // Return `NO` to disallow editing. +- (void)textInputDidBeginEditing; + +- (BOOL)textInputShouldEndEditing; // Return `YES` to allow editing to stop and to resign first responder status. `NO` to disallow the editing session to end. +- (void)textInputDidEndEditing; // May be called if forced even if `textInputShouldEndEditing` returns `NO` (e.g. view removed from window) or `[textInput endEditing:YES]` called. +- (void)textInputDidEndEditingOnExit; // May be called right before `textInputShouldEndEditing` if "Submit" button was pressed. + +- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)string; // Return NO to not change text. +- (void)textInputDidChange; + +- (void)textInputDidChangeSelection; + +@end diff --git a/Libraries/Text/RCTBackedTextInputDelegateAdapter.h b/Libraries/Text/RCTBackedTextInputDelegateAdapter.h new file mode 100644 index 00000000000..261dc893a23 --- /dev/null +++ b/Libraries/Text/RCTBackedTextInputDelegateAdapter.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTBackedTextInputViewProtocol.h" +#import "RCTBackedTextInputDelegate.h" + +#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) + +@interface RCTBackedTextFieldDelegateAdapter : NSObject + +- (instancetype)initWithTextField:(UITextField *)backedTextInput; + +@end + +#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView) + +@interface RCTBackedTextViewDelegateAdapter : NSObject + +- (instancetype)initWithTextView:(UITextView *)backedTextInput; + +@end diff --git a/Libraries/Text/RCTBackedTextInputDelegateAdapter.m b/Libraries/Text/RCTBackedTextInputDelegateAdapter.m new file mode 100644 index 00000000000..0533dec4c61 --- /dev/null +++ b/Libraries/Text/RCTBackedTextInputDelegateAdapter.m @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBackedTextInputDelegateAdapter.h" + +#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) + +static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext; + +@interface RCTBackedTextFieldDelegateAdapter () +@end + +@implementation RCTBackedTextFieldDelegateAdapter { + __weak UITextField *_backedTextInput; + __unsafe_unretained UITextField *_unsafeBackedTextInput; +} + +- (instancetype)initWithTextField:(UITextField *)backedTextInput +{ + if (self = [super init]) { + _backedTextInput = backedTextInput; + _unsafeBackedTextInput = backedTextInput; + backedTextInput.delegate = self; + + [_backedTextInput addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; + [_backedTextInput addTarget:self action:@selector(textFieldDidEndEditingOnExit) forControlEvents:UIControlEventEditingDidEndOnExit]; + + // We have to use `unsafe_unretained` pointer to `backedTextInput` for subscribing (and especially unsubscribing) for it + // because `weak` pointers do not KVO complient, unfortunately. + [_unsafeBackedTextInput addObserver:self forKeyPath:@"selectedTextRange" options:0 context:TextFieldSelectionObservingContext]; + } + + return self; +} + +- (void)dealloc +{ + [_backedTextInput removeTarget:self action:nil forControlEvents:UIControlEventEditingChanged]; + [_backedTextInput removeTarget:self action:nil forControlEvents:UIControlEventEditingDidEndOnExit]; + [_unsafeBackedTextInput removeObserver:self forKeyPath:@"selectedTextRange" context:TextFieldSelectionObservingContext]; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldBeginEditing:(__unused UITextField *)textField +{ + return [_backedTextInput.textInputDelegate textInputShouldBeginEditing]; +} + +- (void)textFieldDidBeginEditing:(__unused UITextField *)textField +{ + [_backedTextInput.textInputDelegate textInputDidBeginEditing]; +} + +- (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField +{ + return [_backedTextInput.textInputDelegate textInputShouldEndEditing]; +} + +- (void)textFieldDidEndEditing:(__unused UITextField *)textField +{ + [_backedTextInput.textInputDelegate textInputDidEndEditing]; +} + +- (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +{ + return [_backedTextInput.textInputDelegate textInputShouldChangeTextInRange:range replacementText:string]; +} + +#pragma mark - Key Value Observing + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(nullable id)object + change:(NSDictionary *)change + context:(void *)context +{ + if (context == TextFieldSelectionObservingContext) { + if ([keyPath isEqualToString:@"selectedTextRange"]) { + [_backedTextInput.textInputDelegate textInputDidChangeSelection]; + } + + return; + } + + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; +} + +#pragma mark - UIControlEventEditing* Family Events + +- (void)textFieldDidChange +{ + [_backedTextInput.textInputDelegate textInputDidChange]; +} + +- (void)textFieldDidEndEditingOnExit +{ + [_backedTextInput.textInputDelegate textInputDidEndEditingOnExit]; +} + +#pragma mark - UIKeyboardInput (private UIKit protocol) + +// This method allows us to detect a [Backspace] `keyPress` +// even when there is no more text in the `UITextField`. +- (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField +{ + [_backedTextInput.textInputDelegate textInputShouldChangeTextInRange:NSMakeRange(0, 0) replacementText:@""]; + return YES; +} + +@end + +#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView) + +@interface RCTBackedTextViewDelegateAdapter () +@end + +@implementation RCTBackedTextViewDelegateAdapter { + __weak UITextView *_backedTextInput; +} + +- (instancetype)initWithTextView:(UITextView *)backedTextInput +{ + if (self = [super init]) { + _backedTextInput = backedTextInput; + backedTextInput.delegate = self; + } + + return self; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textViewShouldBeginEditing:(__unused UITextView *)textView +{ + return [_backedTextInput.textInputDelegate textInputShouldBeginEditing]; +} + +- (void)textViewDidBeginEditing:(__unused UITextView *)textView +{ + [_backedTextInput.textInputDelegate textInputDidBeginEditing]; +} + +- (BOOL)textViewShouldEndEditing:(__unused UITextView *)textView +{ + return [_backedTextInput.textInputDelegate textInputShouldEndEditing]; +} + +- (void)textViewDidEndEditing:(__unused UITextView *)textView +{ + [_backedTextInput.textInputDelegate textInputDidEndEditing]; +} + +- (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + return [_backedTextInput.textInputDelegate textInputShouldChangeTextInRange:range replacementText:text]; +} + +- (void)textViewDidChange:(__unused UITextView *)textView +{ + [_backedTextInput.textInputDelegate textInputDidChange]; +} + +- (void)textViewDidChangeSelection:(__unused UITextView *)textView +{ + [_backedTextInput.textInputDelegate textInputDidChangeSelection]; +} + +@end diff --git a/Libraries/Text/RCTBackedTextInputViewProtocol.h b/Libraries/Text/RCTBackedTextInputViewProtocol.h index 17b39486d6f..92acdff004e 100644 --- a/Libraries/Text/RCTBackedTextInputViewProtocol.h +++ b/Libraries/Text/RCTBackedTextInputViewProtocol.h @@ -9,6 +9,8 @@ #import +@protocol RCTBackedTextInputDelegate; + @protocol RCTBackedTextInputViewProtocol @property (nonatomic, copy, nullable) NSString *text; @@ -19,5 +21,6 @@ @property (nonatomic, strong, nullable) UIFont *font; @property (nonatomic, assign) UIEdgeInsets textContainerInset; @property (nonatomic, strong, nullable) UIView *inputAccessoryView; +@property (nonatomic, weak, nullable) id textInputDelegate; @end diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index a77407e22fc..fcd3c8840db 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */; }; 58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; }; + 598F41261F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 598F41251F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m */; }; + 598F41271F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 598F41251F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m */; }; 599DF25F1F0306660079B53E /* RCTBackedTextInputViewProtocol.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 599DF25D1F0304B30079B53E /* RCTBackedTextInputViewProtocol.h */; }; 599DF2611F0306C30079B53E /* RCTBackedTextInputViewProtocol.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 599DF25D1F0304B30079B53E /* RCTBackedTextInputViewProtocol.h */; }; 599DF2641F03076D0079B53E /* RCTTextInput.m in Sources */ = {isa = PBXBuildFile; fileRef = 599DF2631F03076D0079B53E /* RCTTextInput.m */; }; @@ -95,6 +97,9 @@ 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = ""; }; 58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = ""; }; 58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = ""; }; + 598F41231F145D4900B8495B /* RCTBackedTextInputDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputDelegate.h; sourceTree = ""; }; + 598F41241F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputDelegateAdapter.h; sourceTree = ""; }; + 598F41251F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBackedTextInputDelegateAdapter.m; sourceTree = ""; }; 599DF25D1F0304B30079B53E /* RCTBackedTextInputViewProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputViewProtocol.h; sourceTree = ""; }; 599DF2621F03076D0079B53E /* RCTTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextInput.h; sourceTree = ""; }; 599DF2631F03076D0079B53E /* RCTTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextInput.m; sourceTree = ""; }; @@ -115,6 +120,9 @@ isa = PBXGroup; children = ( 58B5119C1A9E6C1200147676 /* Products */, + 598F41231F145D4900B8495B /* RCTBackedTextInputDelegate.h */, + 598F41241F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.h */, + 598F41251F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m */, 599DF25D1F0304B30079B53E /* RCTBackedTextInputViewProtocol.h */, AF3225F71DE5574F00D3E7E7 /* RCTConvert+Text.h */, AF3225F81DE5574F00D3E7E7 /* RCTConvert+Text.m */, @@ -244,6 +252,7 @@ 2D3B5F361D9B106F00451313 /* RCTShadowText.m in Sources */, 2D3B5F3B1D9B106F00451313 /* RCTTextView.m in Sources */, 59AF89AB1EDCBCC700F004B1 /* RCTUITextField.m in Sources */, + 598F41271F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m in Sources */, 2D3B5F3A1D9B106F00451313 /* RCTTextFieldManager.m in Sources */, 599DF2651F03076D0079B53E /* RCTTextInput.m in Sources */, 2D3B5F341D9B103100451313 /* RCTRawTextManager.m in Sources */, @@ -267,6 +276,7 @@ 19FC5C851D41A4120090108F /* RCTTextSelection.m in Sources */, 1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */, 59AF89AA1EDCBCC700F004B1 /* RCTUITextField.m in Sources */, + 598F41261F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, 599DF2641F03076D0079B53E /* RCTTextInput.m in Sources */, 1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */, diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index 52d30b25d8e..466a4877801 100644 --- a/Libraries/Text/RCTTextField.h +++ b/Libraries/Text/RCTTextField.h @@ -26,6 +26,4 @@ @property (nonatomic, copy) RCTDirectEventBlock onSelectionChange; -@property (nonatomic, strong) RCTUITextField *textField; - @end diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 8c85713bf5a..99d3ac4277d 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -16,15 +16,17 @@ #import #import +#import "RCTBackedTextInputDelegate.h" #import "RCTTextSelection.h" #import "RCTUITextField.h" -@interface RCTTextField () +@interface RCTTextField () @end @implementation RCTTextField { + RCTUITextField *_backedTextInput; NSInteger _nativeEventCount; BOOL _submitted; UITextRange *_previousSelectionRange; @@ -40,24 +42,11 @@ // `blurOnSubmit` defaults to `true` for by design. _blurOnSubmit = YES; - _textField = [[RCTUITextField alloc] initWithFrame:self.bounds]; - _textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _backedTextInput = [[RCTUITextField alloc] initWithFrame:self.bounds]; + _backedTextInput.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _backedTextInput.textInputDelegate = self; - // Note: `UITextField` fires same events to channels in this order: delegate method, notification center, target action. - // Usually (presumably) all events with equivalent semantic fires consistently in specified order... - // but in practice, it is not always true, unfortunately. - // Surprisingly, seems subscribing via Notification Center is the most reliable way to get these events. - - _textField.delegate = self; - - [_textField addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; - [_textField addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin]; - [_textField addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; - [_textField addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; - - [_textField addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; - - [self addSubview:_textField]; + [self addSubview:_backedTextInput]; } return self; @@ -66,14 +55,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) -- (void)dealloc -{ - [_textField removeObserver:self forKeyPath:@"selectedTextRange"]; -} - - (id)backedTextInputView { - return _textField; + return _backedTextInput; } - (void)sendKeyValueForString:(NSString *)string @@ -93,15 +77,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return; } - UITextRange *currentSelection = _textField.selectedTextRange; - UITextPosition *start = [_textField positionFromPosition:_textField.beginningOfDocument offset:selection.start]; - UITextPosition *end = [_textField positionFromPosition:_textField.beginningOfDocument offset:selection.end]; - UITextRange *selectedTextRange = [_textField textRangeFromPosition:start toPosition:end]; + UITextRange *currentSelection = _backedTextInput.selectedTextRange; + UITextPosition *start = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:selection.start]; + UITextPosition *end = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:selection.end]; + UITextRange *selectedTextRange = [_backedTextInput textRangeFromPosition:start toPosition:end]; NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) { _previousSelectionRange = selectedTextRange; - _textField.selectedTextRange = selectedTextRange; + _backedTextInput.selectedTextRange = selectedTextRange; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); } @@ -109,112 +93,44 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (NSString *)text { - return _textField.text; + return _backedTextInput.text; } - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && ![text isEqualToString:self.text]) { - UITextRange *selection = _textField.selectedTextRange; - NSInteger oldTextLength = _textField.text.length; + UITextRange *selection = _backedTextInput.selectedTextRange; + NSInteger oldTextLength = _backedTextInput.text.length; - _textField.text = text; + _backedTextInput.text = text; if (selection.empty) { // maintain cursor position relative to the end of the old text - NSInteger offsetStart = [_textField offsetFromPosition:_textField.beginningOfDocument toPosition:selection.start]; + NSInteger offsetStart = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start]; NSInteger offsetFromEnd = oldTextLength - offsetStart; NSInteger newOffset = text.length - offsetFromEnd; - UITextPosition *position = [_textField positionFromPosition:_textField.beginningOfDocument offset:newOffset]; - _textField.selectedTextRange = [_textField textRangeFromPosition:position toPosition:position]; + UITextPosition *position = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:newOffset]; + _backedTextInput.selectedTextRange = [_backedTextInput textRangeFromPosition:position toPosition:position]; } } else if (eventLag > RCTTextUpdateLagWarningThreshold) { - RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", _textField.text, eventLag); + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", _backedTextInput.text, eventLag); } } #pragma mark - Events -- (void)textFieldDidChange -{ - _nativeEventCount++; - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange - reactTag:self.reactTag - text:_textField.text - key:nil - eventCount:_nativeEventCount]; - - // selectedTextRange observer isn't triggered when you type even though the - // cursor position moves, so we send event again here. - [self sendSelectionEvent]; -} - -- (void)textFieldEndEditing -{ - if (![_finalText isEqualToString:_textField.text]) { - _finalText = nil; - // iOS does't send event `UIControlEventEditingChanged` if the change was happened because of autocorrection - // which was triggered by loosing focus. We assume that if `text` was changed in the middle of loosing focus process, - // we did not receive that event. So, we call `textFieldDidChange` manually. - [self textFieldDidChange]; - } - - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd - reactTag:self.reactTag - text:_textField.text - key:nil - eventCount:_nativeEventCount]; -} - -- (void)textFieldSubmitEditing -{ - _submitted = YES; - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit - reactTag:self.reactTag - text:_textField.text - key:nil - eventCount:_nativeEventCount]; -} - -- (void)textFieldBeginEditing -{ - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus - reactTag:self.reactTag - text:_textField.text - key:nil - eventCount:_nativeEventCount]; - - dispatch_async(dispatch_get_main_queue(), ^{ - if (self->_selectTextOnFocus) { - [self->_textField selectAll:nil]; - } - - [self sendSelectionEvent]; - }); -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(__unused UITextField *)textField - change:(__unused NSDictionary *)change - context:(__unused void *)context -{ - if ([keyPath isEqualToString:@"selectedTextRange"]) { - [self sendSelectionEvent]; - } -} - - (void)sendSelectionEvent { if (_onSelectionChange && - _textField.selectedTextRange != _previousSelectionRange && - ![_textField.selectedTextRange isEqual:_previousSelectionRange]) { + _backedTextInput.selectedTextRange != _previousSelectionRange && + ![_backedTextInput.selectedTextRange isEqual:_previousSelectionRange]) { - _previousSelectionRange = _textField.selectedTextRange; + _previousSelectionRange = _backedTextInput.selectedTextRange; - UITextRange *selection = _textField.selectedTextRange; - NSInteger start = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.start]; - NSInteger end = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.end]; + UITextRange *selection = _backedTextInput.selectedTextRange; + NSInteger start = [_backedTextInput offsetFromPosition:[_backedTextInput beginningOfDocument] toPosition:selection.start]; + NSInteger end = [_backedTextInput offsetFromPosition:[_backedTextInput beginningOfDocument] toPosition:selection.end]; _onSelectionChange(@{ @"selection": @{ @"start": @(start), @@ -224,30 +140,30 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } } -#pragma mark - UITextFieldDelegate +#pragma mark - RCTBackedTextInputDelegate -- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)string { // Only allow single keypresses for `onKeyPress`, pasted text will not be sent. - if (!_textField.textWasPasted) { + if (!_backedTextInput.textWasPasted) { [self sendKeyValueForString:string]; } if (_maxLength != nil && ![string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return. - NSUInteger allowedLength = _maxLength.integerValue - MIN(_maxLength.integerValue, _textField.text.length) + range.length; + NSUInteger allowedLength = _maxLength.integerValue - MIN(_maxLength.integerValue, _backedTextInput.text.length) + range.length; if (string.length > allowedLength) { if (string.length > 1) { // Truncate the input string so the result is exactly `maxLength`. NSString *limitedString = [string substringToIndex:allowedLength]; - NSMutableString *newString = _textField.text.mutableCopy; + NSMutableString *newString = _backedTextInput.text.mutableCopy; [newString replaceCharactersInRange:range withString:limitedString]; - _textField.text = newString; + _backedTextInput.text = newString; // Collapse selection at end of insert to match normal paste behavior. - UITextPosition *insertEnd = [_textField positionFromPosition:_textField.beginningOfDocument + UITextPosition *insertEnd = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:(range.location + allowedLength)]; - _textField.selectedTextRange = [_textField textRangeFromPosition:insertEnd toPosition:insertEnd]; - [self textFieldDidChange]; + _backedTextInput.selectedTextRange = [_backedTextInput textRangeFromPosition:insertEnd toPosition:insertEnd]; + [self textInputDidChange]; } return NO; } @@ -256,17 +172,45 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return YES; } -// This method allows us to detect a `Backspace` keyPress -// even when there is no more text in the TextField. -- (BOOL)keyboardInputShouldDelete:(RCTTextField *)textField +- (void)textInputDidChange +{ + _nativeEventCount++; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange + reactTag:self.reactTag + text:_backedTextInput.text + key:nil + eventCount:_nativeEventCount]; + + // selectedTextRange observer isn't triggered when you type even though the + // cursor position moves, so we send event again here. + [self sendSelectionEvent]; +} + +- (BOOL)textInputShouldBeginEditing { - [self textField:_textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; return YES; } -- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField +- (void)textInputDidBeginEditing { - _finalText = _textField.text; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:_backedTextInput.text + key:nil + eventCount:_nativeEventCount]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_selectTextOnFocus) { + [self->_backedTextInput selectAll:nil]; + } + + [self sendSelectionEvent]; + }); +} + +- (BOOL)textInputShouldEndEditing +{ + _finalText = _backedTextInput.text; if (_submitted) { _submitted = NO; @@ -276,13 +220,42 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return YES; } -- (void)textFieldDidEndEditing:(UITextField *)textField +- (void)textInputDidEndEditing { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:self.text key:nil eventCount:_nativeEventCount]; + + if (![_finalText isEqualToString:_backedTextInput.text]) { + _finalText = nil; + // iOS does't send event `UIControlEventEditingChanged` if the change was happened because of autocorrection + // which was triggered by loosing focus. We assume that if `text` was changed in the middle of loosing focus process, + // we did not receive that event. So, we call `textFieldDidChange` manually. + [self textInputDidChange]; + } + + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd + reactTag:self.reactTag + text:_backedTextInput.text + key:nil + eventCount:_nativeEventCount]; +} + +- (void)textInputDidEndEditingOnExit +{ + _submitted = YES; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit + reactTag:self.reactTag + text:_backedTextInput.text + key:nil + eventCount:_nativeEventCount]; +} + +- (void)textInputDidChangeSelection +{ + [self sendSelectionEvent]; } @end diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m index c75fdf4077c..317a0868b71 100644 --- a/Libraries/Text/RCTTextFieldManager.m +++ b/Libraries/Text/RCTTextFieldManager.m @@ -52,30 +52,30 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, backedTextInputView.textAlignment, NSTextAlig #pragma mark - Singleline (aka TextField) specific properties -RCT_REMAP_VIEW_PROPERTY(caretHidden, textField.caretHidden, BOOL) +RCT_REMAP_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) -RCT_REMAP_VIEW_PROPERTY(clearButtonMode, textField.clearButtonMode, UITextFieldViewMode) -RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, textField.clearsOnBeginEditing, BOOL) +RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode) +RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, backedTextInputView.clearsOnBeginEditing, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RCTTextField) { - view.textField.font = [RCTFont updateFont:view.textField.font withSize:json ?: @(defaultView.textField.font.pointSize)]; + view.backedTextInputView.font = [RCTFont updateFont:view.backedTextInputView.font withSize:json ?: @(defaultView.backedTextInputView.font.pointSize)]; } RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, __unused RCTTextField) { - view.textField.font = [RCTFont updateFont:view.textField.font withWeight:json]; // defaults to normal + view.backedTextInputView.font = [RCTFont updateFont:view.backedTextInputView.font withWeight:json]; // defaults to normal } RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, __unused RCTTextField) { - view.textField.font = [RCTFont updateFont:view.textField.font withStyle:json]; // defaults to normal + view.backedTextInputView.font = [RCTFont updateFont:view.backedTextInputView.font withStyle:json]; // defaults to normal } RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField) { - view.textField.font = [RCTFont updateFont:view.textField.font withFamily:json ?: defaultView.textField.font.familyName]; + view.backedTextInputView.font = [RCTFont updateFont:view.backedTextInputView.font withFamily:json ?: defaultView.backedTextInputView.font.familyName]; } RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 78273cb5877..f3f9a3e12d2 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -16,7 +16,7 @@ @class RCTBridge; -@interface RCTTextView : RCTTextInput +@interface RCTTextView : RCTTextInput @property (nonatomic, assign) UITextAutocorrectionType autocorrectionType; @property (nonatomic, assign) UITextSpellCheckingType spellCheckingType; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 20542b7409a..93ab58f6bac 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -20,9 +20,13 @@ #import "RCTTextSelection.h" #import "RCTUITextView.h" +@interface RCTTextView () + +@end + @implementation RCTTextView { - RCTUITextView *_textView; + RCTUITextView *_backedTextInput; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; @@ -41,19 +45,20 @@ if (self = [super initWithBridge:bridge]) { _blurOnSubmit = NO; - _textView = [[RCTUITextView alloc] initWithFrame:self.bounds]; - _textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _textView.backgroundColor = [UIColor clearColor]; - _textView.textColor = [UIColor blackColor]; + _backedTextInput = [[RCTUITextView alloc] initWithFrame:self.bounds]; + _backedTextInput.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _backedTextInput.backgroundColor = [UIColor clearColor]; + _backedTextInput.textColor = [UIColor blackColor]; // This line actually removes 5pt (default value) left and right padding in UITextView. - _textView.textContainer.lineFragmentPadding = 0; + _backedTextInput.textContainer.lineFragmentPadding = 0; #if !TARGET_OS_TV - _textView.scrollsToTop = NO; + _backedTextInput.scrollsToTop = NO; #endif - _textView.scrollEnabled = YES; - _textView.delegate = self; + _backedTextInput.scrollEnabled = YES; - [self addSubview:_textView]; + _backedTextInput.textInputDelegate = self; + + [self addSubview:_backedTextInput]; } return self; } @@ -63,7 +68,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (id)backedTextInputView { - return _textView; + return _backedTextInput; } #pragma mark - RCTComponent @@ -85,9 +90,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) // // TODO: This should be removed when the related hack in -performPendingTextUpdate is removed. if (subview.backgroundColor) { - NSMutableDictionary *attrs = [_textView.typingAttributes mutableCopy]; + NSMutableDictionary *attrs = [_backedTextInput.typingAttributes mutableCopy]; attrs[NSBackgroundColorAttributeName] = subview.backgroundColor; - _textView.typingAttributes = attrs; + _backedTextInput.typingAttributes = attrs; } [self performTextUpdate]; @@ -128,7 +133,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _pendingAttributedText = _richTextView.textStorage; [self performPendingTextUpdate]; } else if (!self.text) { - _textView.attributedText = nil; + _backedTextInput.attributedText = nil; } } @@ -157,7 +162,7 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) // TODO: Kill this after we finish passing all style/attribute info into JS. _pendingAttributedText = removeReactTagFromString(_pendingAttributedText); - if ([_textView.attributedText isEqualToAttributedString:_pendingAttributedText]) { + if ([_backedTextInput.attributedText isEqualToAttributedString:_pendingAttributedText]) { _pendingAttributedText = nil; // Don't try again. return; } @@ -167,23 +172,23 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) // we temporarily block all textShouldChange events so they are not applied. _blockTextShouldChange = YES; - UITextRange *selection = _textView.selectedTextRange; - NSInteger oldTextLength = _textView.attributedText.length; + UITextRange *selection = _backedTextInput.selectedTextRange; + NSInteger oldTextLength = _backedTextInput.attributedText.length; - _textView.attributedText = _pendingAttributedText; + _backedTextInput.attributedText = _pendingAttributedText; _predictedText = _pendingAttributedText.string; _pendingAttributedText = nil; if (selection.empty) { // maintain cursor position relative to the end of the old text - NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; + NSInteger start = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start]; NSInteger offsetFromEnd = oldTextLength - start; - NSInteger newOffset = _textView.attributedText.length - offsetFromEnd; - UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; - _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; + NSInteger newOffset = _backedTextInput.attributedText.length - offsetFromEnd; + UITextPosition *position = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:newOffset]; + _backedTextInput.selectedTextRange = [_backedTextInput textRangeFromPosition:position toPosition:position]; } - [_textView layoutIfNeeded]; + [_backedTextInput layoutIfNeeded]; [self invalidateContentSize]; @@ -194,12 +199,12 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) - (UIFont *)font { - return _textView.font; + return _backedTextInput.font; } - (void)setFont:(UIFont *)font { - _textView.font = font; + _backedTextInput.font = font; [self setNeedsLayout]; } @@ -209,15 +214,15 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) return; } - UITextRange *currentSelection = _textView.selectedTextRange; - UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start]; - UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end]; - UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end]; + UITextRange *currentSelection = _backedTextInput.selectedTextRange; + UITextPosition *start = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:selection.start]; + UITextPosition *end = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:selection.end]; + UITextRange *selectedTextRange = [_backedTextInput textRangeFromPosition:start toPosition:end]; NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) { _previousSelectionRange = selectedTextRange; - _textView.selectedTextRange = selectedTextRange; + _backedTextInput.selectedTextRange = selectedTextRange; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); } @@ -225,26 +230,26 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) - (NSString *)text { - return _textView.text; + return _backedTextInput.text; } - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![text isEqualToString:_textView.text]) { - UITextRange *selection = _textView.selectedTextRange; - NSInteger oldTextLength = _textView.text.length; + if (eventLag == 0 && ![text isEqualToString:_backedTextInput.text]) { + UITextRange *selection = _backedTextInput.selectedTextRange; + NSInteger oldTextLength = _backedTextInput.text.length; _predictedText = text; - _textView.text = text; + _backedTextInput.text = text; if (selection.empty) { // maintain cursor position relative to the end of the old text - NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; + NSInteger start = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start]; NSInteger offsetFromEnd = oldTextLength - start; NSInteger newOffset = text.length - offsetFromEnd; - UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; - _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; + UITextPosition *position = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:newOffset]; + _backedTextInput.selectedTextRange = [_backedTextInput textRangeFromPosition:position toPosition:position]; } [self invalidateContentSize]; @@ -253,11 +258,11 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) } } -#pragma mark - UITextViewDelegate +#pragma mark - RCTBackedTextInputDelegate -- (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - if (!textView.textWasPasted) { + if (!_backedTextInput.textWasPasted) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress reactTag:self.reactTag text:nil @@ -281,7 +286,7 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) text:self.text key:nil eventCount:_nativeEventCount]; - [_textView resignFirstResponder]; + [_backedTextInput resignFirstResponder]; return NO; } } @@ -294,23 +299,23 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) } if (_maxLength) { - NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length; + NSUInteger allowedLength = _maxLength.integerValue - _backedTextInput.text.length + range.length; if (text.length > allowedLength) { // If we typed/pasted more than one character, limit the text inputted if (text.length > 1) { // Truncate the input string so the result is exactly maxLength NSString *limitedString = [text substringToIndex:allowedLength]; - NSMutableString *newString = textView.text.mutableCopy; + NSMutableString *newString = _backedTextInput.text.mutableCopy; [newString replaceCharactersInRange:range withString:limitedString]; - textView.text = newString; + _backedTextInput.text = newString; _predictedText = newString; // Collapse selection at end of insert to match normal paste behavior - UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument + UITextPosition *insertEnd = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:(range.location + allowedLength)]; - textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd]; + _backedTextInput.selectedTextRange = [_backedTextInput textRangeFromPosition:insertEnd toPosition:insertEnd]; - [self textViewDidChange:textView]; + [self textInputDidChange]; } return NO; } @@ -321,7 +326,7 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) if (range.location + range.length > _predictedText.length) { // _predictedText got out of sync in a bad way, so let's just force sync it. Haven't been able to repro this, but // it's causing a real crash here: #6523822 - _predictedText = textView.text; + _predictedText = _backedTextInput.text; } NSString *previousText = [_predictedText substringWithRange:range]; @@ -346,17 +351,17 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) return YES; } -- (void)textViewDidChangeSelection:(RCTUITextView *)textView +- (void)textInputDidChangeSelection { if (_onSelectionChange && - textView.selectedTextRange != _previousSelectionRange && - ![textView.selectedTextRange isEqual:_previousSelectionRange]) { + _backedTextInput.selectedTextRange != _previousSelectionRange && + ![_backedTextInput.selectedTextRange isEqual:_previousSelectionRange]) { - _previousSelectionRange = textView.selectedTextRange; + _previousSelectionRange = _backedTextInput.selectedTextRange; - UITextRange *selection = textView.selectedTextRange; - NSInteger start = [textView offsetFromPosition:textView.beginningOfDocument toPosition:selection.start]; - NSInteger end = [textView offsetFromPosition:textView.beginningOfDocument toPosition:selection.end]; + UITextRange *selection = _backedTextInput.selectedTextRange; + NSInteger start = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start]; + NSInteger end = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.end]; _onSelectionChange(@{ @"selection": @{ @"start": @(start), @@ -366,20 +371,20 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) } } -- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +- (BOOL)textInputShouldBeginEditing { if (_selectTextOnFocus) { dispatch_async(dispatch_get_main_queue(), ^{ - [textView selectAll:nil]; + [self->_backedTextInput selectAll:nil]; }); } return YES; } -- (void)textViewDidBeginEditing:(UITextView *)textView +- (void)textInputDidBeginEditing { if (_clearTextOnFocus) { - _textView.text = @""; + _backedTextInput.text = @""; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus @@ -418,22 +423,22 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, return YES; } -- (void)textViewDidChange:(UITextView *)textView +- (void)textInputDidChange { [self invalidateContentSize]; - // Detect when textView updates happend that didn't invoke `shouldChangeTextInRange` + // Detect when _backedTextInput updates happend that didn't invoke `shouldChangeTextInRange` // (e.g. typing simplified chinese in pinyin will insert and remove spaces without // calling shouldChangeTextInRange). This will cause JS to get out of sync so we // update the mismatched range. NSRange currentRange; NSRange predictionRange; - if (findMismatch(textView.text, _predictedText, ¤tRange, &predictionRange)) { - NSString *replacement = [textView.text substringWithRange:currentRange]; - [self textView:textView shouldChangeTextInRange:predictionRange replacementText:replacement]; + if (findMismatch(_backedTextInput.text, _predictedText, ¤tRange, &predictionRange)) { + NSString *replacement = [_backedTextInput.text substringWithRange:currentRange]; + [self textInputShouldChangeTextInRange:predictionRange replacementText:replacement]; // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it. - [self textViewDidChangeSelection:textView]; - _predictedText = textView.text; + [self textInputDidChangeSelection]; + _predictedText = _backedTextInput.text; } _nativeUpdatesInFlight = NO; @@ -450,17 +455,23 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, }); } -- (void)textViewDidEndEditing:(UITextView *)textView + +- (BOOL)textInputShouldEndEditing +{ + return YES; +} + +- (void)textInputDidEndEditing { if (_nativeUpdatesInFlight) { // iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection // which was triggered by loosing focus. So, we call it manually. - [self textViewDidChange:textView]; + [self textInputDidChange]; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag - text:textView.text + text:_backedTextInput.text key:nil eventCount:_nativeEventCount]; @@ -471,6 +482,11 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, eventCount:_nativeEventCount]; } +- (void)textInputDidEndEditingOnExit +{ + // Do nothing. +} + #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index e87d4f48769..1028420240e 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -82,7 +82,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) #if !TARGET_OS_TV -RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, textView.dataDetectorTypes, UIDataDetectorTypes) +RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) #endif - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView diff --git a/Libraries/Text/RCTUITextField.h b/Libraries/Text/RCTUITextField.h index 0d4795efdf4..27ae073006f 100644 --- a/Libraries/Text/RCTUITextField.h +++ b/Libraries/Text/RCTUITextField.h @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; +@property (nonatomic, weak) id textInputDelegate; + @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign, readonly) BOOL textWasPasted; @property (nonatomic, strong, nullable) UIColor *placeholderColor; diff --git a/Libraries/Text/RCTUITextField.m b/Libraries/Text/RCTUITextField.m index 46839ecce3d..188091959a3 100644 --- a/Libraries/Text/RCTUITextField.m +++ b/Libraries/Text/RCTUITextField.m @@ -12,7 +12,11 @@ #import #import -@implementation RCTUITextField +#import "RCTBackedTextInputDelegateAdapter.h" + +@implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; +} - (instancetype)initWithFrame:(CGRect)frame { @@ -21,6 +25,8 @@ selector:@selector(_textDidChange) name:UITextFieldTextDidChangeNotification object:self]; + + _textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self]; } return self; diff --git a/Libraries/Text/RCTUITextView.h b/Libraries/Text/RCTUITextView.h index 240e77ef6c5..47b81c32cac 100644 --- a/Libraries/Text/RCTUITextView.h +++ b/Libraries/Text/RCTUITextView.h @@ -11,6 +11,8 @@ #import "RCTBackedTextInputViewProtocol.h" +#import "RCTBackedTextInputDelegate.h" + NS_ASSUME_NONNULL_BEGIN /* @@ -21,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; +@property (nonatomic, weak) id textInputDelegate; + @property (nonatomic, assign, readonly) BOOL textWasPasted; @property (nonatomic, copy, nullable) NSString *placeholder; @property (nonatomic, strong, nullable) UIColor *placeholderColor; diff --git a/Libraries/Text/RCTUITextView.m b/Libraries/Text/RCTUITextView.m index a84680b2cd7..893594de0de 100644 --- a/Libraries/Text/RCTUITextView.m +++ b/Libraries/Text/RCTUITextView.m @@ -12,10 +12,13 @@ #import #import +#import "RCTBackedTextInputDelegateAdapter.h" + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; + RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter; } static UIFont *defaultPlaceholderFont() @@ -42,6 +45,8 @@ static UIColor *defaultPlaceholderColor() _placeholderView.numberOfLines = 0; _placeholderView.textColor = defaultPlaceholderColor(); [self addSubview:_placeholderView]; + + _textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self]; } return self;