@momo-kits/calculator-keyboard 0.150.2-beta.3 → 0.150.2-beta.31

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 (30) hide show
  1. package/README.md +45 -3
  2. package/android/src/main/java/com/calculatorkeyboard/CalculatorKeyboardPackage.kt +1 -1
  3. package/android/src/main/java/com/calculatorkeyboard/CustomKeyboardView.kt +225 -90
  4. package/android/src/main/java/com/calculatorkeyboard/Event.kt +36 -0
  5. package/android/src/main/java/com/calculatorkeyboard/InputCalculatorViewManager.kt +270 -0
  6. package/android/src/main/java/com/calculatorkeyboard/KeyboardOverplayHost.kt +232 -0
  7. package/ios/CalculatorKeyboardView.h +30 -0
  8. package/ios/CalculatorKeyboardView.mm +231 -0
  9. package/ios/NativeInputCalculator.h +11 -0
  10. package/ios/NativeInputCalculator.mm +369 -0
  11. package/package.json +21 -131
  12. package/react-native-calculator-keyboard.podspec +5 -4
  13. package/src/InputCalculatorNativeComponent.ts +62 -0
  14. package/src/index.tsx +104 -31
  15. package/android/src/main/java/com/calculatorkeyboard/RCTInputCalculator.kt +0 -179
  16. package/ios/CalculatorKeyboardView.swift +0 -115
  17. package/ios/InputCalculator-Bridging-Header.h +0 -23
  18. package/ios/InputCalculator.m +0 -79
  19. package/ios/InputCalculator.swift +0 -138
  20. package/ios/extension.swift +0 -23
  21. package/lib/commonjs/index.js +0 -48
  22. package/lib/commonjs/index.js.map +0 -1
  23. package/lib/module/index.js +0 -44
  24. package/lib/module/index.js.map +0 -1
  25. package/lib/typescript/commonjs/package.json +0 -1
  26. package/lib/typescript/commonjs/src/index.d.ts +0 -13
  27. package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
  28. package/lib/typescript/module/package.json +0 -1
  29. package/lib/typescript/module/src/index.d.ts +0 -13
  30. package/lib/typescript/module/src/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -10,15 +10,57 @@ npm install react-native-calculator-keyboard
10
10
 
11
11
  ## Usage
12
12
 
13
-
14
13
  ```js
15
- import { CalculatorKeyboardView } from "react-native-calculator-keyboard";
14
+ import InputCalculator from '@momo-kits/calculator-keyboard';
16
15
 
17
16
  // ...
18
17
 
19
- <CalculatorKeyboardView color="tomato" />
18
+ <InputCalculator
19
+ mode="NumDefault"
20
+ customKeyText="Next"
21
+ onCustomKeyEvent={() => console.log('Custom key pressed')}
22
+ />;
23
+ ```
24
+
25
+ ## Requirements
26
+
27
+ **React Native 0.80+** with **Fabric (New Architecture) enabled**.
28
+
29
+ This library is **pure Fabric** implementation with:
30
+
31
+ - ✅ Zero RCTBridge dependencies
32
+ - ✅ Native C++ ComponentView on iOS
33
+ - ✅ Fabric ViewManager with codegen delegates on Android
34
+ - ✅ All Props, Events, Commands auto-generated by codegen
35
+ - ❌ No Paper (old architecture) support
36
+
37
+ ### Android Setup
38
+
39
+ Add to your `gradle.properties`:
40
+
41
+ ```properties
42
+ newArchEnabled=true
43
+ ```
44
+
45
+ Then rebuild:
46
+
47
+ ```bash
48
+ cd android && ./gradlew clean && cd ..
49
+ npx react-native run-android
50
+ ```
51
+
52
+ ### iOS Setup
53
+
54
+ **Required**: Set the environment variable before installing pods:
55
+
56
+ ```bash
57
+ cd ios
58
+ RCT_NEW_ARCH_ENABLED=1 pod install
59
+ cd ..
60
+ npx react-native run-ios
20
61
  ```
