mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
3d4535a2bb
Summary: Changelog: [Internal] Introducing InputAccessoryView. There is one big difference between Fabric's implementation and Paper's implementation. Fabric searches for text input from InputAccessoryView, unlike Paper where it is the other way around. Reviewed By: shergin Differential Revision: D22160445 fbshipit-source-id: 55313fe50afeced7aead5b57137d711dd1cfd3ae
495 lines
17 KiB
Plaintext
495 lines
17 KiB
Plaintext
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTTextInputComponentView.h"
|
|
|
|
#import <react/components/iostextinput/TextInputComponentDescriptor.h>
|
|
#import <react/graphics/Geometry.h>
|
|
#import <react/textlayoutmanager/RCTAttributedTextUtils.h>
|
|
#import <react/textlayoutmanager/TextLayoutManager.h>
|
|
|
|
#import <React/RCTBackedTextInputViewProtocol.h>
|
|
#import <React/RCTUITextField.h>
|
|
#import <React/RCTUITextView.h>
|
|
#import <React/RCTUtils.h>
|
|
|
|
#import "RCTConversions.h"
|
|
#import "RCTTextInputNativeCommands.h"
|
|
#import "RCTTextInputUtils.h"
|
|
|
|
#import "RCTFabricComponentsPlugins.h"
|
|
|
|
using namespace facebook::react;
|
|
|
|
@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate, RCTTextInputViewProtocol>
|
|
@end
|
|
|
|
@implementation RCTTextInputComponentView {
|
|
TextInputShadowNode::ConcreteState::Shared _state;
|
|
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
|
|
NSUInteger _mostRecentEventCount;
|
|
NSAttributedString *_lastStringStateWasUpdatedWith;
|
|
|
|
/*
|
|
* UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
|
|
* entry, UITextView is for multiline entry. There is a problem with order of events when user types a character. In
|
|
* UITextField (single line text entry), typing a character first triggers `onChange` event and then
|
|
* onSelectionChange. In UITextView (multi line text entry), typing a character first triggers `onSelectionChange` and
|
|
* then onChange. JavaScript depends on `onChange` to be called before `onSelectionChange`. This flag keeps state so
|
|
* if UITextView is backing text input view, inside `-[RCTTextInputComponentView textInputDidChangeSelection]` we make
|
|
* sure to call `onChange` before `onSelectionChange` and ignore next `-[RCTTextInputComponentView
|
|
* textInputDidChange]` call.
|
|
*/
|
|
BOOL _ignoreNextTextInputCall;
|
|
|
|
/*
|
|
* A flag that when set to true, `_mostRecentEventCount` won't be incremented when `[self _updateState]`
|
|
* and delegate methods `textInputDidChange` and `textInputDidChangeSelection` will exit early.
|
|
*
|
|
* Setting `_backedTextInputView.attributedText` triggers delegate methods `textInputDidChange` and
|
|
* `textInputDidChangeSelection` for multiline text input only.
|
|
* In multiline text input this is undesirable as we don't want to be sending events for changes that JS triggered.
|
|
*/
|
|
BOOL _comingFromJS;
|
|
BOOL _didMoveToWindow;
|
|
}
|
|
|
|
#pragma mark - UIView overrides
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
if (self = [super initWithFrame:frame]) {
|
|
static const auto defaultProps = std::make_shared<TextInputProps const>();
|
|
_props = defaultProps;
|
|
auto &props = *defaultProps;
|
|
|
|
_backedTextInputView = props.traits.multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init];
|
|
_backedTextInputView.textInputDelegate = self;
|
|
_ignoreNextTextInputCall = NO;
|
|
_comingFromJS = NO;
|
|
_didMoveToWindow = NO;
|
|
[self addSubview:_backedTextInputView];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)didMoveToWindow
|
|
{
|
|
[super didMoveToWindow];
|
|
|
|
if (self.window && !_didMoveToWindow) {
|
|
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
if (props.autoFocus) {
|
|
[_backedTextInputView becomeFirstResponder];
|
|
}
|
|
_didMoveToWindow = YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - RCTViewComponentView overrides
|
|
|
|
- (NSObject *)accessibilityElement
|
|
{
|
|
return _backedTextInputView;
|
|
}
|
|
|
|
#pragma mark - RCTComponentViewProtocol
|
|
|
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
{
|
|
return concreteComponentDescriptorProvider<TextInputComponentDescriptor>();
|
|
}
|
|
|
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
{
|
|
auto const &oldTextInputProps = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
auto const &newTextInputProps = *std::static_pointer_cast<TextInputProps const>(props);
|
|
|
|
// Traits:
|
|
if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
|
|
[self _setMultiline:newTextInputProps.traits.multiline];
|
|
}
|
|
|
|
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
|
|
_backedTextInputView.autocapitalizationType =
|
|
RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType);
|
|
}
|
|
|
|
if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) {
|
|
_backedTextInputView.autocorrectionType =
|
|
RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect);
|
|
}
|
|
|
|
if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) {
|
|
_backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden;
|
|
}
|
|
|
|
if (newTextInputProps.traits.editable != oldTextInputProps.traits.editable) {
|
|
_backedTextInputView.editable = newTextInputProps.traits.editable;
|
|
}
|
|
|
|
if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
|
|
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
|
|
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
|
|
}
|
|
|
|
if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) {
|
|
_backedTextInputView.keyboardAppearance =
|
|
RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance);
|
|
}
|
|
|
|
if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) {
|
|
_backedTextInputView.spellCheckingType =
|
|
RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck);
|
|
}
|
|
|
|
if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) {
|
|
_backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden;
|
|
}
|
|
|
|
if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) {
|
|
_backedTextInputView.clearButtonMode =
|
|
RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode);
|
|
}
|
|
|
|
if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) {
|
|
_backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled;
|
|
}
|
|
|
|
if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) {
|
|
_backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry;
|
|
}
|
|
|
|
if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) {
|
|
_backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType);
|
|
}
|
|
|
|
if (newTextInputProps.traits.returnKeyType != oldTextInputProps.traits.returnKeyType) {
|
|
_backedTextInputView.returnKeyType = RCTUIReturnKeyTypeFromReturnKeyType(newTextInputProps.traits.returnKeyType);
|
|
}
|
|
|
|
if (newTextInputProps.traits.textContentType != oldTextInputProps.traits.textContentType) {
|
|
if (@available(iOS 10.0, *)) {
|
|
_backedTextInputView.textContentType = RCTUITextContentTypeFromString(newTextInputProps.traits.textContentType);
|
|
}
|
|
}
|
|
|
|
if (newTextInputProps.traits.passwordRules != oldTextInputProps.traits.passwordRules) {
|
|
if (@available(iOS 12.0, *)) {
|
|
_backedTextInputView.passwordRules =
|
|
RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules);
|
|
}
|
|
}
|
|
|
|
// Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentially here
|
|
// because they are being checked on-demand.
|
|
|
|
// Other props:
|
|
if (newTextInputProps.placeholder != oldTextInputProps.placeholder) {
|
|
_backedTextInputView.placeholder = RCTNSStringFromString(newTextInputProps.placeholder);
|
|
}
|
|
|
|
if (newTextInputProps.placeholderTextColor != oldTextInputProps.placeholderTextColor) {
|
|
_backedTextInputView.placeholderColor = RCTUIColorFromSharedColor(newTextInputProps.placeholderTextColor);
|
|
}
|
|
|
|
if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
|
|
_backedTextInputView.defaultTextAttributes =
|
|
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
|
|
}
|
|
|
|
if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
|
|
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
|
|
}
|
|
|
|
if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
|
|
_backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
|
|
}
|
|
|
|
[super updateProps:props oldProps:oldProps];
|
|
}
|
|
|
|
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
|
|
{
|
|
_state = std::static_pointer_cast<TextInputShadowNode::ConcreteState const>(state);
|
|
|
|
if (!_state) {
|
|
assert(false && "State is `null` for <TextInput> component.");
|
|
_backedTextInputView.attributedText = nil;
|
|
return;
|
|
}
|
|
|
|
if (_mostRecentEventCount == _state->getData().mostRecentEventCount) {
|
|
auto data = _state->getData();
|
|
_comingFromJS = YES;
|
|
[self _setAttributedString:RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox)];
|
|
_comingFromJS = NO;
|
|
}
|
|
}
|
|
|
|
- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
|
|
oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics
|
|
{
|
|
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
|
|
_backedTextInputView.frame =
|
|
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
|
|
_backedTextInputView.textContainerInset =
|
|
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);
|
|
}
|
|
|
|
- (void)prepareForRecycle
|
|
{
|
|
[super prepareForRecycle];
|
|
_backedTextInputView.attributedText = nil;
|
|
_mostRecentEventCount = 0;
|
|
_state.reset();
|
|
_comingFromJS = NO;
|
|
_lastStringStateWasUpdatedWith = nil;
|
|
_ignoreNextTextInputCall = NO;
|
|
_didMoveToWindow = NO;
|
|
}
|
|
|
|
#pragma mark - RCTBackedTextInputDelegate
|
|
|
|
- (BOOL)textInputShouldBeginEditing
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)textInputDidBeginEditing
|
|
{
|
|
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
|
|
if (props.traits.clearTextOnFocus) {
|
|
_backedTextInputView.attributedText = nil;
|
|
[self textInputDidChange];
|
|
}
|
|
|
|
if (props.traits.selectTextOnFocus) {
|
|
[_backedTextInputView selectAll:nil];
|
|
[self textInputDidChangeSelection];
|
|
}
|
|
|
|
if (_eventEmitter) {
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onFocus([self _textInputMetrics]);
|
|
}
|
|
}
|
|
|
|
- (BOOL)textInputShouldEndEditing
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)textInputDidEndEditing
|
|
{
|
|
if (_eventEmitter) {
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onEndEditing([self _textInputMetrics]);
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onBlur([self _textInputMetrics]);
|
|
}
|
|
}
|
|
|
|
- (BOOL)textInputShouldReturn
|
|
{
|
|
// We send `submit` event here, in `textInputShouldReturn`
|
|
// (not in `textInputDidReturn)`, because of semantic of the event:
|
|
// `onSubmitEditing` is called when "Submit" button
|
|
// (the blue key on onscreen keyboard) did pressed
|
|
// (no connection to any specific "submitting" process).
|
|
|
|
if (_eventEmitter) {
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSubmitEditing([self _textInputMetrics]);
|
|
}
|
|
|
|
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
return props.traits.blurOnSubmit;
|
|
}
|
|
|
|
- (void)textInputDidReturn
|
|
{
|
|
// Does nothing.
|
|
}
|
|
|
|
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
|
|
{
|
|
if (!_backedTextInputView.textWasPasted) {
|
|
if (_eventEmitter) {
|
|
KeyPressMetrics keyPressMetrics;
|
|
keyPressMetrics.text = RCTStringFromNSString(text);
|
|
keyPressMetrics.eventCount = _mostRecentEventCount;
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onKeyPress(keyPressMetrics);
|
|
}
|
|
}
|
|
|
|
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
if (props.maxLength) {
|
|
NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length;
|
|
|
|
if (allowedLength <= 0) {
|
|
return nil;
|
|
}
|
|
|
|
return allowedLength > text.length ? text : [text substringToIndex:allowedLength];
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)textInputDidChange
|
|
{
|
|
if (_comingFromJS) {
|
|
return;
|
|
}
|
|
|
|
if (_ignoreNextTextInputCall) {
|
|
_ignoreNextTextInputCall = NO;
|
|
return;
|
|
}
|
|
|
|
[self _updateState];
|
|
|
|
if (_eventEmitter) {
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onChange([self _textInputMetrics]);
|
|
}
|
|
}
|
|
|
|
- (void)textInputDidChangeSelection
|
|
{
|
|
if (_comingFromJS) {
|
|
return;
|
|
}
|
|
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
|
|
if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
|
[self textInputDidChange];
|
|
_ignoreNextTextInputCall = YES;
|
|
}
|
|
|
|
if (_eventEmitter) {
|
|
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSelectionChange([self _textInputMetrics]);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Native Commands
|
|
|
|
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
{
|
|
RCTTextInputHandleCommand(self, commandName, args);
|
|
}
|
|
|
|
- (void)focus
|
|
{
|
|
[_backedTextInputView becomeFirstResponder];
|
|
}
|
|
|
|
- (void)blur
|
|
{
|
|
[_backedTextInputView resignFirstResponder];
|
|
}
|
|
|
|
- (void)setTextAndSelection:(NSInteger)eventCount
|
|
value:(NSString *__nullable)value
|
|
start:(NSInteger)start
|
|
end:(NSInteger)end
|
|
{
|
|
if (_mostRecentEventCount != eventCount) {
|
|
return;
|
|
}
|
|
_comingFromJS = YES;
|
|
if (![value isEqualToString:_backedTextInputView.attributedText.string]) {
|
|
NSMutableAttributedString *mutableString =
|
|
[[NSMutableAttributedString alloc] initWithAttributedString:_backedTextInputView.attributedText];
|
|
[mutableString replaceCharactersInRange:NSMakeRange(0, _backedTextInputView.attributedText.length)
|
|
withString:value];
|
|
[self _setAttributedString:mutableString];
|
|
[self _updateState];
|
|
}
|
|
|
|
UITextPosition *startPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
|
|
offset:start];
|
|
UITextPosition *endPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
|
|
offset:end];
|
|
|
|
if (startPosition && endPosition) {
|
|
UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
|
|
[_backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
|
|
}
|
|
_comingFromJS = NO;
|
|
}
|
|
|
|
#pragma mark - Other
|
|
|
|
- (TextInputMetrics)_textInputMetrics
|
|
{
|
|
TextInputMetrics metrics;
|
|
metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string);
|
|
metrics.selectionRange = [self _selectionRange];
|
|
metrics.eventCount = _mostRecentEventCount;
|
|
return metrics;
|
|
}
|
|
|
|
- (void)_updateState
|
|
{
|
|
if (!_state) {
|
|
return;
|
|
}
|
|
NSAttributedString *attributedString = _backedTextInputView.attributedText;
|
|
auto data = _state->getData();
|
|
_lastStringStateWasUpdatedWith = attributedString;
|
|
data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString);
|
|
_mostRecentEventCount += _comingFromJS ? 0 : 1;
|
|
data.mostRecentEventCount = _mostRecentEventCount;
|
|
_state->updateState(std::move(data));
|
|
}
|
|
|
|
- (AttributedString::Range)_selectionRange
|
|
{
|
|
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
|
|
NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
|
|
toPosition:selectedTextRange.start];
|
|
NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
|
|
toPosition:selectedTextRange.end];
|
|
return AttributedString::Range{(int)start, (int)(end - start)};
|
|
}
|
|
|
|
- (void)_setAttributedString:(NSAttributedString *)attributedString
|
|
{
|
|
UITextRange *selectedRange = [_backedTextInputView selectedTextRange];
|
|
_backedTextInputView.attributedText = attributedString;
|
|
if (_lastStringStateWasUpdatedWith.length == attributedString.length) {
|
|
// Calling `[_backedTextInputView setAttributedText]` moves caret
|
|
// to the end of text input field. This cancels any selection as well
|
|
// as position in the text input field. In case the length of string
|
|
// doesn't change, selection and caret position is maintained.
|
|
[_backedTextInputView setSelectedTextRange:selectedRange notifyDelegate:NO];
|
|
}
|
|
_lastStringStateWasUpdatedWith = attributedString;
|
|
}
|
|
|
|
- (void)_setMultiline:(BOOL)multiline
|
|
{
|
|
[_backedTextInputView removeFromSuperview];
|
|
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView =
|
|
multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init];
|
|
backedTextInputView.frame = _backedTextInputView.frame;
|
|
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
|
|
_backedTextInputView = backedTextInputView;
|
|
[self addSubview:_backedTextInputView];
|
|
}
|
|
|
|
@end
|
|
|
|
Class<RCTComponentViewProtocol> RCTTextInputCls(void)
|
|
{
|
|
return RCTTextInputComponentView.class;
|
|
}
|