This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,16 @@
/*
* 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 "RCTBaseTextInputView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTMultilineTextInputView : RCTBaseTextInputView
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,76 @@
/*
* 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 <React/RCTMultilineTextInputView.h>
#import <React/RCTUtils.h>
#import <React/RCTUITextView.h>
@implementation RCTMultilineTextInputView
{
RCTUITextView *_backedTextInputView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `false` for <TextInput multiline={true}> by design.
self.blurOnSubmit = NO;
_backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
[self addSubview:_backedTextInputView];
}
return self;
}
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
return _backedTextInputView;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
RCTDirectEventBlock onScroll = self.onScroll;
if (onScroll) {
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
CGSize size = scrollView.bounds.size;
UIEdgeInsets contentInset = scrollView.contentInset;
onScroll(@{
@"contentOffset": @{
@"x": @(contentOffset.x),
@"y": @(contentOffset.y)
},
@"contentInset": @{
@"top": @(contentInset.top),
@"left": @(contentInset.left),
@"bottom": @(contentInset.bottom),
@"right": @(contentInset.right)
},
@"contentSize": @{
@"width": @(contentSize.width),
@"height": @(contentSize.height)
},
@"layoutMeasurement": @{
@"width": @(size.width),
@"height": @(size.height)
},
@"zoomScale": @(scrollView.zoomScale ?: 1),
});
}
}
@end

View File

@ -0,0 +1,16 @@
/*
* 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 "RCTBaseTextInputViewManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTMultilineTextInputViewManager : RCTBaseTextInputViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,26 @@
/*
* 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 <React/RCTMultilineTextInputViewManager.h>
#import <React/RCTMultilineTextInputView.h>
@implementation RCTMultilineTextInputViewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge];
}
#pragma mark - Multiline <TextInput> (aka TextView) specific properties
#if !TARGET_OS_TV
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes)
#endif
@end

View File

@ -0,0 +1,42 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTBackedTextInputDelegate.h>
NS_ASSUME_NONNULL_BEGIN
/*
* Just regular UITextView... but much better!
*/
@interface RCTUITextView : UITextView <RCTBackedTextInputViewProtocol>
- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth;
// The `clearButtonMode` property actually is not supported yet;
// it's declared here only to conform to the interface.
@property (nonatomic, assign) UITextFieldViewMode clearButtonMode;
// The `caretHidden` property actually is not supported yet;
// it's declared here only to conform to the interface.
@property (nonatomic, assign) BOOL caretHidden;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,309 @@
/*
* 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 <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTBackedTextInputDelegateAdapter.h>
#import <React/RCTTextAttributes.h>
@implementation RCTUITextView
{
UILabel *_placeholderView;
UITextView *_detachedTextView;
RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
}
static UIFont *defaultPlaceholderFont()
{
return [UIFont systemFontOfSize:17];
}
static UIColor *defaultPlaceholderColor()
{
// Default placeholder color from UITextField.
return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(textDidChange)
name:UITextViewTextDidChangeNotification
object:self];
_placeholderView = [[UILabel alloc] initWithFrame:self.bounds];
_placeholderView.isAccessibilityElement = NO;
_placeholderView.numberOfLines = 0;
[self addSubview:_placeholderView];
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
self.backgroundColor = [UIColor clearColor];
self.textColor = [UIColor blackColor];
// This line actually removes 5pt (default value) left and right padding in UITextView.
self.textContainer.lineFragmentPadding = 0;
#if !TARGET_OS_TV
self.scrollsToTop = NO;
#endif
self.scrollEnabled = YES;
}
return self;
}
#pragma mark - Accessibility
- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
{
// UITextView is accessible by default (some nested views are) and disabling that is not supported.
// On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view
// (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component.
}
- (NSString *)accessibilityLabel
{
NSMutableString *accessibilityLabel = [NSMutableString new];
NSString *superAccessibilityLabel = [super accessibilityLabel];
if (superAccessibilityLabel.length > 0) {
[accessibilityLabel appendString:superAccessibilityLabel];
}
if (self.placeholder.length > 0 && self.attributedText.string.length == 0) {
if (accessibilityLabel.length > 0) {
[accessibilityLabel appendString:@" "];
}
[accessibilityLabel appendString:self.placeholder];
}
return accessibilityLabel;
}
#pragma mark - Properties
- (void)setPlaceholder:(NSString *)placeholder
{
_placeholder = placeholder;
[self _updatePlaceholder];
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
_placeholderColor = placeholderColor;
[self _updatePlaceholder];
}
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
return;
}
_defaultTextAttributes = defaultTextAttributes;
self.typingAttributes = defaultTextAttributes;
[self _updatePlaceholder];
}
- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
return _defaultTextAttributes;
}
- (void)textDidChange
{
_textWasPasted = NO;
[self _invalidatePlaceholderVisibility];
}
#pragma mark - Overrides
- (void)setFont:(UIFont *)font
{
[super setFont:font];
[self _updatePlaceholder];
}
- (void)setTextAlignment:(NSTextAlignment)textAlignment
{
[super setTextAlignment:textAlignment];
_placeholderView.textAlignment = textAlignment;
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
// Using `setAttributedString:` while user is typing breaks some internal mechanics
// when entering complex input languages such as Chinese, Korean or Japanese.
// see: https://github.com/facebook/react-native/issues/19339
// We try to avoid calling this method as much as we can.
// If the text has changed, there is nothing we can do.
if (![super.attributedText.string isEqualToString:attributedText.string]) {
[super setAttributedText:attributedText];
} else {
// But if the text is preserved, we just copying the attributes from the source string.
if (![super.attributedText isEqualToAttributedString:attributedText]) {
[self copyTextAttributesFrom:attributedText];
}
}
[self textDidChange];
}
#pragma mark - Overrides
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
{
if (!notifyDelegate) {
// We have to notify an adapter that following selection change was initiated programmatically,
// so the adapter must not generate a notification for it.
[_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
}
[super setSelectedTextRange:selectedTextRange];
}
- (void)paste:(id)sender
{
[super paste:sender];
_textWasPasted = YES;
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated
{
// Turning off scroll animation.
// This fixes the problem also known as "flaky scrolling".
[super setContentOffset:contentOffset animated:NO];
}
- (void)selectAll:(id)sender
{
[super selectAll:sender];
// `selectAll:` does not work for UITextView when it's being called inside UITextView's delegate methods.
dispatch_async(dispatch_get_main_queue(), ^{
UITextRange *selectionRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument];
[self setSelectedTextRange:selectionRange notifyDelegate:NO];
});
}
#pragma mark - Layout
- (CGFloat)preferredMaxLayoutWidth
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return _preferredMaxLayoutWidth ?: self.placeholderSize.width;
}
- (CGSize)placeholderSize
{
UIEdgeInsets textContainerInset = self.textContainerInset;
NSString *placeholder = self.placeholder ?: @"";
CGSize maxPlaceholderSize = CGSizeMake(UIEdgeInsetsInsetRect(self.bounds, textContainerInset).size.width, CGFLOAT_MAX);
CGSize placeholderSize = [placeholder boundingRectWithSize:maxPlaceholderSize options:NSStringDrawingUsesLineFragmentOrigin attributes:[self _placeholderTextAttributes] context:nil].size;
placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height));
placeholderSize.width += textContainerInset.left + textContainerInset.right;
placeholderSize.height += textContainerInset.top + textContainerInset.bottom;
// Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does).
return placeholderSize;
}
- (CGSize)contentSize
{
CGSize contentSize = super.contentSize;
CGSize placeholderSize = _placeholderView.isHidden ? CGSizeZero : self.placeholderSize;
// When a text input is empty, it actually displays a placehoder.
// So, we have to consider `placeholderSize` as a minimum `contentSize`.
// Returning size DOES contain `textContainerInset` (aka `padding`).
return CGSizeMake(
MAX(contentSize.width, placeholderSize.width),
MAX(contentSize.height, placeholderSize.height));
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset);
CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height;
textFrame.size.height = MIN(placeholderHeight, textFrame.size.height);
_placeholderView.frame = textFrame;
}
- (CGSize)intrinsicContentSize
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return [self sizeThatFits:CGSizeMake(self.preferredMaxLayoutWidth, CGFLOAT_MAX)];
}
- (CGSize)sizeThatFits:(CGSize)size
{
// Returned fitting size depends on text size and placeholder size.
CGSize textSize = [super sizeThatFits:size];
CGSize placeholderSize = self.placeholderSize;
// Returning size DOES contain `textContainerInset` (aka `padding`).
return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height));
}
#pragma mark - Context Menu
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_contextMenuHidden) {
return NO;
}
return [super canPerformAction:action withSender:sender];
}
#pragma mark - Placeholder
- (void)_invalidatePlaceholderVisibility
{
BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0;
_placeholderView.hidden = !isVisible;
}
- (void)_updatePlaceholder
{
_placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder ?: @"" attributes:[self _placeholderTextAttributes]];
}
- (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes
{
NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];
[textAttributes setValue:self.placeholderColor ?: defaultPlaceholderColor() forKey:NSForegroundColorAttributeName];
if (![textAttributes objectForKey:NSFontAttributeName]) {
[textAttributes setValue:defaultPlaceholderFont() forKey:NSFontAttributeName];
}
return textAttributes;
}
#pragma mark - Utility Methods
- (void)copyTextAttributesFrom:(NSAttributedString *)sourceString
{
[self.textStorage beginEditing];
NSTextStorage *textStorage = self.textStorage;
[sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length)
options:NSAttributedStringEnumerationReverse
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
[textStorage setAttributes:attrs range:range];
}];
[self.textStorage endEditing];
}
@end

View File

@ -0,0 +1,42 @@
/*
* 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 <UIKit/UIKit.h>
@protocol RCTBackedTextInputViewProtocol;
NS_ASSUME_NONNULL_BEGIN
@protocol RCTBackedTextInputDelegate <NSObject>
- (BOOL)textInputShouldBeginEditing; // Return `NO` to disallow editing.
- (void)textInputDidBeginEditing;
- (BOOL)textInputShouldEndEditing; // Return `YES` to allow editing to stop and to resign first responder status. `NO` to disallow the editing session to end.
- (void)textInputDidEndEditing; // May be called if forced even if `textInputShouldEndEditing` returns `NO` (e.g. view removed from window) or `[textInput endEditing:YES]` called.
- (BOOL)textInputShouldReturn; // May be called right before `textInputShouldEndEditing` if "Return" button was pressed.
- (void)textInputDidReturn;
/*
* Called before any change in the TextInput. The delegate has the opportunity to change the replacement string or reject the change completely.
* To change the replacement, return the changed version of the `text`.
* To accept the change, return `text` argument as-is.
* To reject the change, return `nil`.
*/
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range;
- (void)textInputDidChange;
- (void)textInputDidChangeSelection;
@optional
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,36 @@
/*
* 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 <UIKit/UIKit.h>
#import "RCTBackedTextInputViewProtocol.h"
#import "RCTBackedTextInputDelegate.h"
NS_ASSUME_NONNULL_BEGIN
#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField)
@interface RCTBackedTextFieldDelegateAdapter : NSObject
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)backedTextInputView;
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange;
- (void)selectedTextRangeWasSet;
@end
#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView)
@interface RCTBackedTextViewDelegateAdapter : NSObject
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)backedTextInputView;
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,286 @@
/*
* 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 <React/RCTBackedTextInputDelegateAdapter.h>
#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField)
static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext;
@interface RCTBackedTextFieldDelegateAdapter () <UITextFieldDelegate>
@end
@implementation RCTBackedTextFieldDelegateAdapter {
__weak UITextField<RCTBackedTextInputViewProtocol> *_backedTextInputView;
BOOL _textDidChangeIsComing;
UITextRange *_previousSelectedTextRange;
}
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
if (self = [super init]) {
_backedTextInputView = backedTextInputView;
backedTextInputView.delegate = self;
[_backedTextInputView addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[_backedTextInputView addTarget:self action:@selector(textFieldDidEndEditingOnExit) forControlEvents:UIControlEventEditingDidEndOnExit];
}
return self;
}
- (void)dealloc
{
[_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingChanged];
[_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingDidEndOnExit];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldBeginEditing:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldBeginEditing];
}
- (void)textFieldDidBeginEditing:(__unused UITextField *)textField
{
[_backedTextInputView.textInputDelegate textInputDidBeginEditing];
}
- (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldEndEditing];
}
- (void)textFieldDidEndEditing:(__unused UITextField *)textField
{
if (_textDidChangeIsComing) {
// iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection
// which was triggered by losing focus. So, we call it manually.
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
[_backedTextInputView.textInputDelegate textInputDidEndEditing];
}
- (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *newText =
[_backedTextInputView.textInputDelegate textInputShouldChangeText:string inRange:range];
if (newText == nil) {
return NO;
}
if ([newText isEqualToString:string]) {
_textDidChangeIsComing = YES;
return YES;
}
NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy];
[attributedString replaceCharactersInRange:range withString:newText];
[_backedTextInputView setAttributedText:[attributedString copy]];
// Setting selection to the end of the replaced text.
UITextPosition *position =
[_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:(range.location + newText.length)];
[_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
[self textFieldDidChange];
return NO;
}
- (BOOL)textFieldShouldReturn:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldReturn];
}
#pragma mark - UIControlEventEditing* Family Events
- (void)textFieldDidChange
{
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
// `selectedTextRangeWasSet` isn't triggered during typing.
[self textFieldProbablyDidChangeSelection];
}
- (void)textFieldDidEndEditingOnExit
{
[_backedTextInputView.textInputDelegate textInputDidReturn];
}
#pragma mark - UIKeyboardInput (private UIKit protocol)
// This method allows us to detect a [Backspace] `keyPress`
// even when there is no more text in the `UITextField`.
- (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField
{
[_backedTextInputView.textInputDelegate textInputShouldChangeText:@"" inRange:NSMakeRange(0, 0)];
return YES;
}
#pragma mark - Public Interface
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange
{
_previousSelectedTextRange = textRange;
}
- (void)selectedTextRangeWasSet
{
[self textFieldProbablyDidChangeSelection];
}
#pragma mark - Generalization
- (void)textFieldProbablyDidChangeSelection
{
if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) {
return;
}
_previousSelectedTextRange = _backedTextInputView.selectedTextRange;
[_backedTextInputView.textInputDelegate textInputDidChangeSelection];
}
@end
#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView)
@interface RCTBackedTextViewDelegateAdapter () <UITextViewDelegate>
@end
@implementation RCTBackedTextViewDelegateAdapter {
__weak UITextView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
BOOL _textDidChangeIsComing;
UITextRange *_previousSelectedTextRange;
}
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
if (self = [super init]) {
_backedTextInputView = backedTextInputView;
backedTextInputView.delegate = self;
}
return self;
}
#pragma mark - UITextViewDelegate
- (BOOL)textViewShouldBeginEditing:(__unused UITextView *)textView
{
return [_backedTextInputView.textInputDelegate textInputShouldBeginEditing];
}
- (void)textViewDidBeginEditing:(__unused UITextView *)textView
{
[_backedTextInputView.textInputDelegate textInputDidBeginEditing];
}
- (BOOL)textViewShouldEndEditing:(__unused UITextView *)textView
{
return [_backedTextInputView.textInputDelegate textInputShouldEndEditing];
}
- (void)textViewDidEndEditing:(__unused UITextView *)textView
{
if (_textDidChangeIsComing) {
// iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection
// which was triggered by losing focus. So, we call it manually.
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
[_backedTextInputView.textInputDelegate textInputDidEndEditing];
}
- (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// Custom implementation of `textInputShouldReturn` and `textInputDidReturn` pair for `UITextView`.
if (!_backedTextInputView.textWasPasted && [text isEqualToString:@"\n"]) {
if ([_backedTextInputView.textInputDelegate textInputShouldReturn]) {
[_backedTextInputView.textInputDelegate textInputDidReturn];
[_backedTextInputView endEditing:NO];
return NO;
}
}
NSString *newText =
[_backedTextInputView.textInputDelegate textInputShouldChangeText:text inRange:range];
if (newText == nil) {
return NO;
}
if ([newText isEqualToString:text]) {
_textDidChangeIsComing = YES;
return YES;
}
NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy];
[attributedString replaceCharactersInRange:range withString:newText];
[_backedTextInputView setAttributedText:[attributedString copy]];
// Setting selection to the end of the replaced text.
UITextPosition *position =
[_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:(range.location + newText.length)];
[_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
[self textViewDidChange:_backedTextInputView];
return NO;
}
- (void)textViewDidChange:(__unused UITextView *)textView
{
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
[self textViewProbablyDidChangeSelection];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ([_backedTextInputView.textInputDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[_backedTextInputView.textInputDelegate scrollViewDidScroll:scrollView];
}
}
#pragma mark - Public Interface
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange
{
_previousSelectedTextRange = textRange;
}
#pragma mark - Generalization
- (void)textViewProbablyDidChangeSelection
{
if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) {
return;
}
_previousSelectedTextRange = _backedTextInputView.selectedTextRange;
[_backedTextInputView.textInputDelegate textInputDidChangeSelection];
}
@end

View File

@ -0,0 +1,48 @@
/*
* 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 <UIKit/UIKit.h>
@protocol RCTBackedTextInputDelegate;
@class RCTTextAttributes;
NS_ASSUME_NONNULL_BEGIN
@protocol RCTBackedTextInputViewProtocol <UITextInput>
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, strong, nullable) UIView *inputAccessoryView;
@property (nonatomic, weak, nullable) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, readonly) CGSize contentSize;
@property (nonatomic, strong, nullable) NSDictionary<NSAttributedStringKey,id> *defaultTextAttributes;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL enablesReturnKeyAutomatically;
@property (nonatomic, assign) UITextFieldViewMode clearButtonMode;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
// This protocol disallows direct access to `selectedTextRange` property because
// unwise usage of it can break the `delegate` behavior. So, we always have to
// explicitly specify should `delegate` be notified about the change or not.
// If the change was initiated programmatically, we must NOT notify the delegate.
// If the change was a result of user actions (like typing or touches), we MUST notify the delegate.
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE;
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate;
// This protocol disallows direct access to `text` property because
// unwise usage of it can break the `attributeText` behavior.
// Use `attributedText.string` instead.
@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,25 @@
/*
* 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 "RCTBaseTextShadowView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTBaseTextInputShadowView : RCTBaseTextShadowView
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@property (nonatomic, copy, nullable) NSString *text;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, assign) NSInteger maximumNumberOfLines;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
- (void)uiManagerWillPerformMounting;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,326 @@
/*
* 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 <React/RCTBaseTextInputShadowView.h>
#import <React/RCTBridge.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTUIManager.h>
#import <yoga/Yoga.h>
#import "NSTextStorage+FontScaling.h"
#import <React/RCTBaseTextInputView.h>
@implementation RCTBaseTextInputShadowView
{
__weak RCTBridge *_bridge;
NSAttributedString *_Nullable _previousAttributedText;
BOOL _needsUpdateView;
NSAttributedString *_Nullable _localAttributedText;
CGSize _previousContentSize;
NSString *_text;
NSTextStorage *_textStorage;
NSTextContainer *_textContainer;
NSLayoutManager *_layoutManager;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
_needsUpdateView = YES;
YGNodeSetMeasureFunc(self.yogaNode, RCTBaseTextInputShadowViewMeasure);
YGNodeSetBaselineFunc(self.yogaNode, RCTTextInputShadowViewBaseline);
}
return self;
}
- (BOOL)isYogaLeafNode
{
return YES;
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
// `backgroundColor` and `opacity` are being applied directly to a UIView,
// therefore we need to exclude them from base `textAttributes`.
self.textAttributes.backgroundColor = nil;
self.textAttributes.opacity = NAN;
}
- (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext
{
// Do nothing.
}
- (void)setLocalData:(NSObject *)localData
{
NSAttributedString *attributedText = (NSAttributedString *)localData;
if ([attributedText isEqualToAttributedString:_localAttributedText]) {
return;
}
_localAttributedText = attributedText;
[self dirtyLayout];
}
- (void)dirtyLayout
{
[super dirtyLayout];
_needsUpdateView = YES;
YGNodeMarkDirty(self.yogaNode);
[self invalidateContentSize];
}
- (void)invalidateContentSize
{
if (!_onContentSizeChange) {
return;
}
CGSize maximumSize = self.layoutMetrics.frame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
} else {
maximumSize.height = CGFLOAT_MAX;
}
CGSize contentSize = [self sizeThatFitsMinimumSize:(CGSize)CGSizeZero maximumSize:maximumSize];
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
return;
}
_previousContentSize = contentSize;
_onContentSizeChange(@{
@"contentSize": @{
@"height": @(contentSize.height),
@"width": @(contentSize.width),
},
@"target": self.reactTag,
});
}
- (NSString *)text
{
return _text;
}
- (void)setText:(NSString *)text
{
_text = text;
// Clear `_previousAttributedText` to notify the view about the change
// when `text` native prop is set.
_previousAttributedText = nil;
[self dirtyLayout];
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting
{
if (YGNodeIsDirty(self.yogaNode)) {
return;
}
if (!_needsUpdateView) {
return;
}
_needsUpdateView = NO;
UIEdgeInsets borderInsets = self.borderAsInsets;
UIEdgeInsets paddingInsets = self.paddingAsInsets;
RCTTextAttributes *textAttributes = [self.textAttributes copy];
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]];
// Removing all references to Shadow Views and tags to avoid unnecessary retaining
// and problems with comparing the strings.
[attributedText removeAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
range:NSMakeRange(0, attributedText.length)];
[attributedText removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, attributedText.length)];
if (self.text.length) {
NSAttributedString *propertyAttributedText =
[[NSAttributedString alloc] initWithString:self.text
attributes:self.textAttributes.effectiveTextAttributes];
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
BOOL isAttributedTextChanged = NO;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
// If the value has not changed, we must not notify the view about the change,
// otherwise we may break local (temporary) state of the text input.
isAttributedTextChanged = YES;
_previousAttributedText = [attributedText copy];
}
NSNumber *tag = self.reactTag;
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTBaseTextInputView *baseTextInputView = (RCTBaseTextInputView *)viewRegistry[tag];
if (!baseTextInputView) {
return;
}
baseTextInputView.textAttributes = textAttributes;
baseTextInputView.reactBorderInsets = borderInsets;
baseTextInputView.reactPaddingInsets = paddingInsets;
if (isAttributedTextChanged) {
// Don't set `attributedText` if length equal to zero, otherwise it would shrink when attributes contain like `lineHeight`.
if (attributedText.length != 0) {
baseTextInputView.attributedText = attributedText;
} else {
baseTextInputView.attributedText = nil;
}
}
}];
}
#pragma mark -
- (NSAttributedString *)measurableAttributedText
{
// Only for the very first render when we don't have `_localAttributedText`,
// we use value directly from the property and/or nested content.
NSAttributedString *attributedText =
_localAttributedText ?: [self attributedTextWithBaseTextAttributes:nil];
if (attributedText.length == 0) {
// It's impossible to measure empty attributed string because all attributes are
// associated with some characters, so no characters means no data.
// Placeholder also can represent the intrinsic size when it is visible.
NSString *text = self.placeholder;
if (!text.length) {
// Note: `zero-width space` is insufficient in some cases.
text = @"I";
}
attributedText = [[NSAttributedString alloc] initWithString:text attributes:self.textAttributes.effectiveTextAttributes];
}
return attributedText;
}
- (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize
{
NSAttributedString *attributedText = [self measurableAttributedText];
if (!_textStorage) {
_textContainer = [NSTextContainer new];
_textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
_layoutManager = [NSLayoutManager new];
[_layoutManager addTextContainer:_textContainer];
_textStorage = [NSTextStorage new];
[_textStorage addLayoutManager:_layoutManager];
}
_textContainer.size = maximumSize;
_textContainer.maximumNumberOfLines = _maximumNumberOfLines;
[_textStorage replaceCharactersInRange:(NSRange){0, _textStorage.length}
withAttributedString:attributedText];
[_layoutManager ensureLayoutForTextContainer:_textContainer];
CGSize size = [_layoutManager usedRectForTextContainer:_textContainer].size;
return (CGSize){
MAX(minimumSize.width, MIN(RCTCeilPixelValue(size.width), maximumSize.width)),
MAX(minimumSize.height, MIN(RCTCeilPixelValue(size.height), maximumSize.height))
};
}
- (CGFloat)lastBaselineForSize:(CGSize)size
{
NSAttributedString *attributedText = [self measurableAttributedText];
__block CGFloat maximumDescender = 0.0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (maximumDescender > font.descender) {
maximumDescender = font.descender;
}
}
];
return size.height + maximumDescender;
}
static YGSize RCTBaseTextInputShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
RCTShadowView *shadowView = (__bridge RCTShadowView *)YGNodeGetContext(node);
CGSize minimumSize = CGSizeMake(0, 0);
CGSize maximumSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
CGSize size = {
RCTCoreGraphicsFloatFromYogaFloat(width),
RCTCoreGraphicsFloatFromYogaFloat(height)
};
switch (widthMode) {
case YGMeasureModeUndefined:
break;
case YGMeasureModeExactly:
minimumSize.width = size.width;
maximumSize.width = size.width;
break;
case YGMeasureModeAtMost:
maximumSize.width = size.width;
break;
}
switch (heightMode) {
case YGMeasureModeUndefined:
break;
case YGMeasureModeExactly:
minimumSize.height = size.height;
maximumSize.height = size.height;
break;
case YGMeasureModeAtMost:
maximumSize.height = size.height;
break;
}
CGSize measuredSize = [shadowView sizeThatFitsMinimumSize:minimumSize maximumSize:maximumSize];
return (YGSize){
RCTYogaFloatFromCoreGraphicsFloat(measuredSize.width),
RCTYogaFloatFromCoreGraphicsFloat(measuredSize.height)
};
}
static float RCTTextInputShadowViewBaseline(YGNodeRef node, const float width, const float height)
{
RCTBaseTextInputShadowView *shadowTextView = (__bridge RCTBaseTextInputShadowView *)YGNodeGetContext(node);
CGSize size = (CGSize){
RCTCoreGraphicsFloatFromYogaFloat(width),
RCTCoreGraphicsFloatFromYogaFloat(height)
};
CGFloat lastBaseline = [shadowTextView lastBaselineForSize:size];
return RCTYogaFloatFromCoreGraphicsFloat(lastBaseline);
}
@end

View File

@ -0,0 +1,63 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTView.h>
#import "RCTBackedTextInputDelegate.h"
#import "RCTBackedTextInputViewProtocol.h"
@class RCTBridge;
@class RCTEventDispatcher;
@class RCTTextAttributes;
@class RCTTextSelection;
NS_ASSUME_NONNULL_BEGIN
@interface RCTBaseTextInputView : RCTView <RCTBackedTextInputDelegate>
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
@property (nonatomic, readonly) UIView<RCTBackedTextInputViewProtocol> *backedTextInputView;
@property (nonatomic, strong, nullable) RCTTextAttributes *textAttributes;
@property (nonatomic, assign) UIEdgeInsets reactPaddingInsets;
@property (nonatomic, assign) UIEdgeInsets reactBorderInsets;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onSelectionChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll;
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, assign, readonly) NSInteger nativeEventCount;
@property (nonatomic, assign) BOOL autoFocus;
@property (nonatomic, assign) BOOL blurOnSubmit;
@property (nonatomic, assign) BOOL selectTextOnFocus;
@property (nonatomic, assign) BOOL clearTextOnFocus;
@property (nonatomic, assign) BOOL secureTextEntry;
@property (nonatomic, copy) RCTTextSelection *selection;
@property (nonatomic, strong, nullable) NSNumber *maxLength;
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, copy) NSString *inputAccessoryViewID;
@property (nonatomic, assign) UIKeyboardType keyboardType;
/**
Sets selection intext input if both start and end are within range of the text input.
**/
- (void)setSelectionStart:(NSInteger)start
selectionEnd:(NSInteger)end;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,680 @@
/*
* 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 <React/RCTBaseTextInputView.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 <React/RCTInputAccessoryView.h>
#import <React/RCTInputAccessoryViewContent.h>
#import <React/RCTTextAttributes.h>
#import <React/RCTTextSelection.h>
@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
__weak RCTEventDispatcher *_eventDispatcher;
BOOL _hasInputAccesoryView;
NSString *_Nullable _predictedText;
BOOL _didMoveToWindow;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
if (self = [super initWithFrame:CGRectZero]) {
_bridge = bridge;
_eventDispatcher = bridge.eventDispatcher;
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
- (UIView<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
RCTAssert(NO, @"-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass.");
return nil;
}
#pragma mark - RCTComponent
- (void)didUpdateReactSubviews
{
// Do nothing.
}
#pragma mark - Properties
- (void)setTextAttributes:(RCTTextAttributes *)textAttributes
{
_textAttributes = textAttributes;
[self enforceTextAttributesIfNeeded];
}
- (void)enforceTextAttributesIfNeeded
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
NSDictionary<NSAttributedStringKey,id> *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy];
if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) {
[textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName];
}
backedTextInputView.defaultTextAttributes = textAttributes;
}
- (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];
}
- (NSAttributedString *)attributedText
{
return self.backedTextInputView.attributedText;
}
- (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{
// When the dictation is running we can't update the attributed text on the backed up text view
// because setting the attributed string will kill the dictation. This means that we can't impose
// the settings on a dictation.
// Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
// text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc
// for more info.
// If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in NSOriginalFont.
// Lastly, when entering a password, etc., there will be additional styling on the field as the native text view
// handles showing the last character for a split second.
__block BOOL fontHasBeenUpdatedBySystem = false;
[oldText enumerateAttribute:@"NSOriginalFont" inRange:NSMakeRange(0, oldText.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value){
fontHasBeenUpdatedBySystem = true;
}
}];
BOOL shouldFallbackToBareTextComparison =
[self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
self.backedTextInputView.markedTextRange ||
self.backedTextInputView.isSecureTextEntry ||
fontHasBeenUpdatedBySystem;
if (shouldFallbackToBareTextComparison) {
return ([newText.string isEqualToString:oldText.string]);
} else {
return ([newText isEqualToAttributedString:oldText]);
}
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
BOOL textNeedsUpdate = NO;
// Remove tag attribute to ensure correct attributed string comparison.
NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
[backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
[attributedTextCopy removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, attributedTextCopy.length)];
textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO);
if (eventLag == 0 && textNeedsUpdate) {
UITextRange *selection = self.backedTextInputView.selectedTextRange;
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
self.backedTextInputView.attributedText = attributedText;
if (selection.empty) {
// Maintaining a cursor position relative to the end of the old text.
NSInteger offsetStart =
[self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument
toPosition:selection.start];
NSInteger offsetFromEnd = oldTextLength - offsetStart;
NSInteger newOffset = attributedText.string.length - offsetFromEnd;
UITextPosition *position =
[self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:newOffset];
[self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
}
[self updateLocalData];
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", self.backedTextInputView.attributedText.string, (long long)eventLag);
}
}
- (RCTTextSelection *)selection
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
UITextRange *selectedTextRange = backedTextInputView.selectedTextRange;
return [[RCTTextSelection new] initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start]
end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]];
}
- (void)setSelection:(RCTTextSelection *)selection
{
if (!selection) {
return;
}
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange;
UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.start];
UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.end];
UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end];
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) {
[backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO];
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", backedTextInputView.attributedText.string, (long long)eventLag);
}
}
- (void)setSelectionStart:(NSInteger)start
selectionEnd:(NSInteger)end
{
UITextPosition *startPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:start];
UITextPosition *endPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:end];
if (startPosition && endPosition) {
UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
[self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
}
}
- (void)setTextContentType:(NSString *)type
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED)
static dispatch_once_t onceToken;
static NSDictionary<NSString *, NSString *> *contentTypeMap;
dispatch_once(&onceToken, ^{
contentTypeMap = @{@"none": @"",
@"URL": UITextContentTypeURL,
@"addressCity": UITextContentTypeAddressCity,
@"addressCityAndState":UITextContentTypeAddressCityAndState,
@"addressState": UITextContentTypeAddressState,
@"countryName": UITextContentTypeCountryName,
@"creditCardNumber": UITextContentTypeCreditCardNumber,
@"emailAddress": UITextContentTypeEmailAddress,
@"familyName": UITextContentTypeFamilyName,
@"fullStreetAddress": UITextContentTypeFullStreetAddress,
@"givenName": UITextContentTypeGivenName,
@"jobTitle": UITextContentTypeJobTitle,
@"location": UITextContentTypeLocation,
@"middleName": UITextContentTypeMiddleName,
@"name": UITextContentTypeName,
@"namePrefix": UITextContentTypeNamePrefix,
@"nameSuffix": UITextContentTypeNameSuffix,
@"nickname": UITextContentTypeNickname,
@"organizationName": UITextContentTypeOrganizationName,
@"postalCode": UITextContentTypePostalCode,
@"streetAddressLine1": UITextContentTypeStreetAddressLine1,
@"streetAddressLine2": UITextContentTypeStreetAddressLine2,
@"sublocality": UITextContentTypeSublocality,
@"telephoneNumber": UITextContentTypeTelephoneNumber,
};
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
if (@available(iOS 11.0, tvOS 11.0, *)) {
NSDictionary<NSString *, NSString *> * iOS11extras = @{@"username": UITextContentTypeUsername,
@"password": UITextContentTypePassword};
NSMutableDictionary<NSString *, NSString *> * iOS11baseMap = [contentTypeMap mutableCopy];
[iOS11baseMap addEntriesFromDictionary:iOS11extras];
contentTypeMap = [iOS11baseMap copy];
}
#endif
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */
if (@available(iOS 12.0, tvOS 12.0, *)) {
NSDictionary<NSString *, NSString *> * iOS12extras = @{@"newPassword": UITextContentTypeNewPassword,
@"oneTimeCode": UITextContentTypeOneTimeCode};
NSMutableDictionary<NSString *, NSString *> * iOS12baseMap = [contentTypeMap mutableCopy];
[iOS12baseMap addEntriesFromDictionary:iOS12extras];
contentTypeMap = [iOS12baseMap copy];
}
#endif
});
// Setting textContentType to an empty string will disable any
// default behaviour, like the autofill bar for password inputs
self.backedTextInputView.textContentType = contentTypeMap[type] ?: type;
#endif
}
- (void)setPasswordRules:(NSString *)descriptor
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0
if (@available(iOS 12.0, *)) {
self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor];
}
#endif
}
- (UIKeyboardType)keyboardType
{
return self.backedTextInputView.keyboardType;
}
- (void)setKeyboardType:(UIKeyboardType)keyboardType
{
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
if (textInputView.keyboardType != keyboardType) {
textInputView.keyboardType = keyboardType;
// Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) loses and regains focus.
if (textInputView.isFirstResponder) {
[textInputView reloadInputViews];
}
}
}
#pragma mark - RCTBackedTextInputDelegate
- (BOOL)textInputShouldBeginEditing
{
return YES;
}
- (void)textInputDidBeginEditing
{
if (_clearTextOnFocus) {
self.backedTextInputView.attributedText = [NSAttributedString new];
}
if (_selectTextOnFocus) {
[self.backedTextInputView selectAll:nil];
}
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
}
- (BOOL)textInputShouldEndEditing
{
return YES;
}
- (void)textInputDidEndEditing
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
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.attributedText.string
key:nil
eventCount:_nativeEventCount];
return _blurOnSubmit;
}
- (void)textInputDidReturn
{
// Does nothing.
}
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
if (!backedTextInputView.textWasPasted) {
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
reactTag:self.reactTag
text:nil
key:text
eventCount:_nativeEventCount];
}
if (_maxLength) {
NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 0);
if (text.length > allowedLength) {
// If we typed/pasted more than one character, limit the text inputted.
if (text.length > 1) {
// Truncate the input string so the result is exactly maxLength
NSString *limitedString = [text substringToIndex:allowedLength];
NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy];
// Apply text attributes if original input view doesn't have text.
if (backedTextInputView.attributedText.length == 0) {
newAttributedText = [[NSMutableAttributedString alloc] initWithString:[self.textAttributes applyTextAttributesToText:limitedString] attributes:self.textAttributes.effectiveTextAttributes];
} else {
[newAttributedText replaceCharactersInRange:range withString:limitedString];
}
backedTextInputView.attributedText = newAttributedText;
_predictedText = newAttributedText.string;
// Collapse selection at end of insert to match normal paste behavior.
UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument
offset:(range.location + allowedLength)];
[backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd]
notifyDelegate:YES];
[self textInputDidChange];
}
return nil; // Rejecting the change.
}
}
NSString *previousText = backedTextInputView.attributedText.string ?: @"";
if (range.location + range.length > backedTextInputView.attributedText.string.length) {
_predictedText = backedTextInputView.attributedText.string;
} else {
_predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range withString:text];
}
if (_onTextInput) {
_onTextInput(@{
@"text": text,
@"previousText": previousText,
@"range": @{
@"start": @(range.location),
@"end": @(range.location + range.length)
},
@"eventCount": @(_nativeEventCount),
});
}
return text; // Accepting the change.
}
- (void)textInputDidChange
{
[self updateLocalData];
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
// Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange`
// (e.g. typing simplified Chinese in pinyin will insert and remove spaces without
// calling shouldChangeTextInRange). This will cause JS to get out of sync so we
// update the mismatched range.
NSRange currentRange;
NSRange predictionRange;
if (findMismatch(backedTextInputView.attributedText.string, _predictedText, &currentRange, &predictionRange)) {
NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange];
[self textInputShouldChangeText:replacement inRange:predictionRange];
// JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it.
[self textInputDidChangeSelection];
}
_nativeEventCount++;
if (_onChange) {
_onChange(@{
@"text": self.attributedText.string,
@"target": self.reactTag,
@"eventCount": @(_nativeEventCount),
});
}
}
- (void)textInputDidChangeSelection
{
if (!_onSelectionChange) {
return;
}
RCTTextSelection *selection = self.selection;
_onSelectionChange(@{
@"selection": @{
@"start": @(selection.start),
@"end": @(selection.end),
},
});
}
- (void)updateLocalData
{
[self enforceTextAttributesIfNeeded];
[_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy]
forView:self];
}
#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;
}
#pragma mark - Accessibility
- (UIView *)reactAccessibilityElement
{
return self.backedTextInputView;
}
#pragma mark - Focus Control
- (void)reactFocus
{
[self.backedTextInputView reactFocus];
}
- (void)reactBlur
{
[self.backedTextInputView reactBlur];
}
- (void)didMoveToWindow
{
if (self.autoFocus && !_didMoveToWindow) {
[self.backedTextInputView reactFocus];
} else {
[self.backedTextInputView reactFocusIfNeeded];
}
_didMoveToWindow = YES;
}
#pragma mark - Custom Input Accessory View
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
[self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
} else if (!self.inputAccessoryViewID) {
[self setDefaultInputAccessoryView];
}
}
- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
{
#if !TARGET_OS_TV
__weak RCTBaseTextInputView *weakSelf = self;
[_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
RCTBaseTextInputView *strongSelf = weakSelf;
if (rootView) {
UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
withRootTag:rootView.reactTag];
if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).inputAccessoryView;
[strongSelf reloadInputViewsIfNecessary];
}
}
}];
#endif /* !TARGET_OS_TV */
}
- (void)setDefaultInputAccessoryView
{
#if !TARGET_OS_TV
UIView<RCTBackedTextInputViewProtocol> *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;
if (@available(iOS 10.0, *)) {
shouldHaveInputAccesoryView =
(
keyboardType == UIKeyboardTypeNumberPad ||
keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad ||
keyboardType == UIKeyboardTypeASCIICapableNumberPad
) &&
textInputView.returnKeyType == UIReturnKeyDone;
} else {
shouldHaveInputAccesoryView =
(
keyboardType == UIKeyboardTypeNumberPad ||
keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad
) &&
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;
}
[self reloadInputViewsIfNecessary];
#endif /* !TARGET_OS_TV */
}
- (void)reloadInputViewsIfNecessary
{
// We have to call `reloadInputViews` for focused text inputs to update an accessory view.
if (self.backedTextInputView.isFirstResponder) {
[self.backedTextInputView reloadInputViews];
}
}
- (void)handleInputAccessoryDoneButton
{
if ([self textInputShouldReturn]) {
[self.backedTextInputView endEditing:YES];
}
}
#pragma mark - Helpers
static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange)
{
NSInteger firstMismatch = -1;
for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) {
if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) {
firstMismatch = ii;
break;
}
}
if (firstMismatch == -1) {
return NO;
}
NSUInteger ii = second.length;
NSUInteger lastMismatch = first.length;
while (ii > firstMismatch && lastMismatch > firstMismatch) {
if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) {
break;
}
ii--;
lastMismatch--;
}
*firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch);
*secondRange = NSMakeRange(firstMismatch, ii - firstMismatch);
return YES;
}
@end

