From c09bdebcd532bfb487763d1800aaf1f3e4d2f150 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 29 Apr 2015 01:29:00 -0700 Subject: [PATCH] Add support for multiline TextInput via UITextView Summary: @nicklockwood - Could I get a review of this? Just took `RCTTextField` and ported it from `UITextField` to `UITextView` as you mentioned in another discussion, and removed any `UITextField` specific attributes. - How do you think this should behave when there are subviews? - Do you know how we can respond to the `UIControlEventEditingDidEndOnExit` event to respond to submit? Because `UITextView` isn't a `UIControl` we can't just use `addTarget` with `UIControlEventEditingDidEndOnExit`. - Any other feedback? Still going to look over the `UITextView` docs in more detail and make sure we expose all important options, and add it to the UIExplorer example, just putting this out here for feedback. ![multiline](https://cloud.githubusercontent.com/assets/90494/7310854/32174d6a-e9e8-11e4-919e-71e54cf3c739.gif) Closes https://github.com/facebook/react-native/pull/991 Github Author: Brent Vatne Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Examples/UIExplorer/TextInputExample.js | 33 ++- .../Text/RCTText.xcodeproj/project.pbxproj | 12 ++ Libraries/Text/RCTTextManager.h | 1 - Libraries/Text/RCTTextManager.m | 2 +- Libraries/Text/RCTTextView.h | 27 +++ Libraries/Text/RCTTextView.m | 199 ++++++++++++++++++ Libraries/Text/RCTTextViewManager.h | 14 ++ Libraries/Text/RCTTextViewManager.m | 64 ++++++ React/Base/RCTConvert.m | 67 +++--- React/Views/RCTTextField.m | 6 +- 10 files changed, 392 insertions(+), 33 deletions(-) create mode 100644 Libraries/Text/RCTTextView.h create mode 100644 Libraries/Text/RCTTextView.m create mode 100644 Libraries/Text/RCTTextViewManager.h create mode 100644 Libraries/Text/RCTTextViewManager.m diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index e0ae1b465..a32937fa4 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -88,9 +88,9 @@ var styles = StyleSheet.create({ height: 26, borderWidth: 0.5, borderColor: '#0f0f0f', - padding: 4, flex: 1, fontSize: 13, + padding: 4, }, multiline: { borderWidth: 0.5, @@ -98,6 +98,13 @@ var styles = StyleSheet.create({ flex: 1, fontSize: 13, height: 50, + padding: 4, + }, + multilineWithFontStyles: { + color: 'blue', + fontWeight: 'bold', + fontSize: 18, + fontFamily: 'Cochin', }, eventLabel: { margin: 3, @@ -118,7 +125,7 @@ var styles = StyleSheet.create({ }); exports.title = ''; -exports.description = 'Single-line text inputs.'; +exports.description = 'Single and multi-line text inputs.'; exports.examples = [ { title: 'Auto-focus', @@ -313,7 +320,7 @@ exports.examples = [ }, { title: 'Clear and select', - render: function () { + render: function() { return ( @@ -336,4 +343,24 @@ exports.examples = [ ); } }, + { + title: 'Multiline', + render: function() { + return ( + + + + + ) + } + } ]; diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 3c4bcf5ba..224c7e6b9 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */; }; + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; }; 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; }; 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; }; 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; @@ -27,6 +29,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextView.h; sourceTree = ""; }; + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = ""; }; + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = ""; }; + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = ""; }; 58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; }; 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; }; @@ -64,6 +70,10 @@ 58B512141A9E6EFF00147676 /* RCTText.m */, 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */, 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */, + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */, + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */, + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */, + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */, 58B5119C1A9E6C1200147676 /* Products */, ); indentWidth = 2; @@ -135,8 +145,10 @@ buildActionMask = 2147483647; files = ( 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */, 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, ); diff --git a/Libraries/Text/RCTTextManager.h b/Libraries/Text/RCTTextManager.h index 13e8f8546..91ac87ba8 100644 --- a/Libraries/Text/RCTTextManager.h +++ b/Libraries/Text/RCTTextManager.h @@ -12,4 +12,3 @@ @interface RCTTextManager : RCTViewManager @end - diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index e5e9ad00a..ef518d204 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -123,7 +123,7 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTText *text = (RCTText *)viewRegistry[reactTag]; + RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; text.layoutManager = shadowView.layoutManager; text.textContainer = shadowView.textContainer; diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h new file mode 100644 index 000000000..742c81534 --- /dev/null +++ b/Libraries/Text/RCTTextView.h @@ -0,0 +1,27 @@ +/** + * 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 + +#import "RCTView.h" +#import "UIView+React.h" + +@class RCTEventDispatcher; + +@interface RCTTextView : RCTView + +@property (nonatomic, assign) BOOL autoCorrect; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, strong) UIColor *placeholderTextColor; +@property (nonatomic, assign) UIFont *font; + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m new file mode 100644 index 000000000..a98bbc3ea --- /dev/null +++ b/Libraries/Text/RCTTextView.m @@ -0,0 +1,199 @@ +/** + * 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; +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)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; +} + +- (void)updateFrames +{ + // Adjust the insets so that they are as close as possible to single-line + // RCTTextField defaults + UIEdgeInsets adjustedInset = (UIEdgeInsets){ + _contentInset.top - 5, _contentInset.left - 4, + _contentInset.bottom, _contentInset.right + }; + + [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; + [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; +} + +- (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]; + } +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + _textView.font = _font; + [self updatePlaceholder]; +} + +- (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]; +} + +- (void)setText:(NSString *)text +{ + if (![text isEqualToString:_textView.text]) { + [_textView setText:text]; + [self _setPlaceholderVisibility]; + } +} + +- (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; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:textView.text]; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + [self _setPlaceholderVisibility]; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange + reactTag:self.reactTag + text:textView.text]; + +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd + reactTag:self.reactTag + text:textView.text]; +} + +- (BOOL)becomeFirstResponder +{ + _jsRequestingFirstResponder = YES; + BOOL result = [super becomeFirstResponder]; + _jsRequestingFirstResponder = NO; + return result; +} + +- (BOOL)resignFirstResponder +{ + BOOL result = [super resignFirstResponder]; + if (result) { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur + reactTag:self.reactTag + text:_textView.text]; + } + return result; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self updateFrames]; +} + +- (BOOL)canBecomeFirstResponder +{ + return _jsRequestingFirstResponder; +} + +- (UIFont *)defaultPlaceholderFont +{ + return [UIFont fontWithName:@"Helvetica" size:17]; +} + +- (UIColor *)defaultPlaceholderTextColor +{ + return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; +} + +@end diff --git a/Libraries/Text/RCTTextViewManager.h b/Libraries/Text/RCTTextViewManager.h new file mode 100644 index 000000000..fd2f2b44d --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.h @@ -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 "RCTViewManager.h" + +@interface RCTTextViewManager : RCTViewManager + +@end diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m new file mode 100644 index 000000000..5218544d2 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.m @@ -0,0 +1,64 @@ +/** + * 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 "RCTTextViewManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTShadowView.h" +#import "RCTSparseArray.h" +#import "RCTTextView.h" + +@implementation RCTTextViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) +RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(text, NSString) +RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) +RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) +RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) +RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) +RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL) +RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType, UITextAutocapitalizationType) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; +} +RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withWeight:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withStyle:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; +} + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +{ + NSNumber *reactTag = shadowView.reactTag; + UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + ((RCTTextView *)viewRegistry[reactTag]).contentInset = padding; + }; +} + +@end diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index eacb03b85..465d1f0bb 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -707,50 +707,65 @@ static BOOL RCTFontIsCondensed(UIFont *font) const RCTFontWeight RCTDefaultFontWeight = UIFontWeightRegular; const CGFloat RCTDefaultFontSize = 14; - // Get existing properties + // Initialize properties to defaults + CGFloat fontSize = RCTDefaultFontSize; + RCTFontWeight fontWeight = RCTDefaultFontWeight; + NSString *familyName = RCTDefaultFontFamily; BOOL isItalic = NO; BOOL isCondensed = NO; - RCTFontWeight fontWeight = RCTDefaultFontWeight; + if (font) { - family = font.familyName; + familyName = font.familyName ?: RCTDefaultFontFamily; + fontSize = font.pointSize ?: RCTDefaultFontSize; fontWeight = RCTWeightOfFont(font); isItalic = RCTFontIsItalic(font); isCondensed = RCTFontIsCondensed(font); } + // Get font size + fontSize = [self CGFloat:size] ?: fontSize; + + // Get font family + familyName = [self NSString:family] ?: familyName; + // Get font style if (style) { isItalic = [self RCTFontStyle:style]; } - // Get font size - CGFloat fontSize = [self CGFloat:size] ?: RCTDefaultFontSize; - - // Get font family - NSString *familyName = [self NSString:family] ?: RCTDefaultFontFamily; - if ([UIFont fontNamesForFamilyName:familyName].count == 0) { - font = [UIFont fontWithName:familyName size:fontSize]; - if (font) { - // It's actually a font name, not a font family name, - // but we'll do what was meant, not what was said. - familyName = font.familyName; - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - fontWeight = [traits[UIFontWeightTrait] doubleValue]; - } else { - // Not a valid font or family - RCTLogError(@"Unrecognized font family '%@'", familyName); - familyName = RCTDefaultFontFamily; - } - } - // Get font weight if (weight) { fontWeight = [self RCTFontWeight:weight]; } - // Get closest match - UIFont *bestMatch = font; - CGFloat closestWeight = font ? RCTWeightOfFont(font) : INFINITY; + // Gracefully handle being given a font name rather than font family, for + // example: "Helvetica Light Oblique" rather than just "Helvetica". + if ([UIFont fontNamesForFamilyName:familyName].count == 0) { + font = [UIFont fontWithName:familyName size:fontSize]; + if (font) { + // It's actually a font name, not a font family name, + // but we'll do what was meant, not what was said. + familyName = font.familyName; + fontWeight = RCTWeightOfFont(font); + isItalic = RCTFontIsItalic(font); + isCondensed = RCTFontIsCondensed(font); + } else { + // Not a valid font or family + RCTLogError(@"Unrecognized font family '%@'", familyName); + familyName = RCTDefaultFontFamily; + } + } + + // Get the closest font that matches the given weight for the fontFamily + UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize]; + CGFloat closestWeight; + + if (font && [font.familyName isEqualToString: familyName]) { + closestWeight = RCTWeightOfFont(font); + } else { + closestWeight = INFINITY; + } + for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == RCTFontIsItalic(match) && diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index ff56214d8..12d52b1b8 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -54,12 +54,14 @@ static void RCTUpdatePlaceholder(RCTTextField *self) } } -- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor { +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ _placeholderTextColor = placeholderTextColor; RCTUpdatePlaceholder(self); } -- (void)setPlaceholder:(NSString *)placeholder { +- (void)setPlaceholder:(NSString *)placeholder +{ super.placeholder = placeholder; RCTUpdatePlaceholder(self); }