From c9f63ba50c8b4215ce7c1c6b00c692aac474b11d Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Sun, 10 May 2015 21:26:00 +0200 Subject: [PATCH 1/7] Reimplemented RCTTextView, added a RCTShadowTextView object and added auto height scaling to RCTTextView. --- Libraries/Components/TextInput/TextInput.js | 12 ++ Libraries/Text/RCTAttributedStringHandler.h | 38 ++++ Libraries/Text/RCTAttributedStringHandler.m | 139 +++++++++++++ Libraries/Text/RCTShadowTextView.h | 48 +++++ Libraries/Text/RCTShadowTextView.m | 183 ++++++++++++++++++ .../Text/RCTText.xcodeproj/project.pbxproj | 12 ++ Libraries/Text/RCTTextManager.m | 53 +++-- Libraries/Text/RCTTextView.h | 7 +- Libraries/Text/RCTTextView.m | 108 +++++------ Libraries/Text/RCTTextViewManager.m | 113 +++++++++-- 10 files changed, 615 insertions(+), 98 deletions(-) create mode 100644 Libraries/Text/RCTAttributedStringHandler.h create mode 100644 Libraries/Text/RCTAttributedStringHandler.m create mode 100644 Libraries/Text/RCTShadowTextView.h create mode 100644 Libraries/Text/RCTShadowTextView.m diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index c21184b7da5130..2ecb8735447cd3 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -41,6 +41,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { clearTextOnFocus: true, color: true, editable: true, + scrollEnabled: true, fontFamily: true, fontSize: true, fontStyle: true, @@ -167,6 +168,12 @@ var TextInput = React.createClass({ * If false, text is not editable. Default value is true. */ editable: PropTypes.bool, + /** + * If false, text view is not scrollable. Default value is true. + * Please set to false if your're using the auto height calculation of + * TextView. + */ + scrollEnabled: PropTypes.bool, /** * Determines which keyboard to open, e.g.`numeric`. */ @@ -484,6 +491,7 @@ var TextInput = React.createClass({ children={children} mostRecentEventCounter={this.state.mostRecentEventCounter} editable={this.props.editable} + scrollEnabled={this.props.scrollEnabled} keyboardType={keyboardType} returnKeyType={returnKeyType} enablesReturnKeyAutomatically={this.props.enablesReturnKeyAutomatically} @@ -557,6 +565,10 @@ var TextInput = React.createClass({ _onChange: function(event: Event) { if (this.props.controlled && event.nativeEvent.text !== this.props.value) { this.refs.input.setNativeProps({text: this.props.value}); + } else { + if (this.props.multiline) { + this.refs.input.setNativeProps({textUpdate: {text: event.nativeEvent.text}}); + } } this.props.onChange && this.props.onChange(event); this.props.onChangeText && this.props.onChangeText(event.nativeEvent.text); diff --git a/Libraries/Text/RCTAttributedStringHandler.h b/Libraries/Text/RCTAttributedStringHandler.h new file mode 100644 index 00000000000000..d343b4a275110b --- /dev/null +++ b/Libraries/Text/RCTAttributedStringHandler.h @@ -0,0 +1,38 @@ +/** + * 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 + +#import "RCTShadowView.h" + +/** + * The RCTAttributedStringHandler class stores attributes that can be applied to a string. + * Using the attributedString function one can generate an NSAttributedString from a NSString applying those attributes. + */ +@interface RCTAttributedStringHandler : NSObject + +@property (nonatomic, assign) NSWritingDirection writingDirection; +@property (nonatomic, strong) UIColor *textBackgroundColor; +@property (nonatomic, strong) UIColor *textColor; +@property (nonatomic, copy) NSString *fontFamily; +@property (nonatomic, assign) CGFloat fontSize; +@property (nonatomic, copy) NSString *fontWeight; +@property (nonatomic, copy) NSString *fontStyle; +@property (nonatomic, assign) BOOL isHighlighted; +@property (nonatomic, assign) CGFloat lineHeight; +@property (nonatomic, assign) NSTextAlignment textAlign; + +@property (nonatomic, strong, readonly) NSAttributedString *cachedAttributedString; + +-(instancetype)initWithShadowView:(RCTShadowView *)shadowView; + +- (NSAttributedString *)attributedString:(NSString *)stringToProcess; + +@end diff --git a/Libraries/Text/RCTAttributedStringHandler.m b/Libraries/Text/RCTAttributedStringHandler.m new file mode 100644 index 00000000000000..950b0fc3d1af8a --- /dev/null +++ b/Libraries/Text/RCTAttributedStringHandler.m @@ -0,0 +1,139 @@ +/** + * 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 "RCTAttributedStringHandler.h" + +#import "RCTConvert.h" +#import "RCTUtils.h" +#import "RCTShadowView.h" + +@implementation RCTAttributedStringHandler { + UIFont *_font; + RCTShadowView *_shadowView; +} + +-(instancetype)initWithShadowView:(RCTShadowView *)shadowView; +{ + if ((self = [super init])) { + _fontSize = NAN; + _isHighlighted = NO; + _shadowView = shadowView; + } + + return self; +} + +- (NSAttributedString *)attributedString:(NSString *)stringToProcess +{ + return [self _attributedString:stringToProcess + WithFontFamily:nil + fontSize:0 + fontWeight:nil + fontStyle:nil ]; +} + +- (NSAttributedString *)_attributedString:(NSString *)stringToProcess + WithFontFamily:(NSString *)fontFamily + fontSize:(CGFloat)fontSize + fontWeight:(NSString *)fontWeight + fontStyle:(NSString *)fontStyle + +{ + if (!stringToProcess) { + return [[NSAttributedString alloc]init]; + } + if ( _fontSize && !isnan(_fontSize)) { + fontSize = _fontSize; + } + if (_fontWeight) { + fontWeight = _fontWeight; + } + if (_fontStyle) { + fontStyle = _fontStyle; + } + if (_fontFamily) { + fontFamily = _fontFamily; + } + + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:stringToProcess]; + + if (_textColor) { + [self _addAttribute:NSForegroundColorAttributeName withValue:self.textColor toAttributedString:attributedString]; + } + if (_isHighlighted) { + [self _addAttribute:@"IsHighlightedAttributeName" withValue:@YES toAttributedString:attributedString]; + } + if (_textBackgroundColor) { + [self _addAttribute:NSBackgroundColorAttributeName withValue:self.textBackgroundColor toAttributedString:attributedString]; + } + + _font = [RCTConvert UIFont:nil withFamily:fontFamily size:@(fontSize) weight:fontWeight style:fontStyle]; + [self _addAttribute:NSFontAttributeName withValue:_font toAttributedString:attributedString]; + [self _addAttribute:@"IsHighlightedAttributeName" withValue:_shadowView.reactTag toAttributedString:attributedString]; + [self _setParagraphStyleOnAttributedString:attributedString]; + + // create a non-mutable attributedString for use by the Text system which avoids copies down the line + _cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString]; + + return _cachedAttributedString; +} + +- (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttributedString:(NSMutableAttributedString *)attributedString +{ + [attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { + if (!value) { + [attributedString addAttribute:attribute value:attributeValue range:range]; + } + }]; +} + +/* + * LineHeight works the same way line-height works in the web: if children and self have + * varying lineHeights, we simply take the max. + */ +- (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString +{ + // check if we have lineHeight set on self + __block BOOL hasParagraphStyle = NO; + if (_lineHeight || _textAlign) { + hasParagraphStyle = YES; + } + + if (!_lineHeight) { + self.lineHeight = 0.0; + } + + // check for lineHeight on each of our children, update the max as we go (in self.lineHeight) + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { + if (value) { + NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; + if ([paragraphStyle maximumLineHeight] > _lineHeight) { + self.lineHeight = [paragraphStyle maximumLineHeight]; + } + hasParagraphStyle = YES; + } + }]; + + self.textAlign = _textAlign ?: NSTextAlignmentNatural; + self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; + + // if we found anything, set it :D + if (hasParagraphStyle) { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = _textAlign; + paragraphStyle.baseWritingDirection = _writingDirection; + paragraphStyle.minimumLineHeight = _lineHeight; + paragraphStyle.maximumLineHeight = _lineHeight; + [attributedString addAttribute:NSParagraphStyleAttributeName + value:paragraphStyle + range:(NSRange){0, attributedString.length}]; + } +} + +@end diff --git a/Libraries/Text/RCTShadowTextView.h b/Libraries/Text/RCTShadowTextView.h new file mode 100644 index 00000000000000..79c5e8d22a819f --- /dev/null +++ b/Libraries/Text/RCTShadowTextView.h @@ -0,0 +1,48 @@ +/** + * 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 "RCTAttributedStringHandler.h" +#import "RCTShadowView.h" + +@interface RCTShadowTextView : RCTShadowView + +// Not exposed to JS +@property (nonatomic, copy, readonly) NSAttributedString *attributedString; +@property (nonatomic, copy, readonly) NSAttributedString *attributedPlaceholderString; + +// Used to calculate the height of the UITextView +@property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; +@property (nonatomic, strong, readonly) NSTextContainer *textContainer; + + +// Exposed to JS +// Update the text of the text field. ( Resets the current text field. ) +@property (nonatomic, copy) NSString *text; +// Updates only the value of the shadow text updateTextView is set to false. +// This is used to persist updates from TextInput.js back to the ShadowView wihtout reloading the UITextField. +- (void)setText:(NSString *)text updateTextView:(BOOL)updateTextView; +@property (nonatomic, copy) NSString *placeholder; + +// Styling Text and placeholder text. +@property (nonatomic, strong) UIColor *textColor; +@property (nonatomic, strong) UIColor *placeholderTextColor; + +@property (nonatomic, assign) NSWritingDirection writingDirection; +@property (nonatomic, strong) UIColor *textBackgroundColor; +@property (nonatomic, copy) NSString *fontFamily; +@property (nonatomic, assign) CGFloat fontSize; +@property (nonatomic, copy) NSString *fontWeight; +@property (nonatomic, copy) NSString *fontStyle; +@property (nonatomic, assign) BOOL isHighlighted; +@property (nonatomic, assign) CGFloat lineHeight; +@property (nonatomic, assign) NSTextAlignment textAlign; + +@end diff --git a/Libraries/Text/RCTShadowTextView.m b/Libraries/Text/RCTShadowTextView.m new file mode 100644 index 00000000000000..2042eda3abfe57 --- /dev/null +++ b/Libraries/Text/RCTShadowTextView.m @@ -0,0 +1,183 @@ +/** + * 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 "RCTShadowTextView.h" + +#import "RCTConvert.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +static css_dim_t RCTMeasure(void *context, float width) +{ + RCTShadowTextView *shadowTextView = (__bridge RCTShadowTextView *)context; + + NSAttributedString *attributedString = [shadowTextView attributedString]; + if (attributedString.length == 0) { + // if the text is empty the height is defined by the placeholder string. + attributedString = [shadowTextView attributedPlaceholderString]; + } + NSTextStorage *textStorage = [[NSTextStorage alloc]initWithAttributedString:attributedString]; + + NSTextStorage *previousTextStorage = shadowTextView.layoutManager.textStorage; + if (previousTextStorage) { + [previousTextStorage removeLayoutManager:shadowTextView.layoutManager]; + } + [textStorage addLayoutManager:shadowTextView.layoutManager]; + + shadowTextView.textContainer.size = CGSizeMake(isnan(width) ? CGFLOAT_MAX :width, CGFLOAT_MAX); + [shadowTextView.layoutManager ensureLayoutForTextContainer:shadowTextView.textContainer]; + + CGSize computedSize = [shadowTextView.layoutManager usedRectForTextContainer:shadowTextView.textContainer].size; + + [textStorage removeLayoutManager:shadowTextView.layoutManager]; + if (previousTextStorage) { + [previousTextStorage addLayoutManager:shadowTextView.layoutManager]; + } + + css_dim_t result; + result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width); + result.dimensions[CSS_HEIGHT] = RCTCeilPixelValue(computedSize.height); + return result; +} + + +@implementation RCTShadowTextView { + NSLayoutManager *_layoutManager; + NSTextContainer *_textContainer; + UIFont *_font; + + RCTAttributedStringHandler *_stringHandler; + RCTAttributedStringHandler *_placeholderStringHandler; +} + + +- (instancetype)init +{ + if ((self = [super init])) { + _textContainer = [[NSTextContainer alloc] init]; + _textContainer.lineBreakMode = NSLineBreakByTruncatingTail; + _textContainer.lineFragmentPadding = 0.0; + + _layoutManager = [[NSLayoutManager alloc] init]; + [_layoutManager addTextContainer:_textContainer]; + + _stringHandler = [[RCTAttributedStringHandler alloc] initWithShadowView:self]; + _placeholderStringHandler = [[RCTAttributedStringHandler alloc] initWithShadowView:self]; + _placeholderStringHandler.textColor = [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; + } + + return self; +} + +- (NSAttributedString *)attributedString +{ + if ( !([self isTextDirty] || [self isLayoutDirty]) && _stringHandler.cachedAttributedString) { + return _stringHandler.cachedAttributedString; + } + NSAttributedString *attributedString = [_stringHandler attributedString:_text]; + [self dirtyLayout]; + + return attributedString; +} + +- (NSAttributedString *)attributedPlaceholderString +{ + if (![self isTextDirty] && _placeholderStringHandler.cachedAttributedString) { + return _placeholderStringHandler.cachedAttributedString; + } + NSAttributedString *attributedString = [_placeholderStringHandler attributedString:_placeholder]; + [self dirtyLayout]; + + return attributedString; +} + + +- (void)fillCSSNode:(css_node_t *)node +{ + [super fillCSSNode:node]; + node->measure = RCTMeasure; +} + +#define RCT_TEXT_PROPERTY(setProp, ivar, type) \ +- (void)set##setProp:(type)value; \ +{ \ +ivar=value; \ +[self dirtyText]; \ +} + + +#define RCT_ATTR_STRING_PROPERTY(setProp, attrName, type) \ +- (void)set##setProp:(type)value; \ +{ \ + _stringHandler.attrName = value;\ + _placeholderStringHandler.attrName = value; \ + [self dirtyText]; \ +} \ +- (type)attrName \ +{ \ +return _stringHandler.attrName; \ +} + + +RCT_TEXT_PROPERTY(TextColor, _stringHandler.textColor, UIColor *); +- (UIColor *)textColor +{ + return _stringHandler.textColor; +} +RCT_TEXT_PROPERTY(PlaceholderTextColor, _placeholderStringHandler.textColor, UIColor *); +- (UIColor *)placeholderTextColor +{ + return _placeholderStringHandler.textColor; +} + +RCT_ATTR_STRING_PROPERTY(TextBackgroundColor, textBackgroundColor, UIColor *); +RCT_ATTR_STRING_PROPERTY(FontFamily, fontFamily, NSString *); +RCT_ATTR_STRING_PROPERTY(FontSize, fontSize, CGFloat); +RCT_ATTR_STRING_PROPERTY(FontWeight, fontWeight, NSString *); +RCT_ATTR_STRING_PROPERTY(FontStyle, fontStyle, NSString *); +RCT_ATTR_STRING_PROPERTY(LineHeight, lineHeight, CGFloat ); +RCT_ATTR_STRING_PROPERTY(TextAlign, textAlign, NSTextAlignment ); +RCT_ATTR_STRING_PROPERTY(IsHighlighted, isHighlighted, BOOL ); +RCT_ATTR_STRING_PROPERTY(WritingDirection, writingDirection, NSWritingDirection); + + +- (void)setText:(NSString *)text +{ + if (![_text isEqualToString:text ]) { + _text = [text copy]; + [self dirtyLayout]; + [self dirtyText]; + } +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + if (![_placeholder isEqualToString:placeholder]) { + _placeholder = [placeholder copy]; + [self dirtyLayout]; + [self dirtyText]; + } +} + +- (void)setText:(NSString *)text updateTextView:(BOOL)updateTextView +{ + if (!_text){ + updateTextView = true; + } + if (![_text isEqualToString:text ]) { + _text = [text copy]; + [self dirtyLayout]; + if (updateTextView) { + [self dirtyText]; + } + } +} + +@end + diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 224c7e6b97fa7e..d2390c9b56e393 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -14,6 +14,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 */; }; + B87780921AFA36B50016FC2B /* RCTShadowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = B87780911AFA36B50016FC2B /* RCTShadowTextView.m */; }; + B87780951AFA37CA0016FC2B /* RCTAttributedStringHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -44,6 +46,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 = ""; }; + B87780901AFA36B50016FC2B /* RCTShadowTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextView.h; sourceTree = ""; }; + B87780911AFA36B50016FC2B /* RCTShadowTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowTextView.m; sourceTree = ""; }; + B87780931AFA37CA0016FC2B /* RCTAttributedStringHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAttributedStringHandler.h; sourceTree = ""; }; + B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAttributedStringHandler.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -60,6 +66,10 @@ 58B511921A9E6C1200147676 = { isa = PBXGroup; children = ( + B87780931AFA37CA0016FC2B /* RCTAttributedStringHandler.h */, + B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */, + B87780901AFA36B50016FC2B /* RCTShadowTextView.h */, + B87780911AFA36B50016FC2B /* RCTShadowTextView.m */, 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */, 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */, 58B511C81A9E6C5C00147676 /* RCTShadowRawText.h */, @@ -146,9 +156,11 @@ files = ( 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */, + B87780951AFA37CA0016FC2B /* RCTAttributedStringHandler.m in Sources */, 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */, + B87780921AFA36B50016FC2B /* RCTShadowTextView.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, ); diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index ef518d20483d70..e0bef6b249ac0b 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -16,6 +16,8 @@ #import "RCTShadowText.h" #import "RCTSparseArray.h" #import "RCTText.h" +#import "RCTShadowTextView.h" +#import "RCTTextView.h" #import "UIView+React.h" @implementation RCTTextManager @@ -81,26 +83,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) } RCTSparseArray *reactTaggedAttributedStrings = [[RCTSparseArray alloc] init]; - NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; - for (NSInteger i = 0; i < [queue count]; i++) { - RCTShadowView *shadowView = queue[i]; - RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); - - if ([shadowView isKindOfClass:[RCTShadowText class]]) { - RCTShadowText *shadowText = (RCTShadowText *)shadowView; - reactTaggedAttributedStrings[shadowText.reactTag] = [shadowText attributedString]; - } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { - RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); - } else { - for (RCTShadowView *child in [shadowView reactSubviews]) { - if ([child isTextDirty]) { - [queue addObject:child]; - } - } - } - - [shadowView setTextComputed]; - } + [self _processReactSubview:rootView toTaggedAttributedStrings:reactTaggedAttributedStrings]; [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { [reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) { @@ -117,6 +100,36 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) }; } +- (BOOL)_processReactSubview:(RCTShadowView *)shadowView toTaggedAttributedStrings:(RCTSparseArray *)reactTaggedAttributedStrings +{ + BOOL textComputed = true; + RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); + + if ([shadowView isKindOfClass:[RCTShadowText class]]) { + RCTShadowText *shadowText = (RCTShadowText *)shadowView; + reactTaggedAttributedStrings[shadowText.reactTag] = [shadowText attributedString]; + } else if ([shadowView isKindOfClass:[RCTShadowTextView class]]) { + // not all text has been computed because RCTSahdowTextView has not been handled yet. + textComputed = false; + } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { + RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); + } else { + for (RCTShadowView *child in [shadowView reactSubviews]) { + if ([child isTextDirty]) { + BOOL textComputedResult = [self _processReactSubview:child toTaggedAttributedStrings:reactTaggedAttributedStrings]; + if( !textComputedResult ) { + textComputed = false; + } + } + } + } + if (textComputed) { + [shadowView setTextComputed]; + } + + return textComputed; +} + - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView { NSNumber *reactTag = shadowView.reactTag; diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 19f2fea397b8c5..845385f5c54503 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -16,13 +16,16 @@ @interface RCTTextView : RCTView +// exposed to JS @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) BOOL clearTextOnFocus; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; -@property (nonatomic, strong) UIColor *placeholderTextColor; -@property (nonatomic, assign) UIFont *font; + +// Not exposed to JS +@property (nonatomic, copy) NSAttributedString *attributedText; +@property (nonatomic, copy) NSAttributedString *attributedPlaceholderText; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index c5947f317e0bd8..0174888c06ad54 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -18,7 +18,7 @@ @implementation RCTTextView { RCTEventDispatcher *_eventDispatcher; BOOL _jsRequestingFirstResponder; - NSString *_placeholder; + NSAttributedString *_attributedPlacerholderText; UITextView *_placeholderView; UITextView *_textView; } @@ -28,9 +28,10 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher if ((self = [super initWithFrame:CGRectZero])) { _contentInset = UIEdgeInsetsZero; _eventDispatcher = eventDispatcher; - _placeholderTextColor = [self defaultPlaceholderTextColor]; _textView = [[UITextView alloc] initWithFrame:self.bounds]; + _textView.textContainer.lineFragmentPadding = 0.0; + _textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0 ); _textView.backgroundColor = [UIColor clearColor]; _textView.delegate = self; [self addSubview:_textView]; @@ -39,15 +40,48 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher return self; } +- (NSAttributedString *)attributedText +{ + return [_textView.attributedText copy]; +} + +- (NSAttributedString *)attributedPlaceholderText +{ + return _attributedPlacerholderText; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + // save the cursors current location and disable scrolling -> otherwise UITextView will jump around. + BOOL oldScrollEnabled = _textView.scrollEnabled; + _textView.scrollEnabled = NO; + UITextRange *range = _textView.selectedTextRange; + + _textView.attributedText = attributedText; + + _textView.scrollEnabled = oldScrollEnabled; + _textView.selectedTextRange = range;//you keep before + + [self updatePlaceholder]; + [self setNeedsDisplay]; +} + +- (void)setAttributedPlaceholderText:(NSAttributedString *)attributedPlaceholderText +{ + _attributedPlacerholderText = attributedPlaceholderText; + [self updatePlaceholder]; + [self setNeedsDisplay]; +} + - (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.top, _contentInset.left, _contentInset.bottom, _contentInset.right }; - + [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; } @@ -57,48 +91,19 @@ - (void)updatePlaceholder [_placeholderView removeFromSuperview]; _placeholderView = nil; - if (_placeholder) { + if (_attributedPlacerholderText) { _placeholderView = [[UITextView alloc] initWithFrame:self.bounds]; + _placeholderView.textContainer.lineFragmentPadding = 0.0; + _placeholderView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0 ); _placeholderView.backgroundColor = [UIColor clearColor]; _placeholderView.scrollEnabled = false; - _placeholderView.attributedText = - [[NSAttributedString alloc] initWithString:_placeholder attributes:@{ - NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]), - NSForegroundColorAttributeName : _placeholderTextColor - }]; - + _placeholderView.attributedText = [self attributedPlaceholderText]; + [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 { @@ -106,21 +111,18 @@ - (void)setContentInset:(UIEdgeInsets)contentInset [self updateFrames]; } -- (void)setText:(NSString *)text -{ - if (![text isEqualToString:_textView.text]) { - [_textView setText:text]; - [self _setPlaceholderVisibility]; - } -} - - (void)_setPlaceholderVisibility { - if (_textView.text.length > 0) { + BOOL _placeholderViewWasHidden = _placeholderView.isHidden; + if (_textView.attributedText.length > 0) { [_placeholderView setHidden:YES]; } else { [_placeholderView setHidden:NO]; } + + if (_placeholderViewWasHidden != _placeholderView.isHidden) { + [self setNeedsDisplay]; + } } - (void)setAutoCorrect:(BOOL)autoCorrect @@ -146,8 +148,7 @@ - (BOOL)textViewShouldBeginEditing:(UITextView *)textView - (void)textViewDidBeginEditing:(UITextView *)textView { if (_clearTextOnFocus) { - [_textView setText:@""]; - _textView.text = @""; + _textView.attributedText = [_textView.attributedText attributedSubstringFromRange:NSMakeRange(0, 0)]; [self _setPlaceholderVisibility]; } @@ -203,14 +204,5 @@ - (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.m b/Libraries/Text/RCTTextViewManager.m index 570a511157bee5..12d4f88f1a14e5 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -14,6 +14,9 @@ #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTTextView.h" +#import "RCTShadowText.h" + +#import "RCTShadowTextView.h" @implementation RCTTextViewManager @@ -24,41 +27,115 @@ - (UIView *)view return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } +- (RCTShadowView *)shadowView +{ + return [[RCTShadowTextView alloc] init]; +} + +// Data string properties +RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString) +RCT_EXPORT_SHADOW_PROPERTY(text, NSString) +RCT_CUSTOM_SHADOW_PROPERTY(textUpdate, NSString, RCTShadowTextView) +{ + [view setText:json[@"text"] updateTextView:json[@"updateText"]? json[@"updateText"]: false ]; +} + +// UITextView specific properties RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL) -RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) -RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) -RCT_EXPORT_VIEW_PROPERTY(text, NSString) +RCT_REMAP_VIEW_PROPERTY(scrollEnabled, textView.scrollEnabled, BOOL ) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) -RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.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) + +// Shadow View properties +RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) +RCT_REMAP_SHADOW_PROPERTY(color, textColor, UIColor ) +RCT_EXPORT_SHADOW_PROPERTY(placeholderTextColor, UIColor) +RCT_EXPORT_SHADOW_PROPERTY(fontFamily, NSString) +RCT_EXPORT_SHADOW_PROPERTY(fontSize, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(fontWeight, NSString) +RCT_EXPORT_SHADOW_PROPERTY(fontStyle, NSString) +RCT_EXPORT_SHADOW_PROPERTY(isHighlighted, BOOL) +RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment) +RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textBackgroundColor, UIColor) + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { - view.font = [RCTConvert UIFont:view.font withStyle:json]; // defaults to normal + NSMutableArray *uiBlocks = [NSMutableArray new]; + + for (RCTShadowView *rootView in shadowViewRegistry.allObjects) { + if (![rootView isReactRootView]) { + // This isn't a root view + continue; + } + + if (![rootView isTextDirty]) { + // No text processing to be done + continue; + } + + RCTSparseArray *reactTaggedAttributedStrings = [[RCTSparseArray alloc] init]; + [self _processReactSubview:rootView toTaggedAttributedStrings:reactTaggedAttributedStrings]; + + [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + [reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSDictionary *attributedStringDic, NSNumber *reactTag, BOOL *stop) { + RCTTextView *text = viewRegistry[reactTag]; + text.attributedText = attributedStringDic[@"attributedText"]; + text.attributedPlaceholderText = attributedStringDic[@"attributedPlaceholderText"]; + }]; + }]; + } + + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (RCTViewManagerUIBlock shadowBlock in uiBlocks) { + shadowBlock(uiManager, viewRegistry); + } + }; } -RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) + +- (BOOL)_processReactSubview:(RCTShadowView *)shadowView toTaggedAttributedStrings:(RCTSparseArray *)reactTaggedAttributedStrings { - view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; + BOOL textComputed = true; + RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); + + if ([shadowView isKindOfClass:[RCTShadowTextView class]]) { + RCTShadowTextView *shadowTextView = (RCTShadowTextView *)shadowView; + reactTaggedAttributedStrings[shadowTextView.reactTag] = @{ + @"attributedText": [shadowTextView attributedString], + @"attributedPlaceholderText": [shadowTextView attributedPlaceholderString] + }; + + } else if ([shadowView isKindOfClass:[RCTShadowText class]]) { + // not all text has been computed because a RCTShadowText has not been handled yet. + textComputed = false; + } else { + for (RCTShadowView *child in [shadowView reactSubviews]) { + if ([child isTextDirty]) { + BOOL textComputedResult = [self _processReactSubview:child toTaggedAttributedStrings:reactTaggedAttributedStrings]; + if( !textComputedResult ) { + textComputed = false; + } + } + } + } + if (textComputed) { + [shadowView setTextComputed]; + } + return textComputed; } -- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowTextView *)shadowView { NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - ((RCTTextView *)viewRegistry[reactTag]).contentInset = padding; + RCTTextView *textView = viewRegistry[reactTag]; + textView.contentInset = padding; }; } From 9aa26a03f59517e107bc8e73c395d14337e677e4 Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Sun, 10 May 2015 21:27:40 +0200 Subject: [PATCH 2/7] Updated the UIExplorer example to include the new auto height scaling feature. --- Examples/UIExplorer/TextInputExample.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 922dd9607d4b88..ac50209fd50fcf 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -108,6 +108,14 @@ var styles = StyleSheet.create({ fontFamily: 'Cochin', height: 60, }, + multielineWithoutHeight: { + borderWidth: 0.5, + borderColor: '#0f0f0f', + flex: 1, + fontSize: 13, + padding: 4, + marginBottom: 4, + }, multilineChild: { width: 50, height: 40, @@ -386,6 +394,14 @@ exports.examples = [ style={styles.multiline}> + + ) } From eaa89a24f25d9f4ca81378eb5bce0a3e1cdf3c2d Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Tue, 12 May 2015 09:53:23 +0200 Subject: [PATCH 3/7] Bug fix: empty initialized text field does not have correct styling. --- Libraries/Text/RCTShadowTextView.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/Text/RCTShadowTextView.m b/Libraries/Text/RCTShadowTextView.m index 2042eda3abfe57..a41208ba9a2e99 100644 --- a/Libraries/Text/RCTShadowTextView.m +++ b/Libraries/Text/RCTShadowTextView.m @@ -54,6 +54,7 @@ @implementation RCTShadowTextView { RCTAttributedStringHandler *_stringHandler; RCTAttributedStringHandler *_placeholderStringHandler; + BOOL _textHasBeenSetOnce; } @@ -70,6 +71,7 @@ - (instancetype)init _stringHandler = [[RCTAttributedStringHandler alloc] initWithShadowView:self]; _placeholderStringHandler = [[RCTAttributedStringHandler alloc] initWithShadowView:self]; _placeholderStringHandler.textColor = [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; + _textHasBeenSetOnce = false; } return self; @@ -167,8 +169,9 @@ - (void)setPlaceholder:(NSString *)placeholder - (void)setText:(NSString *)text updateTextView:(BOOL)updateTextView { - if (!_text){ + if (!_textHasBeenSetOnce) { updateTextView = true; + _textHasBeenSetOnce = true; } if (![_text isEqualToString:text ]) { _text = [text copy]; From 6b5864ad5d231bf802916a25bd9c1d6bfbd1154f Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Mon, 8 Jun 2015 11:14:40 +0200 Subject: [PATCH 4/7] Bugfix: RCTTextView now displays correctly styled, even if the initial value is empty. --- ...tributedString+EmptyStringWithAttributes.h | 30 +++++++++++++++++++ ...tributedString+EmptyStringWithAttributes.m | 29 ++++++++++++++++++ Libraries/Text/RCTShadowTextView.m | 15 ++++++++-- .../Text/RCTText.xcodeproj/project.pbxproj | 6 ++++ Libraries/Text/RCTTextView.m | 10 ++++++- 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 Libraries/Text/NSAttributedString+EmptyStringWithAttributes.h create mode 100644 Libraries/Text/NSAttributedString+EmptyStringWithAttributes.m diff --git a/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.h b/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.h new file mode 100644 index 00000000000000..253d5b6b44a85d --- /dev/null +++ b/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.h @@ -0,0 +1,30 @@ +/** + * 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 + +/** + * Problem: The NSAttributedString can not store Attributes if the String is empty, because every Attribute is associated with a certain NSRange in the string. That's why an empty String can not store any attributes, for there is not a single valid range. This results in two problems, when we're dealing with an empty string: + - RCTMeasure function in RCTShadowTextView can not calculate the height correctly, because the Attributes are not set for an empty string. + - The UITextView will not be displayed in the correct height: i.e. the cursor will always be the default height and not the size set by the Font size. + + Solution: The NSAttributedString can never be empty if we want to store our Attributes in the String. That's why RCTShadowTextView will create a NSString only containing one letter if _text is empty before passing it to the RCTAttributedStringHanlder. Also it's sets the isEmptyStringWithAttributes variable to true, so other componenets may check if the value of the string is really meant to be displayed or just so we can store the Text Attributes somehow. + + Problems Solved: + - RCTMeasure works correctly because we always calculate with a non empty string. + - UITextView works correctly because we can check for the isEmptyStingWithAttributes variable in RCTTextView and copy the attributes of the NSAttributedString into UITextViews typingAttributes variable. + + Conclusion: + I am aware that this may not be the most elegant solution :/ . If every one comes up with a better idea, please contact me on twitter: @lukasreichart or open an issue on github. + */ +@interface NSAttributedString (EmptyStringWithAttributes) + +@property (nonatomic, assign) BOOL isEmptyStringWithAttributes; + +@end diff --git a/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.m b/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.m new file mode 100644 index 00000000000000..b11d447e04ab3d --- /dev/null +++ b/Libraries/Text/NSAttributedString+EmptyStringWithAttributes.m @@ -0,0 +1,29 @@ +/** + * 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 "NSAttributedString+EmptyStringWithAttributes.h" + +@implementation NSAttributedString (EmptyStringWithAttributes) + +- (BOOL)isEmptyStringWithAttributes +{ + NSNumber *value = objc_getAssociatedObject(self, @selector(isEmptyStringWithAttributes)); + if (value) { + return [value boolValue]; + } + return false; +} + +- (void)setIsEmptyStringWithAttributes:(BOOL)isEmptyStringWithAttributes +{ + objc_setAssociatedObject(self, @selector(isEmptyStringWithAttributes), [NSNumber numberWithBool:isEmptyStringWithAttributes], OBJC_ASSOCIATION_ASSIGN); +} + +@end diff --git a/Libraries/Text/RCTShadowTextView.m b/Libraries/Text/RCTShadowTextView.m index a41208ba9a2e99..a6fa353f07c598 100644 --- a/Libraries/Text/RCTShadowTextView.m +++ b/Libraries/Text/RCTShadowTextView.m @@ -12,6 +12,7 @@ #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" +#import "NSAttributedString+EmptyStringWithAttributes.h" static css_dim_t RCTMeasure(void *context, float width) { @@ -82,10 +83,20 @@ - (NSAttributedString *)attributedString if ( !([self isTextDirty] || [self isLayoutDirty]) && _stringHandler.cachedAttributedString) { return _stringHandler.cachedAttributedString; } - NSAttributedString *attributedString = [_stringHandler attributedString:_text]; + + // Never pass an empty string to the _stringHandler. + NSString *stringToProcess = _text; + BOOL isEmptyStringWithAttributes = false; + if ( !(_text && _text.length )) { + stringToProcess = @"A"; + isEmptyStringWithAttributes = true; + } + + [_stringHandler attributedString:stringToProcess]; [self dirtyLayout]; - return attributedString; + [_stringHandler.cachedAttributedString setIsEmptyStringWithAttributes:isEmptyStringWithAttributes]; + return _stringHandler.cachedAttributedString; } - (NSAttributedString *)attributedPlaceholderString diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index d2390c9b56e393..396c5d31b5a0f7 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; }; B87780921AFA36B50016FC2B /* RCTShadowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = B87780911AFA36B50016FC2B /* RCTShadowTextView.m */; }; B87780951AFA37CA0016FC2B /* RCTAttributedStringHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */; }; + B8C3BF4C1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = B8C3BF4B1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,6 +51,8 @@ B87780911AFA36B50016FC2B /* RCTShadowTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowTextView.m; sourceTree = ""; }; B87780931AFA37CA0016FC2B /* RCTAttributedStringHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAttributedStringHandler.h; sourceTree = ""; }; B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAttributedStringHandler.m; sourceTree = ""; }; + B8C3BF4A1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+EmptyStringWithAttributes.h"; sourceTree = ""; }; + B8C3BF4B1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+EmptyStringWithAttributes.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -66,6 +69,8 @@ 58B511921A9E6C1200147676 = { isa = PBXGroup; children = ( + B8C3BF4A1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.h */, + B8C3BF4B1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.m */, B87780931AFA37CA0016FC2B /* RCTAttributedStringHandler.h */, B87780941AFA37CA0016FC2B /* RCTAttributedStringHandler.m */, B87780901AFA36B50016FC2B /* RCTShadowTextView.h */, @@ -163,6 +168,7 @@ B87780921AFA36B50016FC2B /* RCTShadowTextView.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, + B8C3BF4C1B25860600BB4D5C /* NSAttributedString+EmptyStringWithAttributes.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 0174888c06ad54..e0e2473226ea85 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -8,6 +8,7 @@ */ #import "RCTTextView.h" +#import "NSAttributedString+EmptyStringWithAttributes.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" @@ -57,7 +58,14 @@ - (void)setAttributedText:(NSAttributedString *)attributedText _textView.scrollEnabled = NO; UITextRange *range = _textView.selectedTextRange; - _textView.attributedText = attributedText; + // Check if we should really display the NSAttributedString's value or this is in fact an empty string. + if (attributedText.isEmptyStringWithAttributes) { + NSRange range = NSMakeRange(0, 1); + _textView.typingAttributes = [attributedText attributesAtIndex:0 effectiveRange:&range]; + _textView.attributedText = [[NSAttributedString alloc]init]; + } else { + _textView.attributedText = attributedText; + } _textView.scrollEnabled = oldScrollEnabled; _textView.selectedTextRange = range;//you keep before From 31ce41279497f47e9a61e3c3c5175781c3172d91 Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Mon, 8 Jun 2015 11:41:09 +0200 Subject: [PATCH 5/7] Added numberOfLines property to RCTTextView. --- Libraries/Components/TextInput/TextInput.js | 4 ++++ Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 26 +++++++++++++++++++++ Libraries/Text/RCTTextViewManager.m | 1 + 4 files changed, 32 insertions(+) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 2ecb8735447cd3..f3b53bb36f9fa5 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -46,6 +46,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { fontSize: true, fontStyle: true, fontWeight: true, + numberOfLines: true, keyboardType: true, returnKeyType: true, enablesReturnKeyAutomatically: true, @@ -69,6 +70,8 @@ var onlyMultiline = { onSelectionChange: true, onTextInput: true, children: true, + scrollEnabled: true, + numberOfLines: true }; var notMultiline = { @@ -506,6 +509,7 @@ var TextInput = React.createClass({ placeholder={this.props.placeholder} placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} + numberOfLines={this.props.numberOfLines} autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} clearButtonMode={clearButtonMode} diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 845385f5c54503..c3100321d2c901 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -22,6 +22,7 @@ @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, assign) NSUInteger maximumNumberOfLines; // Not exposed to JS @property (nonatomic, copy) NSAttributedString *attributedText; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index e0e2473226ea85..0f35cc4d15f9e7 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -143,6 +143,32 @@ - (BOOL)autoCorrect return _textView.autocorrectionType == UITextAutocorrectionTypeYes; } +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + _textView.textContainer.lineBreakMode = truncationMode; + if (_placeholderView) { + _textView.textContainer.lineBreakMode = truncationMode; + } +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + if (maximumNumberOfLines < 1) { + maximumNumberOfLines = 1; + } + + [self setTruncationMode:NSLineBreakByTruncatingTail]; + _textView.textContainer.maximumNumberOfLines = maximumNumberOfLines; + if (_placeholderView) { + _placeholderView.textContainer.maximumNumberOfLines = maximumNumberOfLines; + } +} + +- (NSUInteger)maximumNumberOfLines +{ + return _textView.textContainer.maximumNumberOfLines; +} + - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { if (_selectTextOnFocus) { diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index 12d4f88f1a14e5..984cd0d37e3576 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -50,6 +50,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) +RCT_REMAP_VIEW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger ); // Shadow View properties RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) From 39ab0a39067404cf7404b9092885de915cba3910 Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Tue, 9 Jun 2015 14:01:07 +0200 Subject: [PATCH 6/7] RCTTextView: numberOfLines is now a property of the shadow view and also working correctly with the calculation of the height. --- Libraries/Text/RCTShadowTextView.h | 3 +++ Libraries/Text/RCTShadowTextView.m | 25 +++++++++++++++++++++++++ Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 1 - Libraries/Text/RCTTextViewManager.m | 5 ++++- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Libraries/Text/RCTShadowTextView.h b/Libraries/Text/RCTShadowTextView.h index 79c5e8d22a819f..8cc7c30448b76e 100644 --- a/Libraries/Text/RCTShadowTextView.h +++ b/Libraries/Text/RCTShadowTextView.h @@ -45,4 +45,7 @@ @property (nonatomic, assign) CGFloat lineHeight; @property (nonatomic, assign) NSTextAlignment textAlign; +@property (nonatomic, assign) NSUInteger maximumNumberOfLines; +@property (nonatomic, assign) NSLineBreakMode truncationMode; + @end diff --git a/Libraries/Text/RCTShadowTextView.m b/Libraries/Text/RCTShadowTextView.m index a6fa353f07c598..deb5d63aed3afb 100644 --- a/Libraries/Text/RCTShadowTextView.m +++ b/Libraries/Text/RCTShadowTextView.m @@ -193,5 +193,30 @@ - (void)setText:(NSString *)text updateTextView:(BOOL)updateTextView } } +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + _textContainer.lineBreakMode = truncationMode; +} + +- (NSLineBreakMode)truncationMode +{ + return _textContainer.lineBreakMode; +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + if (maximumNumberOfLines < 1 ) { + maximumNumberOfLines = 1; + } + + self.truncationMode = NSLineBreakByTruncatingTail; + _textContainer.maximumNumberOfLines = maximumNumberOfLines; +} + +- (NSUInteger)maximumNumberOfLines +{ + return _textContainer.maximumNumberOfLines; +} + @end diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index c3100321d2c901..88f35f76591c6d 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -23,6 +23,7 @@ @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) NSUInteger maximumNumberOfLines; +@property (nonatomic, assign) NSLineBreakMode truncationMode; // Not exposed to JS @property (nonatomic, copy) NSAttributedString *attributedText; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 0f35cc4d15f9e7..fd3779a1f0800c 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -157,7 +157,6 @@ - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines maximumNumberOfLines = 1; } - [self setTruncationMode:NSLineBreakByTruncatingTail]; _textView.textContainer.maximumNumberOfLines = maximumNumberOfLines; if (_placeholderView) { _placeholderView.textContainer.maximumNumberOfLines = maximumNumberOfLines; diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index 984cd0d37e3576..d922bc22613534 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -50,7 +50,6 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) -RCT_REMAP_VIEW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger ); // Shadow View properties RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) @@ -64,6 +63,7 @@ - (RCTShadowView *)shadowView RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment) RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textBackgroundColor, UIColor) +RCT_REMAP_SHADOW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger ); - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { @@ -136,7 +136,10 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowTextView *)shado UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTTextView *textView = viewRegistry[reactTag]; + RCTShadowTextView *shadowTextView = (RCTShadowTextView *)shadowView; textView.contentInset = padding; + textView.maximumNumberOfLines = shadowTextView.maximumNumberOfLines; + textView.truncationMode = shadowTextView.truncationMode; }; } From f45f8407d7812ee42d42e8fa3fa3269479441cc4 Mon Sep 17 00:00:00 2001 From: Lukas Reichart Date: Thu, 18 Jun 2015 15:19:46 +0200 Subject: [PATCH 7/7] TextInput property numberOfLines works always correctly. --- Libraries/Text/RCTTextView.m | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index fd3779a1f0800c..a92059305af2d3 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -153,10 +153,6 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { - if (maximumNumberOfLines < 1) { - maximumNumberOfLines = 1; - } - _textView.textContainer.maximumNumberOfLines = maximumNumberOfLines; if (_placeholderView) { _placeholderView.textContainer.maximumNumberOfLines = maximumNumberOfLines;