expo-rich-input 0.1.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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # expo-rich-input
2
+
3
+ A native Expo module that replaces `TextInput` entirely, giving the editor raw OS-level edit deltas — insert, delete, and IME compose events — directly from `UIKeyInput` on iOS and `InputConnection` on Android, so the Rope never has to diff a full string.
4
+
5
+ # API documentation
6
+
7
+ - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/rich-input/)
8
+ - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/rich-input/)
9
+
10
+ # Installation in managed Expo projects
11
+
12
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
13
+
14
+ # Installation in bare React Native projects
15
+
16
+ For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
17
+
18
+ ### Add the package to your npm dependencies
19
+
20
+ ```
21
+ npm install expo-rich-input
22
+ ```
23
+
24
+ ### Configure for Android
25
+
26
+
27
+
28
+
29
+ ### Configure for iOS
30
+
31
+ Run `npx pod-install` after installing the npm package.
32
+
33
+ # Contributing
34
+
35
+ Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
@@ -0,0 +1,18 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'expo.modules.richinput'
7
+ version = '0.1.0'
8
+
9
+ android {
10
+ namespace "expo.modules.richinput"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName "0.1.0"
14
+ }
15
+ lintOptions {
16
+ abortOnError false
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,38 @@
1
+ package expo.modules.richinput
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class ExpoRichInputModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("ExpoRichInput")
9
+
10
+ View(RichInputView::class) {
11
+
12
+ Events("onEditEvent", "onKeyboardAction")
13
+
14
+ AsyncFunction("focus") {
15
+ view: RichInputView ->
16
+ view.focusInput()
17
+ }
18
+
19
+ AsyncFunction("blur") {
20
+ view: RichInputView ->
21
+ view.blurInput()
22
+ }
23
+
24
+ // Wire up event dispatchers into the view
25
+ OnViewDidUpdateProps {
26
+ view ->
27
+ view.onEditEvent = {
28
+ payload ->
29
+ view.dispatchEvent("onEditEvent", payload)
30
+ }
31
+ view.onKeyboardAction = {
32
+ payload ->
33
+ view.dispatchEvent("onKeyboardAction", payload)
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,130 @@
1
+ package expo.modules.richinput
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.text.InputType
6
+ import android.view.KeyEvent
7
+ import android.view.View
8
+ import android.view.inputmethod.BaseInputConnection
9
+ import android.view.inputmethod.EditorInfo
10
+ import android.view.inputmethod.InputConnection
11
+ import android.view.inputmethod.InputMethodManager
12
+ import expo.modules.kotlin.AppContext
13
+ import expo.modules.kotlin.views.ExpoView
14
+
15
+ class RichInputView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
16
+
17
+ // Events dispatched to JS
18
+ var onEditEvent: ((Map<String, Any>) -> Unit)? = null
19
+ var onKeyboardAction: ((Map<String, Any>) -> Unit)? = null
20
+
21
+ init {
22
+ // View must be focusable to receive InputConnection
23
+ isFocusable = true
24
+ isFocusableInTouchMode = true
25
+ }
26
+
27
+ // MARK: InputConnection — this is where all text input flows through
28
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
29
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT or
30
+ InputType.TYPE_TEXT_FLAG_MULTI_LINE or
31
+ InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
32
+
33
+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
34
+ EditorInfo.IME_ACTION_NONE
35
+
36
+ // Disable autocorrect suggestions bar
37
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
38
+ outAttrs.initialCapsMode = 0
39
+ }
40
+
41
+ return EditorInputConnection(this)
42
+ }
43
+
44
+ override fun onCheckIsTextEditor(): Boolean = true
45
+
46
+ // MARK: Hardware key events (physical keyboard, back key etc.)
47
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
48
+ if (event.isCtrlPressed) {
49
+ when (keyCode) {
50
+ KeyEvent.KEYCODE_Z -> {
51
+ if (event.isShiftPressed) {
52
+ onKeyboardAction?.invoke(mapOf("action" to "redo"))
53
+ } else {
54
+ onKeyboardAction?.invoke(mapOf("action" to "undo"))
55
+ }
56
+ return true
57
+ }
58
+ KeyEvent.KEYCODE_SLASH -> {
59
+ onKeyboardAction?.invoke(mapOf("action" to "toggleComment"))
60
+ return true
61
+ }
62
+ }
63
+ }
64
+ return super.onKeyDown(keyCode, event)
65
+ }
66
+
67
+ // MARK: Focus control called from JS
68
+ fun focusInput() {
69
+ requestFocus()
70
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
71
+ imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
72
+ }
73
+
74
+ fun blurInput() {
75
+ clearFocus()
76
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
77
+ imm.hideSoftInputFromWindow(windowToken, 0)
78
+ }
79
+
80
+ // MARK: InputConnection inner class
81
+ inner class EditorInputConnection(view: View) : BaseInputConnection(view, false) {
82
+
83
+ // Regular text commit — typing, paste, swipe keyboard word
84
+ override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
85
+ val str = text?.toString() ?: return false
86
+ onEditEvent?.invoke(mapOf(
87
+ "type" to "insert",
88
+ "text" to str
89
+ ))
90
+ return true
91
+ }
92
+
93
+ // IME composing (Gboard swipe intermediate states, CJK input)
94
+ override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
95
+ val str = text?.toString() ?: ""
96
+ onEditEvent?.invoke(mapOf(
97
+ "type" to "compose",
98
+ "text" to str
99
+ ))
100
+ return true
101
+ }
102
+
103
+ // IME composition confirmed
104
+ override fun finishComposingText(): Boolean {
105
+ onEditEvent?.invoke(mapOf(
106
+ "type" to "composeCommit"
107
+ ))
108
+ return true
109
+ }
110
+
111
+ // Backspace — beforeLength is almost always 1
112
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
113
+ if (beforeLength > 0) {
114
+ onEditEvent?.invoke(mapOf(
115
+ "type" to "delete",
116
+ "count" to beforeLength
117
+ ))
118
+ }
119
+ return true
120
+ }
121
+
122
+ // Disable autocorrect suggestions — return null to tell the OS
123
+ // there is no surrounding text context to work with
124
+ override fun getExtractedText(request: android.view.inputmethod.ExtractedTextRequest?, flags: Int)
125
+ : android.view.inputmethod.ExtractedText? = null
126
+
127
+ override fun getSurroundingText(beforeLength: Int, afterLength: Int, flags: Int)
128
+ : android.view.inputmethod.SurroundingText? = null
129
+ }
130
+ }
@@ -0,0 +1,18 @@
1
+ export type EditEventType = "insert" | "delete" | "compose" | "composeCommit";
2
+ export interface EditEvent {
3
+ type: EditEventType;
4
+ text?: string;
5
+ count?: number;
6
+ }
7
+ export interface KeyboardActionEvent {
8
+ action: "undo" | "redo" | "toggleComment";
9
+ }
10
+ export interface ExpoRichInputRef {
11
+ focus: () => Promise<void>;
12
+ blur: () => Promise<void>;
13
+ }
14
+ export interface ExpoRichInputProps {
15
+ onEditEvent: (event: EditEvent) => void;
16
+ onKeyboardAction?: (event: KeyboardActionEvent) => void;
17
+ }
18
+ //# sourceMappingURL=ExpoRichInput.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInput.types.d.ts","sourceRoot":"","sources":["../src/ExpoRichInput.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,eAAe,CAAC;AAE9E,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,eAAe,CAAC;CAC7C;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAC/B,WAAW,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC3D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoRichInput.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInput.types.js","sourceRoot":"","sources":["../src/ExpoRichInput.types.ts"],"names":[],"mappings":"","sourcesContent":["export type EditEventType = \"insert\" | \"delete\" | \"compose\" | \"composeCommit\";\n\nexport interface EditEvent {\n type: EditEventType;\n text?: string;\n count?: number;\n}\n\nexport interface KeyboardActionEvent {\n action: \"undo\" | \"redo\" | \"toggleComment\";\n}\n\nexport interface ExpoRichInputRef {\n focus: () => Promise<void>;\n blur: () => Promise<void>;\n}\n\nexport interface ExpoRichInputProps {\n onEditEvent: (event: EditEvent) => void;\n onKeyboardAction?: (event: KeyboardActionEvent) => void;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function focus(viewRef: any): Promise<void>;
2
+ export declare function blur(viewRef: any): Promise<void>;
3
+ //# sourceMappingURL=ExpoRichInputModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInputModule.d.ts","sourceRoot":"","sources":["../src/ExpoRichInputModule.ts"],"names":[],"mappings":"AAIA,wBAAgB,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjD;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhD"}
@@ -0,0 +1,9 @@
1
+ import { requireNativeModule } from "expo-modules-core";
2
+ const ExpoRichInput = requireNativeModule("ExpoRichInput");
3
+ export function focus(viewRef) {
4
+ return ExpoRichInput.focus(viewRef);
5
+ }
6
+ export function blur(viewRef) {
7
+ return ExpoRichInput.blur(viewRef);
8
+ }
9
+ //# sourceMappingURL=ExpoRichInputModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInputModule.js","sourceRoot":"","sources":["../src/ExpoRichInputModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,aAAa,GAAG,mBAAmB,CAAC,eAAe,CAAC,CAAC;AAE3D,MAAM,UAAU,KAAK,CAAC,OAAY;IAC9B,OAAO,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,OAAY;IAC7B,OAAO,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC","sourcesContent":["import { requireNativeModule } from \"expo-modules-core\";\n\nconst ExpoRichInput = requireNativeModule(\"ExpoRichInput\");\n\nexport function focus(viewRef: any): Promise<void> {\n return ExpoRichInput.focus(viewRef);\n}\n\nexport function blur(viewRef: any): Promise<void> {\n return ExpoRichInput.blur(viewRef);\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { ExpoRichInputProps, ExpoRichInputRef } from "./ExpoRichInput.types";
2
+ export declare const ExpoRichInputView: import("react").ForwardRefExoticComponent<ExpoRichInputProps & import("react").RefAttributes<ExpoRichInputRef>>;
3
+ //# sourceMappingURL=ExpoRichInputView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInputView.d.ts","sourceRoot":"","sources":["../src/ExpoRichInputView.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAI7E,eAAO,MAAM,iBAAiB,iHAmB5B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { requireNativeViewManager } from "expo-modules-core";
2
+ import { forwardRef, useRef, useImperativeHandle } from "react";
3
+ import { StyleSheet } from "react-native";
4
+ import { focus, blur } from "./ExpoRichInputModule";
5
+ const NativeView = requireNativeViewManager("ExpoRichInput");
6
+ export const ExpoRichInputView = forwardRef(({ onEditEvent, onKeyboardAction }, ref) => {
7
+ const nativeRef = useRef(null);
8
+ useImperativeHandle(ref, () => ({
9
+ focus: () => focus(nativeRef.current),
10
+ blur: () => blur(nativeRef.current)
11
+ }));
12
+ return (<NativeView ref={nativeRef} style={styles.input} onEditEvent={(e) => onEditEvent(e.nativeEvent)} onKeyboardAction={(e) => onKeyboardAction?.(e.nativeEvent)}/>);
13
+ });
14
+ const styles = StyleSheet.create({
15
+ input: {
16
+ position: "absolute",
17
+ top: 0,
18
+ left: 0,
19
+ width: 1,
20
+ height: 1,
21
+ opacity: 0
22
+ }
23
+ });
24
+ //# sourceMappingURL=ExpoRichInputView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoRichInputView.js","sourceRoot":"","sources":["../src/ExpoRichInputView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAGpD,MAAM,UAAU,GAAG,wBAAwB,CAAC,eAAe,CAAC,CAAC;AAE7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAGzC,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,EAAE,GAAG,EAAE,EAAE;IACzC,MAAM,SAAS,GAAG,MAAM,CAAM,IAAI,CAAC,CAAC;IAEpC,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC;QACrC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KACtC,CAAC,CAAC,CAAC;IAEJ,OAAO,CACH,CAAC,UAAU,CACP,GAAG,CAAC,CAAC,SAAS,CAAC,CACf,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACpB,WAAW,CAAC,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CACpD,gBAAgB,CAAC,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAClE,CACL,CAAC;AACN,CAAC,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC7B,KAAK,EAAE;QACH,QAAQ,EAAE,UAAU;QACpB,GAAG,EAAE,CAAC;QACN,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;KACb;CACJ,CAAC,CAAC","sourcesContent":["import { requireNativeViewManager } from \"expo-modules-core\";\nimport { forwardRef, useRef, useImperativeHandle } from \"react\";\nimport { StyleSheet } from \"react-native\";\nimport { focus, blur } from \"./ExpoRichInputModule\";\nimport { ExpoRichInputProps, ExpoRichInputRef } from \"./ExpoRichInput.types\";\n\nconst NativeView = requireNativeViewManager(\"ExpoRichInput\");\n\nexport const ExpoRichInputView = forwardRef<\n ExpoRichInputRef,\n ExpoRichInputProps\n>(({ onEditEvent, onKeyboardAction }, ref) => {\n const nativeRef = useRef<any>(null);\n\n useImperativeHandle(ref, () => ({\n focus: () => focus(nativeRef.current),\n blur: () => blur(nativeRef.current)\n }));\n\n return (\n <NativeView\n ref={nativeRef}\n style={styles.input}\n onEditEvent={(e: any) => onEditEvent(e.nativeEvent)}\n onKeyboardAction={(e: any) => onKeyboardAction?.(e.nativeEvent)}\n />\n );\n});\n\nconst styles = StyleSheet.create({\n input: {\n position: \"absolute\",\n top: 0,\n left: 0,\n width: 1,\n height: 1,\n opacity: 0\n }\n});\n"]}
@@ -0,0 +1,4 @@
1
+ export { ExpoRichInputView } from "./ExpoRichInputView";
2
+ export { focus, blur } from "./ExpoRichInputModule";
3
+ export type { EditEvent, EditEventType, KeyboardActionEvent, ExpoRichInputRef, ExpoRichInputProps } from "./ExpoRichInput.types";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACpD,YAAY,EACR,SAAS,EACT,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EACrB,MAAM,uBAAuB,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ExpoRichInputView } from "./ExpoRichInputView";
2
+ export { focus, blur } from "./ExpoRichInputModule"; // can remove after testing
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC,CAAC,2BAA2B","sourcesContent":["export { ExpoRichInputView } from \"./ExpoRichInputView\";\nexport { focus, blur } from \"./ExpoRichInputModule\"; // can remove after testing\nexport type {\n EditEvent,\n EditEventType,\n KeyboardActionEvent,\n ExpoRichInputRef,\n ExpoRichInputProps\n} from \"./ExpoRichInput.types\";\n"]}
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "expo-rich-input",
3
+ "platforms": ["apple", "android"],
4
+ "apple": {
5
+ "modules": ["ExpoRichInputModule"]
6
+ },
7
+ "android": {
8
+ "modules": ["expo.modules.richinput.ExpoRichInputModule"]
9
+ }
10
+ }
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoRichInput'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.9'
18
+ s.source = { git: 'https://github.com/AdwaithAnandSR/expo-rich-input' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ # Swift/Objective-C compatibility
24
+ s.pod_target_xcconfig = {
25
+ 'DEFINES_MODULE' => 'YES',
26
+ }
27
+
28
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
29
+ end
@@ -0,0 +1,30 @@
1
+ import ExpoModulesCore
2
+
3
+ public class RichInputModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("ExpoRichInput")
6
+
7
+ View(RichInputView.self) {
8
+
9
+ // MARK: Events exposed to JS
10
+ Events("onEditEvent", "onKeyboardAction")
11
+
12
+ // MARK: Functions callable from JS
13
+ // ref.current.focus()
14
+ AsyncFunction("focus") {
15
+ (view: RichInputView) in
16
+ DispatchQueue.main.async {
17
+ view.focusInput()
18
+ }
19
+ }
20
+
21
+ // ref.current.blur()
22
+ AsyncFunction("blur") {
23
+ (view: RichInputView) in
24
+ DispatchQueue.main.async {
25
+ view.blurInput()
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,119 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ class RichInputView: ExpoView, UIKeyInput {
5
+
6
+ // MARK: Events
7
+ let onEditEvent = EventDispatcher()
8
+ let onKeyboardAction = EventDispatcher()
9
+
10
+ // MARK: UIKeyInput — required
11
+ var hasText: Bool {
12
+ true
13
+ } // always true so backspace keeps firing
14
+
15
+ func insertText(_ text: String) {
16
+ // Tab key comes as "\t"
17
+ // Newline comes as "\n"
18
+ onEditEvent([
19
+ "type": "insert",
20
+ "text": text
21
+ ])
22
+ }
23
+
24
+ func deleteBackward() {
25
+ onEditEvent([
26
+ "type": "delete",
27
+ "count": 1
28
+ ])
29
+ }
30
+
31
+ // MARK: UIResponder — keyboard traits
32
+ override var canBecomeFirstResponder: Bool {
33
+ true
34
+ }
35
+
36
+ override var autocorrectionType: UITextAutocorrectionType {
37
+ get {
38
+ .no
39
+ }
40
+ set {}
41
+ }
42
+
43
+ override var autocapitalizationType: UITextAutocapitalizationType {
44
+ get {
45
+ .none
46
+ }
47
+ set {}
48
+ }
49
+
50
+ override var spellCheckingType: UITextSpellCheckingType {
51
+ get {
52
+ .no
53
+ }
54
+ set {}
55
+ }
56
+
57
+ override var keyboardType: UIKeyboardType {
58
+ get {
59
+ .asciiCapable
60
+ }
61
+ set {}
62
+ }
63
+
64
+ override var returnKeyType: UIReturnKeyType {
65
+ get {
66
+ .default
67
+ }
68
+ set {}
69
+ }
70
+
71
+ override var smartQuotesType: UITextSmartQuotesType {
72
+ get {
73
+ .no
74
+ }
75
+ set {}
76
+ }
77
+
78
+ // MARK: Hardware keyboard shortcuts (iPad + external keyboard)
79
+ override var keyCommands: [UIKeyCommand]? {
80
+ return [
81
+ UIKeyCommand(
82
+ input: "z",
83
+ modifierFlags: .command,
84
+ action: #selector(handleUndo)
85
+ ),
86
+ UIKeyCommand(
87
+ input: "z",
88
+ modifierFlags: [.command, .shift],
89
+ action: #selector(handleRedo)
90
+ ),
91
+ UIKeyCommand(
92
+ input: "/",
93
+ modifierFlags: .command,
94
+ action: #selector(handleToggleComment)
95
+ ),
96
+ ]
97
+ }
98
+
99
+ @objc func handleUndo() {
100
+ onKeyboardAction(["action": "undo"])
101
+ }
102
+
103
+ @objc func handleRedo() {
104
+ onKeyboardAction(["action": "redo"])
105
+ }
106
+
107
+ @objc func handleToggleComment() {
108
+ onKeyboardAction(["action": "toggleComment"])
109
+ }
110
+
111
+ // MARK: Focus control (called from JS)
112
+ func focusInput() {
113
+ becomeFirstResponder()
114
+ }
115
+
116
+ func blurInput() {
117
+ resignFirstResponder()
118
+ }
119
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "expo-rich-input",
3
+ "version": "0.1.0",
4
+ "description": "A native Expo module that replaces `TextInput` entirely, giving the editor raw OS-level edit deltas — insert, delete, and IME compose events — directly from `UIKeyInput` on iOS and `InputConnection` on Android, so the Rope never has to diff a full string.",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android"
17
+ },
18
+ "files": [
19
+ "build",
20
+ "android",
21
+ "ios",
22
+ "expo-module.config.json"
23
+ ],
24
+ "keywords": [
25
+ "react-native",
26
+ "expo",
27
+ "expo-rich-input",
28
+ "ExpoRichInput"
29
+ ],
30
+ "repository": "https://github.com/AdwaithAnandSR/expo-rich-input",
31
+ "bugs": {
32
+ "url": "https://github.com/AdwaithAnandSR/expo-rich-input/issues"
33
+ },
34
+ "author": "AdwaithAnandSR <adwaith.anand.dev@gmail.com> (https://github.com/AdwaithAnandSR)",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/AdwaithAnandSR/expo-rich-input#readme",
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "@types/react": "~19.1.1",
40
+ "expo-module-scripts": "^55.0.2",
41
+ "expo": "^55.0.11",
42
+ "react-native": "0.82.1"
43
+ },
44
+ "peerDependencies": {
45
+ "expo": "*",
46
+ "react": "*",
47
+ "react-native": "*"
48
+ }
49
+ }