mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 01:25:39 +00:00
08d1bc8c9f
Summary: With a multiline TextInput, the text is initially rendered correctly but once you try to edit it, the cursor slides off the top of the view and out of its bounds. Also if the TextInput is scrollable, you can scroll the text out of the bounds of the view, which looks buggy unless you clip the overflow. This occurs because the top content inset is applied to the RCTTextView instead of the UITextView's text container. This diff fixes both bugs by applying the vertical insets to the UITextView's textContainerInset instead of the RCTTextView's frame (which is a wrapper around a real UITextView). The left inset is still applied to the frame because there is a bug with the text rendering when the left textContainerInset is negative: the initial text doesn't show up until you focus the text view. The bug doesn't occur when setting the right textContainerInset, so I apply this workaround to only the left inset. Closes https://github.com/facebook/react-native/pull/2297 Github Author: James Ide <ide@jameside.com>
281 lines
7.8 KiB
Objective-C
281 lines
7.8 KiB
Objective-C
/**
|
|
* 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 "RCTTextView.h"
|
|
|
|
#import "RCTConvert.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTUtils.h"
|
|
#import "UIView+React.h"
|
|
|
|
@implementation RCTTextView
|
|
{
|
|
RCTEventDispatcher *_eventDispatcher;
|
|
BOOL _jsRequestingFirstResponder;
|
|
NSString *_placeholder;
|
|
UITextView *_placeholderView;
|
|
UITextView *_textView;
|
|
NSInteger _nativeEventCount;
|
|
}
|
|
|
|
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
|
{
|
|
RCTAssertParam(eventDispatcher);
|
|
|
|
if ((self = [super initWithFrame:CGRectZero])) {
|
|
_contentInset = UIEdgeInsetsZero;
|
|
_eventDispatcher = eventDispatcher;
|
|
_placeholderTextColor = [self defaultPlaceholderTextColor];
|
|
|
|
_textView = [[UITextView alloc] initWithFrame:self.bounds];
|
|
_textView.backgroundColor = [UIColor clearColor];
|
|
_textView.delegate = self;
|
|
[self addSubview:_textView];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
|
|
RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|
|
|
- (void)updateFrames
|
|
{
|
|
// Adjust the insets so that they are as close as possible to single-line
|
|
// RCTTextField defaults, using the system defaults of font size 17 and a
|
|
// height of 31 points.
|
|
//
|
|
// We apply the left inset to the frame since a negative left text-container
|
|
// inset mysteriously causes the text to be hidden until the text view is
|
|
// first focused.
|
|
UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero;
|
|
adjustedFrameInset.left = _contentInset.left - 5;
|
|
|
|
UIEdgeInsets adjustedTextContainerInset = _contentInset;
|
|
adjustedTextContainerInset.top += 5;
|
|
adjustedTextContainerInset.left = 0;
|
|
|
|
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset);
|
|
_textView.frame = frame;
|
|
_placeholderView.frame = frame;
|
|
|
|
_textView.textContainerInset = adjustedTextContainerInset;
|
|
_placeholderView.textContainerInset = adjustedTextContainerInset;
|
|
}
|
|
|
|
- (void)updatePlaceholder
|
|
{
|
|
[_placeholderView removeFromSuperview];
|
|
_placeholderView = nil;
|
|
|
|
if (_placeholder) {
|
|
_placeholderView = [[UITextView alloc] initWithFrame:self.bounds];
|
|
_placeholderView.backgroundColor = [UIColor clearColor];
|
|
_placeholderView.scrollEnabled = false;
|
|
_placeholderView.attributedText =
|
|
[[NSAttributedString alloc] initWithString:_placeholder attributes:@{
|
|
NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]),
|
|
NSForegroundColorAttributeName : _placeholderTextColor
|
|
}];
|
|
|
|
[self insertSubview:_placeholderView belowSubview:_textView];
|
|
[self _setPlaceholderVisibility];
|
|
}
|
|
}
|
|
|
|
- (UIFont *)font
|
|
{
|
|
return _textView.font;
|
|
}
|
|
|
|
- (void)setFont:(UIFont *)font
|
|
{
|
|
_textView.font = font;
|
|
[self updatePlaceholder];
|
|
}
|
|
|
|
- (UIColor *)textColor
|
|
{
|
|
return _textView.textColor;
|
|
}
|
|
|
|
- (void)setTextColor:(UIColor *)textColor
|
|
{
|
|
_textView.textColor = textColor;
|
|
}
|
|
|
|
- (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;
|
|
[self updateFrames];
|
|
}
|
|
|
|
- (NSString *)text
|
|
{
|
|
return _textView.text;
|
|
}
|
|
|
|
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
|
{
|
|
if (_maxLength == nil) {
|
|
return YES;
|
|
}
|
|
NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length;
|
|
if (text.length > allowedLength) {
|
|
if (text.length > 1) {
|
|
// Truncate the input string so the result is exactly maxLength
|
|
NSString *limitedString = [text substringToIndex:allowedLength];
|
|
NSMutableString *newString = textView.text.mutableCopy;
|
|
[newString replaceCharactersInRange:range withString:limitedString];
|
|
textView.text = newString;
|
|
// Collapse selection at end of insert to match normal paste behavior
|
|
UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument
|
|
offset:(range.location + allowedLength)];
|
|
textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd];
|
|
[self textViewDidChange:textView];
|
|
}
|
|
return NO;
|
|
} else {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
- (void)setText:(NSString *)text
|
|
{
|
|
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
|
if (eventLag == 0 && ![text isEqualToString:_textView.text]) {
|
|
UITextRange *selection = _textView.selectedTextRange;
|
|
[_textView setText:text];
|
|
[self _setPlaceholderVisibility];
|
|
_textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
|
|
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
|
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
|
|
}
|
|
}
|
|
|
|
- (void)_setPlaceholderVisibility
|
|
{
|
|
if (_textView.text.length > 0) {
|
|
[_placeholderView setHidden:YES];
|
|
} else {
|
|
[_placeholderView setHidden:NO];
|
|
}
|
|
}
|
|
|
|
- (void)setAutoCorrect:(BOOL)autoCorrect
|
|
{
|
|
_textView.autocorrectionType = (autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo);
|
|
}
|
|
|
|
- (BOOL)autoCorrect
|
|
{
|
|
return _textView.autocorrectionType == UITextAutocorrectionTypeYes;
|
|
}
|
|
|
|
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
|
|
{
|
|
if (_selectTextOnFocus) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[textView selectAll:nil];
|
|
});
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)textViewDidBeginEditing:(UITextView *)textView
|
|
{
|
|
if (_clearTextOnFocus) {
|
|
_textView.text = @"";
|
|
[self _setPlaceholderVisibility];
|
|
}
|
|
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
|
|
reactTag:self.reactTag
|
|
text:textView.text
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
- (void)textViewDidChange:(UITextView *)textView
|
|
{
|
|
[self _setPlaceholderVisibility];
|
|
_nativeEventCount++;
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
|
|
reactTag:self.reactTag
|
|
text:textView.text
|
|
eventCount:_nativeEventCount];
|
|
|
|
}
|
|
|
|
- (void)textViewDidEndEditing:(UITextView *)textView
|
|
{
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
|
|
reactTag:self.reactTag
|
|
text:textView.text
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
|
|
- (BOOL)becomeFirstResponder
|
|
{
|
|
_jsRequestingFirstResponder = YES;
|
|
BOOL result = [_textView becomeFirstResponder];
|
|
_jsRequestingFirstResponder = NO;
|
|
return result;
|
|
}
|
|
|
|
- (BOOL)resignFirstResponder
|
|
{
|
|
[super resignFirstResponder];
|
|
BOOL result = [_textView resignFirstResponder];
|
|
if (result) {
|
|
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
|
|
reactTag:self.reactTag
|
|
text:_textView.text
|
|
eventCount:_nativeEventCount];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
[self updateFrames];
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return _jsRequestingFirstResponder;
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
@end
|