mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-23 11:57:46 +08:00
The Great File Renaming in RCTText
Summary: The previous file/class name convention seemed cool... but now it drives me BANANAS! It makes all this code really hard to maintain. So, evething were renamed following common modern RN convention. Reviewed By: mmmulani Differential Revision: D6605090 fbshipit-source-id: 88ca13d793a5d2adaac2b7922ec6bd4654aacec5
This commit is contained in:
committed by
Facebook Github Bot
parent
5c8481e836
commit
19a9c5e41d
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 <React/RCTShadowView.h>
|
||||
|
||||
@interface RCTMultilineTextInputShadowView : RCTShadowView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 "RCTMultilineTextInputShadowView.h"
|
||||
|
||||
@implementation RCTMultilineTextInputShadowView
|
||||
|
||||
- (BOOL)isYogaLeafNode
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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>
|
||||
|
||||
#import <React/RCTView.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
#import "RCTBaseTextInputView.h"
|
||||
|
||||
@class RCTBridge;
|
||||
|
||||
@interface RCTMultilineTextInputView : RCTBaseTextInputView
|
||||
|
||||
@property (nonatomic, assign) UITextAutocorrectionType autocorrectionType;
|
||||
@property (nonatomic, assign) UITextSpellCheckingType spellCheckingType;
|
||||
@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, strong) NSNumber *maxLength;
|
||||
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onChange;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onTextInput;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
|
||||
|
||||
- (void)performTextUpdate;
|
||||
|
||||
@end
|
||||
411
Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m
Normal file
411
Libraries/Text/TextInput/Multiline/RCTMultilineTextInputView.m
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 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 "RCTMultilineTextInputView.h"
|
||||
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTEventDispatcher.h>
|
||||
#import <React/RCTFont.h>
|
||||
#import <React/RCTUIManager.h>
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
#import "RCTTextShadowView.h"
|
||||
#import "RCTTextView.h"
|
||||
#import "RCTTextSelection.h"
|
||||
#import "RCTUITextView.h"
|
||||
|
||||
@interface RCTMultilineTextInputView () <RCTBackedTextInputDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTMultilineTextInputView
|
||||
{
|
||||
RCTUITextView *_backedTextInput;
|
||||
RCTTextView *_richTextView;
|
||||
NSAttributedString *_pendingAttributedText;
|
||||
|
||||
NSString *_predictedText;
|
||||
|
||||
BOOL _blockTextShouldChange;
|
||||
BOOL _nativeUpdatesInFlight;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
{
|
||||
RCTAssertParam(bridge);
|
||||
|
||||
if (self = [super initWithBridge:bridge]) {
|
||||
// `blurOnSubmit` defaults to `false` for <TextInput multiline={true}> by design.
|
||||
_blurOnSubmit = NO;
|
||||
|
||||
_backedTextInput = [[RCTUITextView alloc] initWithFrame:self.bounds];
|
||||
_backedTextInput.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_backedTextInput.backgroundColor = [UIColor clearColor];
|
||||
_backedTextInput.textColor = [UIColor blackColor];
|
||||
// This line actually removes 5pt (default value) left and right padding in UITextView.
|
||||
_backedTextInput.textContainer.lineFragmentPadding = 0;
|
||||
#if !TARGET_OS_TV
|
||||
_backedTextInput.scrollsToTop = NO;
|
||||
#endif
|
||||
_backedTextInput.scrollEnabled = YES;
|
||||
_backedTextInput.textInputDelegate = self;
|
||||
_backedTextInput.font = self.fontAttributes.font;
|
||||
|
||||
[self addSubview:_backedTextInput];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
|
||||
{
|
||||
return _backedTextInput;
|
||||
}
|
||||
|
||||
#pragma mark - RCTComponent
|
||||
|
||||
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
|
||||
{
|
||||
[super insertReactSubview:subview atIndex:index];
|
||||
|
||||
if ([subview isKindOfClass:[RCTTextView class]]) {
|
||||
if (_richTextView) {
|
||||
RCTLogError(@"Tried to insert a second <Text> into <TextInput> - there can only be one.");
|
||||
}
|
||||
_richTextView = (RCTTextView *)subview;
|
||||
|
||||
// If this <TextInput> is in rich text editing mode, and the child <Text> node providing rich text
|
||||
// styling has a backgroundColor, then the attributedText produced by the child <Text> node will have an
|
||||
// NSBackgroundColor attribute. We need to forward this attribute to the text view manually because the text view
|
||||
// always has a clear background color in `initWithBridge:`.
|
||||
//
|
||||
// TODO: This should be removed when the related hack in -performPendingTextUpdate is removed.
|
||||
if (subview.backgroundColor) {
|
||||
NSMutableDictionary<NSString *, id> *attrs = [_backedTextInput.typingAttributes mutableCopy];
|
||||
attrs[NSBackgroundColorAttributeName] = subview.backgroundColor;
|
||||
_backedTextInput.typingAttributes = attrs;
|
||||
}
|
||||
|
||||
[self performTextUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeReactSubview:(UIView *)subview
|
||||
{
|
||||
[super removeReactSubview:subview];
|
||||
if (_richTextView == subview) {
|
||||
_richTextView = nil;
|
||||
[self performTextUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
// Do nothing, as we don't allow non-text subviews.
|
||||
}
|
||||
|
||||
#pragma mark - Routine
|
||||
|
||||
- (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount
|
||||
{
|
||||
_mostRecentEventCount = mostRecentEventCount;
|
||||
|
||||
// Props are set after uiBlockToAmendWithShadowViewRegistry, which means that
|
||||
// at the time performTextUpdate is called, _mostRecentEventCount will be
|
||||
// behind _eventCount, with the result that performPendingTextUpdate will do
|
||||
// nothing. For that reason we call it again here after mostRecentEventCount
|
||||
// has been set.
|
||||
[self performPendingTextUpdate];
|
||||
}
|
||||
|
||||
- (void)performTextUpdate
|
||||
{
|
||||
if (_richTextView) {
|
||||
_pendingAttributedText = _richTextView.textStorage;
|
||||
[self performPendingTextUpdate];
|
||||
} else if (!self.text) {
|
||||
_backedTextInput.attributedText = nil;
|
||||
}
|
||||
}
|
||||
|
||||
static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
|
||||
{
|
||||
if (string.length == 0) {
|
||||
return string;
|
||||
} else {
|
||||
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
|
||||
[mutableString removeAttribute:RCTReactTagAttributeName range:NSMakeRange(0, mutableString.length)];
|
||||
return mutableString;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)performPendingTextUpdate
|
||||
{
|
||||
if (!_pendingAttributedText || _mostRecentEventCount < _nativeEventCount || _nativeUpdatesInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The underlying <Text> node that produces _pendingAttributedText has a react tag attribute on it that causes the
|
||||
// -isEqualToAttributedString: comparison below to spuriously fail. We don't want that comparison to fail unless it
|
||||
// needs to because when the comparison fails, we end up setting attributedText on the text view, which clears
|
||||
// autocomplete state for CKJ text input.
|
||||
//
|
||||
// TODO: Kill this after we finish passing all style/attribute info into JS.
|
||||
_pendingAttributedText = removeReactTagFromString(_pendingAttributedText);
|
||||
|
||||
if ([_backedTextInput.attributedText isEqualToAttributedString:_pendingAttributedText]) {
|
||||
_pendingAttributedText = nil; // Don't try again.
|
||||
return;
|
||||
}
|
||||
|
||||
// When we update the attributed text, there might be pending autocorrections
|
||||
// that will get accepted by default. In order for this to not garble our text,
|
||||
// we temporarily block all textShouldChange events so they are not applied.
|
||||
_blockTextShouldChange = YES;
|
||||
|
||||
UITextRange *selection = _backedTextInput.selectedTextRange;
|
||||
NSInteger oldTextLength = _backedTextInput.attributedText.length;
|
||||
|
||||
_backedTextInput.attributedText = _pendingAttributedText;
|
||||
_predictedText = _pendingAttributedText.string;
|
||||
_pendingAttributedText = nil;
|
||||
|
||||
if (selection.empty) {
|
||||
// maintain cursor position relative to the end of the old text
|
||||
NSInteger start = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start];
|
||||
NSInteger offsetFromEnd = oldTextLength - start;
|
||||
NSInteger newOffset = _backedTextInput.attributedText.length - offsetFromEnd;
|
||||
UITextPosition *position = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:newOffset];
|
||||
[_backedTextInput setSelectedTextRange:[_backedTextInput textRangeFromPosition:position toPosition:position]
|
||||
notifyDelegate:YES];
|
||||
}
|
||||
|
||||
[_backedTextInput layoutIfNeeded];
|
||||
|
||||
[self invalidateContentSize];
|
||||
|
||||
_blockTextShouldChange = NO;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (UIFont *)font
|
||||
{
|
||||
return _backedTextInput.font;
|
||||
}
|
||||
|
||||
- (void)setFont:(UIFont *)font
|
||||
{
|
||||
_backedTextInput.font = font;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
return _backedTextInput.text;
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![text isEqualToString:_backedTextInput.text]) {
|
||||
UITextRange *selection = _backedTextInput.selectedTextRange;
|
||||
NSInteger oldTextLength = _backedTextInput.text.length;
|
||||
|
||||
_predictedText = text;
|
||||
_backedTextInput.text = text;
|
||||
|
||||
if (selection.empty) {
|
||||
// maintain cursor position relative to the end of the old text
|
||||
NSInteger start = [_backedTextInput offsetFromPosition:_backedTextInput.beginningOfDocument toPosition:selection.start];
|
||||
NSInteger offsetFromEnd = oldTextLength - start;
|
||||
NSInteger newOffset = text.length - offsetFromEnd;
|
||||
UITextPosition *position = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument offset:newOffset];
|
||||
[_backedTextInput setSelectedTextRange:[_backedTextInput textRangeFromPosition:position toPosition:position]
|
||||
notifyDelegate:YES];
|
||||
}
|
||||
|
||||
[self invalidateContentSize];
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", self.text, (long long)eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - RCTBackedTextInputDelegate
|
||||
|
||||
- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
if (!_backedTextInput.textWasPasted) {
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
|
||||
reactTag:self.reactTag
|
||||
text:nil
|
||||
key:text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
// So we need to track that there is a native update in flight just in case JS manages to come back around and update
|
||||
// things /before/ UITextView can update itself asynchronously. If there is a native update in flight, we defer the
|
||||
// JS update when it comes in and apply the deferred update once textViewDidChange fires with the native update applied.
|
||||
if (_blockTextShouldChange) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_maxLength) {
|
||||
NSUInteger allowedLength = _maxLength.integerValue - _backedTextInput.text.length + range.length;
|
||||
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];
|
||||
NSMutableString *newString = _backedTextInput.text.mutableCopy;
|
||||
[newString replaceCharactersInRange:range withString:limitedString];
|
||||
_backedTextInput.text = newString;
|
||||
_predictedText = newString;
|
||||
|
||||
// Collapse selection at end of insert to match normal paste behavior
|
||||
UITextPosition *insertEnd = [_backedTextInput positionFromPosition:_backedTextInput.beginningOfDocument
|
||||
offset:(range.location + allowedLength)];
|
||||
[_backedTextInput setSelectedTextRange:[_backedTextInput textRangeFromPosition:insertEnd toPosition:insertEnd]
|
||||
notifyDelegate:YES];
|
||||
|
||||
[self textInputDidChange];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
_nativeUpdatesInFlight = YES;
|
||||
|
||||
if (range.location + range.length > _predictedText.length) {
|
||||
// _predictedText got out of sync in a bad way, so let's just force sync it. Haven't been able to repro this, but
|
||||
// it's causing a real crash here: #6523822
|
||||
_predictedText = _backedTextInput.text;
|
||||
}
|
||||
|
||||
NSString *previousText = [_predictedText substringWithRange:range];
|
||||
if (_predictedText) {
|
||||
_predictedText = [_predictedText stringByReplacingCharactersInRange:range withString:text];
|
||||
} else {
|
||||
_predictedText = text;
|
||||
}
|
||||
|
||||
if (_onTextInput) {
|
||||
_onTextInput(@{
|
||||
@"text": text,
|
||||
@"previousText": previousText ?: @"",
|
||||
@"range": @{
|
||||
@"start": @(range.location),
|
||||
@"end": @(range.location + range.length)
|
||||
},
|
||||
@"eventCount": @(_nativeEventCount),
|
||||
});
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
- (void)textInputDidChange
|
||||
{
|
||||
[self invalidateContentSize];
|
||||
|
||||
// Detect when _backedTextInput updates happend 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(_backedTextInput.text, _predictedText, ¤tRange, &predictionRange)) {
|
||||
NSString *replacement = [_backedTextInput.text substringWithRange:currentRange];
|
||||
[self textInputShouldChangeTextInRange:predictionRange replacementText:replacement];
|
||||
// JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it.
|
||||
[self textInputDidChangeSelection];
|
||||
_predictedText = _backedTextInput.text;
|
||||
}
|
||||
|
||||
_nativeUpdatesInFlight = NO;
|
||||
_nativeEventCount++;
|
||||
|
||||
if (!self.reactTag || !_onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onChange(@{
|
||||
@"text": self.text,
|
||||
@"target": self.reactTag,
|
||||
@"eventCount": @(_nativeEventCount),
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 <React/RCTViewManager.h>
|
||||
|
||||
@interface RCTMultilineTextInputViewManager : RCTViewManager
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 "RCTMultilineTextInputViewManager.h"
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTFont.h>
|
||||
#import <React/RCTShadowView+Layout.h>
|
||||
#import <React/RCTShadowView.h>
|
||||
|
||||
#import "RCTConvert+Text.h"
|
||||
#import "RCTMultilineTextInputShadowView.h"
|
||||
#import "RCTMultilineTextInputView.h"
|
||||
|
||||
@implementation RCTMultilineTextInputViewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (RCTShadowView *)shadowView
|
||||
{
|
||||
return [RCTMultilineTextInputShadowView new];
|
||||
}
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge];
|
||||
}
|
||||
|
||||
#pragma mark - Unified <TextInput> properties
|
||||
|
||||
RCT_REMAP_VIEW_PROPERTY(allowFontScaling, fontAttributes.allowFontScaling, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, backedTextInputView.autocapitalizationType, UITextAutocapitalizationType)
|
||||
RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType)
|
||||
RCT_REMAP_VIEW_PROPERTY(color, backedTextInputView.textColor, UIColor)
|
||||
RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(fontSize, fontAttributes.fontSize, NSNumber)
|
||||
RCT_REMAP_VIEW_PROPERTY(fontWeight, fontAttributes.fontWeight, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(fontStyle, fontAttributes.fontStyle, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(fontFamily, fontAttributes.fontFamily, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance)
|
||||
RCT_REMAP_VIEW_PROPERTY(keyboardType, backedTextInputView.keyboardType, UIKeyboardType)
|
||||
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(secureTextEntry, backedTextInputView.secureTextEntry, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor)
|
||||
RCT_REMAP_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType)
|
||||
RCT_REMAP_VIEW_PROPERTY(textAlign, backedTextInputView.textAlignment, NSTextAlignment)
|
||||
RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
|
||||
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
|
||||
|
||||
#pragma mark - Multiline <TextInput> (aka TextView) specific properties
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes)
|
||||
#endif
|
||||
|
||||
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
|
||||
{
|
||||
NSNumber *reactTag = shadowView.reactTag;
|
||||
UIEdgeInsets borderAsInsets = shadowView.borderAsInsets;
|
||||
UIEdgeInsets paddingAsInsets = shadowView.paddingAsInsets;
|
||||
return ^(RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTBaseTextInputView *> *viewRegistry) {
|
||||
RCTBaseTextInputView *view = viewRegistry[reactTag];
|
||||
view.reactBorderInsets = borderAsInsets;
|
||||
view.reactPaddingInsets = paddingAsInsets;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
36
Libraries/Text/TextInput/Multiline/RCTUITextView.h
Normal file
36
Libraries/Text/TextInput/Multiline/RCTUITextView.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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>
|
||||
|
||||
#import "RCTBackedTextInputViewProtocol.h"
|
||||
|
||||
#import "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 *)aDecoder NS_UNAVAILABLE;
|
||||
|
||||
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL textWasPasted;
|
||||
@property (nonatomic, copy, nullable) NSString *placeholder;
|
||||
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
|
||||
|
||||
@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
241
Libraries/Text/TextInput/Multiline/RCTUITextView.m
Normal file
241
Libraries/Text/TextInput/Multiline/RCTUITextView.m
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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"
|
||||
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
#import "RCTBackedTextInputDelegateAdapter.h"
|
||||
|
||||
@implementation RCTUITextView
|
||||
{
|
||||
UILabel *_placeholderView;
|
||||
UITextView *_detachedTextView;
|
||||
RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
|
||||
}
|
||||
|
||||
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;
|
||||
_placeholderView.textColor = defaultPlaceholderColor();
|
||||
[self addSubview:_placeholderView];
|
||||
|
||||
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
{
|
||||
NSMutableString *accessibilityLabel = [NSMutableString new];
|
||||
|
||||
NSString *superAccessibilityLabel = [super accessibilityLabel];
|
||||
if (superAccessibilityLabel.length > 0) {
|
||||
[accessibilityLabel appendString:superAccessibilityLabel];
|
||||
}
|
||||
|
||||
if (self.placeholder.length > 0 && self.text.length == 0) {
|
||||
if (accessibilityLabel.length > 0) {
|
||||
[accessibilityLabel appendString:@" "];
|
||||
}
|
||||
[accessibilityLabel appendString:self.placeholder];
|
||||
}
|
||||
|
||||
return accessibilityLabel;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setPlaceholder:(NSString *)placeholder
|
||||
{
|
||||
_placeholder = placeholder;
|
||||
_placeholderView.text = _placeholder;
|
||||
}
|
||||
|
||||
- (void)setPlaceholderColor:(UIColor *)placeholderColor
|
||||
{
|
||||
_placeholderColor = placeholderColor;
|
||||
_placeholderView.textColor = _placeholderColor ?: defaultPlaceholderColor();
|
||||
}
|
||||
|
||||
- (void)textDidChange
|
||||
{
|
||||
_textWasPasted = NO;
|
||||
[self invalidatePlaceholderVisibility];
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)setFont:(UIFont *)font
|
||||
{
|
||||
[super setFont:font];
|
||||
_placeholderView.font = font ?: defaultPlaceholderFont();
|
||||
}
|
||||
|
||||
- (void)setTextAlignment:(NSTextAlignment)textAlignment
|
||||
{
|
||||
[super setTextAlignment:textAlignment];
|
||||
_placeholderView.textAlignment = textAlignment;
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
[super setText:text];
|
||||
[self textDidChange];
|
||||
}
|
||||
|
||||
- (void)setAttributedText:(NSAttributedString *)attributedText
|
||||
{
|
||||
[super setAttributedText: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];
|
||||
}
|
||||
|
||||
#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 placeholderSize = [placeholder sizeWithAttributes:@{NSFontAttributeName: self.font ?: defaultPlaceholderFont()}];
|
||||
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 = 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, INFINITY)];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
// Returned fitting size depends on text size and placeholder size.
|
||||
CGSize textSize = [self fixedSizeThatFits:size];
|
||||
CGSize placeholderSize = self.placeholderSize;
|
||||
// Returning size DOES contain `textContainerInset` (aka `padding`).
|
||||
return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height));
|
||||
}
|
||||
|
||||
- (CGSize)fixedSizeThatFits:(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)invalidatePlaceholderVisibility
|
||||
{
|
||||
BOOL isVisible = _placeholder.length != 0 && self.text.length == 0;
|
||||
_placeholderView.hidden = !isVisible;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user