@momo-kits/calculator-keyboard 0.150.2-phuc.15 → 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.
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,53 +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
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
50
  isClickable = false
47
51
  isFocusable = false
48
52
  isFocusableInTouchMode = false
49
53
  clipToPadding = false
50
54
  clipChildren = false
51
-
52
- renderUI(widthButton, heightButton)
53
- }
54
-
55
55
  }
56
56
 
57
- 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
+
58
62
  var yOffset = separatorWidth
63
+ val keys = if (keyboardMode == "NumWithCTA") numWithCTAKeys else defaultKeys
59
64
  for ((rowIndex, row) in keys.withIndex()) {
60
65
  var xOffset = separatorWidth
61
66
  for ((colIndex, key) in row.withIndex()) {
62
- val isCustomKey = rowIndex == 2 && colIndex == 4
67
+ val isMainKey = rowIndex == 2 && colIndex == 4
68
+ val isMainCTAKey = isMainKey && keyboardMode == "NumWithCTA"
63
69
  val width = if (key == "000") buttonWidth * 2 + separatorWidth else buttonWidth
64
- val height = if (isCustomKey) buttonHeight * 2 + separatorWidth else buttonHeight
70
+ val height = if (isMainKey) buttonHeight * 2 + separatorWidth else buttonHeight
65
71
 
66
72
  val button = if (key == "back") {
67
73
  createImageButton(key, xOffset, yOffset, buttonWidth.toInt(), buttonHeight.toInt())
68
74
  } else {
69
- createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isCustomKey).also { b ->
70
- if (isCustomKey) customKeyButton = b
75
+ createButton(key, xOffset, yOffset, width.toInt(), height.toInt(), isMainKey, isMainCTAKey).also { b ->
76
+ if (isMainCTAKey) customKeyButton = b
71
77
  }
72
78
  }
73
79
 
@@ -85,7 +91,8 @@ class CustomKeyboardView(
85
91
  yOffset: Float,
86
92
  buttonWidth: Int,
87
93
  buttonHeight: Int,
88
- isCustomKey: Boolean
94
+ isMainKey: Boolean,
95
+ isMainCTAKey: Boolean
89
96
  ): Button {
90
97
  return Button(context).apply {
91
98
  val shapeInit = GradientDrawable().apply {
@@ -98,7 +105,7 @@ class CustomKeyboardView(
98
105
  background = shapeInit
99
106
  text = key
100
107
  setTypeface(typeface)
101
- textSize = (if (isCustomKey) 18 else 24).toFloat()
108
+ textSize = (if (isMainCTAKey) 18 else 24).toFloat()
102
109
  setTextColor(Color.BLACK)
103
110
  stateListAnimator = null
104
111
  maxLines = 1
@@ -110,7 +117,7 @@ class CustomKeyboardView(
110
117
  constrainedWidth = false
111
118
  }
112
119
 
113
- if (specialKeys.contains(key)) {
120
+ if (specialKeys.contains(key) || isMainKey) {
114
121
  background = GradientDrawable().apply {
115
122
  shape = GradientDrawable.RECTANGLE
116
123
  cornerRadius = 24f
@@ -125,7 +132,7 @@ class CustomKeyboardView(
125
132
 
126
133
  translationX = xOffset.toInt().toFloat()
127
134
  translationY = yOffset.toInt().toFloat()
128
- setOnClickListener { onKeyPress(key, isCustomKey) }
135
+ setOnClickListener { onKeyPress(key, isMainCTAKey) }
129
136
  }
130
137
  }
131
138
 
@@ -163,51 +170,22 @@ class CustomKeyboardView(
163
170
  }
164
171
  }
165
172
 
166
- fun updateButtonColors(color: Int) {
167
- specialButtonColor = color
168
- for (i in 0 until childCount) {
169
- val child = getChildAt(i)
170
- if (child is Button) {
171
- val key = child.text.toString()
172
- if (specialKeys.contains(key)) {
173
- if (key == "=") {
174
- child.background = GradientDrawable().apply {
175
- shape = GradientDrawable.RECTANGLE
176
- cornerRadius = 24f
177
- setColor(specialButtonColor)
178
- }
179
- } else {
180
- child.background = GradientDrawable().apply {
181
- shape = GradientDrawable.RECTANGLE
182
- cornerRadius = 24f
183
- setColor(specialButtonColor)
184
- }
185
- }
186
- child.setTextColor(Color.BLACK)
187
- }
188
- } else if (child is ImageButton) {
189
- child.background = GradientDrawable().apply {
190
- shape = GradientDrawable.RECTANGLE
191
- cornerRadius = 24f
192
- setColor(specialButtonColor)
193
- }
194
- }
195
- }
196
- }
197
-
198
- private fun onKeyPress(key: String, isCustomKey: Boolean) {
199
- if (isCustomKey) {
200
- emitCustomKey()
173
+ private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
174
+ if (isMainCTAKey) {
175
+ emitCustomKey(context)
201
176
  return
202
177
  }
203
178
 
204
- emitKeyPress(key)
179
+ emitKeyPress(context, key)
205
180
  when (key) {
181
+ "AC" -> clearText()
206
182
  "back" -> onBackSpace()
207
183
  "=" -> calculateResult()
208
184
  "×", "+", "-", "÷" -> keyDidPress(" $key ")
209
185
  else -> editText.text?.insert(editText.selectionStart, key)
210
186
  }
187
+
188
+ reformatAndKeepSelection(editText)
211
189
  }
212
190
 
213
191
  private fun keyDidPress(key: String) {
@@ -215,27 +193,44 @@ class CustomKeyboardView(
215
193
  editText.text?.replace(editText.selectionStart, editText.selectionEnd, key)
216
194
  }
217
195
 
196
+ private fun clearText() {
197
+ editText.text?.clear()
198
+ }
199
+
218
200
  private fun onBackSpace() {
219
- val start = editText.selectionStart
220
- val end = editText.selectionEnd
221
- if (start > 0) {
222
- val newText = end.let { editText.text?.replaceRange(start - 1, it, "") }
223
- editText.setText(newText)
224
- 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)
225
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))
226
218
  }
227
219
 
220
+
228
221
  private fun calculateResult() {
229
- val text = editText?.text.toString().replace("×", "*").replace("÷", "/")
222
+ val raw = editText.text?.toString().orEmpty()
223
+ val normalized = raw.replace(".", "")
224
+ .replace("×", "*")
225
+ .replace("÷", "/")
226
+
230
227
  val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
231
- val regex = Regex(pattern)
232
- if (regex.matches(text)) {
228
+ if (Regex(pattern).matches(normalized)) {
233
229
  try {
234
- val result = eval(text).toString()
235
- editText.setTextKeepState(result)
236
- } catch (e: Exception) {
237
- e.printStackTrace()
238
- }
230
+ val result = eval(normalized)?.toString() ?: return
231
+ editText.setTextKeepState(formatNumberGroups(result))
232
+ editText.setSelection(editText.text?.length ?: 0)
233
+ } catch (_: Exception) { /* ignore */ }
239
234
  } else {
240
235
  println("Invalid expression")
241
236
  }
@@ -251,44 +246,56 @@ class CustomKeyboardView(
251
246
  }
252
247
  }
253
248
 
254
- private fun emitKeyPress(key: String) {
255
- val reactContext = context as ThemedReactContext
256
- val params = Arguments.createMap().apply {
257
- putString("key", key)
258
- }
259
- reactContext.getJSModule(RCTEventEmitter::class.java)
260
- .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)
261
260
  }
262
261
 
263
- private fun emitCustomKey() {
264
- val reactContext = context as ThemedReactContext
265
- reactContext.getJSModule(RCTEventEmitter::class.java)
266
- .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)
267
270
  }
268
271
 
269
272
  fun setCustomKeyText(text: String) {
270
273
  customKeyButton?.text = text
271
274
  }
272
275
 
276
+ fun setMode(mode: String) {
277
+ keyboardMode = mode
278
+ renderUI()
279
+ }
280
+
273
281
  fun setCustomKeyBackground(background: Int) {
274
282
  customKeyButtonBackground = background
275
- updateCustomKeyUI(background, customKeyButtonTextColor, customKeyButtonState)
283
+ updateCustomKeyUI(background, customKeyButtonTextColor)
276
284
  }
277
285
 
278
286
  fun setCustomKeyTextColor(textColor: Int) {
279
287
  customKeyButtonTextColor = textColor
280
- updateCustomKeyUI(customKeyButtonBackground, textColor, customKeyButtonState)
288
+ updateCustomKeyUI(customKeyButtonBackground, textColor)
281
289
  }
282
290
 
283
291
 
284
292
  fun setCustomKeyState(state: String) {
285
293
  customKeyButtonState = state
286
294
  customKeyButton?.isEnabled = state != "disable"
287
- updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor, state)
295
+ updateCustomKeyUI(customKeyButtonBackground, customKeyButtonTextColor)
288
296
  }
289
297
 
290
- private fun updateCustomKeyUI(background: Int, textColor: Int, state: String){
291
-
298
+ private fun updateCustomKeyUI(background: Int, textColor: Int){
292
299
  customKeyButton?.background = GradientDrawable().apply {
293
300
  shape = GradientDrawable.RECTANGLE
294
301
  cornerRadius = 24f
@@ -297,5 +304,74 @@ class CustomKeyboardView(
297
304
  customKeyButton?.setTextColor(textColor)
298
305
  }
299
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
+ }
300
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
+ }
301
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
+