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.
Files changed (78) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +141 -0
  3. package/RNTransformerTextInput.podspec +35 -0
  4. package/android/build.gradle +96 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/spotless.gradle +19 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/appandflow/transformertextinput/TextState.kt +15 -0
  9. package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorView.kt +160 -0
  10. package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorViewManager.kt +44 -0
  11. package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputJni.kt +25 -0
  12. package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputModule.kt +22 -0
  13. package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputPackage.kt +53 -0
  14. package/android/src/main/jni/CMakeLists.txt +62 -0
  15. package/android/src/main/jni/TransformerTextInputJni.cpp +94 -0
  16. package/android/src/main/jni/rntti.h +17 -0
  17. package/cpp/TransformerTextInputDecoratorViewComponentDescriptor.h +16 -0
  18. package/cpp/TransformerTextInputDecoratorViewShadowNode.cpp +21 -0
  19. package/cpp/TransformerTextInputDecoratorViewShadowNode.h +40 -0
  20. package/cpp/TransformerTextInputRuntime.cpp +86 -0
  21. package/cpp/TransformerTextInputRuntime.h +31 -0
  22. package/ios/TransformerTextInputDecoratorView.h +9 -0
  23. package/ios/TransformerTextInputDecoratorView.mm +256 -0
  24. package/ios/TransformerTextInputModule.h +8 -0
  25. package/ios/TransformerTextInputModule.mm +28 -0
  26. package/lib/module/NativeTransformerTextInputModule.js +5 -0
  27. package/lib/module/NativeTransformerTextInputModule.js.map +1 -0
  28. package/lib/module/Transformer.js +15 -0
  29. package/lib/module/Transformer.js.map +1 -0
  30. package/lib/module/TransformerTextInput.js +86 -0
  31. package/lib/module/TransformerTextInput.js.map +1 -0
  32. package/lib/module/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
  33. package/lib/module/formatters/phone-number.js +315 -0
  34. package/lib/module/formatters/phone-number.js.map +1 -0
  35. package/lib/module/index.js +5 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/registry.js +83 -0
  39. package/lib/module/registry.js.map +1 -0
  40. package/lib/module/selection.js +48 -0
  41. package/lib/module/selection.js.map +1 -0
  42. package/lib/module/utils/useMergeRefs.js +49 -0
  43. package/lib/module/utils/useMergeRefs.js.map +1 -0
  44. package/lib/module/utils/useRefEffect.js +37 -0
  45. package/lib/module/utils/useRefEffect.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/src/NativeTransformerTextInputModule.d.ts +7 -0
  48. package/lib/typescript/src/NativeTransformerTextInputModule.d.ts.map +1 -0
  49. package/lib/typescript/src/Transformer.d.ts +19 -0
  50. package/lib/typescript/src/Transformer.d.ts.map +1 -0
  51. package/lib/typescript/src/TransformerTextInput.d.ts +247 -0
  52. package/lib/typescript/src/TransformerTextInput.d.ts.map +1 -0
  53. package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts +12 -0
  54. package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  55. package/lib/typescript/src/formatters/phone-number.d.ts +18 -0
  56. package/lib/typescript/src/formatters/phone-number.d.ts.map +1 -0
  57. package/lib/typescript/src/index.d.ts +3 -0
  58. package/lib/typescript/src/index.d.ts.map +1 -0
  59. package/lib/typescript/src/registry.d.ts +17 -0
  60. package/lib/typescript/src/registry.d.ts.map +1 -0
  61. package/lib/typescript/src/selection.d.ts +4 -0
  62. package/lib/typescript/src/selection.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/useMergeRefs.d.ts +20 -0
  64. package/lib/typescript/src/utils/useMergeRefs.d.ts.map +1 -0
  65. package/lib/typescript/src/utils/useRefEffect.d.ts +24 -0
  66. package/lib/typescript/src/utils/useRefEffect.d.ts.map +1 -0
  67. package/package.json +199 -0
  68. package/react-native.config.js +13 -0
  69. package/src/NativeTransformerTextInputModule.ts +10 -0
  70. package/src/Transformer.ts +32 -0
  71. package/src/TransformerTextInput.tsx +147 -0
  72. package/src/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
  73. package/src/formatters/phone-number.ts +327 -0
  74. package/src/index.tsx +10 -0
  75. package/src/registry.ts +120 -0
  76. package/src/selection.ts +62 -0
  77. package/src/utils/useMergeRefs.ts +59 -0
  78. 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,5 @@
1
+ TransformerInput_kotlinVersion=2.0.21
2
+ TransformerInput_minSdkVersion=24
3
+ TransformerInput_targetSdkVersion=34
4
+ TransformerInput_compileSdkVersion=35
5
+ TransformerInput_ndkVersion=27.1.12297006
@@ -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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }