expo-rich-input 0.1.0 → 0.1.2

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.
@@ -5,11 +5,16 @@ import expo.modules.kotlin.modules.ModuleDefinition
5
5
 
6
6
  class ExpoRichInputModule : Module() {
7
7
  override fun definition() = ModuleDefinition {
8
+
8
9
  Name("ExpoRichInput")
9
10
 
10
11
  View(RichInputView::class) {
11
12
 
12
- Events("onEditEvent", "onKeyboardAction")
13
+ Events(
14
+ "onEditEvent",
15
+ "onKeyboardAction",
16
+ "onSelectionChange"
17
+ )
13
18
 
14
19
  AsyncFunction("focus") {
15
20
  view: RichInputView ->
@@ -20,19 +25,6 @@ class ExpoRichInputModule : Module() {
20
25
  view: RichInputView ->
21
26
  view.blurInput()
22
27
  }
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
28
  }
37
29
  }
38
30
  }
@@ -5,126 +5,150 @@ import android.os.Build
5
5
  import android.text.InputType
6
6
  import android.view.KeyEvent
7
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
8
+ import android.view.inputmethod.*
12
9
  import expo.modules.kotlin.AppContext
13
10
  import expo.modules.kotlin.views.ExpoView
14
11
 
15
12
  class RichInputView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
16
13
 
17
- // Events dispatched to JS
18
- var onEditEvent: ((Map<String, Any>) -> Unit)? = null
19
- var onKeyboardAction: ((Map<String, Any>) -> Unit)? = null
14
+ private var cursorPosition = 0
15
+ private var selectionStart = 0
16
+ private var selectionEnd = 0
20
17
 
21
- init {
22
- // View must be focusable to receive InputConnection
23
- isFocusable = true
24
- isFocusableInTouchMode = true
25
- }
18
+ init {
19
+ isFocusable = true
20
+ isFocusableInTouchMode = true
21
+ }
26
22
 
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
23
+ override fun onCheckIsTextEditor(): Boolean = true
32
24
 
33
- outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
34
- EditorInfo.IME_ACTION_NONE
25
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
26
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT or
27
+ InputType.TYPE_TEXT_FLAG_MULTI_LINE or
28
+ InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
35
29
 
36
- // Disable autocorrect suggestions bar
37
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
38
- outAttrs.initialCapsMode = 0
39
- }
30
+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
31
+ EditorInfo.IME_ACTION_NONE
40
32
 
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
33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
34
+ outAttrs.initialCapsMode = 0
57
35
  }
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
36
+
37
+ return EditorInputConnection(this)
91
38
  }
92
39
 
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
40
+ // 🔥 Hardware keyboard support
41
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
42
+ if (event.isCtrlPressed) {
43
+ val action = when (keyCode) {
44
+ KeyEvent.KEYCODE_Z -> if (event.isShiftPressed) "redo" else "undo"
45
+ KeyEvent.KEYCODE_A -> "selectAll"
46
+ KeyEvent.KEYCODE_C -> "copy"
47
+ KeyEvent.KEYCODE_V -> "paste"
48
+ KeyEvent.KEYCODE_X -> "cut"
49
+ KeyEvent.KEYCODE_SLASH -> "toggleComment"
50
+ else -> null
51
+ }
52
+
53
+ action?.let {
54
+ sendEvent("onKeyboardAction", mapOf("action" to it))
55
+ return true
56
+ }
57
+ }
58
+
59
+ return super.onKeyDown(keyCode, event)
101
60
  }
102
61
 
103
- // IME composition confirmed
104
- override fun finishComposingText(): Boolean {
105
- onEditEvent?.invoke(mapOf(
106
- "type" to "composeCommit"
107
- ))
108
- return true
62
+ fun focusInput() {
63
+ requestFocus()
64
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
65
+ imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
109
66
  }
110
67
 
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
68
+ fun blurInput() {
69
+ clearFocus()
70
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
71
+ imm.hideSoftInputFromWindow(windowToken, 0)
120
72
  }
121
73
 
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
74
+ // 🔥 Core Input Engine
75
+ inner class EditorInputConnection(view: View) : BaseInputConnection(view, false) {
76
+
77
+ override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
78
+ val str = text?.toString() ?: return false
79
+
80
+ sendEvent("onEditEvent", mapOf(
81
+ "type" to "insert",
82
+ "text" to str,
83
+ "cursor" to cursorPosition
84
+ ))
85
+
86
+ cursorPosition += str.length
87
+ selectionStart = cursorPosition
88
+ selectionEnd = cursorPosition
89
+
90
+ return true
91
+ }
92
+
93
+ override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
94
+ val str = text?.toString() ?: ""
126
95
 
127
- override fun getSurroundingText(beforeLength: Int, afterLength: Int, flags: Int)
128
- : android.view.inputmethod.SurroundingText? = null
129
- }
130
- }
96
+ sendEvent("onEditEvent", mapOf(
97
+ "type" to "compose",
98
+ "text" to str,
99
+ "cursor" to cursorPosition
100
+ ))
101
+
102
+ return true
103
+ }
104
+
105
+ override fun finishComposingText(): Boolean {
106
+ sendEvent("onEditEvent", mapOf(
107
+ "type" to "composeCommit",
108
+ "cursor" to cursorPosition
109
+ ))
110
+ return true
111
+ }
112
+
113
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
114
+ if (beforeLength > 0) {
115
+ val deleteCount = minOf(beforeLength, cursorPosition)
116
+
117
+ sendEvent("onEditEvent", mapOf(
118
+ "type" to "delete",
119
+ "count" to deleteCount,
120
+ "cursor" to cursorPosition
121
+ ))
122
+
123
+ cursorPosition -= deleteCount
124
+ selectionStart = cursorPosition
125
+ selectionEnd = cursorPosition
126
+ }
127
+ return true
128
+ }
129
+
130
+ override fun setSelection(start: Int, end: Int): Boolean {
131
+ selectionStart = start
132
+ selectionEnd = end
133
+ cursorPosition = end
134
+
135
+ sendEvent("onSelectionChange", mapOf(
136
+ "start" to start,
137
+ "end" to end
138
+ ))
139
+
140
+ return true
141
+ }
142
+
143
+ override fun getExtractedText(
144
+ request: ExtractedTextRequest?,
145
+ flags: Int
146
+ ): ExtractedText? = null
147
+
148
+ override fun getSurroundingText(
149
+ beforeLength: Int,
150
+ afterLength: Int,
151
+ flags: Int
152
+ ): SurroundingText? = null
153
+ }
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-rich-input",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
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
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  "open:ios": "xed example/ios",
16
16
  "open:android": "open -a \"Android Studio\" example/android"
17
17
  },
18
- "files": [
18
+ "files": [
19
19
  "build",
20
20
  "android",
21
21
  "ios",