@momo-kits/calculator-keyboard 0.150.2-phuc.13 → 0.151.1-beta.1

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 +184 -95
  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 +124 -82
  15. package/android/src/main/java/com/calculatorkeyboard/RCTInputCalculator.kt +0 -339
  16. package/ios/CalculatorKeyboardView.swift +0 -165
  17. package/ios/InputCalculator-Bridging-Header.h +0 -23
  18. package/ios/InputCalculator.m +0 -85
  19. package/ios/InputCalculator.swift +0 -158
  20. package/ios/extension.swift +0 -23
  21. package/lib/commonjs/index.js +0 -72
  22. package/lib/commonjs/index.js.map +0 -1
  23. package/lib/module/index.js +0 -68
  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 -31
  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 -31
  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,19 +1,21 @@
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
12
  import com.facebook.react.uimanager.ThemedReactContext
13
13
  import org.mariuszgromada.math.mxparser.Expression
14
14
  import androidx.core.graphics.toColorInt
15
- import com.calculatorkeyboard.RCTInputCalculator.Companion.calculatorHeight
15
+ import com.calculatorkeyboard.InputCalculatorViewManager.Companion.calculatorHeight
16
16
  import com.facebook.react.bridge.Arguments
17
+ import com.facebook.react.bridge.ReactContext
18
+ import com.facebook.react.uimanager.UIManagerHelper
17
19
  import com.facebook.react.uimanager.events.RCTEventEmitter
18
20
 
19
21
  @SuppressLint("SetTextI18n", "ViewConstructor")
