Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TextInput - Multiline] Reimplemented RCTTextView and added auto height scaling. #1229

Closed
wants to merge 7 commits into from
16 changes: 16 additions & 0 deletions Examples/UIExplorer/TextInputExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -386,6 +394,14 @@ exports.examples = [
style={styles.multiline}>
<View style={styles.multilineChild}/>
</TextInput>
<TextInput
placeholder="multiline with height scaling"
multiline={true}
scrollEnabled={false}
enablesReturnKeyAutomatically={true}
returnKeyType="go"
style={styles.multielineWithoutHeight}>
</TextInput>
</View>
)
}
Expand Down
16 changes: 16 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, {
clearTextOnFocus: true,
color: true,
editable: true,
scrollEnabled: true,
fontFamily: true,
fontSize: true,
fontStyle: true,
fontWeight: true,
numberOfLines: true,
keyboardType: true,
returnKeyType: true,
enablesReturnKeyAutomatically: true,
Expand All @@ -68,6 +70,8 @@ var onlyMultiline = {
onSelectionChange: true,
onTextInput: true,
children: true,
scrollEnabled: true,
numberOfLines: true
};

var notMultiline = {
Expand Down Expand Up @@ -167,6 +171,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`.
*/
Expand Down Expand Up @@ -484,6 +494,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}
Expand All @@ -498,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}
Expand Down Expand Up @@ -557,6 +569,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);
Expand Down
30 changes: 30 additions & 0 deletions Libraries/Text/NSAttributedString+EmptyStringWithAttributes.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

/**
* 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
29 changes: 29 additions & 0 deletions Libraries/Text/NSAttributedString+EmptyStringWithAttributes.m
Original file line number Diff line number Diff line change
@@ -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 <objc/runtime.h>
#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
38 changes: 38 additions & 0 deletions Libraries/Text/RCTAttributedStringHandler.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#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
139 changes: 139 additions & 0 deletions Libraries/Text/RCTAttributedStringHandler.m
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions Libraries/Text/RCTShadowTextView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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 <Foundation/Foundation.h>

#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;

@property (nonatomic, assign) NSUInteger maximumNumberOfLines;
@property (nonatomic, assign) NSLineBreakMode truncationMode;

@end
Loading