View File

@ -0,0 +1,12 @@
/*
* 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 "RCTBaseTextViewManager.h"
@interface RCTBaseTextInputViewManager : RCTBaseTextViewManager
@end

View File

@ -0,0 +1,170 @@
/*
* 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 <React/RCTBaseTextInputViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTFont.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTShadowView.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTUIManagerObserverCoordinator.h>
#import <React/RCTBaseTextInputShadowView.h>
#import <React/RCTBaseTextInputView.h>
#import <React/RCTConvert+Text.h>
@interface RCTBaseTextInputViewManager () <RCTUIManagerObserver>
@end
@implementation RCTBaseTextInputViewManager
{
NSHashTable<RCTBaseTextInputShadowView *> *_shadowViews;
}
RCT_EXPORT_MODULE()
#pragma mark - Unified <TextInput> properties
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, backedTextInputView.autocapitalizationType, UITextAutocapitalizationType)
RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType)
RCT_REMAP_VIEW_PROPERTY(contextMenuHidden, backedTextInputView.contextMenuHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance)
RCT_REMAP_VIEW_PROPERTY(placeholder, backedTextInputView.placeholder, NSString)
RCT_REMAP_VIEW_PROPERTY(placeholderTextColor, backedTextInputView.placeholderColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(returnKeyType, backedTextInputView.returnKeyType, UIReturnKeyType)
RCT_REMAP_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType)
RCT_REMAP_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode)
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL)
RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL)
RCT_EXPORT_VIEW_PROPERTY(autoFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL)
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)
RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString)
RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
RCT_EXPORT_SHADOW_PROPERTY(text, NSString)
RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString)
RCT_EXPORT_SHADOW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(multiline, BOOL, UIView)
{
// No op.
// This View Manager doesn't use this prop but it must be exposed here via ViewConfig to enable Fabric component use it.
}
- (RCTShadowView *)shadowView
{
RCTBaseTextInputShadowView *shadowView = [[RCTBaseTextInputShadowView alloc] initWithBridge:self.bridge];
shadowView.textAttributes.fontSizeMultiplier = [[[self.bridge
moduleForName:@"AccessibilityManager"
lazilyLoadIfNecessary:YES] valueForKey:@"multiplier"] floatValue];
[_shadowViews addObject:shadowView];
return shadowView;
}
- (void)setBridge:(RCTBridge *)bridge
{
[super setBridge:bridge];
_shadowViews = [NSHashTable weakObjectsHashTable];
[bridge.uiManager.observerCoordinator addObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDidUpdateMultiplierNotification)
name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification"
object:[bridge moduleForName:@"AccessibilityManager"
lazilyLoadIfNecessary:YES]];
}
RCT_EXPORT_METHOD(focus : (nonnull NSNumber *)viewTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];
[view reactFocus];
}];
}
RCT_EXPORT_METHOD(blur : (nonnull NSNumber *)viewTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];
[view reactBlur];
}];
}
RCT_EXPORT_METHOD(setTextAndSelection : (nonnull NSNumber *)viewTag
mostRecentEventCount : (NSInteger)mostRecentEventCount
value : (NSString *)value
start : (NSInteger)start
end : (NSInteger)end)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTBaseTextInputView *view = (RCTBaseTextInputView *)viewRegistry[viewTag];
NSInteger eventLag = view.nativeEventCount - mostRecentEventCount;
if (eventLag != 0) {
return;
}
RCTExecuteOnUIManagerQueue(^{
RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[self.bridge.uiManager shadowViewForReactTag:viewTag];
[shadowView setText:value];
[self.bridge.uiManager setNeedsLayout];
RCTExecuteOnMainQueue(^{
[view setSelectionStart:start selectionEnd:end];
});
});
}];
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting:(__unused RCTUIManager *)uiManager
{
for (RCTBaseTextInputShadowView *shadowView in _shadowViews) {
[shadowView uiManagerWillPerformMounting];
}
}
#pragma mark - Font Size Multiplier
- (void)handleDidUpdateMultiplierNotification
{
CGFloat fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager"]
valueForKey:@"multiplier"] floatValue];
NSHashTable<RCTBaseTextInputShadowView *> *shadowViews = _shadowViews;
RCTExecuteOnUIManagerQueue(^{
for (RCTBaseTextInputShadowView *shadowView in shadowViews) {
shadowView.textAttributes.fontSizeMultiplier = fontSizeMultiplier;
[shadowView dirtyLayout];
}
[self.bridge.uiManager setNeedsLayout];
});
}
@end

View File

@ -0,0 +1,12 @@
/*
* 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 <React/RCTShadowView.h>
@interface RCTInputAccessoryShadowView : RCTShadowView
@end

View File

@ -0,0 +1,20 @@
/*
* 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 <React/RCTInputAccessoryShadowView.h>
#import <React/RCTUtils.h>
@implementation RCTInputAccessoryShadowView
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
{
[super insertReactSubview:subview atIndex:atIndex];
subview.width = (YGValue) { RCTScreenSize().width, YGUnitPoint };
}
@end

View File

@ -0,0 +1,17 @@
/*
* 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 <UIKit/UIKit.h>
@class RCTBridge;
@class RCTInputAccessoryViewContent;
@interface RCTInputAccessoryView : UIView
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@end

View File

@ -0,0 +1,79 @@
/*
* 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 <React/RCTInputAccessoryView.h>
#import <React/RCTBridge.h>
#import <React/RCTTouchHandler.h>
#import <React/UIView+React.h>
#import <React/RCTInputAccessoryViewContent.h>
@interface RCTInputAccessoryView()
// Overriding `inputAccessoryView` to `readwrite`.
@property (nonatomic, readwrite, retain) UIView *inputAccessoryView;
@end
@implementation RCTInputAccessoryView
{
BOOL _shouldBecomeFirstResponder;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_inputAccessoryView = [RCTInputAccessoryViewContent new];
RCTTouchHandler *const touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
[touchHandler attachToView:_inputAccessoryView];
}
return self;
}
- (BOOL)canBecomeFirstResponder
{
return true;
}
- (void)reactSetFrame:(CGRect)frame
{
[_inputAccessoryView reactSetFrame:frame];
if (_shouldBecomeFirstResponder) {
_shouldBecomeFirstResponder = NO;
[self becomeFirstResponder];
}
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_inputAccessoryView insertReactSubview:subview atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[_inputAccessoryView removeReactSubview:subview];
}
- (void)didUpdateReactSubviews
{
// Do nothing, as subviews are managed by `insertReactSubview:atIndex:`.
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
// If the accessory view is not linked to a text input via nativeID, assume it is
// a standalone component that should get focus whenever it is rendered.
if (![changedProps containsObject:@"nativeID"] && !self.nativeID) {
_shouldBecomeFirstResponder = YES;
}
}
@end

View File

@ -0,0 +1,12 @@
/*
* 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 <UIKit/UIKit.h>
@interface RCTInputAccessoryViewContent : UIView
@end

View File

@ -0,0 +1,82 @@
/*
* 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 <React/RCTInputAccessoryViewContent.h>
#import <React/UIView+React.h>
@implementation RCTInputAccessoryViewContent
{
UIView *_safeAreaContainer;
NSLayoutConstraint *_heightConstraint;
}
- (instancetype)init
{
if (self = [super init]) {
_safeAreaContainer = [UIView new];
[self addSubview:_safeAreaContainer];
// Use autolayout to position the view properly and take into account
// safe area insets on iPhone X.
// TODO: Support rotation, anchor to left and right without breaking frame x coordinate (T27974328).
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
_safeAreaContainer.translatesAutoresizingMaskIntoConstraints = NO;
_heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0];
_heightConstraint.active = YES;
if (@available(iOS 11.0, tvOS 11.0, *)) {
[_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES;
[_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES;
[_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor].active = YES;
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor].active = YES;
} else {
[_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
[_safeAreaContainer.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = YES;
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = YES;
}
}
return self;
}
- (CGSize)intrinsicContentSize
{
// This is needed so the view size is based on autolayout constraints.
return CGSizeZero;
}
- (void)reactSetFrame:(CGRect)frame
{
// We still need to set the frame here, otherwise it won't be
// measured until moved to the window during the keyboard opening
// animation. If this happens, the height will be animated from 0 to
// its actual size and we don't want that.
[self setFrame:frame];
[_safeAreaContainer setFrame:frame];
_heightConstraint.constant = frame.size.height;
[self layoutIfNeeded];
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_safeAreaContainer insertSubview:subview atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[subview removeFromSuperview];
if ([[_safeAreaContainer subviews] count] == 0 && [self isFirstResponder]) {
[self resignFirstResponder];
}
}
@end

View File

@ -0,0 +1,12 @@
/*
* 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 <React/RCTViewManager.h>
@interface RCTInputAccessoryViewManager : RCTViewManager
@end

View File

@ -0,0 +1,34 @@
/*
* 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 <React/RCTInputAccessoryViewManager.h>
#import <React/RCTInputAccessoryShadowView.h>
#import <React/RCTInputAccessoryView.h>
@implementation RCTInputAccessoryViewManager
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (UIView *)view
{
return [[RCTInputAccessoryView alloc] initWithBridge:self.bridge];
}
- (RCTShadowView *)shadowView
{
return [RCTInputAccessoryShadowView new];
}
RCT_REMAP_VIEW_PROPERTY(backgroundColor, inputAccessoryView.backgroundColor, UIColor)
@end

View File

@ -0,0 +1,26 @@
/*
* 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 <React/RCTConvert.h>
/**
* Object containing information about a TextInput's selection.
*/
@interface RCTTextSelection : NSObject
@property (nonatomic, assign, readonly) NSInteger start;
@property (nonatomic, assign, readonly) NSInteger end;
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end;
@end
@interface RCTConvert (RCTTextSelection)
+ (RCTTextSelection *)RCTTextSelection:(id)json;
@end

