2017-06-27 23:05:05 +00:00
/ * *
2018-09-11 22:27:47 +00:00
* Copyright ( c ) Facebook , Inc . and its affiliates .
2017-06-27 23:05:05 +00:00
*
2018-02-17 02:24:55 +00:00
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
2017-06-27 23:05:05 +00:00
* /
2017-12-20 03:48:22 +00:00
# import "RCTBaseTextInputView.h"
2017-06-27 23:05:05 +00:00
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
# import < React / RCTAccessibilityManager . h >
2017-06-27 23:05:05 +00:00
# import < React / RCTBridge . h >
# import < React / RCTConvert . h >
# import < React / RCTEventDispatcher . h >
2017-06-27 23:05:08 +00:00
# import < React / RCTUIManager . h >
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
# import < React / RCTUtils . h >
2017-06-27 23:05:05 +00:00
# import < React / UIView + React . h >
2018-02-27 18:42:44 +00:00
# import "RCTInputAccessoryView.h"
# import "RCTInputAccessoryViewContent.h"
2018-01-24 07:17:57 +00:00
# import "RCTTextAttributes.h"
2017-07-18 21:33:33 +00:00
# import "RCTTextSelection.h"
2017-12-20 03:48:22 +00:00
@ implementation RCTBaseTextInputView {
2018-01-24 07:17:57 +00:00
__weak RCTBridge * _bridge ;
__weak RCTEventDispatcher * _eventDispatcher ;
2017-10-09 04:41:38 +00:00
BOOL _hasInputAccesoryView ;
2018-01-24 07:17:57 +00:00
NSString * _Nullable _predictedText ;
NSInteger _nativeEventCount ;
2017-06-27 23:05:08 +00:00
}
2017-06-27 23:05:05 +00:00
- ( instancetype ) initWithBridge : ( RCTBridge * ) bridge
{
RCTAssertParam ( bridge ) ;
if ( self = [ super initWithFrame : CGRectZero ] ) {
_bridge = bridge ;
_eventDispatcher = bridge . eventDispatcher ;
}
return self ;
}
RCT_NOT _IMPLEMENTED ( - ( instancetype ) init )
RCT_NOT _IMPLEMENTED ( - ( instancetype ) initWithCoder : ( NSCoder * ) decoder )
RCT_NOT _IMPLEMENTED ( - ( instancetype ) initWithFrame : ( CGRect ) frame )
2018-01-24 07:17:57 +00:00
- ( UIView < RCTBackedTextInputViewProtocol > * ) backedTextInputView
2017-06-27 23:05:05 +00:00
{
2017-12-20 03:48:22 +00:00
RCTAssert ( NO , @ "-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass." ) ;
2017-06-27 23:05:05 +00:00
return nil ;
}
2018-01-24 07:17:57 +00:00
# pragma mark - RCTComponent
- ( void ) didUpdateReactSubviews
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
{
2018-01-24 07:17:57 +00:00
// Do nothing .
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
}
2018-01-24 07:17:57 +00:00
# pragma mark - Properties
- ( void ) setTextAttributes : ( RCTTextAttributes * ) textAttributes
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
{
2018-01-24 07:17:57 +00:00
_textAttributes = textAttributes ;
[ self enforceTextAttributesIfNeeded ] ;
iOS: Support allowFontScaling on TextInput
Summary:
Currently, only `Text` supports the `allowFontScaling` prop. This commit adds support for it on `TextInput`.
As part of this change, the TextInput setters for font attributes (e.g. size, weight) had to be refactored. The problem with them is that they use RCTFont's helpers which create a new font based on an existing font. These helpers lose information. In particular, they lose the scaleMultiplier.
For example, suppose the font size is 12 and the device's font multiplier is set to 1.5. So we'd create a font with size 12 and scaleMultiplier 1.5 which is an effective size of 18 (which is the only thing stored in the font). Next, suppose the device's font multiplier changes to 1. So we'd use an RCTFont helper to create a new font based on the existing font but with a scaleMultiplier of 1. However, the font didn't store the font size (12) and scaleMultiplier (1.5) separately. It just knows the (effective) font size of 18. So RCTFont thinks the new font has a font size of 18 and a scaleMultiplier of 1 so its effective font size is 18. This is incorrect and it should have been 12.
To fix this, the font attributes are now all stored individually. Anytime one of them changes, updateFont is called which recreates the font from scratch. This happens to fix some bugs around fontStyle and fontWeight which were reported several times before: #13730, #12738, #2140, #8533.
Created a test app where I verified that `allowFontScaling` works properly for `TextInputs` for all values (`undefined`, `true`, `false`) for a variety of `TextInputs`:
- Singleline TextInput
- Singleline TextInput's placeholder
- Multiline TextInput
- Multiline TextInput's placeholder
- Multiline TextInput using children instead of `value`
Also, verified switching `fontSize`, `fontWeight`, `fontStyle` and `fontFamily` through a bunch of combinations works properly.
Lastly, my team has been using this change in our app.
Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/14030
Reviewed By: TheSavior
Differential Revision: D5899959
Pulled By: shergin
fbshipit-source-id: c8c8c4d4d670cd2a142286e79bfffef3b58cecd3
2017-10-02 04:40:57 +00:00
}
2018-01-24 07:17:57 +00:00
- ( void ) enforceTextAttributesIfNeeded
{
id < RCTBackedTextInputViewProtocol > backedTextInputView = self . backedTextInputView ;
if ( backedTextInputView . attributedText . string . length ! = 0 ) {
return ;
}
backedTextInputView . font = _textAttributes . effectiveFont ;
backedTextInputView . textColor = _textAttributes . effectiveForegroundColor ;
backedTextInputView . textAlignment = _textAttributes . alignment ;
}
2017-06-27 23:05:08 +00:00
- ( void ) setReactPaddingInsets : ( UIEdgeInsets ) reactPaddingInsets
{
_reactPaddingInsets = reactPaddingInsets ;
// We apply ` paddingInsets` as ` backedTextInputView` ' s ` textContainerInset` .
self . backedTextInputView . textContainerInset = reactPaddingInsets ;
[ self setNeedsLayout ] ;
}
- ( void ) setReactBorderInsets : ( UIEdgeInsets ) reactBorderInsets
{
_reactBorderInsets = reactBorderInsets ;
// We apply ` borderInsets` as ` backedTextInputView` layout offset .
self . backedTextInputView . frame = UIEdgeInsetsInsetRect ( self . bounds , reactBorderInsets ) ;
[ self setNeedsLayout ] ;
}
2018-01-24 07:17:57 +00:00
- ( NSAttributedString * ) attributedText
{
return self . backedTextInputView . attributedText ;
}
2018-06-15 17:34:12 +00:00
- ( BOOL ) textOf : ( NSAttributedString * ) newText equals : ( NSAttributedString * ) oldText {
Fix controlled <TextInput> on iOS when inputting in Chinese/Japanese
Summary:
@public
This should fix #18403.
When the user is inputting in Chinese/Japanese with <TextInput> in a controlled manner, the RCTBaseTextInputView will compare the JS-generated attributed string against the TextInputView attributed string and repeatedly overwrite the TextInputView one. This is because the native TextInputView will provide extra styling to show that some text is provisional.
My solution is to do a plain text string comparison at this point, like how we do for dictation.
Expected behavior when typing in a language that has "multistage" text input: For instance, in Chinese/Japanese it's common to type out the pronunciation for a word and then choose the appropriate word from above the keyboard. In this model, the "pronunciation" shows up in the text box first and then is replaced with the chosen word.
Using the word Japan which is written 日本 but first typed as にほん. It takes 4 key-presses to get to 日本, since に, ほ, ん, are all typed and then 日本 is selected. So here is what should happen:
1. enter に, onChange fires with 'に', markedTextRange covers 'に'
2. enter ほ, onChange fires with 'にほ', markedTextRange covers 'にほ'
3. enter ん, onChange fires with 'にほん', markedTextRange covers 'にほん'
4. user selects 日本 from the menu above the keyboard (provided by the keyboard/OS), onChange fires with '日本', markedTextRange is removed
previously we were overwriting the attributed text which would remove the markedTextRange, preventing the user from selecting 日本 from above the keyboard.
Cheekily, I've also fixed an issue with secure text entry as it's the same type of problem.
Reviewed By: PeteTheHeat
Differential Revision: D9002295
fbshipit-source-id: 7304ede055f301dab9ce1ea70f65308f2a4b4a8f
2018-07-30 14:54:19 +00:00
// When the dictation is running we can ' t update the attibuted text on the backed up text view
// because setting the attributed string will kill the dictation . This means that we can ' t impose
// the settings on a dictation .
// Similarly , when the user is in the middle of inputting some text in Japanese / Chinese , there will be styling on the
// text that we should disregard . See https : // developer . apple . com / documentation / uikit / uitextinput / 1614489 - markedtextrange ? language = objc
// for more info .
// Lastly , when entering a password , etc . , there will be additional styling on the field as the native text view
// handles showing the last character for a split second .
BOOL shouldFallbackToBareTextComparison =
[ self . backedTextInputView . textInputMode . primaryLanguage isEqualToString : @ "dictation" ] ||
self . backedTextInputView . markedTextRange ||
self . backedTextInputView . isSecureTextEntry ;
if ( shouldFallbackToBareTextComparison ) {
2018-06-15 17:34:12 +00:00
return ( [ newText . string isEqualToString : oldText . string ] ) ;
} else {
return ( [ newText isEqualToAttributedString : oldText ] ) ;
}
}
2018-01-24 07:17:57 +00:00
- ( void ) setAttributedText : ( NSAttributedString * ) attributedText
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount ;
2018-06-15 17:34:12 +00:00
BOOL textNeedsUpdate = NO ;
2018-03-19 21:09:32 +00:00
// Remove tag attribute to ensure correct attributed string comparison .
NSMutableAttributedString * const backedTextInputViewTextCopy = [ self . backedTextInputView . attributedText mutableCopy ] ;
NSMutableAttributedString * const attributedTextCopy = [ attributedText mutableCopy ] ;
[ backedTextInputViewTextCopy removeAttribute : RCTTextAttributesTagAttributeName
range : NSMakeRange ( 0 , backedTextInputViewTextCopy . length ) ] ;
[ attributedTextCopy removeAttribute : RCTTextAttributesTagAttributeName
range : NSMakeRange ( 0 , attributedTextCopy . length ) ] ;
2018-06-15 17:34:12 +00:00
textNeedsUpdate = ( [ self textOf : attributedTextCopy equals : backedTextInputViewTextCopy ] = = NO ) ;
if ( eventLag = = 0 && textNeedsUpdate ) {
2018-01-24 07:17:57 +00:00
UITextRange * selection = self . backedTextInputView . selectedTextRange ;
NSInteger oldTextLength = self . backedTextInputView . attributedText . string . length ;
self . backedTextInputView . attributedText = attributedText ;
if ( selection . empty ) {
// Maintaining a cursor position relative to the end of the old text .
NSInteger offsetStart =
2018-06-15 17:34:12 +00:00
[ self . backedTextInputView offsetFromPosition : self . backedTextInputView . beginningOfDocument
toPosition : selection . start ] ;
2018-01-24 07:17:57 +00:00
NSInteger offsetFromEnd = oldTextLength - offsetStart ;
NSInteger newOffset = attributedText . string . length - offsetFromEnd ;
UITextPosition * position =
2018-06-15 17:34:12 +00:00
[ self . backedTextInputView positionFromPosition : self . backedTextInputView . beginningOfDocument
offset : newOffset ] ;
2018-01-24 07:17:57 +00:00
[ self . backedTextInputView setSelectedTextRange : [ self . backedTextInputView textRangeFromPosition : position toPosition : position ]
notifyDelegate : YES ] ;
}
[ self updateLocalData ] ;
} else if ( eventLag > RCTTextUpdateLagWarningThreshold ) {
RCTLogWarn ( @ "Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster." , self . backedTextInputView . attributedText . string , ( long long ) eventLag ) ;
}
}
2017-07-18 21:33:45 +00:00
- ( RCTTextSelection * ) selection
{
2018-01-24 07:17:57 +00:00
id < RCTBackedTextInputViewProtocol > backedTextInputView = self . backedTextInputView ;
UITextRange * selectedTextRange = backedTextInputView . selectedTextRange ;
return [ [ RCTTextSelection new ] initWithStart : [ backedTextInputView offsetFromPosition : backedTextInputView . beginningOfDocument toPosition : selectedTextRange . start ]
end : [ backedTextInputView offsetFromPosition : backedTextInputView . beginningOfDocument toPosition : selectedTextRange . end ] ] ;
2017-07-18 21:33:45 +00:00
}
2017-07-18 21:33:33 +00:00
- ( void ) setSelection : ( RCTTextSelection * ) selection
{
if ( ! selection ) {
return ;
}
2018-01-24 07:17:57 +00:00
id < RCTBackedTextInputViewProtocol > backedTextInputView = self . backedTextInputView ;
2017-07-18 21:33:33 +00:00
2018-01-24 07:17:57 +00:00
UITextRange * previousSelectedTextRange = backedTextInputView . selectedTextRange ;
UITextPosition * start = [ backedTextInputView positionFromPosition : backedTextInputView . beginningOfDocument offset : selection . start ] ;
UITextPosition * end = [ backedTextInputView positionFromPosition : backedTextInputView . beginningOfDocument offset : selection . end ] ;
UITextRange * selectedTextRange = [ backedTextInputView textRangeFromPosition : start toPosition : end ] ;
2017-07-18 21:33:33 +00:00
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount ;
2017-07-18 21:33:45 +00:00
if ( eventLag = = 0 && ! [ previousSelectedTextRange isEqual : selectedTextRange ] ) {
2018-01-24 07:17:57 +00:00
[ backedTextInputView setSelectedTextRange : selectedTextRange notifyDelegate : NO ] ;
2017-07-18 21:33:33 +00:00
} else if ( eventLag > RCTTextUpdateLagWarningThreshold ) {
2018-01-24 07:17:57 +00:00
RCTLogWarn ( @ "Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster." , backedTextInputView . attributedText . string , ( long long ) eventLag ) ;
2017-07-18 21:33:33 +00:00
}
}
2018-04-02 09:31:16 +00:00
- ( void ) setTextContentType : ( NSString * ) type
{
# if defined ( __IPHONE _OS _VERSION _MAX _ALLOWED ) && __IPHONE _OS _VERSION _MAX _ALLOWED >= __IPHONE _10 _0
if ( @ available ( iOS 10.0 , * ) ) {
// Setting textContentType to an empty string will disable any
// default behaviour , like the autofill bar for password inputs
self . backedTextInputView . textContentType = [ type isEqualToString : @ "none" ] ? @ "" : type ;
}
# endif
}
2018-06-14 05:44:29 +00:00
- ( UIKeyboardType ) keyboardType
{
return self . backedTextInputView . keyboardType ;
}
- ( void ) setKeyboardType : ( UIKeyboardType ) keyboardType
{
UIView < RCTBackedTextInputViewProtocol > * textInputView = self . backedTextInputView ;
if ( textInputView . keyboardType ! = keyboardType ) {
textInputView . keyboardType = keyboardType ;
// Without the call to reloadInputViews , the keyboard will not change until the textview field ( the first responder ) loses and regains focus .
if ( textInputView . isFirstResponder ) {
[ textInputView reloadInputViews ] ;
}
}
}
2017-07-18 21:33:35 +00:00
# pragma mark - RCTBackedTextInputDelegate
2017-07-18 21:33:37 +00:00
- ( BOOL ) textInputShouldBeginEditing
{
return YES ;
}
2017-07-18 21:33:41 +00:00
- ( void ) textInputDidBeginEditing
{
if ( _clearTextOnFocus ) {
2018-01-24 07:17:57 +00:00
self . backedTextInputView . attributedText = [ NSAttributedString new ] ;
2017-07-18 21:33:41 +00:00
}
2017-07-18 21:33:54 +00:00
if ( _selectTextOnFocus ) {
[ self . backedTextInputView selectAll : nil ] ;
}
2017-07-18 21:33:41 +00:00
[ _eventDispatcher sendTextEventWithType : RCTTextEventTypeFocus
reactTag : self . reactTag
2018-01-24 07:17:57 +00:00
text : self . backedTextInputView . attributedText . string
key : nil
eventCount : _nativeEventCount ] ;
}
- ( BOOL ) textInputShouldEndEditing
{
return YES ;
}
- ( void ) textInputDidEndEditing
{
[ _eventDispatcher sendTextEventWithType : RCTTextEventTypeEnd
reactTag : self . reactTag
text : self . backedTextInputView . attributedText . string
key : nil
eventCount : _nativeEventCount ] ;
[ _eventDispatcher sendTextEventWithType : RCTTextEventTypeBlur
reactTag : self . reactTag
text : self . backedTextInputView . attributedText . string
2017-07-18 21:33:41 +00:00
key : nil
eventCount : _nativeEventCount ] ;
}
2017-07-18 21:33:35 +00:00
- ( BOOL ) textInputShouldReturn
{
2017-08-11 01:09:35 +00:00
// We send ` submit` event here , in ` textInputShouldReturn`
// ( not in ` textInputDidReturn ) ` , because of semantic of the event :
// ` onSubmitEditing` is called when "Submit" button
// ( the blue key on onscreen keyboard ) did pressed
// ( no connection to any specific "submitting" process ) .
2017-07-18 21:33:35 +00:00
[ _eventDispatcher sendTextEventWithType : RCTTextEventTypeSubmit
reactTag : self . reactTag
2018-01-24 07:17:57 +00:00
text : self . backedTextInputView . attributedText . string
2017-07-18 21:33:35 +00:00
key : nil
eventCount : _nativeEventCount ] ;
2017-08-11 01:09:35 +00:00
return _blurOnSubmit ;
}
- ( void ) textInputDidReturn
{
// Does nothing .
2017-07-18 21:33:35 +00:00
}
2018-01-24 07:17:57 +00:00
- ( BOOL ) textInputShouldChangeTextInRange : ( NSRange ) range replacementText : ( NSString * ) text
2017-07-18 21:33:45 +00:00
{
2018-01-24 07:17:57 +00:00
id < RCTBackedTextInputViewProtocol > backedTextInputView = self . backedTextInputView ;
if ( ! backedTextInputView . textWasPasted ) {
[ _eventDispatcher sendTextEventWithType : RCTTextEventTypeKeyPress
reactTag : self . reactTag
text : nil
key : text
eventCount : _nativeEventCount ] ;
2017-07-18 21:33:45 +00:00
}
2018-01-24 07:17:57 +00:00
if ( _maxLength ) {
NSUInteger allowedLength = _maxLength . integerValue - backedTextInputView . attributedText . string . length + range . length ;
if ( text . length > allowedLength ) {
// If we typed / pasted more than one character , limit the text inputted .
if ( text . length > 1 ) {
// Truncate the input string so the result is exactly maxLength
NSString * limitedString = [ text substringToIndex : allowedLength ] ;
NSMutableAttributedString * newAttributedText = [ backedTextInputView . attributedText mutableCopy ] ;
[ newAttributedText replaceCharactersInRange : range withString : limitedString ] ;
backedTextInputView . attributedText = newAttributedText ;
_predictedText = newAttributedText . string ;
// Collapse selection at end of insert to match normal paste behavior .
UITextPosition * insertEnd = [ backedTextInputView positionFromPosition : backedTextInputView . beginningOfDocument
offset : ( range . location + allowedLength ) ] ;
[ backedTextInputView setSelectedTextRange : [ backedTextInputView textRangeFromPosition : insertEnd toPosition : insertEnd ]
notifyDelegate : YES ] ;
[ self textInputDidChange ] ;
}
return NO ;
}
}
2017-07-18 21:33:45 +00:00
2018-01-24 07:17:57 +00:00
if ( range . location + range . length > _predictedText . length ) {
// _predictedText got out of sync in a bad way , so let ' s just force sync it . Haven ' t been able to repro this , but
// it ' s causing a real crash here : #6523822
_predictedText = backedTextInputView . attributedText . string ;
}
2017-07-18 21:33:47 +00:00
2018-01-24 07:17:57 +00:00
NSString * previousText = [ _predictedText substringWithRange : range ] ? : @ "" ;
2017-07-18 21:33:47 +00:00
2018-06-15 01:06:05 +00:00
// After clearing the text by replacing it with an empty string , ` _predictedText `
// still preserves the deleted text .
// As the first character in the TextInput always comes with the range value ( 0 , 0 ) ,
// we should check the range value in order to avoid appending a character to the deleted string
// ( which caused the issue #18374 )
if ( ! NSEqualRanges ( range , NSMakeRange ( 0 , 0 ) ) && _predictedText ) {
2018-01-24 07:17:57 +00:00
_predictedText = [ _predictedText stringByReplacingCharactersInRange : range withString : text ] ;
} else {
_predictedText = text ;
}
if ( _onTextInput ) {
_onTextInput ( @ {
@ "text" : text ,
@ "previousText" : previousText ,
@ "range" : @ {
@ "start" : @ ( range . location ) ,
@ "end" : @ ( range . location + range . length )
} ,
@ "eventCount" : @ ( _nativeEventCount ) ,
} ) ;
}
return YES ;
}
2017-06-27 23:05:08 +00:00
2018-01-24 07:17:57 +00:00
- ( void ) textInputDidChange
2017-06-27 23:05:08 +00:00
{
2018-01-24 07:17:57 +00:00
[ self updateLocalData ] ;
id < RCTBackedTextInputViewProtocol > backedTextInputView = self . backedTextInputView ;
// Detect when ` backedTextInputView` updates happend that didn ' t invoke ` shouldChangeTextInRange`
// ( e . g . typing simplified chinese in pinyin will insert and remove spaces without
// calling shouldChangeTextInRange ) . This will cause JS to get out of sync so we
// update the mismatched range .
NSRange currentRange ;
NSRange predictionRange ;
if ( findMismatch ( backedTextInputView . attributedText . string , _predictedText , & currentRange , & predictionRange ) ) {
NSString * replacement = [ backedTextInputView . attributedText . string substringWithRange : currentRange ] ;
[ self textInputShouldChangeTextInRange : predictionRange replacementText : replacement ] ;
// JS will assume the selection changed based on the location of our shouldChangeTextInRange , so reset it .
[ self textInputDidChangeSelection ] ;
_predictedText = backedTextInputView . attributedText . string ;
}
_nativeEventCount + + ;
if ( _onChange ) {
_onChange ( @ {
@ "text" : self . attributedText . string ,
@ "target" : self . reactTag ,
@ "eventCount" : @ ( _nativeEventCount ) ,
} ) ;
}
2017-06-27 23:05:08 +00:00
}
2018-01-24 07:17:57 +00:00
- ( void ) textInputDidChangeSelection
2017-06-27 23:05:08 +00:00
{
2018-01-24 07:17:57 +00:00
if ( ! _onSelectionChange ) {
2017-06-27 23:05:08 +00:00
return ;
}
2018-01-24 07:17:57 +00:00
RCTTextSelection * selection = self . selection ;
2017-06-27 23:05:08 +00:00
2018-01-24 07:17:57 +00:00
_onSelectionChange ( @ {
@ "selection" : @ {
@ "start" : @ ( selection . start ) ,
@ "end" : @ ( selection . end ) ,
} ,
} ) ;
}
- ( void ) updateLocalData
{
[ self enforceTextAttributesIfNeeded ] ;
[ _bridge . uiManager setLocalData : [ self . backedTextInputView . attributedText copy ]
forView : self ] ;
2017-06-27 23:05:08 +00:00
}
# pragma mark - Layout ( in UIKit terms , with all insets )
- ( CGSize ) intrinsicContentSize
{
CGSize size = self . backedTextInputView . intrinsicContentSize ;
size . width + = _reactBorderInsets . left + _reactBorderInsets . right ;
size . height + = _reactBorderInsets . top + _reactBorderInsets . bottom ;
// Returning value DOES include border and padding insets .
return size ;
}
- ( CGSize ) sizeThatFits : ( CGSize ) size
{
CGFloat compoundHorizontalBorderInset = _reactBorderInsets . left + _reactBorderInsets . right ;
CGFloat compoundVerticalBorderInset = _reactBorderInsets . top + _reactBorderInsets . bottom ;
size . width - = compoundHorizontalBorderInset ;
size . height - = compoundVerticalBorderInset ;
// Note : ` paddingInsets` was already included in ` backedTextInputView` size
// because it was applied as ` textContainerInset` .
CGSize fittingSize = [ self . backedTextInputView sizeThatFits : size ] ;
fittingSize . width + = compoundHorizontalBorderInset ;
fittingSize . height + = compoundVerticalBorderInset ;
// Returning value DOES include border and padding insets .
return fittingSize ;
}
2017-06-27 23:05:05 +00:00
# pragma mark - Accessibility
2017-07-20 00:12:04 +00:00
- ( UIView * ) reactAccessibilityElement
2017-06-27 23:05:05 +00:00
{
return self . backedTextInputView ;
}
# pragma mark - Focus Control
- ( void ) reactFocus
{
[ self . backedTextInputView reactFocus ] ;
}
- ( void ) reactBlur
{
[ self . backedTextInputView reactBlur ] ;
}
- ( void ) didMoveToWindow
{
[ self . backedTextInputView reactFocusIfNeeded ] ;
}
2017-06-27 23:05:11 +00:00
# pragma mark - Custom Input Accessory View
- ( void ) didSetProps : ( NSArray < NSString * > * ) changedProps
{
2018-02-27 18:42:44 +00:00
if ( [ changedProps containsObject : @ "inputAccessoryViewID" ] && self . inputAccessoryViewID ) {
[ self setCustomInputAccessoryViewWithNativeID : self . inputAccessoryViewID ] ;
} else if ( ! self . inputAccessoryViewID ) {
[ self setDefaultInputAccessoryView ] ;
}
}
- ( void ) setCustomInputAccessoryViewWithNativeID : ( NSString * ) nativeID
{
2018-02-28 01:39:46 +00:00
# if ! TARGET_OS _TV
2018-02-27 18:42:44 +00:00
__weak RCTBaseTextInputView * weakSelf = self ;
[ _bridge . uiManager rootViewForReactTag : self . reactTag withCompletion : ^ ( UIView * rootView ) {
RCTBaseTextInputView * strongSelf = weakSelf ;
if ( rootView ) {
UIView * accessoryView = [ strongSelf -> _bridge . uiManager viewForNativeID : nativeID
withRootTag : rootView . reactTag ] ;
if ( accessoryView && [ accessoryView isKindOfClass : [ RCTInputAccessoryView class ] ] ) {
2018-03-13 18:05:53 +00:00
strongSelf . backedTextInputView . inputAccessoryView = ( ( RCTInputAccessoryView * ) accessoryView ) . inputAccessoryView ;
2018-02-27 18:42:44 +00:00
[ strongSelf reloadInputViewsIfNecessary ] ;
}
}
} ] ;
2018-02-28 01:39:46 +00:00
# endif / * ! TARGET_OS _TV * /
2017-06-27 23:05:11 +00:00
}
2018-02-27 18:42:44 +00:00
- ( void ) setDefaultInputAccessoryView
2017-06-27 23:05:11 +00:00
{
2018-02-28 01:39:46 +00:00
# if ! TARGET_OS _TV
2017-06-27 23:05:11 +00:00
UIView < RCTBackedTextInputViewProtocol > * textInputView = self . backedTextInputView ;
UIKeyboardType keyboardType = textInputView . keyboardType ;
// These keyboard types ( all are number pads ) don ' t have a "Done" button by default ,
// so we create an ` inputAccessoryView` with this button for them .
BOOL shouldHaveInputAccesoryView =
(
keyboardType = = UIKeyboardTypeNumberPad ||
keyboardType = = UIKeyboardTypePhonePad ||
keyboardType = = UIKeyboardTypeDecimalPad ||
keyboardType = = UIKeyboardTypeASCIICapableNumberPad
) &&
textInputView . returnKeyType = = UIReturnKeyDone ;
2017-10-09 04:41:38 +00:00
if ( _hasInputAccesoryView = = shouldHaveInputAccesoryView ) {
2017-06-27 23:05:11 +00:00
return ;
}
2017-10-09 04:41:38 +00:00
_hasInputAccesoryView = shouldHaveInputAccesoryView ;
2017-06-27 23:05:11 +00:00
if ( shouldHaveInputAccesoryView ) {
UIToolbar * toolbarView = [ [ UIToolbar alloc ] init ] ;
[ toolbarView sizeToFit ] ;
UIBarButtonItem * flexibleSpace =
[ [ UIBarButtonItem alloc ] initWithBarButtonSystemItem : UIBarButtonSystemItemFlexibleSpace
target : nil
action : nil ] ;
UIBarButtonItem * doneButton =
[ [ UIBarButtonItem alloc ] initWithBarButtonSystemItem : UIBarButtonSystemItemDone
target : self
action : @ selector ( handleInputAccessoryDoneButton ) ] ;
toolbarView . items = @ [ flexibleSpace , doneButton ] ;
textInputView . inputAccessoryView = toolbarView ;
}
else {
textInputView . inputAccessoryView = nil ;
}
2018-02-27 18:42:44 +00:00
[ self reloadInputViewsIfNecessary ] ;
2018-02-28 01:39:46 +00:00
# endif / * ! TARGET_OS _TV * /
2018-02-27 18:42:44 +00:00
}
2017-06-27 23:05:11 +00:00
2018-02-27 18:42:44 +00:00
- ( void ) reloadInputViewsIfNecessary
{
2017-06-27 23:05:11 +00:00
// We have to call ` reloadInputViews` for focused text inputs to update an accessory view .
2018-02-27 18:42:44 +00:00
if ( self . backedTextInputView . isFirstResponder ) {
[ self . backedTextInputView reloadInputViews ] ;
2017-06-27 23:05:11 +00:00
}
}
- ( void ) handleInputAccessoryDoneButton
{
2017-08-10 10:24:06 +00:00
if ( [ self textInputShouldReturn ] ) {
[ self . backedTextInputView endEditing : YES ] ;
}
2017-06-27 23:05:11 +00:00
}
2018-01-24 07:17:57 +00:00
# pragma mark - Helpers
static BOOL findMismatch ( NSString * first , NSString * second , NSRange * firstRange , NSRange * secondRange )
{
NSInteger firstMismatch = -1 ;
for ( NSUInteger ii = 0 ; ii < MAX ( first . length , second . length ) ; ii + + ) {
if ( ii >= first . length || ii >= second . length || [ first characterAtIndex : ii ] ! = [ second characterAtIndex : ii ] ) {
firstMismatch = ii ;
break ;
}
}
if ( firstMismatch = = -1 ) {
return NO ;
}
NSUInteger ii = second . length ;
NSUInteger lastMismatch = first . length ;
while ( ii > firstMismatch && lastMismatch > firstMismatch ) {
if ( [ first characterAtIndex : ( lastMismatch - 1 ) ] ! = [ second characterAtIndex : ( ii - 1 ) ] ) {
break ;
}
ii - - ;
lastMismatch - - ;
}
* firstRange = NSMakeRange ( firstMismatch , lastMismatch - firstMismatch ) ;
* secondRange = NSMakeRange ( firstMismatch , ii - firstMismatch ) ;
return YES ;
}
2017-06-27 23:05:05 +00:00
@ end