mirror of
https://github.com/status-im/react-native.git
synced 2025-02-24 23:28:12 +00:00
Introducing RCTBackedTextInputDelegate
Summary: Nothing behavioral changed in this diff; just moving code around. `RCTBackedTextInputDelegate` is the new protocol which supposed to be common determinator among of UITextFieldDelegate and UITextViewDelegate (and bunch of events and notifications around UITextInput and UITextView). We need this reach two goals in the future: * Incapsulate UIKit imperfections related hack in dedicated protocol adapter. So, doing this we can fix more UIKit related bugs without touching real RN text handling logic. (Yes, we still have a bunch of bugs, which we cannot fix because it is undoable with the current architecture. This diff does NOT fix anything though.) * We can unify logic in RCTTextField and RCTTextView (even more!), moving it to a superclass. If we do so, we can fix another bunch of bugs related to RN imperfections. And have singleline/multiline inputs implementations even more consistent. Reviewed By: mmmulani Differential Revision: D5296041 fbshipit-source-id: 318fd850e946a3c34933002a6bde34a0a45a6293
This commit is contained in:
parent
2a7bde0164
commit
ee9697e515
28
Libraries/Text/RCTBackedTextInputDelegate.h
Normal file
28
Libraries/Text/RCTBackedTextInputDelegate.h
Normal file
@ -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 <UIKit/UIKit.h>
|
||||
|
||||
@protocol RCTBackedTextInputViewProtocol;
|
||||
|
||||
@protocol RCTBackedTextInputDelegate <NSObject>
|
||||
|
||||
- (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
|
29
Libraries/Text/RCTBackedTextInputDelegateAdapter.h
Normal file
29
Libraries/Text/RCTBackedTextInputDelegateAdapter.h
Normal file
@ -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 <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTBackedTextInputViewProtocol.h"
|
||||
#import "RCTBackedTextInputDelegate.h"
|
||||
|
||||
#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField)
|
||||
|
||||
@interface RCTBackedTextFieldDelegateAdapter : NSObject
|
||||
|
||||
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)backedTextInput;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView)
|
||||
|
||||
@interface RCTBackedTextViewDelegateAdapter : NSObject
|
||||
|
||||
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)backedTextInput;
|
||||
|
||||
@end
|
177
Libraries/Text/RCTBackedTextInputDelegateAdapter.m
Normal file
177
Libraries/Text/RCTBackedTextInputDelegateAdapter.m
Normal file
@ -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 () <UITextFieldDelegate>
|
||||
@end
|
||||
|
||||
@implementation RCTBackedTextFieldDelegateAdapter {
|
||||
__weak UITextField<RCTBackedTextInputViewProtocol> *_backedTextInput;
|
||||
__unsafe_unretained UITextField<RCTBackedTextInputViewProtocol> *_unsafeBackedTextInput;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)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 () <UITextViewDelegate>
|
||||
@end
|
||||
|
||||
@implementation RCTBackedTextViewDelegateAdapter {
|
||||
__weak UITextView<RCTBackedTextInputViewProtocol> *_backedTextInput;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)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
|
@ -9,6 +9,8 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol RCTBackedTextInputDelegate;
|
||||
|
||||
@protocol RCTBackedTextInputViewProtocol <UITextInput>
|
||||
|
||||
@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<RCTBackedTextInputDelegate> textInputDelegate;
|
||||
|
||||
@end
|
||||
|
@ -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 = "<group>"; };
|
||||
58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
|
||||
58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
|
||||
598F41231F145D4900B8495B /* RCTBackedTextInputDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputDelegate.h; sourceTree = "<group>"; };
|
||||
598F41241F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputDelegateAdapter.h; sourceTree = "<group>"; };
|
||||
598F41251F145D4900B8495B /* RCTBackedTextInputDelegateAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBackedTextInputDelegateAdapter.m; sourceTree = "<group>"; };
|
||||
599DF25D1F0304B30079B53E /* RCTBackedTextInputViewProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBackedTextInputViewProtocol.h; sourceTree = "<group>"; };
|
||||
599DF2621F03076D0079B53E /* RCTTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextInput.h; sourceTree = "<group>"; };
|
||||
599DF2631F03076D0079B53E /* RCTTextInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextInput.m; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -26,6 +26,4 @@
|
||||
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
|
||||
|
||||
@property (nonatomic, strong) RCTUITextField *textField;
|
||||
|
||||
@end
|
||||
|
@ -16,15 +16,17 @@
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
#import "RCTBackedTextInputDelegate.h"
|
||||
#import "RCTTextSelection.h"
|
||||
#import "RCTUITextField.h"
|
||||
|
||||
@interface RCTTextField () <UITextFieldDelegate>
|
||||
@interface RCTTextField () <RCTBackedTextInputDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTTextField
|
||||
{
|
||||
RCTUITextField *_backedTextInput;
|
||||
NSInteger _nativeEventCount;
|
||||
BOOL _submitted;
|
||||
UITextRange *_previousSelectionRange;
|
||||
@ -40,24 +42,11 @@
|
||||
// `blurOnSubmit` defaults to `true` for <TextInput multiline={false}> 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<RCTBackedTextInputViewProtocol>)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
|
||||
|
@ -52,30 +52,30 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, backedTextInputView.textAlignment, NSTextAlig
|
||||
|
||||
#pragma mark - Singleline <TextInput> (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)
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
@class RCTBridge;
|
||||
|
||||
@interface RCTTextView : RCTTextInput <UITextViewDelegate>
|
||||
@interface RCTTextView : RCTTextInput
|
||||
|
||||
@property (nonatomic, assign) UITextAutocorrectionType autocorrectionType;
|
||||
@property (nonatomic, assign) UITextSpellCheckingType spellCheckingType;
|
||||
|
@ -20,9 +20,13 @@
|
||||
#import "RCTTextSelection.h"
|
||||
#import "RCTUITextView.h"
|
||||
|
||||
@interface RCTTextView () <RCTBackedTextInputDelegate>
|
||||
|
||||
@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<RCTBackedTextInputViewProtocol>)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<NSString *, id> *attrs = [_textView.typingAttributes mutableCopy];
|
||||
NSMutableDictionary<NSString *, id> *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
|
||||
|
@ -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
|
||||
|
@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
|
||||
|
||||
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
|
||||
|
||||
@property (nonatomic, assign) BOOL caretHidden;
|
||||
@property (nonatomic, assign, readonly) BOOL textWasPasted;
|
||||
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
|
||||
|
@ -12,7 +12,11 @@
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
@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;
|
||||
|
@ -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<RCTBackedTextInputDelegate> textInputDelegate;
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL textWasPasted;
|
||||
@property (nonatomic, copy, nullable) NSString *placeholder;
|
||||
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
|
||||
|
@ -12,10 +12,13 @@
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
#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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user