/** * 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 "RCTTextInput.h" #import #import #import #import #import #import #import #import "RCTTextSelection.h" @implementation RCTTextInput { CGSize _previousContentSize; BOOL _hasInputAccesoryView; } - (instancetype)initWithBridge:(RCTBridge *)bridge { RCTAssertParam(bridge); if (self = [super initWithFrame:CGRectZero]) { _bridge = bridge; _eventDispatcher = bridge.eventDispatcher; _fontAttributes = [[RCTFontAttributes alloc] initWithAccessibilityManager:bridge.accessibilityManager]; _fontAttributes.delegate = self; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)init) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder) RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) - (id)backedTextInputView { RCTAssert(NO, @"-[RCTTextInput backedTextInputView] must be implemented in subclass."); return nil; } - (void)setFont:(UIFont *)font { self.backedTextInputView.font = font; [self invalidateContentSize]; } - (void)fontAttributesDidChangeWithFont:(UIFont *)font { self.font = font; } #pragma mark - Properties - (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets { _reactPaddingInsets = reactPaddingInsets; // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`. self.backedTextInputView.textContainerInset = reactPaddingInsets; [self setNeedsLayout]; } - (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets { _reactBorderInsets = reactBorderInsets; // We apply `borderInsets` as `backedTextInputView` layout offset. self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets); [self setNeedsLayout]; } - (RCTTextSelection *)selection { id backedTextInput = self.backedTextInputView; UITextRange *selectedTextRange = backedTextInput.selectedTextRange; return [[RCTTextSelection new] initWithStart:[backedTextInput offsetFromPosition:backedTextInput.beginningOfDocument toPosition:selectedTextRange.start] end:[backedTextInput offsetFromPosition:backedTextInput.beginningOfDocument toPosition:selectedTextRange.end]]; } - (void)setSelection:(RCTTextSelection *)selection { if (!selection) { return; } id backedTextInput = self.backedTextInputView; UITextRange *previousSelectedTextRange = 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 && ![previousSelectedTextRange isEqual:selectedTextRange]) { [backedTextInput setSelectedTextRange:selectedTextRange notifyDelegate:NO]; } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLogWarn(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", backedTextInput.text, (long long)eventLag); } } #pragma mark - RCTBackedTextInputDelegate - (BOOL)textInputShouldBeginEditing { return YES; } - (void)textInputDidBeginEditing { if (_clearTextOnFocus) { self.backedTextInputView.text = @""; } if (_selectTextOnFocus) { [self.backedTextInputView selectAll:nil]; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:self.backedTextInputView.text key:nil eventCount:_nativeEventCount]; } - (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). [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit reactTag:self.reactTag text:self.backedTextInputView.text key:nil eventCount:_nativeEventCount]; return _blurOnSubmit; } - (void)textInputDidReturn { // Does nothing. } - (void)textInputDidChangeSelection { if (!_onSelectionChange) { return; } RCTTextSelection *selection = self.selection; _onSelectionChange(@{ @"selection": @{ @"start": @(selection.start), @"end": @(selection.end), }, }); } - (BOOL)textInputShouldEndEditing { return YES; } - (void)textInputDidEndEditing { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd reactTag:self.reactTag text:self.backedTextInputView.text key:nil eventCount:_nativeEventCount]; [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur reactTag:self.reactTag text:self.backedTextInputView.text key:nil eventCount:_nativeEventCount]; } #pragma mark - Content Size (in Yoga terms, without any insets) - (CGSize)contentSize { CGSize contentSize = self.backedTextInputView.contentSize; UIEdgeInsets reactPaddingInsets = self.reactPaddingInsets; contentSize.width -= reactPaddingInsets.left + reactPaddingInsets.right; contentSize.height -= reactPaddingInsets.top + reactPaddingInsets.bottom; // Returning value does NOT include border and padding insets. return contentSize; } - (void)invalidateContentSize { // Updates `contentSize` property and notifies Yoga about the change, if necessary. CGSize contentSize = self.contentSize; if (CGSizeEqualToSize(_previousContentSize, contentSize)) { return; } _previousContentSize = contentSize; [_bridge.uiManager setIntrinsicContentSize:contentSize forView:self]; if (_onContentSizeChange) { _onContentSizeChange(@{ @"contentSize": @{ @"height": @(contentSize.height), @"width": @(contentSize.width), }, @"target": self.reactTag, }); } } #pragma mark - Layout (in UIKit terms, with all insets) - (CGSize)intrinsicContentSize { CGSize size = self.backedTextInputView.intrinsicContentSize; size.width += _reactBorderInsets.left + _reactBorderInsets.right; size.height += _reactBorderInsets.top + _reactBorderInsets.bottom; // Returning value DOES include border and padding insets. 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` was already included in `backedTextInputView` size // because it was applied as `textContainerInset`. CGSize fittingSize = [self.backedTextInputView sizeThatFits:size]; fittingSize.width += compoundHorizontalBorderInset; fittingSize.height += compoundVerticalBorderInset; // Returning value DOES include border and padding insets. return fittingSize; } - (void)layoutSubviews { [super layoutSubviews]; [self invalidateContentSize]; } #pragma mark - Accessibility - (UIView *)reactAccessibilityElement { return self.backedTextInputView; } #pragma mark - Focus Control - (void)reactFocus { [self.backedTextInputView reactFocus]; } - (void)reactBlur { [self.backedTextInputView reactBlur]; } - (void)didMoveToWindow { [self.backedTextInputView reactFocusIfNeeded]; } #pragma mark - Custom Input Accessory View - (void)didSetProps:(NSArray *)changedProps { [self invalidateInputAccessoryView]; } - (void)invalidateInputAccessoryView { #if !TARGET_OS_TV UIView *textInputView = self.backedTextInputView; UIKeyboardType keyboardType = textInputView.keyboardType; // These keyboard types (all are number pads) don't have a "Done" button by default, // so we create an `inputAccessoryView` with this button for them. BOOL shouldHaveInputAccesoryView = ( keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad || keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad ) && textInputView.returnKeyType == UIReturnKeyDone; if (_hasInputAccesoryView == shouldHaveInputAccesoryView) { return; } _hasInputAccesoryView = shouldHaveInputAccesoryView; if (shouldHaveInputAccesoryView) { UIToolbar *toolbarView = [[UIToolbar alloc] init]; [toolbarView sizeToFit]; UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(handleInputAccessoryDoneButton)]; toolbarView.items = @[flexibleSpace, doneButton]; textInputView.inputAccessoryView = toolbarView; } else { textInputView.inputAccessoryView = nil; } // We have to call `reloadInputViews` for focused text inputs to update an accessory view. if (textInputView.isFirstResponder) { [textInputView reloadInputViews]; } #endif } - (void)handleInputAccessoryDoneButton { if ([self textInputShouldReturn]) { [self.backedTextInputView endEditing:YES]; } } @end