diff --git a/Examples/UIExplorer/js/TextInputExample.ios.js b/Examples/UIExplorer/js/TextInputExample.ios.js
index a845df091..1c6f47bde 100644
--- a/Examples/UIExplorer/js/TextInputExample.ios.js
+++ b/Examples/UIExplorer/js/TextInputExample.ios.js
@@ -294,6 +294,93 @@ class BlurOnSubmitExample extends React.Component {
}
}
+type SelectionExampleState = {
+ selection: {
+ start: number;
+ end?: number;
+ };
+ value: string;
+};
+
+class SelectionExample extends React.Component {
+ state: SelectionExampleState;
+
+ _textInput: any;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selection: {start: 0, end: 0},
+ value: props.value
+ };
+ }
+
+ onSelectionChange({nativeEvent: {selection}}) {
+ this.setState({selection});
+ }
+
+ getRandomPosition() {
+ var length = this.state.value.length;
+ return Math.round(Math.random() * length);
+ }
+
+ select(start, end) {
+ this._textInput.focus();
+ this.setState({selection: {start, end}});
+ }
+
+ selectRandom() {
+ var positions = [this.getRandomPosition(), this.getRandomPosition()].sort();
+ this.select(...positions);
+ }
+
+ placeAt(position) {
+ this.select(position, position);
+ }
+
+ placeAtRandom() {
+ this.placeAt(this.getRandomPosition());
+ }
+
+ render() {
+ var length = this.state.value.length;
+
+ return (
+
+ this.setState({value})}
+ onSelectionChange={this.onSelectionChange.bind(this)}
+ ref={textInput => (this._textInput = textInput)}
+ selection={this.state.selection}
+ style={this.props.style}
+ value={this.state.value}
+ />
+
+
+ selection = {JSON.stringify(this.state.selection)}
+
+
+ Place at Start (0, 0)
+
+
+ Place at End ({length}, {length})
+
+
+ Place at Random
+
+
+ Select All
+
+
+ Select Random
+
+
+
+ );
+ }
+}
+
var styles = StyleSheet.create({
page: {
paddingBottom: 300,
@@ -728,4 +815,22 @@ exports.examples = [
return ;
}
},
+ {
+ title: 'Text selection & cursor placement',
+ render: function() {
+ return (
+
+
+
+
+ );
+ }
+ }
];
diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index a92d42906..4f7942646 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -46,6 +46,10 @@ if (Platform.OS === 'android') {
}
type Event = Object;
+type Selection = {
+ start: number,
+ end?: number,
+};
const DataDetectorTypes = [
'phoneNumber',
@@ -386,6 +390,15 @@ const TextInput = React.createClass({
* @platform ios
*/
selectionState: PropTypes.instanceOf(DocumentSelectionState),
+ /**
+ * The start and end of the text input's selection. Set start and end to
+ * the same value to position the cursor.
+ * @platform ios
+ */
+ selection: PropTypes.shape({
+ start: PropTypes.number.isRequired,
+ end: PropTypes.number,
+ }),
/**
* The value to show for the text input. `TextInput` is a controlled
* component, which means the native value will be forced to match this
@@ -493,7 +506,7 @@ const TextInput = React.createClass({
*/
isFocused: function(): boolean {
return TextInputState.currentlyFocusedField() ===
- ReactNative.findNodeHandle(this.refs.input);
+ ReactNative.findNodeHandle(this._inputRef);
},
contextTypes: {
@@ -501,8 +514,10 @@ const TextInput = React.createClass({
focusEmitter: React.PropTypes.instanceOf(EventEmitter),
},
+ _inputRef: (undefined: any),
_focusSubscription: (undefined: ?Function),
_lastNativeText: (undefined: ?string),
+ _lastNativeSelection: (undefined: ?Selection),
componentDidMount: function() {
this._lastNativeText = this.props.value;
@@ -563,22 +578,20 @@ const TextInput = React.createClass({
this.props.defaultValue;
},
+ _setNativeRef: function(ref: any) {
+ this._inputRef = ref;
+ },
+
_renderIOS: function() {
var textContainer;
- var onSelectionChange;
- if (this.props.selectionState || this.props.onSelectionChange) {
- onSelectionChange = (event: Event) => {
- if (this.props.selectionState) {
- var selection = event.nativeEvent.selection;
- this.props.selectionState.update(selection.start, selection.end);
- }
- this.props.onSelectionChange && this.props.onSelectionChange(event);
- };
- }
-
var props = Object.assign({}, this.props);
props.style = [styles.input, this.props.style];
+
+ if (props.selection && props.selection.end == null) {
+ props.selection = {start: props.selection.start, end: props.selection.start};
+ }
+
if (!props.multiline) {
if (__DEV__) {
for (var propKey in onlyMultiline) {
@@ -592,12 +605,12 @@ const TextInput = React.createClass({
}
textContainer =
;
@@ -617,14 +630,14 @@ const TextInput = React.createClass({
}
textContainer =
{
- if (this.props.selectionState) {
- var selection = event.nativeEvent.selection;
- this.props.selectionState.update(selection.start, selection.end);
- }
- this.props.onSelectionChange && this.props.onSelectionChange(event);
- };
- }
-
const props = Object.assign({}, this.props);
props.style = [this.props.style];
props.autoCapitalize =
@@ -675,13 +677,13 @@ const TextInput = React.createClass({
const textContainer =
0) {
+ this._inputRef.setNativeProps(nativeProps);
+ }
+
+ if (this.props.selectionState && selection) {
+ this.props.selectionState.update(selection.start, selection.end);
}
},
diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj
index 3e9af6731..46959c630 100644
--- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj
+++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; };
1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 1362F0FD1B4D51F400E06D8C /* RCTTextField.m */; };
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */; };
+ 19FC5C851D41A4120090108F /* RCTTextSelection.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FC5C841D41A4120090108F /* RCTTextSelection.m */; };
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; };
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; };
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; };
@@ -39,6 +40,8 @@
1362F0FD1B4D51F400E06D8C /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = ""; };
1362F0FE1B4D51F400E06D8C /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = ""; };
1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = ""; };
+ 19FC5C841D41A4120090108F /* RCTTextSelection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextSelection.m; sourceTree = ""; };
+ 19FC5C861D41A4220090108F /* RCTTextSelection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextSelection.h; sourceTree = ""; };
58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; };
58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; };
58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; };
@@ -66,6 +69,8 @@
58B511921A9E6C1200147676 = {
isa = PBXGroup;
children = (
+ 19FC5C861D41A4220090108F /* RCTTextSelection.h */,
+ 19FC5C841D41A4120090108F /* RCTTextSelection.m */,
58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */,
58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */,
58B511C81A9E6C5C00147676 /* RCTShadowRawText.h */,
@@ -157,6 +162,7 @@
58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */,
131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */,
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */,
+ 19FC5C851D41A4120090108F /* RCTTextSelection.m in Sources */,
1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */,
58B512161A9E6EFF00147676 /* RCTText.m in Sources */,
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */,
diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m
index 71859ad4d..50b85241f 100644
--- a/Libraries/Text/RCTTextField.m
+++ b/Libraries/Text/RCTTextField.m
@@ -12,6 +12,7 @@
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTUtils.h"
+#import "RCTTextSelection.h"
#import "UIView+React.h"
@implementation RCTTextField
@@ -28,7 +29,6 @@
if ((self = [super initWithFrame:CGRectZero])) {
RCTAssert(eventDispatcher, @"eventDispatcher is a required parameter");
_eventDispatcher = eventDispatcher;
- _previousSelectionRange = self.selectedTextRange;
[self addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[self addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin];
[self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
@@ -64,6 +64,26 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
[super paste:sender];
}
+- (void)setSelection:(RCTTextSelection *)selection
+{
+ if (!selection) {
+ return;
+ }
+
+ UITextRange *currentSelection = self.selectedTextRange;
+ UITextPosition *start = [self positionFromPosition:self.beginningOfDocument offset:selection.start];
+ UITextPosition *end = [self positionFromPosition:self.beginningOfDocument offset:selection.end];
+ UITextRange *selectedTextRange = [self textRangeFromPosition:start toPosition:end];
+
+ NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
+ if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
+ _previousSelectionRange = selectedTextRange;
+ self.selectedTextRange = selectedTextRange;
+ } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
+ RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
+ }
+}
+
- (void)setText:(NSString *)text
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
@@ -173,16 +193,19 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
- (void)textFieldBeginEditing
{
- if (_selectTextOnFocus) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self selectAll:nil];
- });
- }
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
text:self.text
key:nil
eventCount:_nativeEventCount];
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (self->_selectTextOnFocus) {
+ [self selectAll:nil];
+ }
+
+ [self sendSelectionEvent];
+ });
}
- (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m
index 87329492a..c1d965509 100644
--- a/Libraries/Text/RCTTextFieldManager.m
+++ b/Libraries/Text/RCTTextFieldManager.m
@@ -79,6 +79,7 @@ RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL)
RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
+RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode)
diff --git a/Libraries/Text/RCTTextSelection.h b/Libraries/Text/RCTTextSelection.h
new file mode 100644
index 000000000..dfb207187
--- /dev/null
+++ b/Libraries/Text/RCTTextSelection.h
@@ -0,0 +1,28 @@
+/**
+ * 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 "RCTConvert.h"
+
+/**
+ * Object containing information about a TextInput's selection.
+ */
+@interface RCTTextSelection : NSObject
+
+@property (nonatomic, assign, readonly) NSInteger start;
+@property (nonatomic, assign, readonly) NSInteger end;
+
+- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end;
+
+@end
+
+@interface RCTConvert (RCTTextSelection)
+
++ (RCTTextSelection *)RCTTextSelection:(id)json;
+
+@end
diff --git a/Libraries/Text/RCTTextSelection.m b/Libraries/Text/RCTTextSelection.m
new file mode 100644
index 000000000..f37d00662
--- /dev/null
+++ b/Libraries/Text/RCTTextSelection.m
@@ -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 "RCTTextSelection.h"
+
+@implementation RCTTextSelection
+
+- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end
+{
+ if (self = [super init]) {
+ _start = start;
+ _end = end;
+ }
+ return self;
+}
+
+@end
+
+@implementation RCTConvert (RCTTextSelection)
+
++ (RCTTextSelection *)RCTTextSelection:(id)json
+{
+ if ([json isKindOfClass:[NSDictionary class]]) {
+ NSInteger start = [self NSInteger:json[@"start"]];
+ NSInteger end = [self NSInteger:json[@"end"]];
+ return [[RCTTextSelection alloc] initWithStart:start end:end];
+ }
+
+ return nil;
+}
+
+@end
diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m
index 3275a9e6f..1125084b2 100644
--- a/Libraries/Text/RCTTextView.m
+++ b/Libraries/Text/RCTTextView.m
@@ -14,6 +14,7 @@
#import "RCTShadowText.h"
#import "RCTText.h"
#import "RCTUtils.h"
+#import "RCTTextSelection.h"
#import "UIView+React.h"
@interface RCTUITextView : UITextView
@@ -103,8 +104,6 @@
_scrollView.scrollsToTop = NO;
[_scrollView addSubview:_textView];
- _previousSelectionRange = _textView.selectedTextRange;
-
[self addSubview:_scrollView];
}
return self;
@@ -452,6 +451,26 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
return _textView.text;
}
+- (void)setSelection:(RCTTextSelection *)selection
+{
+ if (!selection) {
+ return;
+ }
+
+ UITextRange *currentSelection = _textView.selectedTextRange;
+ UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start];
+ UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end];
+ UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end];
+
+ NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
+ if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) {
+ _previousSelectionRange = selectedTextRange;
+ _textView.selectedTextRange = selectedTextRange;
+ } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
+ RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
+ }
+}
+
- (void)setText:(NSString *)text
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m
index e9e4c1d31..0166cc622 100644
--- a/Libraries/Text/RCTTextViewManager.m
+++ b/Libraries/Text/RCTTextViewManager.m
@@ -45,6 +45,7 @@ RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType)
RCT_REMAP_VIEW_PROPERTY(secureTextEntry, textView.secureTextEntry, BOOL)
RCT_REMAP_VIEW_PROPERTY(selectionColor, tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RCTTextView)
{