Better TextInput: RCTUITextView was decoupled in separate file and now handles placeholder feature
Reviewed By: mmmulani Differential Revision: D4663151 fbshipit-source-id: ce57ca4bebf4676df2ae5e586a1b175ec2aac760
This commit is contained in:
parent
26e2c08544
commit
b53d76efb7
|
@ -779,7 +779,7 @@ exports.examples = [
|
|||
<View>
|
||||
<TextInput
|
||||
placeholder="height increases with content"
|
||||
defaultValue="React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native."
|
||||
defaultValue="React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about - learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native."
|
||||
multiline={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
returnKeyType="go"
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; };
|
||||
58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */; };
|
||||
58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; };
|
||||
59B125C91E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; };
|
||||
59B125CA1E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; };
|
||||
59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */; };
|
||||
59F60E921E661BDD0081153B /* RCTShadowTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */; };
|
||||
59F60E931E661BDD0081153B /* RCTShadowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E901E661BDD0081153B /* RCTShadowTextView.m */; };
|
||||
|
@ -58,6 +60,8 @@
|
|||
58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = "<group>"; };
|
||||
58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
|
||||
58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
|
||||
59B125C71E6E4E15004E2A67 /* RCTUITextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUITextView.h; sourceTree = "<group>"; };
|
||||
59B125C81E6E4E15004E2A67 /* RCTUITextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUITextView.m; sourceTree = "<group>"; };
|
||||
59F60E8D1E661BDD0081153B /* RCTShadowTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextField.h; sourceTree = "<group>"; };
|
||||
59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowTextField.m; sourceTree = "<group>"; };
|
||||
59F60E8F1E661BDD0081153B /* RCTShadowTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextView.h; sourceTree = "<group>"; };
|
||||
|
@ -97,6 +101,8 @@
|
|||
131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */,
|
||||
131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */,
|
||||
131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */,
|
||||
59B125C71E6E4E15004E2A67 /* RCTUITextView.h */,
|
||||
59B125C81E6E4E15004E2A67 /* RCTUITextView.m */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
|
@ -194,6 +200,7 @@
|
|||
2D3B5F341D9B103100451313 /* RCTRawTextManager.m in Sources */,
|
||||
59F60E921E661BDD0081153B /* RCTShadowTextField.m in Sources */,
|
||||
AF3225FA1DE5574F00D3E7E7 /* RCTConvert+Text.m in Sources */,
|
||||
59B125CA1E6E4E15004E2A67 /* RCTUITextView.m in Sources */,
|
||||
2D3B5F3C1D9B106F00451313 /* RCTTextViewManager.m in Sources */,
|
||||
59F60E941E661BDD0081153B /* RCTShadowTextView.m in Sources */,
|
||||
2D3B5F331D9B102D00451313 /* RCTTextSelection.m in Sources */,
|
||||
|
@ -214,6 +221,7 @@
|
|||
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */,
|
||||
59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */,
|
||||
AF3225F91DE5574F00D3E7E7 /* RCTConvert+Text.m in Sources */,
|
||||
59B125C91E6E4E15004E2A67 /* RCTUITextView.m in Sources */,
|
||||
131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */,
|
||||
59F60E931E661BDD0081153B /* RCTShadowTextView.m in Sources */,
|
||||
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */,
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
|
||||
@property (nonatomic, copy) NSString *text;
|
||||
@property (nonatomic, strong) UIColor *placeholderTextColor;
|
||||
@property (nonatomic, copy) NSString *placeholder;
|
||||
@property (nonatomic, strong) UIFont *font;
|
||||
@property (nonatomic, assign) NSInteger mostRecentEventCount;
|
||||
@property (nonatomic, strong) NSNumber *maxLength;
|
||||
|
|
|
@ -18,64 +18,14 @@
|
|||
#import "RCTShadowText.h"
|
||||
#import "RCTText.h"
|
||||
#import "RCTTextSelection.h"
|
||||
|
||||
@interface RCTUITextView : UITextView
|
||||
|
||||
@property (nonatomic, assign) BOOL textWasPasted;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTUITextView
|
||||
{
|
||||
BOOL _jsRequestingFirstResponder;
|
||||
}
|
||||
|
||||
- (void)paste:(id)sender
|
||||
{
|
||||
_textWasPasted = YES;
|
||||
[super paste:sender];
|
||||
}
|
||||
|
||||
- (void)reactWillMakeFirstResponder
|
||||
{
|
||||
_jsRequestingFirstResponder = YES;
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return _jsRequestingFirstResponder;
|
||||
}
|
||||
|
||||
- (void)reactDidMakeFirstResponder
|
||||
{
|
||||
_jsRequestingFirstResponder = NO;
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
if (_jsRequestingFirstResponder) {
|
||||
[self becomeFirstResponder];
|
||||
[self reactDidMakeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
@end
|
||||
#import "RCTUITextView.h"
|
||||
|
||||
@implementation RCTTextView
|
||||
{
|
||||
RCTBridge *_bridge;
|
||||
RCTEventDispatcher *_eventDispatcher;
|
||||
|
||||
NSString *_placeholder;
|
||||
UITextView *_placeholderView;
|
||||
UITextView *_textView;
|
||||
RCTUITextView *_textView;
|
||||
RCTText *_richTextView;
|
||||
NSAttributedString *_pendingAttributedText;
|
||||
|
||||
|
@ -99,7 +49,6 @@
|
|||
_contentInset = UIEdgeInsetsZero;
|
||||
_bridge = bridge;
|
||||
_eventDispatcher = bridge.eventDispatcher;
|
||||
_placeholderTextColor = [self defaultPlaceholderTextColor];
|
||||
_blurOnSubmit = NO;
|
||||
|
||||
_textView = [[RCTUITextView alloc] initWithFrame:self.bounds];
|
||||
|
@ -241,39 +190,12 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
|
|||
|
||||
[_textView layoutIfNeeded];
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
[self invalidateContentSize];
|
||||
|
||||
_blockTextShouldChange = NO;
|
||||
}
|
||||
|
||||
- (void)updatePlaceholder
|
||||
{
|
||||
[_placeholderView removeFromSuperview];
|
||||
_placeholderView = nil;
|
||||
|
||||
if (_placeholder) {
|
||||
_placeholderView = [[UITextView alloc] initWithFrame:_textView.frame];
|
||||
_placeholderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_placeholderView.textContainer.lineFragmentPadding = 0;
|
||||
_placeholderView.userInteractionEnabled = NO;
|
||||
_placeholderView.backgroundColor = [UIColor clearColor];
|
||||
_placeholderView.scrollEnabled = NO;
|
||||
#if !TARGET_OS_TV
|
||||
_placeholderView.editable = NO;
|
||||
_placeholderView.scrollsToTop = NO;
|
||||
#endif
|
||||
_placeholderView.attributedText =
|
||||
[[NSAttributedString alloc] initWithString:_placeholder attributes:@{
|
||||
NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]),
|
||||
NSForegroundColorAttributeName : _placeholderTextColor
|
||||
}];
|
||||
_placeholderView.textAlignment = _textView.textAlignment;
|
||||
|
||||
[self insertSubview:_placeholderView belowSubview:_textView];
|
||||
[self updatePlaceholderVisibility];
|
||||
}
|
||||
}
|
||||
#pragma mark - Properties
|
||||
|
||||
- (UIFont *)font
|
||||
{
|
||||
|
@ -283,40 +205,110 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
|
|||
- (void)setFont:(UIFont *)font
|
||||
{
|
||||
_textView.font = font;
|
||||
[self updatePlaceholder];
|
||||
}
|
||||
|
||||
- (void)setPlaceholder:(NSString *)placeholder
|
||||
{
|
||||
_placeholder = placeholder;
|
||||
[self updatePlaceholder];
|
||||
}
|
||||
|
||||
- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
|
||||
{
|
||||
if (placeholderTextColor) {
|
||||
_placeholderTextColor = placeholderTextColor;
|
||||
} else {
|
||||
_placeholderTextColor = [self defaultPlaceholderTextColor];
|
||||
}
|
||||
[self updatePlaceholder];
|
||||
}
|
||||
|
||||
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||||
{
|
||||
_contentInset = contentInset;
|
||||
_textView.textContainerInset = contentInset;
|
||||
_placeholderView.textContainerInset = contentInset;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)setSelection:(RCTTextSelection *)selection
|
||||
{
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITextRange *currentSelection = _textView.selectedTextRange;
|
||||
UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start];
|
||||
UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end];
|
||||
UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end];
|
||||
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
|
||||
_previousSelectionRange = selectedTextRange;
|
||||
_textView.selectedTextRange = selectedTextRange;
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
return _textView.text;
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![text isEqualToString:_textView.text]) {
|
||||
UITextRange *selection = _textView.selectedTextRange;
|
||||
NSInteger oldTextLength = _textView.text.length;
|
||||
|
||||
_predictedText = text;
|
||||
_textView.text = text;
|
||||
|
||||
if (selection.empty) {
|
||||
// maintain cursor position relative to the end of the old text
|
||||
NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start];
|
||||
NSInteger offsetFromEnd = oldTextLength - start;
|
||||
NSInteger newOffset = text.length - offsetFromEnd;
|
||||
UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset];
|
||||
_textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position];
|
||||
}
|
||||
|
||||
[self invalidateContentSize];
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)placeholder
|
||||
{
|
||||
return _textView.placeholderText;
|
||||
}
|
||||
|
||||
- (void)setPlaceholder:(NSString *)placeholder
|
||||
{
|
||||
_textView.placeholderText = placeholder;
|
||||
}
|
||||
|
||||
- (UIColor *)placeholderTextColor
|
||||
{
|
||||
return _textView.placeholderTextColor;
|
||||
}
|
||||
|
||||
- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
|
||||
{
|
||||
_textView.placeholderTextColor = placeholderTextColor;
|
||||
}
|
||||
|
||||
- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType
|
||||
{
|
||||
_textView.autocorrectionType = autocorrectionType;
|
||||
}
|
||||
|
||||
- (UITextAutocorrectionType)autocorrectionType
|
||||
{
|
||||
return _textView.autocorrectionType;
|
||||
}
|
||||
|
||||
- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType
|
||||
{
|
||||
_textView.spellCheckingType = spellCheckingType;
|
||||
}
|
||||
|
||||
- (UITextSpellCheckingType)spellCheckingType
|
||||
{
|
||||
return _textView.spellCheckingType;
|
||||
}
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
if (textView.textWasPasted) {
|
||||
textView.textWasPasted = NO;
|
||||
} else {
|
||||
if (!textView.textWasPasted) {
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
|
||||
reactTag:self.reactTag
|
||||
text:nil
|
||||
|
@ -425,86 +417,6 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
|
|||
}
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
return _textView.text;
|
||||
}
|
||||
|
||||
- (void)setSelection:(RCTTextSelection *)selection
|
||||
{
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITextRange *currentSelection = _textView.selectedTextRange;
|
||||
UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start];
|
||||
UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end];
|
||||
UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end];
|
||||
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
|
||||
_previousSelectionRange = selectedTextRange;
|
||||
_textView.selectedTextRange = selectedTextRange;
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![text isEqualToString:_textView.text]) {
|
||||
UITextRange *selection = _textView.selectedTextRange;
|
||||
NSInteger oldTextLength = _textView.text.length;
|
||||
|
||||
_predictedText = text;
|
||||
_textView.text = text;
|
||||
|
||||
if (selection.empty) {
|
||||
// maintain cursor position relative to the end of the old text
|
||||
NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start];
|
||||
NSInteger offsetFromEnd = oldTextLength - start;
|
||||
NSInteger newOffset = text.length - offsetFromEnd;
|
||||
UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset];
|
||||
_textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position];
|
||||
}
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
[self invalidateContentSize];
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updatePlaceholderVisibility
|
||||
{
|
||||
if (_textView.text.length > 0) {
|
||||
[_placeholderView setHidden:YES];
|
||||
} else {
|
||||
[_placeholderView setHidden:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType
|
||||
{
|
||||
_textView.autocorrectionType = autocorrectionType;
|
||||
}
|
||||
|
||||
- (UITextAutocorrectionType)autocorrectionType
|
||||
{
|
||||
return _textView.autocorrectionType;
|
||||
}
|
||||
|
||||
- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType
|
||||
{
|
||||
_textView.spellCheckingType = spellCheckingType;
|
||||
}
|
||||
|
||||
- (UITextSpellCheckingType)spellCheckingType
|
||||
{
|
||||
return _textView.spellCheckingType;
|
||||
}
|
||||
|
||||
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
|
||||
{
|
||||
if (_selectTextOnFocus) {
|
||||
|
@ -519,7 +431,6 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
|
|||
{
|
||||
if (_clearTextOnFocus) {
|
||||
_textView.text = @"";
|
||||
[self updatePlaceholderVisibility];
|
||||
}
|
||||
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
|
||||
|
@ -560,7 +471,6 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
|
|||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
[self updatePlaceholderVisibility];
|
||||
[self invalidateContentSize];
|
||||
|
||||
// Detect when textView updates happend that didn't invoke `shouldChangeTextInRange`
|
||||
|
@ -580,6 +490,7 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
|
|||
_nativeUpdatesInFlight = NO;
|
||||
_nativeEventCount++;
|
||||
|
||||
// TODO: t16435709 This part will be removed soon.
|
||||
if (!self.reactTag || !_onChange) {
|
||||
return;
|
||||
}
|
||||
|
@ -718,18 +629,6 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
|
|||
[self invalidateContentSize];
|
||||
}
|
||||
|
||||
#pragma mark - Default values
|
||||
|
||||
- (UIFont *)defaultPlaceholderFont
|
||||
{
|
||||
return [UIFont systemFontOfSize:17];
|
||||
}
|
||||
|
||||
- (UIColor *)defaultPlaceholderTextColor
|
||||
{
|
||||
return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22];
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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 <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/*
|
||||
* Just regular UITextView... but much better!
|
||||
*/
|
||||
@interface RCTUITextView : UITextView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE;
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL textWasPasted;
|
||||
@property (nonatomic, copy, nullable) NSString *placeholderText;
|
||||
@property (nonatomic, assign, nullable) UIColor *placeholderTextColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* 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 "RCTUITextView.h"
|
||||
|
||||
@implementation RCTUITextView
|
||||
{
|
||||
BOOL _jsRequestingFirstResponder;
|
||||
UILabel *_placeholderView;
|
||||
UITextView *_detachedTextView;
|
||||
}
|
||||
|
||||
static UIFont *defaultPlaceholderFont()
|
||||
{
|
||||
return [UIFont systemFontOfSize:17];
|
||||
}
|
||||
|
||||
static UIColor *defaultPlaceholderTextColor()
|
||||
{
|
||||
// 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.hidden = YES;
|
||||
_placeholderView.isAccessibilityElement = NO;
|
||||
_placeholderView.numberOfLines = 0;
|
||||
[self addSubview:_placeholderView];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setPlaceholderText:(NSString *)placeholderText
|
||||
{
|
||||
_placeholderText = placeholderText;
|
||||
[self invalidatePlaceholder];
|
||||
}
|
||||
|
||||
- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
|
||||
{
|
||||
_placeholderTextColor = placeholderTextColor;
|
||||
[self invalidatePlaceholder];
|
||||
}
|
||||
|
||||
|
||||
- (void)textDidChange
|
||||
{
|
||||
_textWasPasted = NO;
|
||||
[self invalidatePlaceholder];
|
||||
}
|
||||
|
||||
#pragma mark - UIResponder
|
||||
|
||||
- (void)reactWillMakeFirstResponder
|
||||
{
|
||||
_jsRequestingFirstResponder = YES;
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return _jsRequestingFirstResponder;
|
||||
}
|
||||
|
||||
- (void)reactDidMakeFirstResponder
|
||||
{
|
||||
_jsRequestingFirstResponder = NO;
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
if (_jsRequestingFirstResponder) {
|
||||
[self becomeFirstResponder];
|
||||
[self reactDidMakeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)setFont:(UIFont *)font
|
||||
{
|
||||
[super setFont:font];
|
||||
[self invalidatePlaceholder];
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
[super setText:text];
|
||||
[self textDidChange];
|
||||
}
|
||||
|
||||
- (void)setAttributedText:(NSAttributedString *)attributedText
|
||||
{
|
||||
[super setAttributedText:attributedText];
|
||||
[self textDidChange];
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
#pragma mark - Layout
|
||||
|
||||
- (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)sizeThatFits:(CGSize)size
|
||||
{
|
||||
// UITextView on iOS 8 has a bug that automatically scrolls to the top
|
||||
// when calling `sizeThatFits:`. Use a copy so that self is not screwed up.
|
||||
static BOOL useCustomImplementation = NO;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
useCustomImplementation = ![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}];
|
||||
});
|
||||
|
||||
if (!useCustomImplementation) {
|
||||
return [super sizeThatFits:size];
|
||||
}
|
||||
|
||||
if (!_detachedTextView) {
|
||||
_detachedTextView = [UITextView new];
|
||||
}
|
||||
|
||||
_detachedTextView.attributedText = self.attributedText;
|
||||
_detachedTextView.font = self.font;
|
||||
_detachedTextView.textContainerInset = self.textContainerInset;
|
||||
|
||||
return [_detachedTextView sizeThatFits:size];
|
||||
}
|
||||
|
||||
#pragma mark - Placeholder
|
||||
|
||||
- (void)invalidatePlaceholder
|
||||
{
|
||||
BOOL wasVisible = !_placeholderView.isHidden;
|
||||
BOOL isVisible = _placeholderText.length != 0 && self.text.length == 0;
|
||||
|
||||
if (wasVisible != isVisible) {
|
||||
_placeholderView.hidden = !isVisible;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
_placeholderView.font = self.font ?: defaultPlaceholderFont();
|
||||
_placeholderView.textColor = _placeholderTextColor ?: defaultPlaceholderTextColor();
|
||||
_placeholderView.textAlignment = self.textAlignment;
|
||||
_placeholderView.text = _placeholderText;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -10,6 +10,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "RCTAssert.h"
|
||||
|
||||
#import "RCTBridge+Private.h"
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTBridgeMethod.h"
|
||||
|
|
Loading…
Reference in New Issue