Add TextInput controlled selection prop on iOS
Summary: This adds support for a controlled `selection` prop on `TextInput` on iOS (Android PR coming soon). This is based on the work by ehd in #2668 which hasn't been updated for a while, kept the original commit and worked on fixing what was missing based on the feedback in the original PR. What I changed is: - Make the prop properly controlled by JS - Add a RCTTextSelection class to map the JS object into and the corresponding RCTConvert category - Make sure the selection change event is properly triggered when the input is focused - Cleanup setSelection - Changed TextInput to use function refs to appease the linter ** Test plan ** Tested using the TextInput selection example in UIExplorer on iOS. Also tested that it doesn't break Android. Closes https://github.com/facebook/react-native/pull/8958 Differential Revision: D3771229 Pulled By: javache fbshipit-source-id: b8ede46b97fb3faf3061bb2dac102160c4b20ce7
This commit is contained in:
parent
c40fee9405
commit
f0a3c56048
|
@ -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 (
|
||||
<View>
|
||||
<TextInput
|
||||
multiline={this.props.multiline}
|
||||
onChangeText={(value) => 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}
|
||||
/>
|
||||
<View>
|
||||
<Text>
|
||||
selection = {JSON.stringify(this.state.selection)}
|
||||
</Text>
|
||||
<Text onPress={this.placeAt.bind(this, 0)}>
|
||||
Place at Start (0, 0)
|
||||
</Text>
|
||||
<Text onPress={this.placeAt.bind(this, length)}>
|
||||
Place at End ({length}, {length})
|
||||
</Text>
|
||||
<Text onPress={this.placeAtRandom.bind(this)}>
|
||||
Place at Random
|
||||
</Text>
|
||||
<Text onPress={this.select.bind(this, 0, length)}>
|
||||
Select All
|
||||
</Text>
|
||||
<Text onPress={this.selectRandom.bind(this)}>
|
||||
Select Random
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingBottom: 300,
|
||||
|
@ -728,4 +815,22 @@ exports.examples = [
|
|||
return <TokenizedTextExample />;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Text selection & cursor placement',
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<SelectionExample
|
||||
style={styles.default}
|
||||
value="text selection can be changed"
|
||||
/>
|
||||
<SelectionExample
|
||||
multiline
|
||||
style={styles.multiline}
|
||||
value={"multiline text selection\ncan also be changed"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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 =
|
||||
<RCTTextField
|
||||
ref="input"
|
||||
ref={this._setNativeRef}
|
||||
{...props}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
onChange={this._onChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onSelectionChange={this._onSelectionChange}
|
||||
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
|
||||
text={this._getText()}
|
||||
/>;
|
||||
|
@ -617,14 +630,14 @@ const TextInput = React.createClass({
|
|||
}
|
||||
textContainer =
|
||||
<RCTTextView
|
||||
ref="input"
|
||||
ref={this._setNativeRef}
|
||||
{...props}
|
||||
children={children}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
onChange={this._onChange}
|
||||
onContentSizeChange={this.props.onContentSizeChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onSelectionChange={this._onSelectionChange}
|
||||
onTextInput={this._onTextInput}
|
||||
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
|
||||
text={this._getText()}
|
||||
|
@ -647,17 +660,6 @@ const TextInput = React.createClass({
|
|||
},
|
||||
|
||||
_renderAndroid: function() {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
const props = Object.assign({}, this.props);
|
||||
props.style = [this.props.style];
|
||||
props.autoCapitalize =
|
||||
|
@ -675,13 +677,13 @@ const TextInput = React.createClass({
|
|||
|
||||
const textContainer =
|
||||
<AndroidTextInput
|
||||
ref="input"
|
||||
ref={this._setNativeRef}
|
||||
{...props}
|
||||
mostRecentEventCount={0}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
onChange={this._onChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onSelectionChange={this._onSelectionChange}
|
||||
onTextInput={this._onTextInput}
|
||||
text={this._getText()}
|
||||
children={children}
|
||||
|
@ -719,7 +721,7 @@ const TextInput = React.createClass({
|
|||
_onChange: function(event: Event) {
|
||||
// Make sure to fire the mostRecentEventCount first so it is already set on
|
||||
// native when the text value is set.
|
||||
this.refs.input.setNativeProps({
|
||||
this._inputRef.setNativeProps({
|
||||
mostRecentEventCount: event.nativeEvent.eventCount,
|
||||
});
|
||||
|
||||
|
@ -727,7 +729,7 @@ const TextInput = React.createClass({
|
|||
this.props.onChange && this.props.onChange(event);
|
||||
this.props.onChangeText && this.props.onChangeText(text);
|
||||
|
||||
if (!this.refs.input) {
|
||||
if (!this._inputRef) {
|
||||
// calling `this.props.onChange` or `this.props.onChangeText`
|
||||
// may clean up the input itself. Exits here.
|
||||
return;
|
||||
|
@ -737,14 +739,47 @@ const TextInput = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
_onSelectionChange: function(event: Event) {
|
||||
this.props.onSelectionChange && this.props.onSelectionChange(event);
|
||||
|
||||
if (!this._inputRef) {
|
||||
// calling `this.props.onSelectionChange`
|
||||
// may clean up the input itself. Exits here.
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastNativeSelection = event.nativeEvent.selection;
|
||||
|
||||
if (this.props.selection || this.props.selectionState) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function () {
|
||||
// This is necessary in case native updates the text and JS decides
|
||||
// that the update should be ignored and we should stick with the value
|
||||
// that we have in JS.
|
||||
const nativeProps = {};
|
||||
|
||||
if (this._lastNativeText !== this.props.value && typeof this.props.value === 'string') {
|
||||
this.refs.input.setNativeProps({
|
||||
text: this.props.value,
|
||||
});
|
||||
nativeProps.text = this.props.value;
|
||||
}
|
||||
|
||||
// Selection is also a controlled prop, if the native value doesn't match
|
||||
// JS, update to the JS value.
|
||||
const {selection} = this.props;
|
||||
if (this._lastNativeSelection && selection &&
|
||||
(this._lastNativeSelection.start !== selection.start ||
|
||||
this._lastNativeSelection.end !== selection.end)) {
|
||||
nativeProps.selection = this.props.selection;
|
||||
}
|
||||
|
||||
if (Object.keys(nativeProps).length > 0) {
|
||||
this._inputRef.setNativeProps(nativeProps);
|
||||
}
|
||||
|
||||
if (this.props.selectionState && selection) {
|
||||
this.props.selectionState.update(selection.start, selection.end);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
1362F0FE1B4D51F400E06D8C /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = "<group>"; };
|
||||
1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = "<group>"; };
|
||||
19FC5C841D41A4120090108F /* RCTTextSelection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextSelection.m; sourceTree = "<group>"; };
|
||||
19FC5C861D41A4220090108F /* RCTTextSelection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextSelection.h; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue