Permalink
Browse files

Open sourced the onSelectionChange event

Summary: public

Open-sourced the onSelectionChange event for RCTTextView, and also added onSelectionChange support for RCTTextField.

Reviewed By: javache

Differential Revision: D2647541

fb-gh-sync-id: ab0ab37f5f087e708a199461ffc33231a47d2133
  • Loading branch information...
nicklockwood authored and facebook-github-bot-7 committed Nov 14, 2015
1 parent 397791f commit 5a34a097f2d2324e5515e876a20fd5b7a376aa3e
@@ -31,7 +31,6 @@ var invariant = require('invariant');
var requireNativeComponent = require('requireNativeComponent');
var onlyMultiline = {
- onSelectionChange: true, // not supported in Open Source yet
onTextInput: true, // not supported in Open Source yet
children: true,
};
@@ -413,6 +412,17 @@ var TextInput = React.createClass({
_renderIOS: function() {
var textContainer;
+ var onSelectionChange;
+ if (this.props.selectionState || this.props.onSelectionChange) {
+ onSelectionChange = function(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.multiline) {
@@ -430,7 +440,8 @@ var TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
- onSelectionChangeShouldSetResponder={() => true}
+ onSelectionChange={onSelectionChange}
+ onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
text={this._getText()}
mostRecentEventCount={this.state.mostRecentEventCount}
/>;
@@ -465,7 +476,7 @@ var TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
- onSelectionChange={this._onSelectionChange}
+ onSelectionChange={onSelectionChange}
onTextInput={this._onTextInput}
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
text={this._getText()}
@@ -581,14 +592,6 @@ var TextInput = React.createClass({
}
},
- _onSelectionChange: function(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);
- },
-
_onTextInput: function(event: Event) {
this.props.onTextInput && this.props.onTextInput(event);
},
@@ -9,6 +9,8 @@
#import <UIKit/UIKit.h>
+#import "RCTComponent.h"
+
@class RCTEventDispatcher;
@interface RCTTextField : UITextField
@@ -23,6 +25,8 @@
@property (nonatomic, strong) NSNumber *maxLength;
@property (nonatomic, assign) BOOL textWasPasted;
+@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
+
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (void)textFieldDidChange;
@@ -21,22 +21,30 @@ @implementation RCTTextField
BOOL _jsRequestingFirstResponder;
NSInteger _nativeEventCount;
BOOL _submitted;
+ UITextRange *_previousSelectionRange;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
{
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];
[self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
+ [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil];
_reactSubviews = [NSMutableArray new];
}
return self;
}
+- (void)dealloc
+{
+ [self removeObserver:self forKeyPath:@"selectedTextRange"];
+}
+
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
@@ -154,6 +162,10 @@ - (void)textFieldDidChange
text:self.text
key:nil
eventCount:_nativeEventCount];
+
+ // selectedTextRange observer isn't triggered when you type even though the
+ // cursor position moves, so we send event again here.
+ [self sendSelectionEvent];
}
- (void)textFieldEndEditing
@@ -197,6 +209,36 @@ - (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
return YES;
}
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(RCTTextField *)textField
+ change:(NSDictionary *)change
+ context:(void *)context
+{
+ if ([keyPath isEqualToString:@"selectedTextRange"]) {
+ [self sendSelectionEvent];
+ }
+}
+
+- (void)sendSelectionEvent
+{
+ if (_onSelectionChange &&
+ self.selectedTextRange != _previousSelectionRange &&
+ ![self.selectedTextRange isEqual:_previousSelectionRange]) {
+
+ _previousSelectionRange = self.selectedTextRange;
+
+ UITextRange *selection = self.selectedTextRange;
+ NSInteger start = [self offsetFromPosition:[self beginningOfDocument] toPosition:selection.start];
+ NSInteger end = [self offsetFromPosition:[self beginningOfDocument] toPosition:selection.end];
+ _onSelectionChange(@{
+ @"selection": @{
+ @"start": @(start),
+ @"end": @(end),
+ },
+ });
+ }
+}
+
- (BOOL)becomeFirstResponder
{
_jsRequestingFirstResponder = YES;
@@ -87,6 +87,7 @@ - (BOOL)textFieldShouldEndEditing:(RCTTextField *)textField
RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL)
RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType)
RCT_EXPORT_VIEW_PROPERTY(keyboardAppearance, UIKeyboardAppearance)
+RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType)
RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL)
RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL)
@@ -28,6 +28,8 @@
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, strong) NSNumber *maxLength;
+@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
+
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (void)performTextUpdate;
@@ -43,6 +43,7 @@ @implementation RCTTextView
NSAttributedString *_pendingAttributedText;
NSMutableArray<UIView<RCTComponent> *> *_subviews;
BOOL _blockTextShouldChange;
+ UITextRange *_previousSelectionRange;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@@ -59,6 +60,8 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
_textView.scrollsToTop = NO;
_textView.delegate = self;
+ _previousSelectionRange = _textView.selectedTextRange;
+
_subviews = [NSMutableArray new];
[self addSubview:_textView];
}
@@ -284,6 +287,30 @@ - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)rang
}
}
+- (void)textViewDidChangeSelection:(RCTUITextView *)textView
+{
+ if (_onSelectionChange &&
+ textView.selectedTextRange != _previousSelectionRange &&
+ ![textView.selectedTextRange isEqual:_previousSelectionRange]) {
+
+ _previousSelectionRange = textView.selectedTextRange;
+
+ UITextRange *selection = textView.selectedTextRange;
+ NSInteger start = [textView offsetFromPosition:[textView beginningOfDocument] toPosition:selection.start];
+ NSInteger end = [textView offsetFromPosition:[textView beginningOfDocument] toPosition:selection.end];
+ _onSelectionChange(@{
+ @"selection": @{
+ @"start": @(start),
+ @"end": @(end),
+ },
+ });
+ }
+
+ if (textView.editable && [textView isFirstResponder]) {
+ [textView scrollRangeToVisible:textView.selectedRange];
+ }
+}
+
- (void)setText:(NSString *)text
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
@@ -34,6 +34,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType)
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance)
+RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL)
RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor)

1 comment on commit 5a34a09

@ehd

This comment has been minimized.

Show comment
Hide comment
@ehd

ehd Dec 3, 2015

@nicklockwood I've noticed something that may be unexpected with this event. When setting the value prop of a controlled TextInput there are two selectionChange events: One with the selection set to the end of the new value, one with the selection set to the current selection. For example, if value = "foobar" and then setting value = "foobars", there's these two events:

  1. {start: 7, end: 7}
  2. {start: 6, end: 6}

I've noticed this when working on #2668 where I ran into a bug where setting the selection programatically would trigger the event handler even though it should not.

ehd commented on 5a34a09 Dec 3, 2015

@nicklockwood I've noticed something that may be unexpected with this event. When setting the value prop of a controlled TextInput there are two selectionChange events: One with the selection set to the end of the new value, one with the selection set to the current selection. For example, if value = "foobar" and then setting value = "foobars", there's these two events:

  1. {start: 7, end: 7}
  2. {start: 6, end: 6}

I've noticed this when working on #2668 where I ran into a bug where setting the selection programatically would trigger the event handler even though it should not.

Please sign in to comment.