Added rich text input support
Summary: public It is now possible to display and edit rich text inside a multiline `<textInput>` by nesting a `<Text>` node inside it. Note that this doesn't yet provide everything needed to build a full rich text editor (as there is no facility to capture or control the selected text range, or insert/remove text) but it does make it possible to apply token-based styling to text as the user types. See the 'Attributed text' example in the UIExplorer > TextInput demo for details. Reviewed By: javache Differential Revision: D2622493 fb-gh-sync-id: b6bc9a46005322c806934541966460edccb59e70
This commit is contained in:
parent
8dac41b7f0
commit
7779e06a7f
|
@ -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 style={styles.hashtag}>{text}</Text>;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextInput
|
||||
multiline={true}
|
||||
style={styles.multiline}
|
||||
onChangeText={(text) => {
|
||||
this.setState({text});
|
||||
}}>
|
||||
<Text>{parts}</Text>
|
||||
</TextInput>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 <TokenizedTextExample />;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Blur on submit',
|
||||
render: function(): ReactElement { return <BlurOnSubmitExample />; },
|
||||
|
|
|
@ -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<UIView<RCTComponent> *> *_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<UIView<RCTComponent> *> *)reactSubviews
|
||||
{
|
||||
return _subviews;
|
||||
}
|
||||
|
||||
- (void)insertReactSubview:(UIView<RCTComponent> *)subview atIndex:(NSInteger)index
|
||||
{
|
||||
if ([subview isKindOfClass:[RCTText class]]) {
|
||||
if (_richTextView) {
|
||||
RCTLogError(@"Tried to insert a second <Text> into <TextInput> - 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<RCTComponent> *)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
|
||||
|
|
Loading…
Reference in New Issue