diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js
index df0407bdb..c7e196222 100644
--- a/Examples/UIExplorer/TextInputExample.ios.js
+++ b/Examples/UIExplorer/TextInputExample.ios.js
@@ -120,6 +120,60 @@ class RewriteExample extends React.Component {
}
}
+class TokenizedTextExample extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {text: 'Hello #World'};
+ }
+ render() {
+
+ //define delimiter
+ let delimiter = /\s+/;
+
+ //split string
+ let _text = this.state.text;
+ let token, index, parts = [];
+ while (_text) {
+ delimiter.lastIndex = 0;
+ token = delimiter.exec(_text);
+ if (token === null) {
+ break;
+ }
+ index = token.index;
+ if (token[0].length === 0) {
+ index = 1;
+ }
+ parts.push(_text.substr(0, index));
+ parts.push(token[0]);
+ index = index + token[0].length;
+ _text = _text.slice(index);
+ }
+ parts.push(_text);
+
+ //highlight hashtags
+ parts = parts.map((text) => {
+ if (/^#/.test(text)) {
+ return {text};
+ } else {
+ return text;
+ }
+ });
+
+ return (
+
+ {
+ this.setState({text});
+ }}>
+ {parts}
+
+
+ );
+ }
+}
+
var BlurOnSubmitExample = React.createClass({
focusNextField(nextField) {
this.refs[nextField].focus()
@@ -232,6 +286,10 @@ var styles = StyleSheet.create({
textAlign: 'right',
width: 24,
},
+ hashtag: {
+ color: 'blue',
+ fontWeight: 'bold',
+ },
});
exports.displayName = (undefined: ?string);
@@ -498,6 +556,12 @@ exports.examples = [
);
}
},
+ {
+ title: 'Attributed text',
+ render: function() {
+ return ;
+ }
+ },
{
title: 'Blur on submit',
render: function(): ReactElement { return ; },
diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m
index 1080b4d92..7ad01b0ce 100644
--- a/Libraries/Text/RCTTextView.m
+++ b/Libraries/Text/RCTTextView.m
@@ -11,6 +11,7 @@
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
+#import "RCTText.h"
#import "RCTUtils.h"
#import "UIView+React.h"
@@ -38,6 +39,10 @@
UITextView *_placeholderView;
UITextView *_textView;
NSInteger _nativeEventCount;
+ RCTText *_richTextView;
+ NSAttributedString *_pendingAttributedText;
+ NSMutableArray *> *_subviews;
+ BOOL _blockTextShouldChange;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@@ -53,6 +58,8 @@
_textView.backgroundColor = [UIColor clearColor];
_textView.scrollsToTop = NO;
_textView.delegate = self;
+
+ _subviews = [NSMutableArray new];
[self addSubview:_textView];
}
return self;
@@ -61,6 +68,90 @@
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
+- (NSArray *> *)reactSubviews
+{
+ return _subviews;
+}
+
+- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
+{
+ if ([subview isKindOfClass:[RCTText class]]) {
+ if (_richTextView) {
+ RCTLogError(@"Tried to insert a second into - there can only be one.");
+ }
+ _richTextView = (RCTText *)subview;
+ [_subviews insertObject:_richTextView atIndex:index];
+ } else {
+ [_subviews insertObject:subview atIndex:index];
+ [self insertSubview:subview atIndex:index];
+ }
+}
+
+- (void)removeReactSubview:(UIView *)subview
+{
+ if (_richTextView == subview) {
+ [_subviews removeObject:_richTextView];
+ _richTextView = nil;
+ } else {
+ [_subviews removeObject:subview];
+ [subview removeFromSuperview];
+ }
+}
+
+- (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount
+{
+ _mostRecentEventCount = mostRecentEventCount;
+
+ // Props are set after uiBlockToAmendWithShadowViewRegistry, which means that
+ // at the time performTextUpdate is called, _mostRecentEventCount will be
+ // behind _eventCount, with the result that performPendingTextUpdate will do
+ // nothing. For that reason we call it again here after mostRecentEventCount
+ // has been set.
+ [self performPendingTextUpdate];
+}
+
+- (void)performTextUpdate
+{
+ if (_richTextView) {
+ _pendingAttributedText = _richTextView.textStorage;
+ [self performPendingTextUpdate];
+ } else if (!self.text) {
+ _textView.attributedText = nil;
+ }
+}
+
+- (void)performPendingTextUpdate
+{
+ if (!_pendingAttributedText || _mostRecentEventCount < _nativeEventCount) {
+ return;
+ }
+
+ if ([_textView.attributedText isEqualToAttributedString:_pendingAttributedText]) {
+ _pendingAttributedText = nil; // Don't try again.
+ return;
+ }
+
+ // When we update the attributed text, there might be pending autocorrections
+ // that will get accepted by default. In order for this to not garble our text,
+ // we temporarily block all textShouldChange events so they are not applied.
+ _blockTextShouldChange = YES;
+
+ // We compute the new selectedRange manually to make sure the cursor is at the
+ // end of the newly inserted/deleted text after update.
+ NSRange range = _textView.selectedRange;
+ CGPoint contentOffset = _textView.contentOffset;
+
+ _textView.attributedText = _pendingAttributedText;
+ _pendingAttributedText = nil;
+ _textView.selectedRange = range;
+ [_textView layoutIfNeeded];
+ _textView.contentOffset = contentOffset;
+
+ [self _setPlaceholderVisibility];
+
+ _blockTextShouldChange = NO;
+}
+
- (void)updateFrames
{
// Adjust the insets so that they are as close as possible to single-line
@@ -156,6 +247,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
+ if (_blockTextShouldChange) {
+ return NO;
+ }
+
if (textView.textWasPasted) {
textView.textWasPasted = NO;
} else {
@@ -309,9 +404,4 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22];
}
-- (void)performTextUpdate
-{
- // Not used (yet)
-}
-
@end