react-native-transformer-text-input 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +141 -0
- package/RNTransformerTextInput.podspec +35 -0
- package/android/build.gradle +96 -0
- package/android/gradle.properties +5 -0
- package/android/spotless.gradle +19 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TextState.kt +15 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorView.kt +160 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorViewManager.kt +44 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputJni.kt +25 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputModule.kt +22 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputPackage.kt +53 -0
- package/android/src/main/jni/CMakeLists.txt +62 -0
- package/android/src/main/jni/TransformerTextInputJni.cpp +94 -0
- package/android/src/main/jni/rntti.h +17 -0
- package/cpp/TransformerTextInputDecoratorViewComponentDescriptor.h +16 -0
- package/cpp/TransformerTextInputDecoratorViewShadowNode.cpp +21 -0
- package/cpp/TransformerTextInputDecoratorViewShadowNode.h +40 -0
- package/cpp/TransformerTextInputRuntime.cpp +86 -0
- package/cpp/TransformerTextInputRuntime.h +31 -0
- package/ios/TransformerTextInputDecoratorView.h +9 -0
- package/ios/TransformerTextInputDecoratorView.mm +256 -0
- package/ios/TransformerTextInputModule.h +8 -0
- package/ios/TransformerTextInputModule.mm +28 -0
- package/lib/module/NativeTransformerTextInputModule.js +5 -0
- package/lib/module/NativeTransformerTextInputModule.js.map +1 -0
- package/lib/module/Transformer.js +15 -0
- package/lib/module/Transformer.js.map +1 -0
- package/lib/module/TransformerTextInput.js +86 -0
- package/lib/module/TransformerTextInput.js.map +1 -0
- package/lib/module/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
- package/lib/module/formatters/phone-number.js +315 -0
- package/lib/module/formatters/phone-number.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/registry.js +83 -0
- package/lib/module/registry.js.map +1 -0
- package/lib/module/selection.js +48 -0
- package/lib/module/selection.js.map +1 -0
- package/lib/module/utils/useMergeRefs.js +49 -0
- package/lib/module/utils/useMergeRefs.js.map +1 -0
- package/lib/module/utils/useRefEffect.js +37 -0
- package/lib/module/utils/useRefEffect.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeTransformerTextInputModule.d.ts +7 -0
- package/lib/typescript/src/NativeTransformerTextInputModule.d.ts.map +1 -0
- package/lib/typescript/src/Transformer.d.ts +19 -0
- package/lib/typescript/src/Transformer.d.ts.map +1 -0
- package/lib/typescript/src/TransformerTextInput.d.ts +247 -0
- package/lib/typescript/src/TransformerTextInput.d.ts.map +1 -0
- package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts +12 -0
- package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/formatters/phone-number.d.ts +18 -0
- package/lib/typescript/src/formatters/phone-number.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/registry.d.ts +17 -0
- package/lib/typescript/src/registry.d.ts.map +1 -0
- package/lib/typescript/src/selection.d.ts +4 -0
- package/lib/typescript/src/selection.d.ts.map +1 -0
- package/lib/typescript/src/utils/useMergeRefs.d.ts +20 -0
- package/lib/typescript/src/utils/useMergeRefs.d.ts.map +1 -0
- package/lib/typescript/src/utils/useRefEffect.d.ts +24 -0
- package/lib/typescript/src/utils/useRefEffect.d.ts.map +1 -0
- package/package.json +199 -0
- package/react-native.config.js +13 -0
- package/src/NativeTransformerTextInputModule.ts +10 -0
- package/src/Transformer.ts +32 -0
- package/src/TransformerTextInput.tsx +147 -0
- package/src/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
- package/src/formatters/phone-number.ts +327 -0
- package/src/index.tsx +10 -0
- package/src/registry.ts +120 -0
- package/src/selection.ts +62 -0
- package/src/utils/useMergeRefs.ts +59 -0
- package/src/utils/useRefEffect.ts +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Janic Duplessis
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# react-native-transformer-text-input
|
|
2
|
+
|
|
3
|
+
TextInput component that allows transforming text synchronously with a worklet.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```sh
|
|
7
|
+
npm install react-native-transformer-text-input
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```tsx
|
|
12
|
+
import { useRef } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
Transformer,
|
|
15
|
+
TransformerTextInput,
|
|
16
|
+
type TransformerTextInputInstance,
|
|
17
|
+
} from 'react-native-transformer-text-input';
|
|
18
|
+
|
|
19
|
+
// Transformer that formats input as a lowercase username with @ prefix
|
|
20
|
+
const usernameTransformer = new Transformer(({ value }) => {
|
|
21
|
+
'worklet';
|
|
22
|
+
|
|
23
|
+
const cleaned = value.replace(/[^0-9a-zA-Z]/g, '').toLowerCase();
|
|
24
|
+
return { value: cleaned ? '@' + cleaned : '' };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function UsernameTextInput() {
|
|
28
|
+
const inputRef = useRef<TransformerTextInputInstance>(null);
|
|
29
|
+
|
|
30
|
+
const handleSubmit = () => {
|
|
31
|
+
const username = inputRef.current?.getValue();
|
|
32
|
+
console.log('Submitted:', username);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<TransformerTextInput
|
|
37
|
+
ref={inputRef}
|
|
38
|
+
transformer={usernameTransformer}
|
|
39
|
+
placeholder="@username"
|
|
40
|
+
autoCapitalize="none"
|
|
41
|
+
autoCorrect={false}
|
|
42
|
+
onSubmitEditing={handleSubmit}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Transformer
|
|
51
|
+
|
|
52
|
+
Create a transformer by passing a worklet function:
|
|
53
|
+
|
|
54
|
+
- **Constructor**: `new Transformer(worklet)`
|
|
55
|
+
- **worklet input**: an object with
|
|
56
|
+
- `value`: current text value.
|
|
57
|
+
- `previousValue`: previous text value (falls back to `value` on first call).
|
|
58
|
+
- `selection`: current selection `{ start, end }`.
|
|
59
|
+
- `previousSelection`: previous selection `{ start, end }` (falls back to `selection` on first call).
|
|
60
|
+
- **worklet return**:
|
|
61
|
+
- Return `null` or `undefined` to apply no transform.
|
|
62
|
+
- Return an object where each field can also be `null` or `undefined` to leave that part unchanged:
|
|
63
|
+
- `value?: string | null` to update the text.
|
|
64
|
+
- `selection?: { start: number; end: number } | null` to update the selection.
|
|
65
|
+
|
|
66
|
+
### TransformerTextInput
|
|
67
|
+
|
|
68
|
+
`TransformerTextInput` wraps React Native `TextInput` and applies a `Transformer` on the UI thread.
|
|
69
|
+
|
|
70
|
+
- **Props**: all `TextInput` props (except `value`) plus:
|
|
71
|
+
- `transformer`: a `Transformer` instance.
|
|
72
|
+
- **Ref**: `TransformerTextInputInstance` with:
|
|
73
|
+
- `getValue(): string` - Returns the current text value.
|
|
74
|
+
- `update(options): void` - Programmatically update the input.
|
|
75
|
+
- `options.value: string` - The new text value.
|
|
76
|
+
- `options.selection?: { start: number; end: number }` - Optional cursor/selection position.
|
|
77
|
+
- `options.transform?: boolean` - Whether to run the transformer on the new value (default: `true`).
|
|
78
|
+
- `clear(): void` - Clear the input value.
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- The transformer must be a worklet; the `Transformer` constructor will throw if it isn't.
|
|
83
|
+
- Prefer creating `Transformer` instances at module scope to avoid recreating worklets on every render.
|
|
84
|
+
- This library supports the New Architecture only.
|
|
85
|
+
|
|
86
|
+
## Selection Control
|
|
87
|
+
|
|
88
|
+
Selection control is needed because transforms can insert or remove characters, which would otherwise move the cursor unpredictably. The transformer can return a `selection` to fully control the caret/selection after a change.
|
|
89
|
+
|
|
90
|
+
Default behavior when no `selection` is returned:
|
|
91
|
+
- If the cursor was at the end, it stays at the end.
|
|
92
|
+
- If the cursor was in the middle, it moves forward by the number of inserted/removed characters.
|
|
93
|
+
- If the position is ambiguous, it falls back to the end.
|
|
94
|
+
|
|
95
|
+
## Built-in Transformers (Experimental)
|
|
96
|
+
|
|
97
|
+
> **Warning**: Built-in transformers are experimental. Breaking changes may occur in minor versions.
|
|
98
|
+
|
|
99
|
+
The library includes ready-to-use transformers for common use cases.
|
|
100
|
+
|
|
101
|
+
### PhoneNumberTransformer
|
|
102
|
+
|
|
103
|
+
Formats phone numbers as the user types.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { PhoneNumberTransformer } from 'react-native-transformer-text-input/formatters/phone-number';
|
|
107
|
+
|
|
108
|
+
const phoneTransformer = new PhoneNumberTransformer({
|
|
109
|
+
country: 'US', // Only 'US' supported currently
|
|
110
|
+
debug: false, // Enable debug logging (default: false)
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Formats as: +1 (555) 123-4567
|
|
114
|
+
<TransformerTextInput
|
|
115
|
+
transformer={phoneTransformer}
|
|
116
|
+
keyboardType="phone-pad"
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## AI Disclosure
|
|
121
|
+
|
|
122
|
+
Code in this repository is thought through and mostly written by humans, with AI used to improve clarity, consistency, and implementation details.
|
|
123
|
+
|
|
124
|
+
## Acknowledgments
|
|
125
|
+
|
|
126
|
+
- [react-native-live-markdown](https://github.com/Expensify/react-native-live-markdown) for an example of how to extend TextInput.
|
|
127
|
+
- [react-native-worklets](https://github.com/software-mansion/react-native-reanimated/tree/main/packages/react-native-worklets) for the worklet runtime powering UI-thread execution.
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
132
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
133
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
pods_root = Pod::Config.instance.project_pods_root
|
|
6
|
+
react_native_worklets_node_modules_dir = ENV['REACT_NATIVE_WORKLETS_NODE_MODULES_DIR'] ||
|
|
7
|
+
File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-worklets/package.json')"`)
|
|
8
|
+
react_native_worklets_node_modules_dir_from_pods_root = Pathname.new(react_native_worklets_node_modules_dir).relative_path_from(pods_root).to_s
|
|
9
|
+
|
|
10
|
+
Pod::Spec.new do |s|
|
|
11
|
+
s.name = "RNTransformerTextInput"
|
|
12
|
+
s.version = package["version"]
|
|
13
|
+
s.summary = package["description"]
|
|
14
|
+
s.homepage = package["homepage"]
|
|
15
|
+
s.license = package["license"]
|
|
16
|
+
s.authors = package["author"]
|
|
17
|
+
|
|
18
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
19
|
+
s.source = { :git => "https://github.com/AppAndFlow/react-native-transformer-text-input.git", :tag => "#{s.version}" }
|
|
20
|
+
|
|
21
|
+
s.source_files = [
|
|
22
|
+
"ios/**/*.{h,m,mm,swift}",
|
|
23
|
+
"cpp/**/*.{h,cpp}"
|
|
24
|
+
]
|
|
25
|
+
s.private_header_files = "ios/**/*.h"
|
|
26
|
+
|
|
27
|
+
install_modules_dependencies(s)
|
|
28
|
+
|
|
29
|
+
s.xcconfig = {
|
|
30
|
+
"HEADER_SEARCH_PATHS" => [
|
|
31
|
+
"\"$(PODS_ROOT)/#{react_native_worklets_node_modules_dir_from_pods_root}/apple\"",
|
|
32
|
+
"\"$(PODS_ROOT)/#{react_native_worklets_node_modules_dir_from_pods_root}/Common/cpp\"",
|
|
33
|
+
].join(' '),
|
|
34
|
+
}
|
|
35
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
dependencies {
|
|
8
|
+
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (project == rootProject) {
|
|
13
|
+
apply from: "spotless.gradle"
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def reactNativeArchitectures() {
|
|
18
|
+
def value = project.getProperties().get("reactNativeArchitectures")
|
|
19
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.android.library"
|
|
23
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
24
|
+
apply plugin: "com.facebook.react"
|
|
25
|
+
|
|
26
|
+
def getExtOrIntegerDefault(name) {
|
|
27
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TransformerInput_" + name]).toInteger()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def getExtOrDefault(name, defaultValue) {
|
|
31
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TransformerInput_" + name] ?: defaultValue)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def kotlinVersion = getExtOrDefault("kotlinVersion", "2.0.21")
|
|
35
|
+
|
|
36
|
+
android {
|
|
37
|
+
namespace "com.appandflow.transformertextinput"
|
|
38
|
+
|
|
39
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
40
|
+
|
|
41
|
+
defaultConfig {
|
|
42
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
43
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
44
|
+
|
|
45
|
+
ndk {
|
|
46
|
+
abiFilters (*reactNativeArchitectures())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
buildFeatures {
|
|
51
|
+
buildConfig true
|
|
52
|
+
prefab true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lintOptions {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
textReport true
|
|
58
|
+
textOutput 'stdout'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compileOptions {
|
|
62
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
63
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
kotlinOptions {
|
|
67
|
+
allWarningsAsErrors = System.getenv("RNTTI_WARNINGS_AS_ERRORS") == "true"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
repositories {
|
|
72
|
+
mavenCentral()
|
|
73
|
+
google()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
dependencies {
|
|
77
|
+
implementation "com.facebook.react:react-android"
|
|
78
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
79
|
+
|
|
80
|
+
if (rootProject.subprojects.find { it.name == "react-native-worklets" }) {
|
|
81
|
+
implementation project(":react-native-worklets")
|
|
82
|
+
} else {
|
|
83
|
+
throw new GradleException("[rntti] `react-native-worklets` library not found. Please install it as a dependency in your project.")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
evaluationDependsOn(":react-native-worklets")
|
|
88
|
+
|
|
89
|
+
// afterEvaluate {
|
|
90
|
+
// tasks.named("externalNativeBuildDebug").configure {
|
|
91
|
+
// dependsOn(findProject(":react-native-worklets").tasks.named("externalNativeBuildDebug"))
|
|
92
|
+
// }
|
|
93
|
+
// tasks.named("externalNativeBuildRelease").configure {
|
|
94
|
+
// dependsOn(findProject(":react-native-worklets").tasks.named("externalNativeBuildRelease"))
|
|
95
|
+
// }
|
|
96
|
+
// }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
apply plugin: 'com.diffplug.spotless'
|
|
2
|
+
|
|
3
|
+
allprojects {
|
|
4
|
+
repositories {
|
|
5
|
+
google()
|
|
6
|
+
mavenCentral()
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
spotless {
|
|
12
|
+
kotlin {
|
|
13
|
+
target 'src/**/*.kt'
|
|
14
|
+
ktlint("1.5.0").setEditorConfigPath("$projectDir/../.editorconfig")
|
|
15
|
+
trimTrailingWhitespace()
|
|
16
|
+
indentWithSpaces()
|
|
17
|
+
endWithNewline()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStripAny
|
|
4
|
+
|
|
5
|
+
@DoNotStripAny
|
|
6
|
+
data class TextSelection(
|
|
7
|
+
val start: Int,
|
|
8
|
+
val end: Int,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
@DoNotStripAny
|
|
12
|
+
data class TextState(
|
|
13
|
+
val value: String,
|
|
14
|
+
val selection: TextSelection,
|
|
15
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.Editable
|
|
5
|
+
import android.text.TextWatcher
|
|
6
|
+
import com.facebook.react.views.textinput.ReactEditText
|
|
7
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
8
|
+
import kotlinx.coroutines.Dispatchers
|
|
9
|
+
import kotlinx.coroutines.Job
|
|
10
|
+
import kotlinx.coroutines.MainScope
|
|
11
|
+
import kotlinx.coroutines.launch
|
|
12
|
+
|
|
13
|
+
class TransformerTextInputDecoratorView(
|
|
14
|
+
context: Context,
|
|
15
|
+
) : ReactViewGroup(context),
|
|
16
|
+
TextWatcher {
|
|
17
|
+
private var transformerId: Int = 0
|
|
18
|
+
private var lastEventValue: String? = null
|
|
19
|
+
private var resetLastEventValueJob: Job? = null
|
|
20
|
+
private var reactEditText: ReactEditText? = null
|
|
21
|
+
private var isUpdating = false
|
|
22
|
+
|
|
23
|
+
private fun currentValue(): String = reactEditText?.text?.toString() ?: ""
|
|
24
|
+
|
|
25
|
+
private fun currentSelection(): TextSelection {
|
|
26
|
+
val input = reactEditText
|
|
27
|
+
return if (input == null) {
|
|
28
|
+
TextSelection(0, 0)
|
|
29
|
+
} else {
|
|
30
|
+
TextSelection(input.selectionStart.coerceAtLeast(0), input.selectionEnd.coerceAtLeast(0))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun applyValue(value: String) {
|
|
35
|
+
reactEditText?.setText(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private fun applySelection(selection: TextSelection) {
|
|
39
|
+
reactEditText?.setSelection(selection.start, selection.end)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun transformTextState(state: TextState) =
|
|
43
|
+
TransformerTextInputJni.transform(
|
|
44
|
+
transformerId,
|
|
45
|
+
state.value,
|
|
46
|
+
state.selection.start,
|
|
47
|
+
state.selection.end,
|
|
48
|
+
) ?: state
|
|
49
|
+
|
|
50
|
+
fun setTransformerId(newTransformerId: Int) {
|
|
51
|
+
transformerId = newTransformerId
|
|
52
|
+
lastEventValue = null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun onAttachedToWindow() {
|
|
56
|
+
super.onAttachedToWindow()
|
|
57
|
+
|
|
58
|
+
val child = getChildAt(0)
|
|
59
|
+
if (child is ReactEditText) {
|
|
60
|
+
reactEditText = child
|
|
61
|
+
reactEditText?.addTextChangedListener(this)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onDetachedFromWindow() {
|
|
66
|
+
super.onDetachedFromWindow()
|
|
67
|
+
reactEditText?.removeTextChangedListener(this)
|
|
68
|
+
reactEditText = null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun beforeTextChanged(
|
|
72
|
+
s: CharSequence?,
|
|
73
|
+
start: Int,
|
|
74
|
+
count: Int,
|
|
75
|
+
after: Int,
|
|
76
|
+
) {
|
|
77
|
+
// noop
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun onTextChanged(
|
|
81
|
+
s: CharSequence?,
|
|
82
|
+
start: Int,
|
|
83
|
+
before: Int,
|
|
84
|
+
count: Int,
|
|
85
|
+
) {
|
|
86
|
+
// noop
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun afterTextChanged(s: Editable?) {
|
|
90
|
+
if (isUpdating) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
val editValue = s?.toString() ?: ""
|
|
95
|
+
|
|
96
|
+
// For some reason, text change events are dispatched multiple times with the same value, which
|
|
97
|
+
// causes issue with how we track previous values. To avoid this and match iOS behavior we ignore
|
|
98
|
+
// events in the same frame that have the same text value.
|
|
99
|
+
if (lastEventValue == editValue) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
lastEventValue = editValue
|
|
103
|
+
resetLastEventValueJob?.cancel()
|
|
104
|
+
resetLastEventValueJob =
|
|
105
|
+
MainScope().launch(Dispatchers.Main) {
|
|
106
|
+
lastEventValue = null
|
|
107
|
+
resetLastEventValueJob = null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
val currentSelection = currentSelection()
|
|
111
|
+
val current = TextState(editValue, currentSelection)
|
|
112
|
+
val next = transformTextState(current)
|
|
113
|
+
val didTransformValue = next.value != current.value
|
|
114
|
+
isUpdating = true
|
|
115
|
+
try {
|
|
116
|
+
if (didTransformValue) {
|
|
117
|
+
applyValue(next.value)
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
didTransformValue || next.selection != currentSelection
|
|
121
|
+
) {
|
|
122
|
+
applySelection(next.selection)
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
isUpdating = false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun update(
|
|
130
|
+
transform: Boolean,
|
|
131
|
+
value: String?,
|
|
132
|
+
selectionStart: Int,
|
|
133
|
+
selectionEnd: Int,
|
|
134
|
+
) {
|
|
135
|
+
if (reactEditText == null) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
val currentValue = currentValue()
|
|
139
|
+
val currentSelection = currentSelection()
|
|
140
|
+
val providedValue = value ?: currentValue
|
|
141
|
+
val providedSelection = TextSelection(selectionStart, selectionEnd)
|
|
142
|
+
val provided = TextState(providedValue, providedSelection)
|
|
143
|
+
val next = if (transform) transformTextState(provided) else provided
|
|
144
|
+
|
|
145
|
+
val didTransformValue = next.value != currentValue
|
|
146
|
+
isUpdating = true
|
|
147
|
+
try {
|
|
148
|
+
if (didTransformValue) {
|
|
149
|
+
applyValue(next.value)
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
didTransformValue || next.selection != currentSelection
|
|
153
|
+
) {
|
|
154
|
+
applySelection(next.selection)
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
isUpdating = false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
6
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
7
|
+
import com.facebook.react.viewmanagers.TransformerTextInputDecoratorViewManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.TransformerTextInputDecoratorViewManagerInterface
|
|
9
|
+
|
|
10
|
+
@ReactModule(name = TransformerTextInputDecoratorViewManager.NAME)
|
|
11
|
+
class TransformerTextInputDecoratorViewManager :
|
|
12
|
+
ViewGroupManager<TransformerTextInputDecoratorView>(),
|
|
13
|
+
TransformerTextInputDecoratorViewManagerInterface<TransformerTextInputDecoratorView> {
|
|
14
|
+
private val mDelegate: ViewManagerDelegate<TransformerTextInputDecoratorView> =
|
|
15
|
+
TransformerTextInputDecoratorViewManagerDelegate(this)
|
|
16
|
+
|
|
17
|
+
override fun getDelegate(): ViewManagerDelegate<TransformerTextInputDecoratorView>? = mDelegate
|
|
18
|
+
|
|
19
|
+
override fun getName(): String = NAME
|
|
20
|
+
|
|
21
|
+
public override fun createViewInstance(context: ThemedReactContext): TransformerTextInputDecoratorView =
|
|
22
|
+
TransformerTextInputDecoratorView(context)
|
|
23
|
+
|
|
24
|
+
override fun setTransformerId(
|
|
25
|
+
view: TransformerTextInputDecoratorView?,
|
|
26
|
+
transformerId: Int,
|
|
27
|
+
) {
|
|
28
|
+
view?.setTransformerId(transformerId)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun update(
|
|
32
|
+
view: TransformerTextInputDecoratorView?,
|
|
33
|
+
transform: Boolean,
|
|
34
|
+
value: String?,
|
|
35
|
+
selectionStart: Int,
|
|
36
|
+
selectionEnd: Int,
|
|
37
|
+
) {
|
|
38
|
+
view?.update(transform, value, selectionStart, selectionEnd)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
companion object {
|
|
42
|
+
const val NAME = "TransformerTextInputDecoratorView"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
4
|
+
import com.facebook.soloader.SoLoader
|
|
5
|
+
import com.swmansion.worklets.WorkletsModule
|
|
6
|
+
|
|
7
|
+
@DoNotStrip
|
|
8
|
+
object TransformerTextInputJni {
|
|
9
|
+
init {
|
|
10
|
+
SoLoader.loadLibrary("react_codegen_rntti")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@DoNotStrip
|
|
14
|
+
@JvmStatic
|
|
15
|
+
external fun setWorkletsModule(workletsModule: WorkletsModule)
|
|
16
|
+
|
|
17
|
+
@DoNotStrip
|
|
18
|
+
@JvmStatic
|
|
19
|
+
external fun transform(
|
|
20
|
+
transformerId: Int,
|
|
21
|
+
value: String,
|
|
22
|
+
selectionStart: Int,
|
|
23
|
+
selectionEnd: Int,
|
|
24
|
+
): TextState?
|
|
25
|
+
}
|
package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputModule.kt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.swmansion.worklets.WorkletsModule
|
|
6
|
+
|
|
7
|
+
@ReactModule(name = TransformerTextInputModule.NAME)
|
|
8
|
+
class TransformerTextInputModule(
|
|
9
|
+
reactContext: ReactApplicationContext,
|
|
10
|
+
) : NativeTransformerTextInputModuleSpec(reactContext) {
|
|
11
|
+
override fun install(): Boolean {
|
|
12
|
+
val workletsModule = reactApplicationContext.getNativeModule(WorkletsModule::class.java)
|
|
13
|
+
if (workletsModule != null) {
|
|
14
|
+
TransformerTextInputJni.setWorkletsModule(workletsModule)
|
|
15
|
+
}
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
const val NAME: String = NativeTransformerTextInputModuleSpec.NAME
|
|
21
|
+
}
|
|
22
|
+
}
|
package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputPackage.kt
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.appandflow.transformertextinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.ReactPackage
|
|
5
|
+
import com.facebook.react.bridge.NativeModule
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
8
|
+
import com.facebook.react.module.annotations.ReactModuleList
|
|
9
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
10
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
11
|
+
import com.facebook.react.uimanager.ViewManager
|
|
12
|
+
import java.util.ArrayList
|
|
13
|
+
import java.util.HashMap
|
|
14
|
+
|
|
15
|
+
@ReactModuleList(nativeModules = [TransformerTextInputModule::class])
|
|
16
|
+
class TransformerTextInputPackage :
|
|
17
|
+
BaseReactPackage(),
|
|
18
|
+
ReactPackage {
|
|
19
|
+
override fun getModule(
|
|
20
|
+
name: String,
|
|
21
|
+
reactContext: ReactApplicationContext,
|
|
22
|
+
): NativeModule? =
|
|
23
|
+
if (name == TransformerTextInputModule.NAME) {
|
|
24
|
+
TransformerTextInputModule(reactContext)
|
|
25
|
+
} else {
|
|
26
|
+
null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
30
|
+
val moduleList = arrayOf(TransformerTextInputModule::class.java)
|
|
31
|
+
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
32
|
+
for (moduleClass in moduleList) {
|
|
33
|
+
val reactModule = moduleClass.getAnnotation(ReactModule::class.java) ?: continue
|
|
34
|
+
reactModuleInfoMap[reactModule.name] =
|
|
35
|
+
ReactModuleInfo(
|
|
36
|
+
reactModule.name,
|
|
37
|
+
moduleClass.name,
|
|
38
|
+
reactModule.canOverrideExistingModule,
|
|
39
|
+
reactModule.needsEagerInit,
|
|
40
|
+
reactModule.isCxxModule,
|
|
41
|
+
true,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return ReactModuleInfoProvider { reactModuleInfoMap }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
49
|
+
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
|
|
50
|
+
viewManagers.add(TransformerTextInputDecoratorViewManager())
|
|
51
|
+
return viewManagers
|
|
52
|
+
}
|
|
53
|
+
}
|