21
62
 
63
+ **Note**: This library uses Fabric ComponentView (`.mm` files) and will not work without `RCT_NEW_ARCH_ENABLED=1`.
22
64
 
23
65
  ## Contributing
24
66
 
@@ -12,6 +12,6 @@ class CalculatorKeyboardPackage : ReactPackage {
12
12
  }
13
13
 
14
14
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
- return listOf(RCTInputCalculator())
15
+ return listOf(InputCalculatorViewManager())
16
16
  }
17
17
  }
@@ -1,61 +1,80 @@
1
1
  package com.calculatorkeyboard
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.content.Context
4
5
  import android.content.res.ColorStateList
5
6
  import android.graphics.Color
6
7
  import android.graphics.drawable.GradientDrawable
7
8
  import android.view.Gravity
8
9
  import android.widget.Button
9
10
  import android.widget.ImageButton
10
- import androidx.appcompat.app.AppCompatActivity
11
11
  import androidx.constraintlayout.widget.ConstraintLayout
12
- import androidx.core.graphics.ColorUtils
13
12
  import com.facebook.react.uimanager.ThemedReactContext
14
13
  import org.mariuszgromada.math.mxparser.Expression
15
14
  import androidx.core.graphics.toColorInt
16
- import com.facebook.react.uimanager.PixelUtil.dpToPx
17
-
15
+ import com.calculatorkeyboard.InputCalculatorViewManager.Companion.calculatorHeight
16
+ import com.facebook.react.bridge.Arguments
17
+ import com.facebook.react.bridge.ReactContext
18
+ import com.facebook.react.uimanager.UIManagerHelper
19
+ import com.facebook.react.uimanager.events.RCTEventEmitter
18
20
 
19
21
  @SuppressLint("SetTextI18n", "ViewConstructor")
