facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.19k stars 24.33k forks source link

[TextInput] Update cursor position #2478

Closed yelled3 closed 7 years ago

yelled3 commented 9 years ago

I'm implementing a custom autocomplete UI (a TextInput with a ScrollView with a list of suggestions). I'm keeping the keyboard open (by using ScrollView keyboardDismissMode="none" & keyboardShouldPersistTaps={true} props) and updating the TextInput's value when a suggestion is chosen.

the only issue so far is I would like to update the TextInput's cursor each time I add a suggestion to point to the end of the text - so you can keep writing naturally.

from my little research, it seems that what keeps the cursor in place is this: (well sort of) https://github.com/facebook/react-native/blob/master/Libraries/Text/RCTTextField.m#L46-L48

I also found: https://github.com/facebook/react-native/blob/master/Libraries/Text/RCTTextFieldManager.m#L45-L49 but this is a UITextFieldDelegate method that's only called when the user edits the textfield manually (not when I change the TextInput's value)

this seems like the way to do this, according to: http://stackoverflow.com/a/11532718/2857906

is there currently a way to do this?

@ide @brentvatne /cc

yelled3 commented 9 years ago

Also, my initial intuition was to use DocumentSelectionState, which is fairly undocumented: http://facebook.github.io/react-native/docs/textinput.html#selectionstate https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js

but it seems all you can do with it is listen to update events, when the selection changes: https://github.com/facebook/react-native/blob/master/Libraries/Components/TextInput/TextInput.js#L555-L558

although the documentation says

It is intended for... and for programatically setting browser selection when components re-render. https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js#L27-L28

brentvatne commented 9 years ago

So this seems like a very important feature that is actually quite hard to implement due to the asynchronous nature of React Native. @vjeux and @sahrens are surely the most qualified people to discuss this :) I would love to see support for this.

yelled3 commented 9 years ago

@brentvatne

actually quite hard to implement due to the asynchronous nature of React Native

wouldn't be possible to add a property:

RCT_EXPORT_VIEW_PROPERTY(selectedTextRange, NSRange)

which then converts the NSRange to UITextRange, like the example:

+ (void)selectTextForInput:(UITextField *)input atRange:(NSRange)range {
    UITextPosition *start = [input positionFromPosition:[input beginningOfDocument] 
                                                 offset:range.location];
    UITextPosition *end = [input positionFromPosition:start
                                               offset:range.length];
    [input setSelectedTextRange:[input textRangeFromPosition:start toPosition:end]];
}

also, it might be easier to create an RCTextRange that conforms to UITextRange and RCTextPosition that conforms to UITextPosition, since you can only create a UITextPosition from an instance of UITextField.

for example: https://github.com/allending/Notekata/blob/master/Classes/NKTTextRange.h https://github.com/allending/Notekata/blob/master/Classes/NKTTextPosition.h http://stackoverflow.com/a/26013327/2857906

other refs: http://stackoverflow.com/questions/1500233/control-cursor-position-in-uitextfield/11532718#11532718

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITextInput_Protocol/#//apple_ref/occ/intfp/UITextInput/selectedTextRange

@vjeux @sahrens /cc

seidtgeist commented 9 years ago

I'm doing something similar and I also need to move the cursor to the end after inserting text.

Would it be possible to accomplish this with an extension module until the react-native team has figured out the proper way to do this in core? Or would I have to change existing implementation methods?

yelled3 commented 9 years ago

@ehd based on this example: http://stackoverflow.com/questions/1500233/control-cursor-position-in-uitextfield/11532718#11532718

I can imagine you could somehow hack a solution to this... if only to move the cursor to the end, after insertion. but i'm not sure it's gonna be pretty :-)

sahrens commented 9 years ago

Yeah, we have a more fully featured TextInput internally that has some dependencies and funkiness that make it hard to open source, but most of the js (like DocumentSelectionState) leaked out in a slightly broken state unfortunately.

It shouldn't be too hard to add selection state control to the open source TextInput. If someone wants to put up a PR, that would be great, otherwise we'll add it to our list of desired features. Note it should probably use the same synchronization trick as text updates to preserve auto-complete functionality, etc.

seidtgeist commented 9 years ago

It shouldn't be too hard to add selection state control to the open source TextInput.

What do you think the API should look like? Maybe as a method on the TextInput class?

textInputRef.setSelection({start, end})

Note it should probably use the same synchronization trick as text updates to preserve auto-complete functionality, etc.

I looked into the implementation code and the recent controlled TextInputs PR. Does "sync trick" mean checking the event count in RCTTextField's setText method to avoid races between selection changes and text updates?

yelled3 commented 9 years ago

@ehd how about we stick to the iOS naming

textInputRef.selectedTextRange({start, end})

it might also be useful to have a helper to method that just moves the cursor (i.e when start == end)

textInputRef.placeCursor(index)
// or
textInputRef.updateCursor(index)

// same as doing
textInputRef.selectedTextRange({index, index})

seems more readable to me...

@sahrens WDYT?

seidtgeist commented 9 years ago

