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 +35 -0
- package/android/build.gradle +18 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/richinput/ExpoRichInputModule.kt +38 -0
- package/android/src/main/java/expo/modules/richinput/ExpoRichInputView.kt +130 -0
- package/build/ExpoRichInput.types.d.ts +18 -0
- package/build/ExpoRichInput.types.d.ts.map +1 -0
- package/build/ExpoRichInput.types.js +2 -0
- package/build/ExpoRichInput.types.js.map +1 -0
- package/build/ExpoRichInputModule.d.ts +3 -0
- package/build/ExpoRichInputModule.d.ts.map +1 -0
- package/build/ExpoRichInputModule.js +9 -0
- package/build/ExpoRichInputModule.js.map +1 -0
- package/build/ExpoRichInputView.d.ts +3 -0
- package/build/ExpoRichInputView.d.ts.map +1 -0
- package/build/ExpoRichInputView.js +24 -0
- package/build/ExpoRichInputView.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/ExpoRichInput.podspec +29 -0
- package/ios/ExpoRichInputModule.swift +30 -0
- package/ios/ExpoRichInputView.swift +119 -0
- package/package.json +49 -0
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,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 @@
|
|
|
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 @@
|
|
|
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"]}
|
package/build/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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,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
|
+
}
|