20
22
  class CustomKeyboardView(
21
23
  context: ThemedReactContext,
22
24
  private val editText: CalculatorEditText
23
25
  ) : ConstraintLayout(context) {
24
- private val keys = listOf(
25
- listOf("AC", "÷", "×", "back"),
26
- listOf("7", "8", "9", "-"),
27
- listOf("4", "5", "6", "+"),
28
- listOf("1", "2", "3", "="),
29
- listOf("000", "0")
26
+ private val numWithCTAKeys = listOf(
27
+ listOf("1", "2", "3", "÷", "back"),
28
+ listOf("4", "5", "6", "×", "="),
29
+ listOf("7", "8", "9", "-", "Tiếp"),
30
+ listOf("000", "0", "+")
31
+ )
32
+ private val defaultKeys = listOf(
33
+ listOf("1", "2", "3", "÷", "AC"),
34
+ listOf("4", "5", "6", "×", "back"),
35
+ listOf("7", "8", "9", "-", "="),
36
+ listOf("000", "0", "+")
30
37
  )
31
- private val specialKeys = listOf("=", "-", "×", "÷", "AC", "back", "+")
38
+ private val specialKeys = listOf("=", "-", "×", "÷", "back", "+", "AC")
32
39
  private val separatorWidth = 8f
33
40
  private var specialButtonColor: Int = "#D8D8D8".toColorInt()
34
41
 
35
- init {
36
- val activity = context.currentActivity as? AppCompatActivity
37
- if (activity != null) {
38
- val displayMetrics = resources.displayMetrics
39
- val widthButton = (displayMetrics.widthPixels - separatorWidth * 2 - 3 * separatorWidth) / 4f
40
- val heightButton = (290.dpToPx() - separatorWidth * 2 - 4 * separatorWidth) / 5
42
+ private var keyboardMode: String = "NumDefault"
41
43
 
42
- renderUI(widthButton, heightButton)
43
- }
44
+ private var customKeyButton: Button? = null
45
+ private var customKeyButtonBackground: Int = "#D8D8D8".toColorInt()
46
+ private var customKeyButtonTextColor: Int = Color.BLACK
47
+ private var customKeyButtonState: String = "default"
44
48
 
49
+ init {
50
+ isClickable = false
51
+ isFocusable = false
52
+ isFocusableInTouchMode = false
53
+ clipToPadding = false
54
+ clipChildren = false
45
55
  }
46
56
 
47
- private fun renderUI(buttonWidth: Float, buttonHeight: Float) {
57
+ private fun renderUI() {
58
+ val displayMetrics = resources.displayMetrics
59
+ val buttonWidth = (displayMetrics.widthPixels - separatorWidth * 2 - 4 * separatorWidth) / 5f
60
+ val buttonHeight = (calculatorHeight - separatorWidth * 2 - 3 * separatorWidth) / 4
61
+
48
62
  var yOffset = separatorWidth
49
- for ((_, row) in keys.withIndex()) {
63
+ val keys = if (keyboardMode == "NumWithCTA") numWithCTAKeys else defaultKeys
64
+ for ((rowIndex, row) in keys.withIndex()) {
50
65
  var xOffset = separatorWidth
51
- for ((_, key) in row.withIndex()) {
66
+ for ((colIndex, key) in row.withIndex()) {
67
+ val isMainKey = rowIndex == 2 && colIndex == 4
68
+ val isMainCTAKey = isMainKey && keyboardMode == "NumWithCTA"
52
69
  val width = if (key == "000") buttonWidth * 2 + separatorWidth else buttonWidth
53
- val height = if (key == "=") buttonWidth + separatorWidth else buttonHeight
70
+ val height = if (isMainKey) buttonHeight * 2 + separatorWidth else buttonHeight
54
71
 
55
72
  val button = if (key == "back") {
56
73
  createImageButton(key, xOffset, yOffset, buttonWidth.toInt(), buttonHeight.toInt())
57
74
  } else {
58
- createButton(key, xOffset, yOffset, width.toInt(), height.toInt())
75
+ createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isMainKey, isMainCTAKey).also { b ->
76
+ if (isMainCTAKey) customKeyButton = b
77
+ }
59
78
  }
60
79
 
61
80
  addView(button)
@@ -72,8 +91,9 @@ class CustomKeyboardView(
72
91
  yOffset: Float,
73
92
  buttonWidth: Int,
74
93
  buttonHeight: Int,
94
+ isMainKey: Boolean,
95
+ isMainCTAKey: Boolean
75
96
  ): Button {
76
- val specialKeys = listOf("=", "-", "×", "÷", "AC", "back", "+")
77
97
  return Button(context).apply {
78
98
  val shapeInit = GradientDrawable().apply {
79
99
  shape = GradientDrawable.RECTANGLE
@@ -85,9 +105,11 @@ class CustomKeyboardView(
85
105
  background = shapeInit
86
106
  text = key
87
107
  setTypeface(typeface)
88
- textSize = 24.toFloat()
108
+ textSize = (if (isMainCTAKey) 18 else 24).toFloat()
89
109
  setTextColor(Color.BLACK)
90
110
  stateListAnimator = null
111
+ maxLines = 1
112
+ isAllCaps = false
91
113
  layoutParams = LayoutParams(
92
114
  buttonWidth,
93
115
  buttonHeight
@@ -95,7 +117,7 @@ class CustomKeyboardView(
95
117
  constrainedWidth = false
96
118
  }
97
119
 
98
- if (specialKeys.contains(key)) {
120
+ if (specialKeys.contains(key) || isMainKey) {
99
121
  background = GradientDrawable().apply {
100
122
  shape = GradientDrawable.RECTANGLE
101
123
  cornerRadius = 24f
@@ -104,10 +126,13 @@ class CustomKeyboardView(
104
126
  setTextColor(Color.BLACK)
105
127
  }
106
128
 
129
+ isClickable = true
130
+ isFocusable = false
131
+ isFocusableInTouchMode = false
107
132
 
108
133
  translationX = xOffset.toInt().toFloat()
109
134
  translationY = yOffset.toInt().toFloat()
110
- setOnClickListener { onKeyPress(key) }
135
+ setOnClickListener { onKeyPress(key, isMainCTAKey) }
111
136
  }
112
137
  }
113
138
 
@@ -132,66 +157,35 @@ class CustomKeyboardView(
132
157
  ).apply {
133
158
  constrainedWidth = false
134
159
  }
160
+
161
+ isClickable = true
162
+ isFocusable = false
163
+ isFocusableInTouchMode = false
164
+
135
165
  translationX = xOffset
136
166
  translationY = yOffset
137
167
  setImageResource(android.R.drawable.ic_input_delete)
138
168
  setImageTintList(ColorStateList.valueOf(Color.BLACK))
139
- setOnClickListener { onKeyPress(key) }
169
+ setOnClickListener { onKeyPress(key, false) }
140
170
  }
141
171
  }
142
172
 
143
- fun updateButtonColors(color: Int) {
144
- specialButtonColor = color
145
- for (i in 0 until childCount) {
146
- val child = getChildAt(i)
147
- if (child is Button) {
148
- val key = child.text.toString()
149
- if (specialKeys.contains(key)) {
150
- if (key == "=") {
151
- child.background = GradientDrawable().apply {
152
- shape = GradientDrawable.RECTANGLE
153
- cornerRadius = 24f
154
- setColor(specialButtonColor)
155
- }
156
- } else {
157
- child.background = GradientDrawable().apply {
158
- shape = GradientDrawable.RECTANGLE
159
- cornerRadius = 24f
160
- setColor(specialButtonColor)
161
- }
162
- }
163
- child.setTextColor(Color.BLACK)
164
- }
165
- } else if (child is ImageButton) {
166
- child.background = GradientDrawable().apply {
167
- shape = GradientDrawable.RECTANGLE
168
- cornerRadius = 24f
169
- setColor(specialButtonColor)
170
- }
171
- }
173
+ private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
174
+ if (isMainCTAKey) {
175
+ emitCustomKey(context)
176
+ return
172
177
  }
173
- }
174
178
 
175
- private fun onKeyPress(key: String) {
179
+ emitKeyPress(context, key)
176
180
  when (key) {
177
- "AC" -> {
178
- clearText()
179
- }
180
-
181
- "back" -> {
182
- onBackSpace()
183
- }
184
-
185
- "=" -> {
186
- calculateResult()
187
- }
188
-
181
+ "AC" -> clearText()
182
+ "back" -> onBackSpace()
183
+ "=" -> calculateResult()
189
184
  "×", "+", "-", "÷" -> keyDidPress(" $key ")
190
-
191
- else -> {
192
- editText.text?.insert(editText.selectionStart, key)
193
- }
185
+ else -> editText.text?.insert(editText.selectionStart, key)
194
186
  }
187
+
188
+ reformatAndKeepSelection(editText)
195
189
  }
196
190
 
197
191
  private fun keyDidPress(key: String) {
@@ -204,26 +198,39 @@ class CustomKeyboardView(
204
198
  }
205
199
 
206
200
  private fun onBackSpace() {
207
- val start = editText.selectionStart
208
- val end = editText.selectionEnd
209
- if (start > 0) {
210
- val newText = end.let { editText.text?.replaceRange(start - 1, it, "") }
211
- editText.setText(newText)
212
- editText.setSelection(start - 1)
201
+ val formatted = editText.text?.toString().orEmpty()
202
+ val caretFmt = editText.selectionStart.coerceAtLeast(0)
203
+
204
+ val rawBefore = stripGroupDots(formatted)
205
+ val caretRaw = formattedCaretToRaw(formatted, caretFmt)
206
+
207
+ if (caretRaw <= 0) return
208
+
209
+ val rawAfter = buildString(rawBefore.length - 1) {
210
+ append(rawBefore, 0, caretRaw - 1)
211
+ append(rawBefore, caretRaw, rawBefore.length)
213
212
  }
213
+
214
+ val formattedAfter = formatNumberGroups(rawAfter)
215
+ editText.setText(formattedAfter)
216
+ val newCaretFmt = rawCaretToFormatted(caretRaw - 1, formattedAfter)
217
+ editText.setSelection(newCaretFmt.coerceIn(0, formattedAfter.length))
214
218
  }
215
219
 
220
+
216
221
  private fun calculateResult() {
217
- val text = editText?.text.toString().replace("×", "*").replace("÷", "/")
222
+ val raw = editText.text?.toString().orEmpty()
223
+ val normalized = raw.replace(".", "")
224
+ .replace("×", "*")
225
+ .replace("÷", "/")
226
+
218
227
  val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
219
- val regex = Regex(pattern)
220
- if (regex.matches(text)) {
228
+ if (Regex(pattern).matches(normalized)) {
221
229
  try {
222
- val result = eval(text).toString()
223
- editText.setTextKeepState(result)
224
- } catch (e: Exception) {
225
- e.printStackTrace()
226
- }
230
+ val result = eval(normalized)?.toString() ?: return
231
+ editText.setTextKeepState(formatNumberGroups(result))
232
+ editText.setSelection(editText.text?.length ?: 0)
233
+ } catch (_: Exception) { /* ignore */ }
227
234
  } else {
228
235
  println("Invalid expression")
229
236
  }
@@ -239,4 +246,132 @@ class CustomKeyboardView(
239
246
  }
240
247
  }
241
248
 
249
+ private fun emitKeyPress(context: Context, key: String) {
250
+ val reactContext = context as ReactContext
251
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
252
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, editText.id)
253
+ val payload =
254
+ Arguments.createMap().apply {
255
+ putString("key", key)
256
+ }
257
+ val event = OnKeyPressEvent(surfaceId, editText.id, payload)
258
+
259
+ eventDispatcher?.dispatchEvent(event)
260
+ }
261
+
262
+ private fun emitCustomKey(context: Context) {
263
+ val reactContext = context as ReactContext
264
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
265
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, editText.id)
266
+
267
+ val event = OnCustomKeyPressEvent(surfaceId, editText.id)
268
+
269
+ eventDispatcher?.dispatchEvent(event)
270
+ }
271
+
272
+ fun setCustomKeyText(text: String) {
273
+ customKeyButton?.text = text
274
+ }
275
+
276
+ fun setMode(mode: String) {
277
+ keyboardMode = mode
278
+ renderUI()
279
+ }
280
+
281
+ fun setCustomKeyBackground(background: Int) {
282
+ customKeyButtonBackground = background
283
+ updateCustomKeyUI(background, customKeyButtonTextColor)
284
+ }
285
+
286
+ fun setCustomKeyTextColor(textColor: Int) {
287
+ customKeyButtonTextColor = textColor
288
+ updateCustomKeyUI(customKeyButtonBackground, textColor)
289
+ }
290
+
291
+
292
+ fun setCustomKeyState(state: String) {
293
+ customKeyButtonState = state
294
+ customKeyButton?.isEnabled = state != "disable"
295
+ updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor)
296
+ }
297
+
298
+ private fun updateCustomKeyUI(background: Int, textColor: Int){
299
+ customKeyButton?.background = GradientDrawable().apply {
300
+ shape = GradientDrawable.RECTANGLE
301
+ cornerRadius = 24f
302
+ setColor(background)
303
+ }
304
+ customKeyButton?.setTextColor(textColor)
305
+ }
306
+
307
+ private fun reformatAndKeepSelection(editText: CalculatorEditText) {
308
+ val formattedBefore = editText.text?.toString() ?: return
309
+ val caretFmtBefore = editText.selectionStart.coerceAtLeast(0)
310
+
311
+ val caretRaw = formattedCaretToRaw(formattedBefore, caretFmtBefore)
312
+
313
+ val formattedAfter = formatNumberGroups(formattedBefore)
314
+
315
+ if (formattedAfter != formattedBefore) {
316
+ editText.setText(formattedAfter)
317
+ }
318
+
319
+ val caretFmtAfter = rawCaretToFormatted(caretRaw, formattedAfter)
320
+ editText.setSelection(caretFmtAfter.coerceIn(0, formattedAfter.length))
321
+ }
322
+ private fun stripGroupDots(input: String): String {
323
+ val out = StringBuilder(input.length)
324
+ for (i in input.indices) {
325
+ val c = input[i]
326
+ if (c == '.' && isGroupDotAt(input, i)) {
327
+ } else {
328
+ out.append(c)
329
+ }
330
+ }
331
+ return out.toString()
332
+ }
333
+
334
+ private fun formatNumberGroups(input: String): String {
335
+ val noSep = stripGroupDots(input)
336
+ return Regex("\\d+").replace(noSep) { m ->
337
+ val s = m.value
338
+ val rev = s.reversed().chunked(3).joinToString(".")
339
+ rev.reversed()
340
+ }
341
+ }
342
+
343
+ private fun isGroupDotAt(s: String, i: Int): Boolean {
344
+ if (i < 0 || i >= s.length) return false
345
+ if (s[i] != '.') return false
346
+ val leftIsDigit = i - 1 >= 0 && s[i - 1].isDigit()
347
+ val rightIsDigit = i + 1 < s.length && s[i + 1].isDigit()
348
+ return leftIsDigit && rightIsDigit
349
+ }
350
+
351
+ private fun formattedCaretToRaw(formatted: String, caretFmt: Int): Int {
352
+ var rawIdx = 0
353
+ var i = 0
354
+ while (i < caretFmt && i < formatted.length) {
355
+ val c = formatted[i]
356
+ if (!(c == '.' && isGroupDotAt(formatted, i))) {
357
+ rawIdx++
358
+ }
359
+ i++
360
+ }
361
+ return rawIdx
362
+ }
363
+
364
+ private fun rawCaretToFormatted(rawCaret: Int, formatted: String): Int {
365
+ var rawSeen = 0
366
+ var i = 0
367
+ while (i < formatted.length) {
368
+ val c = formatted[i]
369
+ if (!(c == '.' && isGroupDotAt(formatted, i))) {
370
+ if (rawSeen == rawCaret) return i
371
+ rawSeen++
372
+ }
373
+ i++
374
+ }
375
+ return formatted.length
376
+ }
242
377
  }
@@ -0,0 +1,36 @@
1
+ package com.calculatorkeyboard
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.facebook.react.uimanager.events.Event
5
+
6
+ class OnChangeEvent(
7
+ surfaceId: Int,
8
+ viewId: Int,
9
+ private val payload: WritableMap
10
+ ) : Event<OnChangeEvent>(surfaceId, viewId) {
11
+ override fun getEventName() = "onChange"
12
+
13
+ override fun getEventData() = payload
14
+ }
15
+
16
+ class OnKeyPressEvent(
17
+ surfaceId: Int,
18
+ viewId: Int,
19
+ private val payload: WritableMap
20
+ ) : Event<OnChangeEvent>(surfaceId, viewId) {
21
+ override fun getEventName() = "onKeyPress"
22
+
23
+ override fun getEventData() = payload
24
+ }
25
+
26
+ class OnCustomKeyPressEvent(
27
+ surfaceId: Int,
28
+ viewId: Int
29
+ ) : Event<OnChangeEvent>(surfaceId, viewId) {
30
+ override fun getEventName() = "onCustomKeyEvent"
31
+
32
+ override fun getEventData() = null
33
+ }
34
+
35
+
36
+