@yelled3 Yeah, I'm all for making it fit with react-native/iOS/Android nomenclature. Regarding the cursor-placement method, unless it's difficult (which doesn't seem so) I'd maybe put it in a "cursor placement" subsection of TextInput's upcoming selection documentation?

sahrens commented 9 years ago

Does "sync trick" mean checking the event count

Yup.

how about we stick to the iOS naming

We want this on Android and other platforms too, and in general should prefer web naming conventions. setSelectionRange seems nice and clear: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange

seidtgeist commented 9 years ago

@sahrens @yelled3 I'd be interested in creating a PR unless anyone is really into it or already on it? :)

sahrens commented 9 years ago

I'm not aware of anyone else working on it, so go for it ;)

yelled3 commented 9 years ago

@ehd great, go ahead.

seidtgeist commented 9 years ago

@sahrens @yelled3 Here's my first stab at this: https://github.com/facebook/react-native/commit/be41c4536ab84f7841ffd1941b24247c386549ad

Short description:

I also had some open questions:

yelled3 commented 9 years ago

@ehd

Should this use an NSDictionary or another type for the selectionRange native property

how about using NSRange, like I mentioned before https://github.com/facebook/react-native/issues/2478#issuecomment-136303797 you may need to extend RCTConvert to support NSRange

Where can I add documentation and tests as part of a PR?

add docs here: https://github.com/facebook/react-native/blob/master/docs/Text.md

and maybe add a new UIExample or extend the current one: https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/TextInputExample.ios.js

seidtgeist commented 9 years ago

Thanks @yelled3! Yeah, I prefer using NSRange over a generic NSDictionary and we should then add a conversion for it. I think I was put off by the fact that setSeelctionRange is start and end whereas NSRange has location and length. So maybe we compute the length in the JavaScript method and pass that down as a native prop.

I'll add docs and a extend the UIExample.

One more question: As it happens I also do need selection change notification support. Is this something that'll be open sourced anyway, or something that could be done as part of this or another PR?

yelled3 commented 9 years ago

@ehd

So maybe we compute the length in the JavaScript method and pass that down as a native prop.

sounds like a good idea :+1:

As it happens I also do need selection change notification support.

IMO, this can be very useful, as well. @sahrens WDYT?

Is this something that'll be open sourced anyway, or something that could be done as part of this or another PR?

I would suggest splitting this to a separate PR, to keep each PR short and minimal and also too keep the discussion focused on the issue...

sahrens commented 9 years ago

Smaller PRs better :)

An event for selection change would be great.

On NSRange vs NSDictionary, you probably want to do whatever is more natural and doesn't require conversion back and forth. If you're going to translate the NSRange into a dictionary anyway to serialize to JS, might as well just keep it as a dict? Less code, fewer errors, more performant. On Sep 10, 2015 5:39 AM, "Adam Farhi" notifications@github.com wrote:

@ehd https://github.com/ehd

So maybe we compute the length in the JavaScript method and pass that down as a native prop.

sounds like a good idea [image: :+1:]

As it happens I also do need selection change notification support.

IMO, this can be very useful, as well. @sahrens https://github.com/sahrens WDYT?

Is this something that'll be open sourced anyway, or something that could be done as part of this or another PR?

I would suggest splitting this to a separate PR, to keep each PR short and minimal and also too keep the discussion focused on the issue...

— Reply to this email directly or view it on GitHub https://github.com/facebook/react-native/issues/2478#issuecomment-139222697 .

seidtgeist commented 9 years ago

Great, I'll create a PR in a bit :)

keeth commented 9 years ago

:+1: would love a fix for this, thanks!

nodkrot commented 8 years ago

+1, I need it too for my phone masking implementation. Can anyone recommend any existing input masks for react native?

seidtgeist commented 8 years ago

@nodkrot On iOS you could use 0.15.0-rc's TextInput to tokenize the text and render it accordingly. That commit also includes a nice example in the UIExplorer app where it's rendering hashtags as bold. No reason you can't tokenize into segments and add margin in-between tokens.

nodkrot commented 8 years ago

Ill take a look thx @ehd

nodkrot commented 8 years ago

The problem is when the value after masking changes length, for example: input: 55555 output: (555) 55 Then the cursor position needs to be updated to be at the end of the value.

diedu89 commented 8 years ago

Has someone tried this extension? https://github.com/DickyT/react-native-textinput-utils

tomprogers commented 8 years ago

FWIW: I think it'd be hard to go wrong if you patterned this after the HTML equivalent: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange

I.e.: ref.setSelectionRange(startIndex, endIndex, direction).

Especially since I assume that react-native appeals especially to web devs -- who are likely to be familiar with a web standard -- than traditional iOS devs.

seidtgeist commented 8 years ago

@tomprogers Yeah, that's what we started to do in #2668 but then decided that a controlled prop would actually better. If you think about it it's not much different from value and onChange[Value] events. The controlled prop API was actually easier to work with.

tegon commented 8 years ago

Are you guys planning to do something like this for Android also? I think I can do a PR using the [setSelection](https://developer.android.com/reference/android/widget/EditText.html#setSelection%28int, int%29) method from EditText.

RGreenberger commented 8 years ago

@tegon Did you end up implementing this feature for Android?

tegon commented 8 years ago

@RGreenberger Nope, I wasn't sure if it was on the React Native plans.

lacker commented 7 years ago

There is now a selection prop on the TextInput so I think this should be resolved by https://github.com/facebook/react-native/pull/8958