View File

@ -0,0 +1,36 @@
/*
* 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 <React/RCTTextSelection.h>
@implementation RCTTextSelection
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end
{
if (self = [super init]) {
_start = start;
_end = end;
}
return self;
}
@end
@implementation RCTConvert (RCTTextSelection)
+ (RCTTextSelection *)RCTTextSelection:(id)json
{
if ([json isKindOfClass:[NSDictionary class]]) {
NSInteger start = [self NSInteger:json[@"start"]];
NSInteger end = [self NSInteger:json[@"end"]];
return [[RCTTextSelection alloc] initWithStart:start end:end];
}
return nil;
}
@end

View File

@ -0,0 +1,16 @@
/*
* 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 "RCTBaseTextInputView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTSinglelineTextInputView : RCTBaseTextInputView
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,40 @@
/*
* 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 <React/RCTSinglelineTextInputView.h>
#import <React/RCTBridge.h>
#import <React/RCTUITextField.h>
@implementation RCTSinglelineTextInputView
{
RCTUITextField *_backedTextInputView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `true` for <TextInput multiline={false}> by design.
self.blurOnSubmit = YES;
_backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
[self addSubview:_backedTextInputView];
}
return self;
}
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
return _backedTextInputView;
}
@end

View File

@ -0,0 +1,16 @@
/*
* 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 "RCTBaseTextInputViewManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTSinglelineTextInputViewManager : RCTBaseTextInputViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,32 @@
/*
* 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 <React/RCTSinglelineTextInputViewManager.h>
#import <React/RCTBaseTextInputShadowView.h>
#import <React/RCTSinglelineTextInputView.h>
@implementation RCTSinglelineTextInputViewManager
RCT_EXPORT_MODULE()
- (RCTShadowView *)shadowView
{
RCTBaseTextInputShadowView *shadowView =
(RCTBaseTextInputShadowView *)[super shadowView];
shadowView.maximumNumberOfLines = 1;
return shadowView;
}
- (UIView *)view
{
return [[RCTSinglelineTextInputView alloc] initWithBridge:self.bridge];
}
@end

View File

@ -0,0 +1,34 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTBackedTextInputDelegate.h>
NS_ASSUME_NONNULL_BEGIN
/*
* Just regular UITextField... but much better!
*/
@interface RCTUITextField : UITextField <RCTBackedTextInputViewProtocol>
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,222 @@
/*
* 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 <React/RCTUITextField.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTBackedTextInputDelegateAdapter.h>
#import <React/RCTTextAttributes.h>
@implementation RCTUITextField {
RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter;
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_textDidChange)
name:UITextFieldTextDidChangeNotification
object:self];
_textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self];
_scrollEnabled = YES;
}
return self;
}
- (void)_textDidChange
{
_textWasPasted = NO;
}
#pragma mark - Accessibility
- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
{
// UITextField is accessible by default (some nested views are) and disabling that is not supported.
// On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view
// (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component.
}
#pragma mark - Properties
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
{
_textContainerInset = textContainerInset;
[self setNeedsLayout];
}
- (void)setPlaceholder:(NSString *)placeholder
{
[super setPlaceholder:placeholder];
[self _updatePlaceholder];
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
_placeholderColor = placeholderColor;
[self _updatePlaceholder];
}
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
return;
}
_defaultTextAttributes = defaultTextAttributes;
[super setDefaultTextAttributes:defaultTextAttributes];
[self _updatePlaceholder];
}
- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
return _defaultTextAttributes;
}
- (void)_updatePlaceholder
{
self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder ?: @""
attributes:[self _placeholderTextAttributes]];
}
- (BOOL)isEditable
{
return self.isEnabled;
}
- (void)setEditable:(BOOL)editable
{
self.enabled = editable;
}
- (void)setSecureTextEntry:(BOOL)secureTextEntry
{
if (self.secureTextEntry == secureTextEntry) {
return;
}
[super setSecureTextEntry:secureTextEntry];
// Fix for trailing whitespate issue
// Read more:
// https://stackoverflow.com/questions/14220187/uitextfield-has-trailing-whitespace-after-securetextentry-toggle/22537788#22537788
NSAttributedString *originalText = [self.attributedText copy];
self.attributedText = [NSAttributedString new];
self.attributedText = originalText;
}
#pragma mark - Placeholder
- (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes
{
NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];
if (self.placeholderColor) {
[textAttributes setValue:self.placeholderColor forKey:NSForegroundColorAttributeName];
} else {
[textAttributes removeObjectForKey:NSForegroundColorAttributeName];
}
return textAttributes;
}
#pragma mark - Context Menu
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_contextMenuHidden) {
return NO;
}
return [super canPerformAction:action withSender:sender];
}
#pragma mark - Caret Manipulation
- (CGRect)caretRectForPosition:(UITextPosition *)position
{
if (_caretHidden) {
return CGRectZero;
}
return [super caretRectForPosition:position];
}
#pragma mark - Positioning Overrides
- (CGRect)textRectForBounds:(CGRect)bounds
{
return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], _textContainerInset);
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
return [self textRectForBounds:bounds];
}
#pragma mark - Overrides
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
// Overrides selectedTextRange setter to get notify when selectedTextRange changed.
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange
{
[super setSelectedTextRange:selectedTextRange];
[_textInputDelegateAdapter selectedTextRangeWasSet];
}
#pragma clang diagnostic pop
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
{
if (!notifyDelegate) {
// We have to notify an adapter that following selection change was initiated programmatically,
// so the adapter must not generate a notification for it.
[_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
}
[super setSelectedTextRange:selectedTextRange];
}
- (void)paste:(id)sender
{
[super paste:sender];
_textWasPasted = YES;
}
#pragma mark - Layout
- (CGSize)contentSize
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return self.intrinsicContentSize;
}
- (CGSize)intrinsicContentSize
{
// Note: `placeholder` defines intrinsic size for `<TextInput>`.
NSString *text = self.placeholder ?: @"";
CGSize size = [text sizeWithAttributes:[self _placeholderTextAttributes]];
size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height));
size.width += _textContainerInset.left + _textContainerInset.right;
size.height += _textContainerInset.top + _textContainerInset.bottom;
// Returning size DOES contain `textContainerInset` (aka `padding`).
return size;
}
- (CGSize)sizeThatFits:(CGSize)size
{
// All size values here contain `textContainerInset` (aka `padding`).
CGSize intrinsicSize = self.intrinsicContentSize;
return CGSizeMake(MIN(size.width, intrinsicSize.width), MIN(size.height, intrinsicSize.height));
}
@end