@@ -21,47 +23,57 @@ class CustomKeyboardView(
21
23
  context: ThemedReactContext,
22
24
  private val editText: CalculatorEditText
23
25
  ) : ConstraintLayout(context) {
24
- private val keys = listOf(
26
+ private val numWithCTAKeys = listOf(
25
27
  listOf("1", "2", "3", "÷", "back"),
26
28
  listOf("4", "5", "6", "×", "="),
27
- listOf("7", "8", "9", "-", "Xong"),
29
+ listOf("7", "8", "9", "-", "Tiếp"),
28
30
  listOf("000", "0", "+")
29
31
  )
30
- private val specialKeys = listOf("=", "-", "×", "÷", "back", "+")
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", "+")
37
+ )
38
+ private val specialKeys = listOf("=", "-", "×", "÷", "back", "+", "AC")
31
39
  private val separatorWidth = 8f
32
40
  private var specialButtonColor: Int = "#D8D8D8".toColorInt()
33
41
 
42
+ private var keyboardMode: String = "NumDefault"
43
+
34
44
  private var customKeyButton: Button? = null
35
45
  private var customKeyButtonBackground: Int = "#D8D8D8".toColorInt()
36
46
  private var customKeyButtonTextColor: Int = Color.BLACK
37
- private var customKeyButtonState: Int = 0
47
+ private var customKeyButtonState: String = "default"
38
48
 
39
49
  init {
40
- val activity = context.currentActivity as? AppCompatActivity
41
- if (activity != null) {
42
- val displayMetrics = resources.displayMetrics
43
- val widthButton = (displayMetrics.widthPixels - separatorWidth * 2 - 4 * separatorWidth) / 5f
44
- val heightButton = (calculatorHeight - separatorWidth * 2 - 3 * separatorWidth) / 4
45
-
46
- renderUI(widthButton, heightButton)
47
- }
48
-
50
+ isClickable = false
51
+ isFocusable = false
52
+ isFocusableInTouchMode = false
53
+ clipToPadding = false
54
+ clipChildren = false
49
55
  }
50
56
 
51
- 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
+
52
62
  var yOffset = separatorWidth
63
+ val keys = if (keyboardMode == "NumWithCTA") numWithCTAKeys else defaultKeys
53
64
  for ((rowIndex, row) in keys.withIndex()) {
54
65
  var xOffset = separatorWidth
55
66
  for ((colIndex, key) in row.withIndex()) {
56
- val isCustomKey = rowIndex == 2 && colIndex == 4
67
+ val isMainKey = rowIndex == 2 && colIndex == 4
68
+ val isMainCTAKey = isMainKey && keyboardMode == "NumWithCTA"
57
69
  val width = if (key == "000") buttonWidth * 2 + separatorWidth else buttonWidth
58
- val height = if (isCustomKey) buttonHeight * 2 + separatorWidth else buttonHeight
70
+ val height = if (isMainKey) buttonHeight * 2 + separatorWidth else buttonHeight
59
71
 
60
72
  val button = if (key == "back") {
61
73
  createImageButton(key, xOffset, yOffset, buttonWidth.toInt(), buttonHeight.toInt())
62
74
  } else {
63
- createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isCustomKey).also { b ->
64
- if (isCustomKey) customKeyButton = b
75
+ createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isMainKey, isMainCTAKey).also { b ->
76
+ if (isMainCTAKey) customKeyButton = b
65
77
  }
66
78
  }
67
79
 
@@ -79,7 +91,8 @@ class CustomKeyboardView(
79
91
  yOffset: Float,
80
92
  buttonWidth: Int,
81
93
  buttonHeight: Int,
82
- isCustomKey: Boolean
94
+ isMainKey: Boolean,
95
+ isMainCTAKey: Boolean
83
96
  ): Button {
84
97
  return Button(context).apply {
85
98
  val shapeInit = GradientDrawable().apply {
@@ -92,7 +105,7 @@ class CustomKeyboardView(
92
105
  background = shapeInit
93
106
  text = key
94
107
  setTypeface(typeface)
95
- textSize = (if (isCustomKey) 18 else 24).toFloat()
108
+ textSize = (if (isMainCTAKey) 18 else 24).toFloat()
96
109
  setTextColor(Color.BLACK)
97
110
  stateListAnimator = null
98
111
  maxLines = 1
@@ -104,7 +117,7 @@ class CustomKeyboardView(
104
117
  constrainedWidth = false
105
118
  }
106
119
 
107
- if (specialKeys.contains(key)) {
120
+ if (specialKeys.contains(key) || isMainKey) {
108
121
  background = GradientDrawable().apply {
109
122
  shape = GradientDrawable.RECTANGLE
110
123
  cornerRadius = 24f
@@ -113,10 +126,13 @@ class CustomKeyboardView(
113
126
  setTextColor(Color.BLACK)
114
127
  }
115
128
 
129
+ isClickable = true
130
+ isFocusable = false
131
+ isFocusableInTouchMode = false
116
132
 
117
133
  translationX = xOffset.toInt().toFloat()
118
134
  translationY = yOffset.toInt().toFloat()
119
- setOnClickListener { onKeyPress(key, isCustomKey) }
135
+ setOnClickListener { onKeyPress(key, isMainCTAKey) }
120
136
  }
121
137
  }
122
138
 
@@ -141,6 +157,11 @@ class CustomKeyboardView(
141
157
  ).apply {
142
158
  constrainedWidth = false
143
159
  }
160
+
161
+ isClickable = true
162
+ isFocusable = false
163
+ isFocusableInTouchMode = false
164
+
144
165
  translationX = xOffset
145
166
  translationY = yOffset
146
167
  setImageResource(android.R.drawable.ic_input_delete)
@@ -149,51 +170,22 @@ class CustomKeyboardView(
149
170
  }
150
171
  }
151
172
 
152
- fun updateButtonColors(color: Int) {
153
- specialButtonColor = color
154
- for (i in 0 until childCount) {
155
- val child = getChildAt(i)
156
- if (child is Button) {
157
- val key = child.text.toString()
158
- if (specialKeys.contains(key)) {
159
- if (key == "=") {
160
- child.background = GradientDrawable().apply {
161
- shape = GradientDrawable.RECTANGLE
162
- cornerRadius = 24f
163
- setColor(specialButtonColor)
164
- }
165
- } else {
166
- child.background = GradientDrawable().apply {
167
- shape = GradientDrawable.RECTANGLE
168
- cornerRadius = 24f
169
- setColor(specialButtonColor)
170
- }
171
- }
172
- child.setTextColor(Color.BLACK)
173
- }
174
- } else if (child is ImageButton) {
175
- child.background = GradientDrawable().apply {
176
- shape = GradientDrawable.RECTANGLE
177
- cornerRadius = 24f
178
- setColor(specialButtonColor)
179
- }
180
- }
181
- }
182
- }
183
-
184
- private fun onKeyPress(key: String, isCustomKey: Boolean) {
185
- if (isCustomKey) {
186
- emitCustomKey()
173
+ private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
174
+ if (isMainCTAKey) {
175
+ emitCustomKey(context)
187
176
  return
188
177
  }
189
178
 
190
- emitKeyPress(key)
179
+ emitKeyPress(context, key)
191
180
  when (key) {
181
+ "AC" -> clearText()
192
182
  "back" -> onBackSpace()
193
183
  "=" -> calculateResult()
194
184
  "×", "+", "-", "÷" -> keyDidPress(" $key ")
195
185
  else -> editText.text?.insert(editText.selectionStart, key)
196
186
  }
187
+
188
+ reformatAndKeepSelection(editText)
197
189
  }
198
190
 
199
191
  private fun keyDidPress(key: String) {
@@ -201,27 +193,44 @@ class CustomKeyboardView(
201
193
  editText.text?.replace(editText.selectionStart, editText.selectionEnd, key)
202
194
  }
203
195
 
196
+ private fun clearText() {
197
+ editText.text?.clear()
198
+ }
199
+
204
200
  private fun onBackSpace() {
205
- val start = editText.selectionStart
206
- val end = editText.selectionEnd
207
- if (start > 0) {
208
- val newText = end.let { editText.text?.replaceRange(start - 1, it, "") }
209
- editText.setText(newText)
210
- 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)
211
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))
212
218
  }
213
219
 
220
+
214
221
  private fun calculateResult() {
215
- val text = editText?.text.toString().replace("×", "*").replace("÷", "/")
222
+ val raw = editText.text?.toString().orEmpty()
223
+ val normalized = raw.replace(".", "")
224
+ .replace("×", "*")
225
+ .replace("÷", "/")
226
+
216
227
  val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
217
- val regex = Regex(pattern)
218
- if (regex.matches(text)) {
228
+ if (Regex(pattern).matches(normalized)) {
219
229
  try {
220
- val result = eval(text).toString()
221
- editText.setTextKeepState(result)
222
- } catch (e: Exception) {
223
- e.printStackTrace()
224
- }
230
+ val result = eval(normalized)?.toString() ?: return
231
+ editText.setTextKeepState(formatNumberGroups(result))
232
+ editText.setSelection(editText.text?.length ?: 0)
233
+ } catch (_: Exception) { /* ignore */ }
225
234
  } else {
226
235
  println("Invalid expression")
227
236
  }
@@ -237,52 +246,132 @@ class CustomKeyboardView(
237
246
  }
238
247
  }
239
248
 
240
- private fun emitKeyPress(key: String) {
241
- val reactContext = context as ThemedReactContext
242
- val params = Arguments.createMap().apply {
243
- putString("key", key)
244
- }
245
- reactContext.getJSModule(RCTEventEmitter::class.java)
246
- .receiveEvent(editText.id, "onKeyPress", params)
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)
247
260
  }
248
261
 
249
- private fun emitCustomKey() {
250
- val reactContext = context as ThemedReactContext
251
- reactContext.getJSModule(RCTEventEmitter::class.java)
252
- .receiveEvent(editText.id, "onCustomKeyEvent", null)
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)
253
270
  }
254
271
 
255
272
  fun setCustomKeyText(text: String) {
256
273
  customKeyButton?.text = text
257
274
  }
258
275
 
276
+ fun setMode(mode: String) {
277
+ keyboardMode = mode
278
+ renderUI()
279
+ }
280
+
259
281
  fun setCustomKeyBackground(background: Int) {
260
282
  customKeyButtonBackground = background
261
- updateCustomKeyUI(background, customKeyButtonTextColor, customKeyButtonState)
283
+ updateCustomKeyUI(background, customKeyButtonTextColor)
262
284
  }
263
285
 
264
286
  fun setCustomKeyTextColor(textColor: Int) {
265
287
  customKeyButtonTextColor = textColor
266
- updateCustomKeyUI(customKeyButtonBackground, textColor, customKeyButtonState)
288
+ updateCustomKeyUI(customKeyButtonBackground, textColor)
267
289
  }
268
290
 
269
291
 
270
- fun setCustomKeyState(state: Int) {
292
+ fun setCustomKeyState(state: String) {
271
293
  customKeyButtonState = state
272
- customKeyButton?.isEnabled = state == 0
273
- updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor, state)
294
+ customKeyButton?.isEnabled = state != "disable"
295
+ updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor)
274
296
  }
275
297
 
276
- private fun updateCustomKeyUI(background: Int, textColor: Int, state: Int){
277
- val backgroundColor = if(state == 1) "#EBEBF2".toColorInt() else background
278
- val textColor = if (state == 1) Color.WHITE else textColor
279
-
298
+ private fun updateCustomKeyUI(background: Int, textColor: Int){
280
299
  customKeyButton?.background = GradientDrawable().apply {
281
300
  shape = GradientDrawable.RECTANGLE
282
301
  cornerRadius = 24f
283
- setColor(backgroundColor)
302
+ setColor(background)
284
303
  }
285
304
  customKeyButton?.setTextColor(textColor)
286
305
  }
287
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
+ }
288
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
+