├── index.ios.js ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── fr │ │ └── bamlab │ │ └── textinput │ │ ├── SelectionWatcher.java │ │ ├── BUCK │ │ ├── ReactTextInputBlurEvent.java │ │ ├── ReactTextInputFocusEvent.java │ │ ├── ReactTextInputSubmitEditingEvent.java │ │ ├── ReactTextInputEndEditingEvent.java │ │ ├── ReactTextInputPackage.java │ │ ├── ReactTextInputSelectionEvent.java │ │ ├── ReactTextChangedEvent.java │ │ ├── ReactTextInputEvent.java │ │ ├── ReactTextInputShadowNode.java │ │ ├── ReactEditText.java │ │ └── ReactTextInputManager.java └── build.gradle ├── package.json ├── README.md ├── LICENCE └── index.android.js /index.ios.js: -------------------------------------------------------------------------------- 1 | import { 2 | TextInput, 3 | } from 'react-native'; 4 | 5 | export default TextInput; 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | compile 'com.android.support:appcompat-v7:23.1.0' 22 | compile 'com.facebook.react:react-native:0.20.+' 23 | } 24 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/SelectionWatcher.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | /** 13 | * Implement this interface to be informed of selection changes in the ReactTextEdit 14 | * This is used by the ReactTextInputManager to forward events from the EditText to JS 15 | */ 16 | interface SelectionWatcher { 17 | public void onSelectionChanged(int start, int end); 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-text-input", 3 | "version": "1.0.0", 4 | "description": "Provides Text Input component with colorization for password both on Android & iOS", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/bamlab/react-native-text-input.git" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "android", 12 | "text-input", 13 | "secure", 14 | "password" 15 | ], 16 | "peerDependencies": { 17 | "react-native": ">=v0.18.0" 18 | }, 19 | "author": "Almouro (almouro.com)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/bamlab/react-native-text-input/issues" 23 | }, 24 | "homepage": "https://github.com/bamlab/react-native-text-input#readme" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Text Input 2 | 3 | **On React Native Android, you cannot change the color of an input of type password.** 4 | It is a known issue and [a PR has already been merged](https://github.com/facebook/react-native/pull/6563) 5 | and should land in React Native 0.23 or 0.24. 6 | 7 | In the meantime, you can use this module which is basically a copy-paste of the 8 | TextInput component in React Native 0.20 including the fix made in the PR above. 9 | 10 | ## Install it 11 | 12 | ```shell 13 | npm install --save react-native-text-input 14 | ``` 15 | 16 | Link the module with [RNPM](https://github.com/rnpm/rnpm): 17 | ```shell 18 | npm install -g rnpm 19 | rnpm link react-native-text-input 20 | ``` 21 | 22 | Now you can replace: 23 | ```javascript 24 | import { TextInput } from 'react-native'; 25 | ``` 26 | by: 27 | ```javascript 28 | import TextInput from 'react-native-text-input'; 29 | ``` 30 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/BUCK: -------------------------------------------------------------------------------- 1 | include_defs('//ReactAndroid/DEFS') 2 | 3 | android_library( 4 | name = 'textinput', 5 | srcs = glob(['*.java']), 6 | deps = [ 7 | react_native_target('java/com/facebook/react/bridge:bridge'), 8 | react_native_target('java/com/facebook/react/common:common'), 9 | react_native_target('java/com/facebook/react/views/text:text'), 10 | react_native_target('java/com/facebook/csslayout:csslayout'), 11 | react_native_target('java/com/facebook/react/modules/core:core'), 12 | react_native_target('java/com/facebook/react/uimanager:uimanager'), 13 | react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), 14 | react_native_dep('third-party/java/infer-annotations:infer-annotations'), 15 | react_native_dep('third-party/java/jsr-305:jsr-305'), 16 | ], 17 | visibility = [ 18 | 'PUBLIC', 19 | ], 20 | ) 21 | 22 | project_config( 23 | src_target = ':textinput', 24 | ) 25 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BAM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputBlurEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when it loses focus. 19 | */ 20 | /* package */ class ReactTextInputBlurEvent extends Event { 21 | 22 | private static final String EVENT_NAME = "topBlur"; 23 | 24 | public ReactTextInputBlurEvent( 25 | int viewId, 26 | long timestampMs) { 27 | super(viewId, timestampMs); 28 | } 29 | 30 | @Override 31 | public String getEventName() { 32 | return EVENT_NAME; 33 | } 34 | 35 | @Override 36 | public boolean canCoalesce() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public void dispatch(RCTEventEmitter rctEventEmitter) { 42 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 43 | } 44 | 45 | private WritableMap serializeEventData() { 46 | WritableMap eventData = Arguments.createMap(); 47 | eventData.putInt("target", getViewTag()); 48 | return eventData; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputFocusEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when it receives focus. 19 | */ 20 | /* package */ class ReactTextInputFocusEvent extends Event { 21 | 22 | private static final String EVENT_NAME = "topFocus"; 23 | 24 | public ReactTextInputFocusEvent( 25 | int viewId, 26 | long timestampMs) { 27 | super(viewId, timestampMs); 28 | } 29 | 30 | @Override 31 | public String getEventName() { 32 | return EVENT_NAME; 33 | } 34 | 35 | @Override 36 | public boolean canCoalesce() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public void dispatch(RCTEventEmitter rctEventEmitter) { 42 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 43 | } 44 | 45 | private WritableMap serializeEventData() { 46 | WritableMap eventData = Arguments.createMap(); 47 | eventData.putInt("target", getViewTag()); 48 | return eventData; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputSubmitEditingEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when the user submits the text. 19 | */ 20 | /* package */ class ReactTextInputSubmitEditingEvent 21 | extends Event { 22 | 23 | private static final String EVENT_NAME = "topSubmitEditing"; 24 | 25 | private String mText; 26 | 27 | public ReactTextInputSubmitEditingEvent( 28 | int viewId, 29 | long timestampMs, 30 | String text) { 31 | super(viewId, timestampMs); 32 | mText = text; 33 | } 34 | 35 | @Override 36 | public String getEventName() { 37 | return EVENT_NAME; 38 | } 39 | 40 | @Override 41 | public boolean canCoalesce() { 42 | return false; 43 | } 44 | 45 | @Override 46 | public void dispatch(RCTEventEmitter rctEventEmitter) { 47 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 48 | } 49 | 50 | private WritableMap serializeEventData() { 51 | WritableMap eventData = Arguments.createMap(); 52 | eventData.putInt("target", getViewTag()); 53 | eventData.putString("text", mText); 54 | return eventData; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputEndEditingEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when text editing ends, 19 | * because of the user leaving the text input. 20 | */ 21 | class ReactTextInputEndEditingEvent extends Event { 22 | 23 | private static final String EVENT_NAME = "topEndEditing"; 24 | 25 | private String mText; 26 | 27 | public ReactTextInputEndEditingEvent( 28 | int viewId, 29 | long timestampMs, 30 | String text) { 31 | super(viewId, timestampMs); 32 | mText = text; 33 | } 34 | 35 | @Override 36 | public String getEventName() { 37 | return EVENT_NAME; 38 | } 39 | 40 | @Override 41 | public boolean canCoalesce() { 42 | return false; 43 | } 44 | 45 | @Override 46 | public void dispatch(RCTEventEmitter rctEventEmitter) { 47 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 48 | } 49 | 50 | private WritableMap serializeEventData() { 51 | WritableMap eventData = Arguments.createMap(); 52 | eventData.putInt("target", getViewTag()); 53 | eventData.putString("text", mText); 54 | return eventData; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputPackage.java: -------------------------------------------------------------------------------- 1 | package fr.bamlab.textinput; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | import com.facebook.react.views.image.ReactImageManager; 9 | 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class ReactTextInputPackage implements ReactPackage { 15 | 16 | /** 17 | * @param reactContext react application context that can be used to create modules 18 | * @return list of native modules to register with the newly created catalyst instance 19 | */ 20 | @Override 21 | public List createNativeModules(ReactApplicationContext reactContext) { 22 | return Collections.emptyList(); 23 | } 24 | 25 | /** 26 | * @return list of JS modules to register with the newly created catalyst instance. 27 | *

28 | * IMPORTANT: Note that only modules that needs to be accessible from the native code should be 29 | * listed here. Also listing a native module here doesn't imply that the JS implementation of it 30 | * will be automatically included in the JS bundle. 31 | */ 32 | @Override 33 | public List> createJSModules() { 34 | return Collections.emptyList(); 35 | } 36 | 37 | /** 38 | * @param reactContext 39 | * @return a list of view managers that should be registered with {@link UIManagerModule} 40 | */ 41 | @Override 42 | public List createViewManagers(ReactApplicationContext reactContext) { 43 | return Arrays.asList( 44 | new ReactTextInputManager() 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputSelectionEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when the text selection changes. 19 | */ 20 | /* package */ class ReactTextInputSelectionEvent 21 | extends Event { 22 | 23 | private static final String EVENT_NAME = "topSelectionChange"; 24 | 25 | private int mSelectionStart; 26 | private int mSelectionEnd; 27 | 28 | public ReactTextInputSelectionEvent( 29 | int viewId, 30 | long timestampMs, 31 | int selectionStart, 32 | int selectionEnd) { 33 | super(viewId, timestampMs); 34 | mSelectionStart = selectionStart; 35 | mSelectionEnd = selectionEnd; 36 | } 37 | 38 | @Override 39 | public String getEventName() { 40 | return EVENT_NAME; 41 | } 42 | 43 | @Override 44 | public void dispatch(RCTEventEmitter rctEventEmitter) { 45 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 46 | } 47 | 48 | private WritableMap serializeEventData() { 49 | WritableMap eventData = Arguments.createMap(); 50 | 51 | WritableMap selectionData = Arguments.createMap(); 52 | selectionData.putInt("start", mSelectionStart); 53 | selectionData.putInt("end", mSelectionEnd); 54 | 55 | eventData.putMap("selection", selectionData); 56 | return eventData; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextChangedEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when text changes. 19 | * VisibleForTesting from {@link TextInputEventsTestCase}. 20 | */ 21 | public class ReactTextChangedEvent extends Event { 22 | 23 | public static final String EVENT_NAME = "topChange"; 24 | 25 | private String mText; 26 | private int mContentWidth; 27 | private int mContentHeight; 28 | private int mEventCount; 29 | 30 | public ReactTextChangedEvent( 31 | int viewId, 32 | long timestampMs, 33 | String text, 34 | int contentSizeWidth, 35 | int contentSizeHeight, 36 | int eventCount) { 37 | super(viewId, timestampMs); 38 | mText = text; 39 | mContentWidth = contentSizeWidth; 40 | mContentHeight = contentSizeHeight; 41 | mEventCount = eventCount; 42 | } 43 | 44 | @Override 45 | public String getEventName() { 46 | return EVENT_NAME; 47 | } 48 | 49 | @Override 50 | public void dispatch(RCTEventEmitter rctEventEmitter) { 51 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 52 | } 53 | 54 | private WritableMap serializeEventData() { 55 | WritableMap eventData = Arguments.createMap(); 56 | eventData.putString("text", mText); 57 | 58 | WritableMap contentSize = Arguments.createMap(); 59 | contentSize.putDouble("width", mContentWidth); 60 | contentSize.putDouble("height", mContentHeight); 61 | eventData.putMap("contentSize", contentSize); 62 | eventData.putInt("eventCount", mEventCount); 63 | 64 | eventData.putInt("target", getViewTag()); 65 | return eventData; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import com.facebook.react.bridge.Arguments; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.uimanager.events.Event; 15 | import com.facebook.react.uimanager.events.RCTEventEmitter; 16 | 17 | /** 18 | * Event emitted by EditText native view when text changes. 19 | * VisibleForTesting from {@link TextInputEventsTestCase}. 20 | */ 21 | public class ReactTextInputEvent extends Event { 22 | 23 | public static final String EVENT_NAME = "topTextInput"; 24 | 25 | private String mText; 26 | private String mPreviousText; 27 | private int mRangeStart; 28 | private int mRangeEnd; 29 | 30 | public ReactTextInputEvent( 31 | int viewId, 32 | long timestampMs, 33 | String text, 34 | String previousText, 35 | int rangeStart, 36 | int rangeEnd) { 37 | super(viewId, timestampMs); 38 | mText = text; 39 | mPreviousText = previousText; 40 | mRangeStart = rangeStart; 41 | mRangeEnd = rangeEnd; 42 | } 43 | 44 | @Override 45 | public String getEventName() { 46 | return EVENT_NAME; 47 | } 48 | 49 | @Override 50 | public boolean canCoalesce() { 51 | // We don't want to miss any textinput event, as event data is incremental. 52 | return false; 53 | } 54 | 55 | @Override 56 | public void dispatch(RCTEventEmitter rctEventEmitter) { 57 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); 58 | } 59 | 60 | private WritableMap serializeEventData() { 61 | WritableMap eventData = Arguments.createMap(); 62 | WritableMap range = Arguments.createMap(); 63 | range.putDouble("start", mRangeStart); 64 | range.putDouble("end", mRangeEnd); 65 | 66 | eventData.putString("text", mText); 67 | eventData.putString("previousText", mPreviousText); 68 | eventData.putMap("range", range); 69 | 70 | eventData.putInt("target", getViewTag()); 71 | return eventData; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputShadowNode.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | import android.text.Spannable; 15 | import android.util.TypedValue; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.EditText; 19 | 20 | import com.facebook.csslayout.CSSNode; 21 | import com.facebook.csslayout.MeasureOutput; 22 | import com.facebook.csslayout.Spacing; 23 | import com.facebook.infer.annotation.Assertions; 24 | import com.facebook.react.common.annotations.VisibleForTesting; 25 | import com.facebook.react.uimanager.PixelUtil; 26 | import com.facebook.react.uimanager.annotations.ReactProp; 27 | import com.facebook.react.uimanager.ThemedReactContext; 28 | import com.facebook.react.uimanager.UIViewOperationQueue; 29 | import com.facebook.react.uimanager.ViewDefaults; 30 | import com.facebook.react.views.text.ReactTextShadowNode; 31 | import com.facebook.react.views.text.ReactTextUpdate; 32 | 33 | @VisibleForTesting 34 | public class ReactTextInputShadowNode extends ReactTextShadowNode implements 35 | CSSNode.MeasureFunction { 36 | 37 | private static final int MEASURE_SPEC = View.MeasureSpec.makeMeasureSpec( 38 | ViewGroup.LayoutParams.WRAP_CONTENT, 39 | View.MeasureSpec.UNSPECIFIED); 40 | 41 | private @Nullable EditText mEditText; 42 | private @Nullable float[] mComputedPadding; 43 | private int mJsEventCount = UNSET; 44 | 45 | public ReactTextInputShadowNode() { 46 | super(false); 47 | setMeasureFunction(this); 48 | } 49 | 50 | @Override 51 | public void setThemedContext(ThemedReactContext themedContext) { 52 | super.setThemedContext(themedContext); 53 | 54 | // TODO #7120264: cache this stuff better 55 | mEditText = new EditText(getThemedContext()); 56 | // This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure, 57 | // setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877 58 | mEditText.setLayoutParams( 59 | new ViewGroup.LayoutParams( 60 | ViewGroup.LayoutParams.WRAP_CONTENT, 61 | ViewGroup.LayoutParams.WRAP_CONTENT)); 62 | 63 | setDefaultPadding(Spacing.LEFT, mEditText.getPaddingLeft()); 64 | setDefaultPadding(Spacing.TOP, mEditText.getPaddingTop()); 65 | setDefaultPadding(Spacing.RIGHT, mEditText.getPaddingRight()); 66 | setDefaultPadding(Spacing.BOTTOM, mEditText.getPaddingBottom()); 67 | mComputedPadding = spacingToFloatArray(getPadding()); 68 | } 69 | 70 | @Override 71 | public void measure(CSSNode node, float width, float height, MeasureOutput measureOutput) { 72 | // measure() should never be called before setThemedContext() 73 | EditText editText = Assertions.assertNotNull(mEditText); 74 | 75 | measureOutput.width = width; 76 | editText.setTextSize( 77 | TypedValue.COMPLEX_UNIT_PX, 78 | mFontSize == UNSET ? 79 | (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)) : mFontSize); 80 | mComputedPadding = spacingToFloatArray(getPadding()); 81 | editText.setPadding( 82 | (int) Math.ceil(getPadding().get(Spacing.LEFT)), 83 | (int) Math.ceil(getPadding().get(Spacing.TOP)), 84 | (int) Math.ceil(getPadding().get(Spacing.RIGHT)), 85 | (int) Math.ceil(getPadding().get(Spacing.BOTTOM))); 86 | 87 | if (mNumberOfLines != UNSET) { 88 | editText.setLines(mNumberOfLines); 89 | } 90 | 91 | editText.measure(MEASURE_SPEC, MEASURE_SPEC); 92 | measureOutput.height = editText.getMeasuredHeight(); 93 | } 94 | 95 | @Override 96 | public void onBeforeLayout() { 97 | // We don't have to measure the text within the text input. 98 | return; 99 | } 100 | 101 | @ReactProp(name = "mostRecentEventCount") 102 | public void setMostRecentEventCount(int mostRecentEventCount) { 103 | mJsEventCount = mostRecentEventCount; 104 | } 105 | 106 | @Override 107 | public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { 108 | super.onCollectExtraUpdates(uiViewOperationQueue); 109 | if (mComputedPadding != null) { 110 | uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), mComputedPadding); 111 | mComputedPadding = null; 112 | } 113 | 114 | if (mJsEventCount != UNSET) { 115 | Spannable preparedSpannableText = fromTextCSSNode(this); 116 | ReactTextUpdate reactTextUpdate = 117 | new ReactTextUpdate(preparedSpannableText, mJsEventCount, mContainsImages); 118 | uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); 119 | } 120 | } 121 | 122 | @Override 123 | public void setPadding(int spacingType, float padding) { 124 | super.setPadding(spacingType, padding); 125 | mComputedPadding = spacingToFloatArray(getPadding()); 126 | markUpdated(); 127 | } 128 | 129 | private static float[] spacingToFloatArray(Spacing spacing) { 130 | return new float[] { 131 | spacing.get(Spacing.LEFT), 132 | spacing.get(Spacing.TOP), 133 | spacing.get(Spacing.RIGHT), 134 | spacing.get(Spacing.BOTTOM), 135 | }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactEditText.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | import java.util.ArrayList; 15 | 16 | import android.content.Context; 17 | import android.graphics.Rect; 18 | import android.graphics.drawable.Drawable; 19 | import android.text.Editable; 20 | import android.text.InputType; 21 | import android.text.SpannableStringBuilder; 22 | import android.text.Spanned; 23 | import android.text.TextWatcher; 24 | import android.text.method.KeyListener; 25 | import android.text.method.QwertyKeyListener; 26 | import android.text.style.AbsoluteSizeSpan; 27 | import android.text.style.BackgroundColorSpan; 28 | import android.text.style.ForegroundColorSpan; 29 | import android.view.Gravity; 30 | import android.view.KeyEvent; 31 | import android.view.View; 32 | import android.view.inputmethod.InputMethodManager; 33 | import android.widget.EditText; 34 | 35 | import com.facebook.infer.annotation.Assertions; 36 | import com.facebook.react.views.text.CustomStyleSpan; 37 | import com.facebook.react.views.text.ReactTagSpan; 38 | import com.facebook.react.views.text.ReactTextUpdate; 39 | import com.facebook.react.views.text.TextInlineImageSpan; 40 | 41 | /** 42 | * A wrapper around the EditText that lets us better control what happens when an EditText gets 43 | * focused or blurred, and when to display the soft keyboard and when not to. 44 | * 45 | * ReactEditTexts have setFocusableInTouchMode set to false automatically because touches on the 46 | * EditText are managed on the JS side. This also removes the nasty side effect that EditTexts 47 | * have, which is that focus is always maintained on one of the EditTexts. 48 | * 49 | * The wrapper stops the EditText from triggering *TextChanged events, in the case where JS 50 | * has called this explicitly. This is the default behavior on other platforms as well. 51 | * VisibleForTesting from {@link TextInputEventsTestCase}. 52 | */ 53 | public class ReactEditText extends EditText { 54 | 55 | private final InputMethodManager mInputMethodManager; 56 | // This flag is set to true when we set the text of the EditText explicitly. In that case, no 57 | // *TextChanged events should be triggered. This is less expensive than removing the text 58 | // listeners and adding them back again after the text change is completed. 59 | private boolean mIsSettingTextFromJS; 60 | // This component is controlled, so we want it to get focused only when JS ask it to do so. 61 | // Whenever android requests focus (which it does for random reasons), it will be ignored. 62 | private boolean mIsJSSettingFocus; 63 | private int mDefaultGravityHorizontal; 64 | private int mDefaultGravityVertical; 65 | private int mNativeEventCount; 66 | private @Nullable ArrayList mListeners; 67 | private @Nullable TextWatcherDelegator mTextWatcherDelegator; 68 | private int mStagedInputType; 69 | private boolean mContainsImages; 70 | private @Nullable SelectionWatcher mSelectionWatcher; 71 | private final InternalKeyListener mKeyListener; 72 | 73 | private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard(); 74 | 75 | public ReactEditText(Context context) { 76 | super(context); 77 | setFocusableInTouchMode(false); 78 | 79 | mInputMethodManager = (InputMethodManager) 80 | Assertions.assertNotNull(getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); 81 | mDefaultGravityHorizontal = 82 | getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK); 83 | mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 84 | mNativeEventCount = 0; 85 | mIsSettingTextFromJS = false; 86 | mIsJSSettingFocus = false; 87 | mListeners = null; 88 | mTextWatcherDelegator = null; 89 | mStagedInputType = getInputType(); 90 | mKeyListener = new InternalKeyListener(); 91 | } 92 | 93 | // After the text changes inside an EditText, TextView checks if a layout() has been requested. 94 | // If it has, it will not scroll the text to the end of the new text inserted, but wait for the 95 | // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so 96 | // we need to override isLayoutRequested to force EditText to scroll to the end of the new text 97 | // immediately. 98 | // TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout() 99 | @Override 100 | public boolean isLayoutRequested() { 101 | return false; 102 | } 103 | 104 | // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't 105 | // since we only allow JS to change focus, which in turn causes TextView to crash. 106 | @Override 107 | public boolean onKeyUp(int keyCode, KeyEvent event) { 108 | if (keyCode == KeyEvent.KEYCODE_ENTER && 109 | ((getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0 )) { 110 | hideSoftKeyboard(); 111 | return true; 112 | } 113 | return super.onKeyUp(keyCode, event); 114 | } 115 | 116 | @Override 117 | public void clearFocus() { 118 | setFocusableInTouchMode(false); 119 | super.clearFocus(); 120 | hideSoftKeyboard(); 121 | } 122 | 123 | @Override 124 | public boolean requestFocus(int direction, Rect previouslyFocusedRect) { 125 | // Always return true if we are already focused. This is used by android in certain places, 126 | // such as text selection. 127 | if (isFocused()) { 128 | return true; 129 | } 130 | if (!mIsJSSettingFocus) { 131 | return false; 132 | } 133 | setFocusableInTouchMode(true); 134 | boolean focused = super.requestFocus(direction, previouslyFocusedRect); 135 | showSoftKeyboard(); 136 | return focused; 137 | } 138 | 139 | @Override 140 | public void addTextChangedListener(TextWatcher watcher) { 141 | if (mListeners == null) { 142 | mListeners = new ArrayList<>(); 143 | super.addTextChangedListener(getTextWatcherDelegator()); 144 | } 145 | 146 | mListeners.add(watcher); 147 | } 148 | 149 | @Override 150 | public void removeTextChangedListener(TextWatcher watcher) { 151 | if (mListeners != null) { 152 | mListeners.remove(watcher); 153 | 154 | if (mListeners.isEmpty()) { 155 | mListeners = null; 156 | super.removeTextChangedListener(getTextWatcherDelegator()); 157 | } 158 | } 159 | } 160 | 161 | @Override 162 | protected void onSelectionChanged(int selStart, int selEnd) { 163 | super.onSelectionChanged(selStart, selEnd); 164 | if (mSelectionWatcher != null && hasFocus()) { 165 | mSelectionWatcher.onSelectionChanged(selStart, selEnd); 166 | } 167 | } 168 | 169 | @Override 170 | protected void onFocusChanged( 171 | boolean focused, int direction, Rect previouslyFocusedRect) { 172 | super.onFocusChanged(focused, direction, previouslyFocusedRect); 173 | if (focused && mSelectionWatcher != null) { 174 | mSelectionWatcher.onSelectionChanged(getSelectionStart(), getSelectionEnd()); 175 | } 176 | } 177 | 178 | public void setSelectionWatcher(SelectionWatcher selectionWatcher) { 179 | mSelectionWatcher = selectionWatcher; 180 | } 181 | 182 | /*protected*/ int getStagedInputType() { 183 | return mStagedInputType; 184 | } 185 | 186 | /*package*/ void setStagedInputType(int stagedInputType) { 187 | mStagedInputType = stagedInputType; 188 | } 189 | 190 | /*package*/ void commitStagedInputType() { 191 | if (getInputType() != mStagedInputType) { 192 | setInputType(mStagedInputType); 193 | } 194 | } 195 | 196 | @Override 197 | public void setInputType(int type) { 198 | super.setInputType(type); 199 | mStagedInputType = type; 200 | 201 | // We override the KeyListener so that all keys on the soft input keyboard as well as hardware 202 | // keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not 203 | // accept all input from it 204 | mKeyListener.setInputType(type); 205 | setKeyListener(mKeyListener); 206 | } 207 | 208 | // VisibleForTesting from {@link TextInputEventsTestCase}. 209 | public void requestFocusFromJS() { 210 | mIsJSSettingFocus = true; 211 | requestFocus(); 212 | mIsJSSettingFocus = false; 213 | } 214 | 215 | /* package */ void clearFocusFromJS() { 216 | clearFocus(); 217 | } 218 | 219 | // VisibleForTesting from {@link TextInputEventsTestCase}. 220 | public int incrementAndGetEventCounter() { 221 | return ++mNativeEventCount; 222 | } 223 | 224 | // VisibleForTesting from {@link TextInputEventsTestCase}. 225 | public void maybeSetText(ReactTextUpdate reactTextUpdate) { 226 | // Only set the text if it is up to date. 227 | if (reactTextUpdate.getJsEventCounter() < mNativeEventCount) { 228 | return; 229 | } 230 | 231 | // The current text gets replaced with the text received from JS. However, the spans on the 232 | // current text need to be adapted to the new text. Since TextView#setText() will remove or 233 | // reset some of these spans even if they are set directly, SpannableStringBuilder#replace() is 234 | // used instead (this is also used by the the keyboard implementation underneath the covers). 235 | SpannableStringBuilder spannableStringBuilder = 236 | new SpannableStringBuilder(reactTextUpdate.getText()); 237 | manageSpans(spannableStringBuilder); 238 | mContainsImages = reactTextUpdate.containsImages(); 239 | mIsSettingTextFromJS = true; 240 | getText().replace(0, length(), spannableStringBuilder); 241 | mIsSettingTextFromJS = false; 242 | } 243 | 244 | /** 245 | * Remove and/or add {@link Spanned.SPAN_EXCLUSIVE_EXCLUSIVE} spans, since they should only exist 246 | * as long as the text they cover is the same. All other spans will remain the same, since they 247 | * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes 248 | * them. 249 | */ 250 | private void manageSpans(SpannableStringBuilder spannableStringBuilder) { 251 | Object[] spans = getText().getSpans(0, length(), Object.class); 252 | for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { 253 | // Remove all styling spans we might have previously set 254 | if (ForegroundColorSpan.class.isInstance(spans[spanIdx]) || 255 | BackgroundColorSpan.class.isInstance(spans[spanIdx]) || 256 | AbsoluteSizeSpan.class.isInstance(spans[spanIdx]) || 257 | CustomStyleSpan.class.isInstance(spans[spanIdx]) || 258 | ReactTagSpan.class.isInstance(spans[spanIdx])) { 259 | getText().removeSpan(spans[spanIdx]); 260 | } 261 | 262 | if ((getText().getSpanFlags(spans[spanIdx]) & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) != 263 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) { 264 | continue; 265 | } 266 | Object span = spans[spanIdx]; 267 | final int spanStart = getText().getSpanStart(spans[spanIdx]); 268 | final int spanEnd = getText().getSpanEnd(spans[spanIdx]); 269 | final int spanFlags = getText().getSpanFlags(spans[spanIdx]); 270 | 271 | // Make sure the span is removed from existing text, otherwise the spans we set will be 272 | // ignored or it will cover text that has changed. 273 | getText().removeSpan(spans[spanIdx]); 274 | if (sameTextForSpan(getText(), spannableStringBuilder, spanStart, spanEnd)) { 275 | spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); 276 | } 277 | } 278 | } 279 | 280 | private static boolean sameTextForSpan( 281 | final Editable oldText, 282 | final SpannableStringBuilder newText, 283 | final int start, 284 | final int end) { 285 | if (start > newText.length() || end > newText.length()) { 286 | return false; 287 | } 288 | for (int charIdx = start; charIdx < end; charIdx++) { 289 | if (oldText.charAt(charIdx) != newText.charAt(charIdx)) { 290 | return false; 291 | } 292 | } 293 | return true; 294 | } 295 | 296 | private boolean showSoftKeyboard() { 297 | return mInputMethodManager.showSoftInput(this, 0); 298 | } 299 | 300 | private void hideSoftKeyboard() { 301 | mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 302 | } 303 | 304 | private TextWatcherDelegator getTextWatcherDelegator() { 305 | if (mTextWatcherDelegator == null) { 306 | mTextWatcherDelegator = new TextWatcherDelegator(); 307 | } 308 | return mTextWatcherDelegator; 309 | } 310 | 311 | /* package */ void setGravityHorizontal(int gravityHorizontal) { 312 | if (gravityHorizontal == 0) { 313 | gravityHorizontal = mDefaultGravityHorizontal; 314 | } 315 | setGravity( 316 | (getGravity() & ~Gravity.HORIZONTAL_GRAVITY_MASK & 317 | ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravityHorizontal); 318 | } 319 | 320 | /* package */ void setGravityVertical(int gravityVertical) { 321 | if (gravityVertical == 0) { 322 | gravityVertical = mDefaultGravityVertical; 323 | } 324 | setGravity((getGravity() & ~Gravity.VERTICAL_GRAVITY_MASK) | gravityVertical); 325 | } 326 | 327 | @Override 328 | protected boolean verifyDrawable(Drawable drawable) { 329 | if (mContainsImages && getText() instanceof Spanned) { 330 | Spanned text = (Spanned) getText(); 331 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 332 | for (TextInlineImageSpan span : spans) { 333 | if (span.getDrawable() == drawable) { 334 | return true; 335 | } 336 | } 337 | } 338 | return super.verifyDrawable(drawable); 339 | } 340 | 341 | @Override 342 | public void invalidateDrawable(Drawable drawable) { 343 | if (mContainsImages && getText() instanceof Spanned) { 344 | Spanned text = (Spanned) getText(); 345 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 346 | for (TextInlineImageSpan span : spans) { 347 | if (span.getDrawable() == drawable) { 348 | invalidate(); 349 | } 350 | } 351 | } 352 | super.invalidateDrawable(drawable); 353 | } 354 | 355 | @Override 356 | public void onDetachedFromWindow() { 357 | super.onDetachedFromWindow(); 358 | if (mContainsImages && getText() instanceof Spanned) { 359 | Spanned text = (Spanned) getText(); 360 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 361 | for (TextInlineImageSpan span : spans) { 362 | span.onDetachedFromWindow(); 363 | } 364 | } 365 | } 366 | 367 | @Override 368 | public void onStartTemporaryDetach() { 369 | super.onStartTemporaryDetach(); 370 | if (mContainsImages && getText() instanceof Spanned) { 371 | Spanned text = (Spanned) getText(); 372 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 373 | for (TextInlineImageSpan span : spans) { 374 | span.onStartTemporaryDetach(); 375 | } 376 | } 377 | } 378 | 379 | @Override 380 | public void onAttachedToWindow() { 381 | super.onAttachedToWindow(); 382 | if (mContainsImages && getText() instanceof Spanned) { 383 | Spanned text = (Spanned) getText(); 384 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 385 | for (TextInlineImageSpan span : spans) { 386 | span.onAttachedToWindow(); 387 | } 388 | } 389 | } 390 | 391 | @Override 392 | public void onFinishTemporaryDetach() { 393 | super.onFinishTemporaryDetach(); 394 | if (mContainsImages && getText() instanceof Spanned) { 395 | Spanned text = (Spanned) getText(); 396 | TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); 397 | for (TextInlineImageSpan span : spans) { 398 | span.onFinishTemporaryDetach(); 399 | } 400 | } 401 | } 402 | 403 | /** 404 | * This class will redirect *TextChanged calls to the listeners only in the case where the text 405 | * is changed by the user, and not explicitly set by JS. 406 | */ 407 | private class TextWatcherDelegator implements TextWatcher { 408 | @Override 409 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 410 | if (!mIsSettingTextFromJS && mListeners != null) { 411 | for (TextWatcher listener : mListeners) { 412 | listener.beforeTextChanged(s, start, count, after); 413 | } 414 | } 415 | } 416 | 417 | @Override 418 | public void onTextChanged(CharSequence s, int start, int before, int count) { 419 | if (!mIsSettingTextFromJS && mListeners != null) { 420 | for (TextWatcher listener : mListeners) { 421 | listener.onTextChanged(s, start, before, count); 422 | } 423 | } 424 | } 425 | 426 | @Override 427 | public void afterTextChanged(Editable s) { 428 | if (!mIsSettingTextFromJS && mListeners != null) { 429 | for (android.text.TextWatcher listener : mListeners) { 430 | listener.afterTextChanged(s); 431 | } 432 | } 433 | } 434 | } 435 | 436 | /* 437 | * This class is set as the KeyListener for the underlying TextView 438 | * It does two things 439 | * 1) Provides the same answer to getInputType() as the real KeyListener would have which allows 440 | * the proper keyboard to pop up on screen 441 | * 2) Permits all keyboard input through 442 | */ 443 | private static class InternalKeyListener implements KeyListener { 444 | 445 | private int mInputType = 0; 446 | 447 | public InternalKeyListener() { 448 | } 449 | 450 | public void setInputType(int inputType) { 451 | mInputType = inputType; 452 | } 453 | 454 | /* 455 | * getInputType will return whatever value is passed in. This will allow the proper keyboard 456 | * to be shown on screen but without the actual filtering done by other KeyListeners 457 | */ 458 | @Override 459 | public int getInputType() { 460 | return mInputType; 461 | } 462 | 463 | /* 464 | * All overrides of key handling defer to the underlying KeyListener which is shared by all 465 | * ReactEditText instances. It will basically allow any/all keyboard input whether from 466 | * physical keyboard or from soft input. 467 | */ 468 | @Override 469 | public boolean onKeyDown(View view, Editable text, int keyCode, KeyEvent event) { 470 | return sKeyListener.onKeyDown(view, text, keyCode, event); 471 | } 472 | 473 | @Override 474 | public boolean onKeyUp(View view, Editable text, int keyCode, KeyEvent event) { 475 | return sKeyListener.onKeyUp(view, text, keyCode, event); 476 | } 477 | 478 | @Override 479 | public boolean onKeyOther(View view, Editable text, KeyEvent event) { 480 | return sKeyListener.onKeyOther(view, text, event); 481 | } 482 | 483 | @Override 484 | public void clearMetaKeyState(View view, Editable content, int states) { 485 | sKeyListener.clearMetaKeyState(view, content, states); 486 | } 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule TextInput 10 | * @flow 11 | */ 12 | 'use strict'; 13 | 14 | var DocumentSelectionState = require('DocumentSelectionState'); 15 | var EventEmitter = require('EventEmitter'); 16 | var NativeMethodsMixin = require('NativeMethodsMixin'); 17 | var Platform = require('Platform'); 18 | var PropTypes = require('ReactPropTypes'); 19 | var React = require('React'); 20 | var ReactChildren = require('ReactChildren'); 21 | var StyleSheet = require('StyleSheet'); 22 | var Text = require('Text'); 23 | var TextInputState = require('TextInputState'); 24 | var TimerMixin = require('react-native/node_modules/react-timer-mixin'); 25 | var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); 26 | var UIManager = require('UIManager'); 27 | var View = require('View'); 28 | 29 | var createReactNativeComponentClass = require('createReactNativeComponentClass'); 30 | var emptyFunction = require('react-native/node_modules/fbjs/lib/emptyFunction'); 31 | var invariant = require('react-native/node_modules/fbjs/lib/invariant'); 32 | var requireNativeComponent = require('requireNativeComponent'); 33 | 34 | var onlyMultiline = { 35 | onTextInput: true, // not supported in Open Source yet 36 | children: true, 37 | }; 38 | 39 | var notMultiline = { 40 | // nothing yet 41 | }; 42 | 43 | if (Platform.OS === 'android') { 44 | // Bamlab: This is the only changed line. 45 | var AndroidTextInput = requireNativeComponent('FixedTextInput', null); 46 | // --- 47 | } else if (Platform.OS === 'ios') { 48 | var RCTTextView = requireNativeComponent('RCTTextView', null); 49 | var RCTTextField = requireNativeComponent('RCTTextField', null); 50 | } 51 | 52 | type Event = Object; 53 | 54 | /** 55 | * A foundational component for inputting text into the app via a 56 | * keyboard. Props provide configurability for several features, such as 57 | * auto-correction, auto-capitalization, placeholder text, and different keyboard 58 | * types, such as a numeric keypad. 59 | * 60 | * The simplest use case is to plop down a `TextInput` and subscribe to the 61 | * `onChangeText` events to read the user input. There are also other events, 62 | * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple 63 | * example: 64 | * 65 | * ``` 66 | * this.setState({text})} 69 | * value={this.state.text} 70 | * /> 71 | * ``` 72 | * 73 | * Note that some props are only available with `multiline={true/false}`: 74 | */ 75 | var TextInput = React.createClass({ 76 | statics: { 77 | /* TODO(brentvatne) docs are needed for this */ 78 | State: TextInputState, 79 | }, 80 | 81 | propTypes: { 82 | ...View.propTypes, 83 | /** 84 | * Can tell TextInput to automatically capitalize certain characters. 85 | * 86 | * - characters: all characters, 87 | * - words: first letter of each word 88 | * - sentences: first letter of each sentence (default) 89 | * - none: don't auto capitalize anything 90 | */ 91 | autoCapitalize: PropTypes.oneOf([ 92 | 'none', 93 | 'sentences', 94 | 'words', 95 | 'characters', 96 | ]), 97 | /** 98 | * If false, disables auto-correct. The default value is true. 99 | */ 100 | autoCorrect: PropTypes.bool, 101 | /** 102 | * If true, focuses the input on componentDidMount. 103 | * The default value is false. 104 | */ 105 | autoFocus: PropTypes.bool, 106 | /** 107 | * If false, text is not editable. The default value is true. 108 | */ 109 | editable: PropTypes.bool, 110 | /** 111 | * Determines which keyboard to open, e.g.`numeric`. 112 | * 113 | * The following values work across platforms: 114 | * - default 115 | * - numeric 116 | * - email-address 117 | */ 118 | keyboardType: PropTypes.oneOf([ 119 | // Cross-platform 120 | 'default', 121 | 'email-address', 122 | 'numeric', 123 | 'phone-pad', 124 | // iOS-only 125 | 'ascii-capable', 126 | 'numbers-and-punctuation', 127 | 'url', 128 | 'number-pad', 129 | 'name-phone-pad', 130 | 'decimal-pad', 131 | 'twitter', 132 | 'web-search', 133 | ]), 134 | /** 135 | * Determines the color of the keyboard. 136 | * @platform ios 137 | */ 138 | keyboardAppearance: PropTypes.oneOf([ 139 | 'default', 140 | 'light', 141 | 'dark', 142 | ]), 143 | /** 144 | * Determines how the return key should look. 145 | * @platform ios 146 | */ 147 | returnKeyType: PropTypes.oneOf([ 148 | 'default', 149 | 'go', 150 | 'google', 151 | 'join', 152 | 'next', 153 | 'route', 154 | 'search', 155 | 'send', 156 | 'yahoo', 157 | 'done', 158 | 'emergency-call', 159 | ]), 160 | /** 161 | * Limits the maximum number of characters that can be entered. Use this 162 | * instead of implementing the logic in JS to avoid flicker. 163 | */ 164 | maxLength: PropTypes.number, 165 | /** 166 | * Sets the number of lines for a TextInput. Use it with multiline set to 167 | * true to be able to fill the lines. 168 | * @platform android 169 | */ 170 | numberOfLines: PropTypes.number, 171 | /** 172 | * If true, the keyboard disables the return key when there is no text and 173 | * automatically enables it when there is text. The default value is false. 174 | * @platform ios 175 | */ 176 | enablesReturnKeyAutomatically: PropTypes.bool, 177 | /** 178 | * If true, the text input can be multiple lines. 179 | * The default value is false. 180 | */ 181 | multiline: PropTypes.bool, 182 | /** 183 | * Callback that is called when the text input is blurred 184 | */ 185 | onBlur: PropTypes.func, 186 | /** 187 | * Callback that is called when the text input is focused 188 | */ 189 | onFocus: PropTypes.func, 190 | /** 191 | * Callback that is called when the text input's text changes. 192 | */ 193 | onChange: PropTypes.func, 194 | /** 195 | * Callback that is called when the text input's text changes. 196 | * Changed text is passed as an argument to the callback handler. 197 | */ 198 | onChangeText: PropTypes.func, 199 | /** 200 | * Callback that is called when text input ends. 201 | */ 202 | onEndEditing: PropTypes.func, 203 | /** 204 | * Callback that is called when the text input selection is changed 205 | */ 206 | onSelectionChange: PropTypes.func, 207 | /** 208 | * Callback that is called when the text input's submit button is pressed. 209 | * Invalid if multiline={true} is specified. 210 | */ 211 | onSubmitEditing: PropTypes.func, 212 | /** 213 | * Callback that is called when a key is pressed. 214 | * Pressed key value is passed as an argument to the callback handler. 215 | * Fires before onChange callbacks. 216 | * @platform ios 217 | */ 218 | onKeyPress: PropTypes.func, 219 | /** 220 | * Invoked on mount and layout changes with `{x, y, width, height}`. 221 | */ 222 | onLayout: PropTypes.func, 223 | /** 224 | * The string that will be rendered before text input has been entered 225 | */ 226 | placeholder: PropTypes.string, 227 | /** 228 | * The text color of the placeholder string 229 | */ 230 | placeholderTextColor: PropTypes.string, 231 | /** 232 | * If true, the text input obscures the text entered so that sensitive text 233 | * like passwords stay secure. The default value is false. 234 | */ 235 | secureTextEntry: PropTypes.bool, 236 | /** 237 | * The highlight (and cursor on ios) color of the text input 238 | */ 239 | selectionColor: PropTypes.string, 240 | /** 241 | * See DocumentSelectionState.js, some state that is responsible for 242 | * maintaining selection information for a document 243 | * @platform ios 244 | */ 245 | selectionState: PropTypes.instanceOf(DocumentSelectionState), 246 | /** 247 | * The value to show for the text input. TextInput is a controlled 248 | * component, which means the native value will be forced to match this 249 | * value prop if provided. For most uses this works great, but in some 250 | * cases this may cause flickering - one common cause is preventing edits 251 | * by keeping value the same. In addition to simply setting the same value, 252 | * either set `editable={false}`, or set/update `maxLength` to prevent 253 | * unwanted edits without flicker. 254 | */ 255 | value: PropTypes.string, 256 | /** 257 | * Provides an initial value that will change when the user starts typing. 258 | * Useful for simple use-cases where you don't want to deal with listening 259 | * to events and updating the value prop to keep the controlled state in sync. 260 | */ 261 | defaultValue: PropTypes.string, 262 | /** 263 | * When the clear button should appear on the right side of the text view 264 | * @platform ios 265 | */ 266 | clearButtonMode: PropTypes.oneOf([ 267 | 'never', 268 | 'while-editing', 269 | 'unless-editing', 270 | 'always', 271 | ]), 272 | /** 273 | * If true, clears the text field automatically when editing begins 274 | * @platform ios 275 | */ 276 | clearTextOnFocus: PropTypes.bool, 277 | /** 278 | * If true, all text will automatically be selected on focus 279 | * @platform ios 280 | */ 281 | selectTextOnFocus: PropTypes.bool, 282 | /** 283 | * If true, the text field will blur when submitted. 284 | * The default value is true for single-line fields and false for 285 | * multiline fields. Note that for multiline fields, setting blurOnSubmit 286 | * to true means that pressing return will blur the field and trigger the 287 | * onSubmitEditing event instead of inserting a newline into the field. 288 | * @platform ios 289 | */ 290 | blurOnSubmit: PropTypes.bool, 291 | /** 292 | * Styles 293 | */ 294 | style: Text.propTypes.style, 295 | /** 296 | * The color of the textInput underline. 297 | * @platform android 298 | */ 299 | underlineColorAndroid: PropTypes.string, 300 | }, 301 | 302 | /** 303 | * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We 304 | * make `this` look like an actual native component class. 305 | */ 306 | mixins: [NativeMethodsMixin, TimerMixin], 307 | 308 | viewConfig: 309 | ((Platform.OS === 'ios' && RCTTextField ? 310 | RCTTextField.viewConfig : 311 | (Platform.OS === 'android' && AndroidTextInput ? 312 | AndroidTextInput.viewConfig : 313 | {})) : Object), 314 | 315 | isFocused: function(): boolean { 316 | return TextInputState.currentlyFocusedField() === 317 | React.findNodeHandle(this.refs.input); 318 | }, 319 | 320 | contextTypes: { 321 | onFocusRequested: React.PropTypes.func, 322 | focusEmitter: React.PropTypes.instanceOf(EventEmitter), 323 | }, 324 | 325 | _focusSubscription: (undefined: ?Function), 326 | 327 | componentDidMount: function() { 328 | if (!this.context.focusEmitter) { 329 | if (this.props.autoFocus) { 330 | this.requestAnimationFrame(this.focus); 331 | } 332 | return; 333 | } 334 | this._focusSubscription = this.context.focusEmitter.addListener( 335 | 'focus', 336 | (el) => { 337 | if (this === el) { 338 | this.requestAnimationFrame(this.focus); 339 | } else if (this.isFocused()) { 340 | this.blur(); 341 | } 342 | } 343 | ); 344 | if (this.props.autoFocus) { 345 | this.context.onFocusRequested(this); 346 | } 347 | }, 348 | 349 | componentWillUnmount: function() { 350 | this._focusSubscription && this._focusSubscription.remove(); 351 | if (this.isFocused()) { 352 | this.blur(); 353 | } 354 | }, 355 | 356 | getChildContext: function(): Object { 357 | return {isInAParentText: true}; 358 | }, 359 | 360 | childContextTypes: { 361 | isInAParentText: React.PropTypes.bool 362 | }, 363 | 364 | clear: function() { 365 | this.setNativeProps({text: ''}); 366 | }, 367 | 368 | render: function() { 369 | if (Platform.OS === 'ios') { 370 | return this._renderIOS(); 371 | } else if (Platform.OS === 'android') { 372 | return this._renderAndroid(); 373 | } 374 | }, 375 | 376 | _getText: function(): ?string { 377 | return typeof this.props.value === 'string' ? 378 | this.props.value : 379 | this.props.defaultValue; 380 | }, 381 | 382 | _renderIOS: function() { 383 | var textContainer; 384 | 385 | var onSelectionChange; 386 | if (this.props.selectionState || this.props.onSelectionChange) { 387 | onSelectionChange = (event: Event) => { 388 | if (this.props.selectionState) { 389 | var selection = event.nativeEvent.selection; 390 | this.props.selectionState.update(selection.start, selection.end); 391 | } 392 | this.props.onSelectionChange && this.props.onSelectionChange(event); 393 | }; 394 | } 395 | 396 | var props = Object.assign({}, this.props); 397 | props.style = [styles.input, this.props.style]; 398 | if (!props.multiline) { 399 | for (var propKey in onlyMultiline) { 400 | if (props[propKey]) { 401 | throw new Error( 402 | 'TextInput prop `' + propKey + '` is only supported with multiline.' 403 | ); 404 | } 405 | } 406 | textContainer = 407 | ; 417 | } else { 418 | for (var propKey in notMultiline) { 419 | if (props[propKey]) { 420 | throw new Error( 421 | 'TextInput prop `' + propKey + '` cannot be used with multiline.' 422 | ); 423 | } 424 | } 425 | 426 | var children = props.children; 427 | var childCount = 0; 428 | ReactChildren.forEach(children, () => ++childCount); 429 | invariant( 430 | !(props.value && childCount), 431 | 'Cannot specify both value and children.' 432 | ); 433 | if (childCount > 1) { 434 | children = {children}; 435 | } 436 | if (props.inputView) { 437 | children = [children, props.inputView]; 438 | } 439 | textContainer = 440 | ; 452 | } 453 | 454 | return ( 455 | 462 | {textContainer} 463 | 464 | ); 465 | }, 466 | 467 | _renderAndroid: function() { 468 | var onSelectionChange; 469 | if (this.props.selectionState || this.props.onSelectionChange) { 470 | onSelectionChange = (event: Event) => { 471 | if (this.props.selectionState) { 472 | var selection = event.nativeEvent.selection; 473 | this.props.selectionState.update(selection.start, selection.end); 474 | } 475 | this.props.onSelectionChange && this.props.onSelectionChange(event); 476 | }; 477 | } 478 | 479 | var autoCapitalize = 480 | UIManager.AndroidTextInput.Constants.AutoCapitalizationType[this.props.autoCapitalize]; 481 | var children = this.props.children; 482 | var childCount = 0; 483 | ReactChildren.forEach(children, () => ++childCount); 484 | invariant( 485 | !(this.props.value && childCount), 486 | 'Cannot specify both value and children.' 487 | ); 488 | if (childCount > 1) { 489 | children = {children}; 490 | } 491 | 492 | var textContainer = 493 | ; 520 | 521 | return ( 522 | 528 | {textContainer} 529 | 530 | ); 531 | }, 532 | 533 | _onFocus: function(event: Event) { 534 | if (this.props.onFocus) { 535 | this.props.onFocus(event); 536 | } 537 | 538 | if (this.props.selectionState) { 539 | this.props.selectionState.focus(); 540 | } 541 | }, 542 | 543 | _onPress: function(event: Event) { 544 | if (this.props.editable || this.props.editable === undefined) { 545 | this.focus(); 546 | } 547 | }, 548 | 549 | _onChange: function(event: Event) { 550 | // Make sure to fire the mostRecentEventCount first so it is already set on 551 | // native when the text value is set. 552 | this.refs.input.setNativeProps({ 553 | mostRecentEventCount: event.nativeEvent.eventCount, 554 | }); 555 | 556 | var text = event.nativeEvent.text; 557 | this.props.onChange && this.props.onChange(event); 558 | this.props.onChangeText && this.props.onChangeText(text); 559 | 560 | if (!this.refs.input) { 561 | // calling `this.props.onChange` or `this.props.onChangeText` 562 | // may clean up the input itself. Exits here. 563 | return; 564 | } 565 | 566 | // This is necessary in case native updates the text and JS decides 567 | // that the update should be ignored and we should stick with the value 568 | // that we have in JS. 569 | if (text !== this.props.value && typeof this.props.value === 'string') { 570 | this.refs.input.setNativeProps({ 571 | text: this.props.value, 572 | }); 573 | } 574 | }, 575 | 576 | _onBlur: function(event: Event) { 577 | this.blur(); 578 | if (this.props.onBlur) { 579 | this.props.onBlur(event); 580 | } 581 | 582 | if (this.props.selectionState) { 583 | this.props.selectionState.blur(); 584 | } 585 | }, 586 | 587 | _onTextInput: function(event: Event) { 588 | this.props.onTextInput && this.props.onTextInput(event); 589 | }, 590 | }); 591 | 592 | var styles = StyleSheet.create({ 593 | input: { 594 | alignSelf: 'stretch', 595 | }, 596 | }); 597 | 598 | module.exports = TextInput; 599 | -------------------------------------------------------------------------------- /android/src/main/java/fr/bamlab/textinput/ReactTextInputManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package fr.bamlab.textinput; 11 | 12 | import android.graphics.PorterDuff; 13 | import android.os.SystemClock; 14 | import android.text.Editable; 15 | import android.text.InputFilter; 16 | import android.text.InputType; 17 | import android.text.Spannable; 18 | import android.text.TextWatcher; 19 | import android.util.TypedValue; 20 | import android.view.Gravity; 21 | import android.view.KeyEvent; 22 | import android.view.View; 23 | import android.view.inputmethod.EditorInfo; 24 | import android.widget.TextView; 25 | 26 | import com.facebook.infer.annotation.Assertions; 27 | import com.facebook.react.bridge.JSApplicationIllegalArgumentException; 28 | import com.facebook.react.bridge.ReactContext; 29 | import com.facebook.react.bridge.ReadableArray; 30 | import com.facebook.react.common.MapBuilder; 31 | import com.facebook.react.uimanager.BaseViewManager; 32 | import com.facebook.react.uimanager.PixelUtil; 33 | import com.facebook.react.uimanager.ThemedReactContext; 34 | import com.facebook.react.uimanager.UIManagerModule; 35 | import com.facebook.react.uimanager.ViewDefaults; 36 | import com.facebook.react.uimanager.ViewProps; 37 | import com.facebook.react.uimanager.annotations.ReactProp; 38 | import com.facebook.react.uimanager.events.EventDispatcher; 39 | import com.facebook.react.views.text.DefaultStyleValuesUtil; 40 | import com.facebook.react.views.text.ReactTextUpdate; 41 | import com.facebook.react.views.text.TextInlineImageSpan; 42 | 43 | import java.util.LinkedList; 44 | import java.util.Map; 45 | 46 | import javax.annotation.Nullable; 47 | 48 | /** 49 | * Manages instances of TextInput. 50 | */ 51 | public class ReactTextInputManager extends 52 | BaseViewManager { 53 | 54 | /* package */ static final String REACT_CLASS = "FixedTextInput"; 55 | 56 | private static final int FOCUS_TEXT_INPUT = 1; 57 | private static final int BLUR_TEXT_INPUT = 2; 58 | 59 | private static final int INPUT_TYPE_KEYBOARD_NUMBERED = 60 | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | 61 | InputType.TYPE_NUMBER_FLAG_SIGNED; 62 | 63 | private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"; 64 | private static final String KEYBOARD_TYPE_NUMERIC = "numeric"; 65 | private static final String KEYBOARD_TYPE_PHONE_PAD = "phone-pad"; 66 | private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0]; 67 | 68 | @Override 69 | public String getName() { 70 | return REACT_CLASS; 71 | } 72 | 73 | @Override 74 | public ReactEditText createViewInstance(ThemedReactContext context) { 75 | ReactEditText editText = new ReactEditText(context); 76 | int inputType = editText.getInputType(); 77 | editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); 78 | editText.setImeOptions(EditorInfo.IME_ACTION_DONE); 79 | editText.setTextSize( 80 | TypedValue.COMPLEX_UNIT_PX, 81 | (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))); 82 | return editText; 83 | } 84 | 85 | @Override 86 | public ReactTextInputShadowNode createShadowNodeInstance() { 87 | return new ReactTextInputShadowNode(); 88 | } 89 | 90 | @Override 91 | public Class getShadowNodeClass() { 92 | return ReactTextInputShadowNode.class; 93 | } 94 | 95 | @Nullable 96 | @Override 97 | public Map getExportedCustomBubblingEventTypeConstants() { 98 | return MapBuilder.builder() 99 | .put( 100 | "topSubmitEditing", 101 | MapBuilder.of( 102 | "phasedRegistrationNames", 103 | MapBuilder.of( 104 | "bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture"))) 105 | .put( 106 | "topEndEditing", 107 | MapBuilder.of( 108 | "phasedRegistrationNames", 109 | MapBuilder.of("bubbled", "onEndEditing", "captured", "onEndEditingCapture"))) 110 | .put( 111 | "topTextInput", 112 | MapBuilder.of( 113 | "phasedRegistrationNames", 114 | MapBuilder.of("bubbled", "onTextInput", "captured", "onTextInputCapture"))) 115 | .put( 116 | "topFocus", 117 | MapBuilder.of( 118 | "phasedRegistrationNames", 119 | MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture"))) 120 | .put( 121 | "topBlur", 122 | MapBuilder.of( 123 | "phasedRegistrationNames", 124 | MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture"))) 125 | .build(); 126 | } 127 | 128 | @Override 129 | public @Nullable Map getCommandsMap() { 130 | return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT); 131 | } 132 | 133 | @Override 134 | public void receiveCommand( 135 | ReactEditText reactEditText, 136 | int commandId, 137 | @Nullable ReadableArray args) { 138 | switch (commandId) { 139 | case FOCUS_TEXT_INPUT: 140 | reactEditText.requestFocusFromJS(); 141 | break; 142 | case BLUR_TEXT_INPUT: 143 | reactEditText.clearFocusFromJS(); 144 | break; 145 | } 146 | } 147 | 148 | @Override 149 | public void updateExtraData(ReactEditText view, Object extraData) { 150 | if (extraData instanceof float[]) { 151 | float[] padding = (float[]) extraData; 152 | 153 | view.setPadding( 154 | (int) Math.ceil(padding[0]), 155 | (int) Math.ceil(padding[1]), 156 | (int) Math.ceil(padding[2]), 157 | (int) Math.ceil(padding[3])); 158 | } else if (extraData instanceof ReactTextUpdate) { 159 | ReactTextUpdate update = (ReactTextUpdate) extraData; 160 | if (update.containsImages()) { 161 | Spannable spannable = update.getText(); 162 | TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view); 163 | } 164 | view.maybeSetText(update); 165 | } 166 | } 167 | 168 | @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = ViewDefaults.FONT_SIZE_SP) 169 | public void setFontSize(ReactEditText view, float fontSize) { 170 | view.setTextSize( 171 | TypedValue.COMPLEX_UNIT_PX, 172 | (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize))); 173 | } 174 | 175 | @ReactProp(name = "onSelectionChange", defaultBoolean = false) 176 | public void setOnSelectionChange(final ReactEditText view, boolean onSelectionChange) { 177 | if (onSelectionChange) { 178 | view.setSelectionWatcher(new ReactSelectionWatcher(view)); 179 | } else { 180 | view.setSelectionWatcher(null); 181 | } 182 | } 183 | 184 | @ReactProp(name = "placeholder") 185 | public void setPlaceholder(ReactEditText view, @Nullable String placeholder) { 186 | view.setHint(placeholder); 187 | } 188 | 189 | @ReactProp(name = "placeholderTextColor", customType = "Color") 190 | public void setPlaceholderTextColor(ReactEditText view, @Nullable Integer color) { 191 | if (color == null) { 192 | view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext())); 193 | } else { 194 | view.setHintTextColor(color); 195 | } 196 | } 197 | 198 | @ReactProp(name = "selectionColor", customType = "Color") 199 | public void setSelectionColor(ReactEditText view, @Nullable Integer color) { 200 | if (color == null) { 201 | view.setHighlightColor(DefaultStyleValuesUtil.getDefaultTextColorHighlight(view.getContext())); 202 | } else { 203 | view.setHighlightColor(color); 204 | } 205 | } 206 | 207 | @ReactProp(name = "underlineColorAndroid", customType = "Color") 208 | public void setUnderlineColor(ReactEditText view, @Nullable Integer underlineColor) { 209 | if (underlineColor == null) { 210 | view.getBackground().clearColorFilter(); 211 | } else { 212 | view.getBackground().setColorFilter(underlineColor, PorterDuff.Mode.SRC_IN); 213 | } 214 | } 215 | 216 | // Bamlab: This is the added method see https://github.com/facebook/react-native/pull/6064 217 | @ReactProp(name = ViewProps.COLOR, defaultInt = 0, customType="color") 218 | public void setColor(ReactEditText view, @Nullable Integer color) { 219 | view.setTextColor(color); 220 | } 221 | // --- 222 | 223 | @ReactProp(name = ViewProps.TEXT_ALIGN) 224 | public void setTextAlign(ReactEditText view, @Nullable String textAlign) { 225 | if (textAlign == null || "auto".equals(textAlign)) { 226 | view.setGravityHorizontal(Gravity.NO_GRAVITY); 227 | } else if ("left".equals(textAlign)) { 228 | view.setGravityHorizontal(Gravity.LEFT); 229 | } else if ("right".equals(textAlign)) { 230 | view.setGravityHorizontal(Gravity.RIGHT); 231 | } else if ("center".equals(textAlign)) { 232 | view.setGravityHorizontal(Gravity.CENTER_HORIZONTAL); 233 | } else { 234 | throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); 235 | } 236 | } 237 | 238 | @ReactProp(name = ViewProps.TEXT_ALIGN_VERTICAL) 239 | public void setTextAlignVertical(ReactEditText view, @Nullable String textAlignVertical) { 240 | if (textAlignVertical == null || "auto".equals(textAlignVertical)) { 241 | view.setGravityVertical(Gravity.NO_GRAVITY); 242 | } else if ("top".equals(textAlignVertical)) { 243 | view.setGravityVertical(Gravity.TOP); 244 | } else if ("bottom".equals(textAlignVertical)) { 245 | view.setGravityVertical(Gravity.BOTTOM); 246 | } else if ("center".equals(textAlignVertical)) { 247 | view.setGravityVertical(Gravity.CENTER_VERTICAL); 248 | } else { 249 | throw new JSApplicationIllegalArgumentException("Invalid textAlignVertical: " + textAlignVertical); 250 | } 251 | } 252 | 253 | @ReactProp(name = "editable", defaultBoolean = true) 254 | public void setEditable(ReactEditText view, boolean editable) { 255 | view.setEnabled(editable); 256 | } 257 | 258 | @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = 1) 259 | public void setNumLines(ReactEditText view, int numLines) { 260 | view.setLines(numLines); 261 | } 262 | 263 | @ReactProp(name = "maxLength") 264 | public void setMaxLength(ReactEditText view, @Nullable Integer maxLength) { 265 | InputFilter [] currentFilters = view.getFilters(); 266 | InputFilter[] newFilters = EMPTY_FILTERS; 267 | 268 | if (maxLength == null) { 269 | if (currentFilters.length > 0) { 270 | LinkedList list = new LinkedList<>(); 271 | for (int i = 0; i < currentFilters.length; i++) { 272 | if (!(currentFilters[i] instanceof InputFilter.LengthFilter)) { 273 | list.add(currentFilters[i]); 274 | } 275 | } 276 | if (!list.isEmpty()) { 277 | newFilters = (InputFilter[]) list.toArray(); 278 | } 279 | } 280 | } else { 281 | if (currentFilters.length > 0) { 282 | newFilters = currentFilters; 283 | boolean replaced = false; 284 | for (int i = 0; i < currentFilters.length; i++) { 285 | if (currentFilters[i] instanceof InputFilter.LengthFilter) { 286 | currentFilters[i] = new InputFilter.LengthFilter(maxLength); 287 | replaced = true; 288 | } 289 | } 290 | if (!replaced) { 291 | newFilters = new InputFilter[currentFilters.length + 1]; 292 | System.arraycopy(currentFilters, 0, newFilters, 0, currentFilters.length); 293 | currentFilters[currentFilters.length] = new InputFilter.LengthFilter(maxLength); 294 | } 295 | } else { 296 | newFilters = new InputFilter[1]; 297 | newFilters[0] = new InputFilter.LengthFilter(maxLength); 298 | } 299 | } 300 | 301 | view.setFilters(newFilters); 302 | } 303 | 304 | @ReactProp(name = "autoCorrect") 305 | public void setAutoCorrect(ReactEditText view, @Nullable Boolean autoCorrect) { 306 | // clear auto correct flags, set SUGGESTIONS or NO_SUGGESTIONS depending on value 307 | updateStagedInputTypeFlag( 308 | view, 309 | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, 310 | autoCorrect != null ? 311 | (autoCorrect.booleanValue() ? 312 | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT : InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) 313 | : 0); 314 | } 315 | 316 | @ReactProp(name = "multiline", defaultBoolean = false) 317 | public void setMultiline(ReactEditText view, boolean multiline) { 318 | updateStagedInputTypeFlag( 319 | view, 320 | multiline ? 0 : InputType.TYPE_TEXT_FLAG_MULTI_LINE, 321 | multiline ? InputType.TYPE_TEXT_FLAG_MULTI_LINE : 0); 322 | } 323 | 324 | @ReactProp(name = "password", defaultBoolean = false) 325 | public void setPassword(ReactEditText view, boolean password) { 326 | updateStagedInputTypeFlag( 327 | view, 328 | password ? 0 : 329 | InputType.TYPE_NUMBER_VARIATION_PASSWORD | InputType.TYPE_TEXT_VARIATION_PASSWORD, 330 | password ? InputType.TYPE_TEXT_VARIATION_PASSWORD : 0); 331 | checkPasswordType(view); 332 | } 333 | 334 | @ReactProp(name = "autoCapitalize") 335 | public void setAutoCapitalize(ReactEditText view, int autoCapitalize) { 336 | updateStagedInputTypeFlag( 337 | view, 338 | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_CAP_WORDS | 339 | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, 340 | autoCapitalize); 341 | } 342 | 343 | @ReactProp(name = "keyboardType") 344 | public void setKeyboardType(ReactEditText view, @Nullable String keyboardType) { 345 | int flagsToSet = InputType.TYPE_CLASS_TEXT; 346 | if (KEYBOARD_TYPE_NUMERIC.equalsIgnoreCase(keyboardType)) { 347 | flagsToSet = INPUT_TYPE_KEYBOARD_NUMBERED; 348 | } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) { 349 | flagsToSet = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_CLASS_TEXT; 350 | } else if (KEYBOARD_TYPE_PHONE_PAD.equalsIgnoreCase(keyboardType)) { 351 | flagsToSet = InputType.TYPE_CLASS_PHONE; 352 | } 353 | updateStagedInputTypeFlag( 354 | view, 355 | INPUT_TYPE_KEYBOARD_NUMBERED | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | 356 | InputType.TYPE_CLASS_TEXT, 357 | flagsToSet); 358 | checkPasswordType(view); 359 | } 360 | 361 | @Override 362 | protected void onAfterUpdateTransaction(ReactEditText view) { 363 | super.onAfterUpdateTransaction(view); 364 | view.commitStagedInputType(); 365 | } 366 | 367 | // Sets the correct password type, since numeric and text passwords have different types 368 | private static void checkPasswordType(ReactEditText view) { 369 | if ((view.getStagedInputType() & INPUT_TYPE_KEYBOARD_NUMBERED) != 0 && 370 | (view.getStagedInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD) != 0) { 371 | // Text input type is numbered password, remove text password variation, add numeric one 372 | updateStagedInputTypeFlag( 373 | view, 374 | InputType.TYPE_TEXT_VARIATION_PASSWORD, 375 | InputType.TYPE_NUMBER_VARIATION_PASSWORD); 376 | } 377 | } 378 | 379 | private static void updateStagedInputTypeFlag( 380 | ReactEditText view, 381 | int flagsToUnset, 382 | int flagsToSet) { 383 | view.setStagedInputType((view.getStagedInputType() & ~flagsToUnset) | flagsToSet); 384 | } 385 | 386 | private class ReactTextInputTextWatcher implements TextWatcher { 387 | 388 | private EventDispatcher mEventDispatcher; 389 | private ReactEditText mEditText; 390 | private String mPreviousText; 391 | 392 | public ReactTextInputTextWatcher( 393 | final ReactContext reactContext, 394 | final ReactEditText editText) { 395 | mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 396 | mEditText = editText; 397 | mPreviousText = null; 398 | } 399 | 400 | @Override 401 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 402 | // Incoming charSequence gets mutated before onTextChanged() is invoked 403 | mPreviousText = s.toString(); 404 | } 405 | 406 | @Override 407 | public void onTextChanged(CharSequence s, int start, int before, int count) { 408 | // Rearranging the text (i.e. changing between singleline and multiline attributes) can 409 | // also trigger onTextChanged, call the event in JS only when the text actually changed 410 | if (count == 0 && before == 0) { 411 | return; 412 | } 413 | 414 | Assertions.assertNotNull(mPreviousText); 415 | String newText = s.toString().substring(start, start + count); 416 | String oldText = mPreviousText.substring(start, start + before); 417 | // Don't send same text changes 418 | if (count == before && newText.equals(oldText)) { 419 | return; 420 | } 421 | int contentWidth = mEditText.getWidth(); 422 | int contentHeight = mEditText.getHeight(); 423 | 424 | // Use instead size of text content within EditText when available 425 | if (mEditText.getLayout() != null) { 426 | contentWidth = mEditText.getCompoundPaddingLeft() + mEditText.getLayout().getWidth() + 427 | mEditText.getCompoundPaddingRight(); 428 | contentHeight = mEditText.getCompoundPaddingTop() + mEditText.getLayout().getHeight() + 429 | mEditText.getCompoundPaddingTop(); 430 | } 431 | 432 | // The event that contains the event counter and updates it must be sent first. 433 | // TODO: t7936714 merge these events 434 | mEventDispatcher.dispatchEvent( 435 | new ReactTextChangedEvent( 436 | mEditText.getId(), 437 | SystemClock.uptimeMillis(), 438 | s.toString(), 439 | (int) PixelUtil.toDIPFromPixel(contentWidth), 440 | (int) PixelUtil.toDIPFromPixel(contentHeight), 441 | mEditText.incrementAndGetEventCounter())); 442 | 443 | mEventDispatcher.dispatchEvent( 444 | new ReactTextInputEvent( 445 | mEditText.getId(), 446 | SystemClock.uptimeMillis(), 447 | newText, 448 | oldText, 449 | start, 450 | start + before)); 451 | } 452 | 453 | @Override 454 | public void afterTextChanged(Editable s) { 455 | } 456 | } 457 | 458 | @Override 459 | protected void addEventEmitters( 460 | final ThemedReactContext reactContext, 461 | final ReactEditText editText) { 462 | editText.addTextChangedListener(new ReactTextInputTextWatcher(reactContext, editText)); 463 | editText.setOnFocusChangeListener( 464 | new View.OnFocusChangeListener() { 465 | public void onFocusChange(View v, boolean hasFocus) { 466 | EventDispatcher eventDispatcher = 467 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 468 | if (hasFocus) { 469 | eventDispatcher.dispatchEvent( 470 | new ReactTextInputFocusEvent( 471 | editText.getId(), 472 | SystemClock.uptimeMillis())); 473 | } else { 474 | eventDispatcher.dispatchEvent( 475 | new ReactTextInputBlurEvent( 476 | editText.getId(), 477 | SystemClock.uptimeMillis())); 478 | 479 | eventDispatcher.dispatchEvent( 480 | new ReactTextInputEndEditingEvent( 481 | editText.getId(), 482 | SystemClock.uptimeMillis(), 483 | editText.getText().toString())); 484 | } 485 | } 486 | }); 487 | 488 | editText.setOnEditorActionListener( 489 | new TextView.OnEditorActionListener() { 490 | @Override 491 | public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) { 492 | // Any 'Enter' action will do 493 | if ((actionId & EditorInfo.IME_MASK_ACTION) > 0 || 494 | actionId == EditorInfo.IME_NULL) { 495 | EventDispatcher eventDispatcher = 496 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 497 | eventDispatcher.dispatchEvent( 498 | new ReactTextInputSubmitEditingEvent( 499 | editText.getId(), 500 | SystemClock.uptimeMillis(), 501 | editText.getText().toString())); 502 | } 503 | return false; 504 | } 505 | }); 506 | } 507 | 508 | private class ReactSelectionWatcher implements SelectionWatcher { 509 | 510 | private ReactEditText mReactEditText; 511 | private EventDispatcher mEventDispatcher; 512 | private int mPreviousSelectionStart; 513 | private int mPreviousSelectionEnd; 514 | 515 | public ReactSelectionWatcher(ReactEditText editText) { 516 | mReactEditText = editText; 517 | ReactContext reactContext = (ReactContext) editText.getContext(); 518 | mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); 519 | } 520 | 521 | @Override 522 | public void onSelectionChanged(int start, int end) { 523 | // Android will call us back for both the SELECTION_START span and SELECTION_END span in text 524 | // To prevent double calling back into js we cache the result of the previous call and only 525 | // forward it on if we have new values 526 | if (mPreviousSelectionStart != start || mPreviousSelectionEnd != end) { 527 | mEventDispatcher.dispatchEvent( 528 | new ReactTextInputSelectionEvent( 529 | mReactEditText.getId(), 530 | SystemClock.uptimeMillis(), 531 | start, 532 | end 533 | ) 534 | ); 535 | 536 | mPreviousSelectionStart = start; 537 | mPreviousSelectionEnd = end; 538 | } 539 | } 540 | } 541 | 542 | @Override 543 | public @Nullable Map getExportedViewConstants() { 544 | return MapBuilder.of( 545 | "AutoCapitalizationType", 546 | MapBuilder.of( 547 | "none", 548 | 0, 549 | "characters", 550 | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, 551 | "words", 552 | InputType.TYPE_TEXT_FLAG_CAP_WORDS, 553 | "sentences", 554 | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)); 555 | } 556 | } 557 | --------------------------------------------------------------------------------