react-native/Libraries/Text/RCTTextField.m

385 lines
12 KiB
Objective-C

/**
* 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 "RCTTextField.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import "RCTTextSelection.h"
#import "RCTUITextField.h"
@interface RCTTextField () <UITextFieldDelegate>
@end
@implementation RCTTextField
{
RCTBridge *_bridge;
RCTEventDispatcher *_eventDispatcher;
NSInteger _nativeEventCount;
BOOL _submitted;
UITextRange *_previousSelectionRange;
NSString *_finalText;
CGSize _previousContentSize;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithFrame:CGRectZero]) {
RCTAssertParam(bridge);
_bridge = bridge;
_eventDispatcher = bridge.eventDispatcher;
_textField = [[RCTUITextField alloc] initWithFrame:self.bounds];
_textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// 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];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)dealloc
{
[_textField removeObserver:self forKeyPath:@"selectedTextRange"];
}
- (void)sendKeyValueForString:(NSString *)string
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
reactTag:self.reactTag
text:nil
key:string
eventCount:_nativeEventCount];
}
#pragma mark - Properties
- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
{
_reactPaddingInsets = reactPaddingInsets;
// We apply `paddingInsets` as `_textField`'s `textContainerInset`.
_textField.textContainerInset = reactPaddingInsets;
[self setNeedsLayout];
}
- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
{
_reactBorderInsets = reactBorderInsets;
// We apply `borderInsets` as `_textView` layout offset.
_textField.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
[self setNeedsLayout];
}
- (void)setSelection:(RCTTextSelection *)selection
{
if (!selection) {
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];
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
_previousSelectionRange = selectedTextRange;
_textField.selectedTextRange = selectedTextRange;
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
}
}
- (NSString *)text
{
return _textField.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;
_textField.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 offsetFromEnd = oldTextLength - offsetStart;
NSInteger newOffset = text.length - offsetFromEnd;
UITextPosition *position = [_textField positionFromPosition:_textField.beginningOfDocument offset:newOffset];
_textField.selectedTextRange = [_textField 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);
}
}
#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]) {
_previousSelectionRange = _textField.selectedTextRange;
UITextRange *selection = _textField.selectedTextRange;
NSInteger start = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.start];
NSInteger end = [_textField offsetFromPosition:[_textField beginningOfDocument] toPosition:selection.end];
_onSelectionChange(@{
@"selection": @{
@"start": @(start),
@"end": @(end),
},
});
}
}
#pragma mark - Content Size (in Yoga terms, without any insets)
- (CGSize)contentSize
{
// Returning value does NOT include border and padding insets.
CGSize contentSize = self.intrinsicContentSize;
UIEdgeInsets compoundInsets = self.reactCompoundInsets;
contentSize.width -= compoundInsets.left + compoundInsets.right;
contentSize.height -= compoundInsets.top + compoundInsets.bottom;
return contentSize;
}
- (void)invalidateContentSize
{
CGSize contentSize = self.contentSize;
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
return;
}
_previousContentSize = contentSize;
[_bridge.uiManager setIntrinsicContentSize:contentSize forView:self];
}
#pragma mark - Layout (in UIKit terms, with all insets)
- (CGSize)intrinsicContentSize
{
// Returning value DOES include border and padding insets.
CGSize size = _textField.intrinsicContentSize;
size.width += _reactBorderInsets.left + _reactBorderInsets.right;
size.height += _reactBorderInsets.top + _reactBorderInsets.bottom;
return size;
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat compoundHorizontalBorderInset = _reactBorderInsets.left + _reactBorderInsets.right;
CGFloat compoundVerticalBorderInset = _reactBorderInsets.top + _reactBorderInsets.bottom;
size.width -= compoundHorizontalBorderInset;
size.height -= compoundVerticalBorderInset;
// Note: `paddingInsets` already included in `_textView` size
// because it was applied as `textContainerInset`.
CGSize fittingSize = [_textField sizeThatFits:size];
fittingSize.width += compoundHorizontalBorderInset;
fittingSize.height += compoundVerticalBorderInset;
return fittingSize;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self invalidateContentSize];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
// Only allow single keypresses for `onKeyPress`, pasted text will not be sent.
if (!_textField.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;
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;
[newString replaceCharactersInRange:range withString:limitedString];
_textField.text = newString;
// Collapse selection at end of insert to match normal paste behavior.
UITextPosition *insertEnd = [_textField positionFromPosition:_textField.beginningOfDocument
offset:(range.location + allowedLength)];
_textField.selectedTextRange = [_textField textRangeFromPosition:insertEnd toPosition:insertEnd];
[self textFieldDidChange];
}
return NO;
}
}
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
{
[self textField:_textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""];
return YES;
}
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
{
_finalText = _textField.text;
if (_submitted) {
_submitted = NO;
return _blurOnSubmit;
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
reactTag:self.reactTag
text:self.text
key:nil
eventCount:_nativeEventCount];
}
#pragma mark - Accessibility
- (UIView *)reactAccessibilityElement
{
return _textField;
}
#pragma mark - Focus control deledation
- (void)reactFocus
{
[_textField reactFocus];
}
- (void)reactBlur
{
[_textField reactBlur];
}
- (void)didMoveToWindow
{
[_textField reactFocusIfNeeded];
}
@end