├── 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 |
--------------------------------------------------------------------------------