Skip to content

Commit

Permalink
Open sourced the onSelectionChange event
Browse files Browse the repository at this point in the history
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 5a34a09
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 11 deletions.
25 changes: 14 additions & 11 deletions Libraries/Components/TextInput/TextInput.js
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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) {
Expand All @@ -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}
/>;
Expand Down Expand Up @@ -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()}
Expand Down Expand Up @@ -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);
},
Expand Down
4 changes: 4 additions & 0 deletions Libraries/Text/RCTTextField.h
Expand Up @@ -9,6 +9,8 @@

#import <UIKit/UIKit.h>

#import "RCTComponent.h"

@class RCTEventDispatcher;

@interface RCTTextField : UITextField
Expand All @@ -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;
Expand Down
42 changes: 42 additions & 0 deletions Libraries/Text/RCTTextField.m
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/RCTTextFieldManager.m
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Text/RCTTextView.h
Expand Up @@ -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;
Expand Down
27 changes: 27 additions & 0 deletions Libraries/Text/RCTTextView.m
Expand Up @@ -43,6 +43,7 @@ @implementation RCTTextView
NSAttributedString *_pendingAttributedText;
NSMutableArray<UIView<RCTComponent> *> *_subviews;
BOOL _blockTextShouldChange;
UITextRange *_previousSelectionRange;
}

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
Expand All @@ -59,6 +60,8 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
_textView.scrollsToTop = NO;
_textView.delegate = self;

_previousSelectionRange = _textView.selectedTextRange;

_subviews = [NSMutableArray new];
[self addSubview:_textView];
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/RCTTextViewManager.m
Expand Up @@ -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)
Expand Down

1 comment on commit 5a34a09

@seidtgeist
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.