/* * 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 #import #import #import #import #import #import #import #import "RCTConversions.h" #import "RCTTextInputNativeCommands.h" #import "RCTTextInputUtils.h" #import "RCTFabricComponentsPlugins.h" using namespace facebook::react; @interface RCTTextInputComponentView () @end @implementation RCTTextInputComponentView { TextInputShadowNode::ConcreteStateTeller _stateTeller; UIView *_backedTextInputView; NSUInteger _mostRecentEventCount; NSAttributedString *_lastStringStateWasUpdatedWith; /* * UIKit uses either UITextField or UITextView as its UIKit element for . 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(); _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(_props); if (props.autoFocus) { [_backedTextInputView becomeFirstResponder]; } _didMoveToWindow = YES; } } #pragma mark - RCTViewComponentView overrides - (NSObject *)accessibilityElement { return _backedTextInputView; } #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider { return concreteComponentDescriptorProvider(); } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { auto const &oldTextInputProps = *std::static_pointer_cast(_props); auto const &newTextInputProps = *std::static_pointer_cast(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) { _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 { _stateTeller.setConcreteState(state); if (!_stateTeller.isValid()) { assert(false && "State is `null` for component."); _backedTextInputView.attributedText = nil; return; } auto data = _stateTeller.getData().value(); if (!oldState) { _mostRecentEventCount = data.mostRecentEventCount; } if (_mostRecentEventCount == data.mostRecentEventCount) { _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; _stateTeller.invalidate(); _comingFromJS = NO; _lastStringStateWasUpdatedWith = nil; _ignoreNextTextInputCall = NO; _didMoveToWindow = NO; } #pragma mark - RCTBackedTextInputDelegate - (BOOL)textInputShouldBeginEditing { return YES; } - (void)textInputDidBeginEditing { auto const &props = *std::static_pointer_cast(_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(_eventEmitter)->onFocus([self _textInputMetrics]); } } - (BOOL)textInputShouldEndEditing { return YES; } - (void)textInputDidEndEditing { if (_eventEmitter) { std::static_pointer_cast(_eventEmitter)->onEndEditing([self _textInputMetrics]); std::static_pointer_cast(_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(_eventEmitter)->onSubmitEditing([self _textInputMetrics]); } auto const &props = *std::static_pointer_cast(_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(_eventEmitter)->onKeyPress(keyPressMetrics); } } auto const &props = *std::static_pointer_cast(_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(_eventEmitter)->onChange([self _textInputMetrics]); } } - (void)textInputDidChangeSelection { if (_comingFromJS) { return; } auto const &props = *std::static_pointer_cast(_props); if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { [self textInputDidChange]; _ignoreNextTextInputCall = YES; } if (_eventEmitter) { std::static_pointer_cast(_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 (!_stateTeller.isValid()) { return; } NSAttributedString *attributedString = _backedTextInputView.attributedText; auto data = _stateTeller.getData().value(); _lastStringStateWasUpdatedWith = attributedString; data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); _mostRecentEventCount += _comingFromJS ? 0 : 1; data.mostRecentEventCount = _mostRecentEventCount; _stateTeller.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 *backedTextInputView = multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; backedTextInputView.frame = _backedTextInputView.frame; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); _backedTextInputView = backedTextInputView; [self addSubview:_backedTextInputView]; } @end Class RCTTextInputCls(void) { return RCTTextInputComponentView.class; }