diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index e0ae1b46517070..21b9606da3033d 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,21 @@ var styles = StyleSheet.create({ flex: 1, fontSize: 13, height: 50, + padding: 4, + }, + multilineWithFontStyles: { + color: 'purple', + fontWeight: 'bold', + fontSize: 18, + fontFamily: 'Cochin', + height: 80, + }, + multilineChild: { + width: 50, + height: 40, + position: 'absolute', + right: 5, + backgroundColor: 'red', }, eventLabel: { margin: 3, @@ -118,7 +133,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 +328,7 @@ exports.examples = [ }, { title: 'Clear and select', - render: function () { + render: function() { return ( @@ -336,4 +351,51 @@ exports.examples = [ ); } }, + { + title: 'Multiline', + render: function() { + return ( + + + + + + + + + + + + + + + + + + ) + } + } ]; diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index dfd3ab1a128469..f93d4819f72509 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -38,6 +38,7 @@ var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType; var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { autoCorrect: true, autoCapitalize: true, + clearTextOnFocus: true, color: true, editable: true, fontFamily: true, @@ -48,6 +49,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { returnKeyType: true, enablesReturnKeyAutomatically: true, secureTextEntry: true, + selectTextOnFocus: true, mostRecentEventCounter: true, placeholder: true, placeholderTextColor: true, @@ -58,8 +60,6 @@ var RCTTextFieldAttributes = merge(RCTTextViewAttributes, { caretHidden: true, enabled: true, clearButtonMode: true, - clearTextOnFocus: true, - selectTextOnFocus: true, }); var onlyMultiline = { @@ -498,6 +498,8 @@ var TextInput = React.createClass({ autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} clearButtonMode={clearButtonMode} + selectTextOnFocus={this.props.selectTextOnFocus} + clearTextOnFocus={this.props.clearTextOnFocus} />; } diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 3c4bcf5bae846f..e3924e4ce0acff 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -12,6 +12,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 */; }; + BBD12D921AEF2D7900F7DEDD /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD12D8F1AEF2D7900F7DEDD /* RCTTextView.m */; }; + BBD12D931AEF2D7900F7DEDD /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD12D911AEF2D7900F7DEDD /* RCTTextViewManager.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -38,6 +40,10 @@ 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = ""; }; 58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = ""; }; 58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = ""; }; + BBD12D8E1AEF2D7900F7DEDD /* RCTTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextView.h; sourceTree = ""; }; + BBD12D8F1AEF2D7900F7DEDD /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = ""; }; + BBD12D901AEF2D7900F7DEDD /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = ""; }; + BBD12D911AEF2D7900F7DEDD /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -64,6 +70,10 @@ 58B512141A9E6EFF00147676 /* RCTText.m */, 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */, 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */, + BBD12D8E1AEF2D7900F7DEDD /* RCTTextView.h */, + BBD12D8F1AEF2D7900F7DEDD /* RCTTextView.m */, + BBD12D901AEF2D7900F7DEDD /* RCTTextViewManager.h */, + BBD12D911AEF2D7900F7DEDD /* RCTTextViewManager.m */, 58B5119C1A9E6C1200147676 /* Products */, ); indentWidth = 2; @@ -135,8 +145,10 @@ buildActionMask = 2147483647; files = ( 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, + BBD12D921AEF2D7900F7DEDD /* RCTTextView.m in Sources */, 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, + BBD12D931AEF2D7900F7DEDD /* RCTTextViewManager.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, ); diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h new file mode 100644 index 00000000000000..ccd5a184180cd0 --- /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 00000000000000..31aea6b93c0f2f --- /dev/null +++ b/Libraries/Text/RCTTextView.m @@ -0,0 +1,249 @@ +/** + * 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; + BOOL _clearTextOnFocus; + BOOL _selectTextOnFocus; + 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 = UIEdgeInsetsMake(_contentInset.top - 5, _contentInset.left - 4, + _contentInset.bottom, _contentInset.right); + + [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; + [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + _textView.font = _font; + [self _setupPlaceholder]; +} + +- (void)setTextColor:(UIColor *)textColor +{ + _textView.textColor = textColor; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + _placeholder = placeholder; + [self _setupPlaceholder]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + if (placeholderTextColor) { + _placeholderTextColor = placeholderTextColor; + } else { + _placeholderTextColor = [self defaultPlaceholderTextColor]; + } + + [self _setupPlaceholder]; +} + +- (void)setClearTextOnFocus:(BOOL)clearTextOnFocus +{ + _clearTextOnFocus = clearTextOnFocus; +} + +- (void)_setupPlaceholder +{ + [_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)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); +} + +- (void)setEditable:(BOOL)editable +{ + _textView.editable = editable; +} + +- (BOOL)autoCorrect +{ + return _textView.autocorrectionType == UITextAutocorrectionTypeYes; +} + +- (void)setKeyboardType:(UIKeyboardType)keyboardType +{ + _textView.keyboardType = keyboardType; +} + +- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType +{ + _textView.returnKeyType = returnKeyType; +} + +- (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically +{ + _textView.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically; +} + +- (void)setAutocapitalizationType:(BOOL)autocapitalizationType +{ + _textView.autocapitalizationType = autocapitalizationType; +} + +- (void)setSelectTextOnFocus:(BOOL)selectTextOnFocus +{ + _selectTextOnFocus = selectTextOnFocus; +} + +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView { + if (_selectTextOnFocus) { + dispatch_async(dispatch_get_main_queue(), ^{ + [textView selectAll:nil]; + }); + } + return YES; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + if (_clearTextOnFocus) { + [_textView setText:@""]; + _textView.text = @""; + [self _setPlaceholderVisibility]; + } + + [_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 = [_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]; + } + + 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 00000000000000..f35992443f1e8c --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.h @@ -0,0 +1,15 @@ +/** + * 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 00000000000000..400f10f4bd4706 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.m @@ -0,0 +1,65 @@ +/** + * 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 "RCTTextView.h" +#import "RCTShadowView.h" +#import "RCTSparseArray.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(editable, BOOL) +RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(text, NSString) +RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) +RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) +RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) +RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) +RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, 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 f1ed77298dafdb..7c0387e88e3930 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -698,35 +698,48 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family 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 weight + if (weight) { + fontWeight = [self RCTFontWeight:weight]; + } - // Get font family - NSString *familyName = [self NSString:family] ?: RCTDefaultFontFamily; + // 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; - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - fontWeight = [traits[UIFontWeightTrait] doubleValue]; + fontWeight = RCTWeightOfFont(font); + isItalic = RCTFontIsItalic(font); + isCondensed = RCTFontIsCondensed(font); } else { // Not a valid font or family RCTLogError(@"Unrecognized font family '%@'", familyName); @@ -734,14 +747,16 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family } } - // Get font weight - if (weight) { - fontWeight = [self RCTFontWeight:weight]; + // 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; } - // Get closest match - UIFont *bestMatch = font; - CGFloat closestWeight = font ? RCTWeightOfFont(font) : INFINITY; for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == RCTFontIsItalic(match) &&