/* * 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 "RCTConversions.h" #import "RCTTextInputNativeCommands.h" #import "RCTTextInputUtils.h" using namespace facebook::react; @interface RCTTextInputComponentView () @end @implementation RCTTextInputComponentView { TextInputShadowNode::ConcreteState::Shared _state; UIView *_backedTextInputView; size_t _stateRevision; } - (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.frame = self.bounds; _backedTextInputView.textInputDelegate = self; _stateRevision = State::initialRevisionValue; [self addSubview:_backedTextInputView]; } return self; } #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) { 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.textAttributes != oldTextInputProps.textAttributes) { _backedTextInputView.defaultTextAttributes = RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes()); } if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) { _backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor); } [super updateProps:props oldProps:oldProps]; } - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState { _state = std::static_pointer_cast(state); if (!_state) { assert(false && "State is `null` for component."); _backedTextInputView.attributedText = nil; return; } if (_state->getRevision() != _stateRevision) { auto data = _state->getData(); _stateRevision = _state->getRevision(); _backedTextInputView.attributedText = RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox); } } - (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 = [[NSAttributedString alloc] init]; _state.reset(); _stateRevision = State::initialRevisionValue; } #pragma mark - RCTComponentViewProtocol - (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]; } #pragma mark - RCTBackedTextInputDelegate - (BOOL)textInputShouldBeginEditing { return YES; } - (void)textInputDidBeginEditing { auto const &props = *std::static_pointer_cast(_props); if (props.traits.clearTextOnFocus) { _backedTextInputView.attributedText = [NSAttributedString new]; [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) { std::static_pointer_cast(_eventEmitter)->onKeyPress([self _textInputMetrics]); } } 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 { [self _updateState]; if (_eventEmitter) { std::static_pointer_cast(_eventEmitter)->onChange([self _textInputMetrics]); } } - (void)textInputDidChangeSelection { if (_eventEmitter) { std::static_pointer_cast(_eventEmitter)->onSelectionChange([self _textInputMetrics]); } } #pragma mark - Other - (TextInputMetrics)_textInputMetrics { TextInputMetrics metrics; metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string); metrics.selectionRange = [self _selectionRange]; return metrics; } - (void)_updateState { NSAttributedString *attributedString = _backedTextInputView.attributedText; if (!_state) { return; } auto data = _state->getData(); data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); _state->updateState(std::move(data), EventPriority::SynchronousUnbatched); _stateRevision = _state->getRevision() + 1; } - (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)}; } #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)setMostRecentEventCount:(NSInteger)eventCount { // no-op. `eventCount` isn't used in Fabric's TextInput. // We are keeping it so commands are backwards // compatible with Paper's TextInput. } - (void)setTextAndSelection:(NSInteger)eventCount value:(NSString *__nullable)value start:(NSInteger)start end:(NSInteger)end { // `eventCount` is ignored, isn't used in Fabric's TextInput. // We are keeping it so commands are // backwards compatible with Paper's TextInput. if (value) { NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:_backedTextInputView.attributedText]; [mutableString replaceCharactersInRange:NSMakeRange(0, _backedTextInputView.attributedText.length) withString:value]; _backedTextInputView.attributedText = 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]; } } @end