@momo-kits/calculator-keyboard 0.151.1-beta.1 → 0.151.1-beta.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.
- package/android/src/main/java/com/calculatorkeyboard/CustomKeyboardView.kt +147 -89
- package/android/src/main/java/com/calculatorkeyboard/Event.kt +95 -2
- package/android/src/main/java/com/calculatorkeyboard/InputCalculatorViewManager.kt +22 -23
- package/ios/CalculatorKeyboardView.h +2 -0
- package/ios/CalculatorKeyboardView.mm +72 -29
- package/ios/NativeInputCalculator.mm +146 -15
- package/ios/Utils.h +10 -0
- package/ios/Utils.mm +25 -0
- package/package.json +1 -1
- package/src/InputCalculatorNativeComponent.ts +11 -2
- package/src/index.tsx +19 -3
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
package com.calculatorkeyboard
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
-
import android.content.Context
|
|
5
4
|
import android.content.res.ColorStateList
|
|
6
5
|
import android.graphics.Color
|
|
7
6
|
import android.graphics.drawable.GradientDrawable
|
|
7
|
+
import android.text.Editable
|
|
8
8
|
import android.view.Gravity
|
|
9
9
|
import android.widget.Button
|
|
10
|
+
import android.widget.EditText
|
|
10
11
|
import android.widget.ImageButton
|
|
11
12
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
12
|
-
import com.facebook.react.uimanager.ThemedReactContext
|
|
13
|
-
import org.mariuszgromada.math.mxparser.Expression
|
|
14
13
|
import androidx.core.graphics.toColorInt
|
|
15
14
|
import com.calculatorkeyboard.InputCalculatorViewManager.Companion.calculatorHeight
|
|
16
|
-
import com.facebook.react.
|
|
17
|
-
import
|
|
18
|
-
import com.facebook.react.uimanager.UIManagerHelper
|
|
19
|
-
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
15
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
16
|
+
import org.mariuszgromada.math.mxparser.Expression
|
|
20
17
|
|
|
21
18
|
@SuppressLint("SetTextI18n", "ViewConstructor")
|
|
22
19
|
class CustomKeyboardView(
|
|
@@ -170,27 +167,77 @@ class CustomKeyboardView(
|
|
|
170
167
|
}
|
|
171
168
|
}
|
|
172
169
|
|
|
170
|
+
private fun String.endsWithAny(vararg tails: String): Boolean =
|
|
171
|
+
tails.any { endsWith(it) }
|
|
172
|
+
|
|
173
|
+
private fun Editable.insertAtCaret(editText: EditText, s: String) {
|
|
174
|
+
insert(editText.selectionStart.coerceAtLeast(0), s)
|
|
175
|
+
}
|
|
176
|
+
|
|
173
177
|
private fun onKeyPress(key: String, isMainCTAKey: Boolean) {
|
|
174
178
|
if (isMainCTAKey) {
|
|
175
|
-
emitCustomKey(context)
|
|
179
|
+
Event.emitCustomKey(context, editText.id)
|
|
176
180
|
return
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
emitKeyPress(context, key)
|
|
183
|
+
Event.emitKeyPress(context, editText.id, key)
|
|
184
|
+
|
|
185
|
+
val text = editText.text ?: return
|
|
186
|
+
val plain = text.toString()
|
|
187
|
+
val isEmpty = plain.isEmpty()
|
|
188
|
+
val isJustZero = plain == "0"
|
|
189
|
+
val endsWithOperator = plain.endsWithAny(" + ", " - ", " × ", " ÷ ")
|
|
190
|
+
val endsWithSingleZeroAfterOp = plain.endsWith(" 0")
|
|
191
|
+
|
|
180
192
|
when (key) {
|
|
181
193
|
"AC" -> clearText()
|
|
182
194
|
"back" -> onBackSpace()
|
|
183
195
|
"=" -> calculateResult()
|
|
184
|
-
"×", "+", "-", "÷" -> keyDidPress(" $key ")
|
|
185
|
-
|
|
196
|
+
"×", "+", "-", "÷" -> if (!text.isEmpty()) keyDidPress(" $key ")
|
|
197
|
+
"0", "000" -> {
|
|
198
|
+
when {
|
|
199
|
+
isEmpty -> {}
|
|
200
|
+
key == "000" && endsWithOperator -> text.insertAtCaret(editText, "0")
|
|
201
|
+
endsWithSingleZeroAfterOp || isJustZero -> {}
|
|
202
|
+
else -> text.insertAtCaret(editText, key)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else -> {
|
|
206
|
+
when {
|
|
207
|
+
isJustZero -> {
|
|
208
|
+
text.replace(0, 1, key)
|
|
209
|
+
}
|
|
210
|
+
endsWithSingleZeroAfterOp -> {
|
|
211
|
+
val i = text.length - 1
|
|
212
|
+
text.replace(i, i + 1, key)
|
|
213
|
+
}
|
|
214
|
+
else -> text.insertAtCaret(editText, key)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
reformatAndKeepSelection(editText)
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
private fun keyDidPress(key: String) {
|
|
192
|
-
|
|
193
|
-
editText.
|
|
223
|
+
val editable = editText.text ?: return
|
|
224
|
+
val selStart = editText.selectionStart.coerceAtLeast(0)
|
|
225
|
+
val selEnd = editText.selectionEnd.coerceAtLeast(selStart)
|
|
226
|
+
|
|
227
|
+
if (selStart != selEnd) {
|
|
228
|
+
editable.replace(selStart, selEnd, key)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
val s = editable.toString()
|
|
233
|
+
val ops3 = setOf(" + ", " - ", " × ", " ÷ ")
|
|
234
|
+
|
|
235
|
+
if (selStart >= 3 && ops3.contains(s.substring(selStart - 3, selStart))) {
|
|
236
|
+
editable.replace(selStart - 3, selStart, key)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
editable.replace(selStart, selEnd, key)
|
|
194
241
|
}
|
|
195
242
|
|
|
196
243
|
private fun clearText() {
|
|
@@ -206,67 +253,77 @@ class CustomKeyboardView(
|
|
|
206
253
|
|
|
207
254
|
if (caretRaw <= 0) return
|
|
208
255
|
|
|
209
|
-
val
|
|
210
|
-
|
|
211
|
-
|
|
256
|
+
val ops = setOf(" + ", " - ", " × ", " ÷ ")
|
|
257
|
+
val len = rawBefore.length
|
|
258
|
+
|
|
259
|
+
var delStart = (caretRaw - 1).coerceAtLeast(0)
|
|
260
|
+
var delEnd = caretRaw
|
|
261
|
+
|
|
262
|
+
fun matches(start: Int, end: Int): Boolean {
|
|
263
|
+
if (start < 0 || end > len) return false
|
|
264
|
+
if (end - start != 3) return false
|
|
265
|
+
return ops.contains(rawBefore.substring(start, end))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (matches(caretRaw - 3, caretRaw)) {
|
|
269
|
+
delStart = caretRaw - 3
|
|
270
|
+
delEnd = caretRaw
|
|
271
|
+
}
|
|
272
|
+
else if (matches(caretRaw - 2, caretRaw + 1)) {
|
|
273
|
+
delStart = caretRaw - 2
|
|
274
|
+
delEnd = caretRaw + 1
|
|
275
|
+
}
|
|
276
|
+
else if (matches(caretRaw - 1, caretRaw + 2)) {
|
|
277
|
+
delStart = caretRaw - 1
|
|
278
|
+
delEnd = caretRaw + 2
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
val rawAfter = buildString(len - (delEnd - delStart)) {
|
|
282
|
+
append(rawBefore, 0, delStart)
|
|
283
|
+
append(rawBefore, delEnd, len)
|
|
212
284
|
}
|
|
213
285
|
|
|
214
286
|
val formattedAfter = formatNumberGroups(rawAfter)
|
|
215
287
|
editText.setText(formattedAfter)
|
|
216
|
-
|
|
288
|
+
|
|
289
|
+
val newCaretFmt = rawCaretToFormatted(delStart, formattedAfter)
|
|
217
290
|
editText.setSelection(newCaretFmt.coerceIn(0, formattedAfter.length))
|
|
218
291
|
}
|
|
219
292
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.replace("÷", "/")
|
|
226
|
-
|
|
227
|
-
val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
|
|
228
|
-
if (Regex(pattern).matches(normalized)) {
|
|
229
|
-
try {
|
|
230
|
-
val result = eval(normalized)?.toString() ?: return
|
|
231
|
-
editText.setTextKeepState(formatNumberGroups(result))
|
|
232
|
-
editText.setSelection(editText.text?.length ?: 0)
|
|
233
|
-
} catch (_: Exception) { /* ignore */ }
|
|
234
|
-
} else {
|
|
235
|
-
println("Invalid expression")
|
|
236
|
-
}
|
|
293
|
+
private fun calculateResult(){
|
|
294
|
+
val result = editText.calculate()
|
|
295
|
+
if (result == "invalid") return
|
|
296
|
+
editText.setTextKeepState(formatNumberGroups(result))
|
|
297
|
+
editText.setSelection(editText.text?.length ?: 0)
|
|
237
298
|
}
|
|
238
299
|
|
|
239
|
-
|
|
240
|
-
val
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
null
|
|
300
|
+
fun formatNumberGroups(input: String): String {
|
|
301
|
+
val noSep = stripGroupDots(input)
|
|
302
|
+
return Regex("\\d+").replace(noSep) { m ->
|
|
303
|
+
val s = m.value
|
|
304
|
+
val rev = s.reversed().chunked(3).joinToString(".")
|
|
305
|
+
rev.reversed()
|
|
246
306
|
}
|
|
247
307
|
}
|
|
248
308
|
|
|
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
309
|
|
|
259
|
-
|
|
310
|
+
fun stripGroupDots(input: String): String {
|
|
311
|
+
val out = StringBuilder(input.length)
|
|
312
|
+
for (i in input.indices) {
|
|
313
|
+
val c = input[i]
|
|
314
|
+
if (c == '.' && isGroupDotAt(input, i)) {
|
|
315
|
+
} else {
|
|
316
|
+
out.append(c)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out.toString()
|
|
260
320
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
val
|
|
265
|
-
val
|
|
266
|
-
|
|
267
|
-
val event = OnCustomKeyPressEvent(surfaceId, editText.id)
|
|
268
|
-
|
|
269
|
-
eventDispatcher?.dispatchEvent(event)
|
|
321
|
+
private fun isGroupDotAt(s: String, i: Int): Boolean {
|
|
322
|
+
if (i < 0 || i >= s.length) return false
|
|
323
|
+
if (s[i] != '.') return false
|
|
324
|
+
val leftIsDigit = i - 1 >= 0 && s[i - 1].isDigit()
|
|
325
|
+
val rightIsDigit = i + 1 < s.length && s[i + 1].isDigit()
|
|
326
|
+
return leftIsDigit && rightIsDigit
|
|
270
327
|
}
|
|
271
328
|
|
|
272
329
|
fun setCustomKeyText(text: String) {
|
|
@@ -304,7 +361,7 @@ class CustomKeyboardView(
|
|
|
304
361
|
customKeyButton?.setTextColor(textColor)
|
|
305
362
|
}
|
|
306
363
|
|
|
307
|
-
|
|
364
|
+
fun reformatAndKeepSelection(editText: CalculatorEditText) {
|
|
308
365
|
val formattedBefore = editText.text?.toString() ?: return
|
|
309
366
|
val caretFmtBefore = editText.selectionStart.coerceAtLeast(0)
|
|
310
367
|
|
|
@@ -319,34 +376,6 @@ class CustomKeyboardView(
|
|
|
319
376
|
val caretFmtAfter = rawCaretToFormatted(caretRaw, formattedAfter)
|
|
320
377
|
editText.setSelection(caretFmtAfter.coerceIn(0, formattedAfter.length))
|
|
321
378
|
}
|
|
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
379
|
|
|
351
380
|
private fun formattedCaretToRaw(formatted: String, caretFmt: Int): Int {
|
|
352
381
|
var rawIdx = 0
|
|
@@ -374,4 +403,33 @@ class CustomKeyboardView(
|
|
|
374
403
|
}
|
|
375
404
|
return formatted.length
|
|
376
405
|
}
|
|
406
|
+
|
|
407
|
+
companion object {
|
|
408
|
+
fun CalculatorEditText.calculate() : String {
|
|
409
|
+
val invalid = "invalid"
|
|
410
|
+
val raw = this.text?.toString().orEmpty()
|
|
411
|
+
val normalized = raw.replace(".", "")
|
|
412
|
+
.replace("×", "*")
|
|
413
|
+
.replace("÷", "/")
|
|
414
|
+
|
|
415
|
+
val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
|
|
416
|
+
if (Regex(pattern).matches(normalized)) {
|
|
417
|
+
try {
|
|
418
|
+
val result = eval(normalized)?.toString() ?: return invalid
|
|
419
|
+
return result
|
|
420
|
+
} catch (_: Exception) { return invalid }
|
|
421
|
+
} else {
|
|
422
|
+
return invalid
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
private fun eval(str: String): Long? {
|
|
426
|
+
val e = Expression(str)
|
|
427
|
+
println("Expression: $e")
|
|
428
|
+
return if (e.checkSyntax()) {
|
|
429
|
+
e.calculate().toLong()
|
|
430
|
+
} else {
|
|
431
|
+
null
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
377
435
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
package com.calculatorkeyboard
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.bridge.ReactContext
|
|
3
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
4
8
|
import com.facebook.react.uimanager.events.Event
|
|
5
9
|
|
|
6
10
|
class OnChangeEvent(
|
|
@@ -17,20 +21,109 @@ class OnKeyPressEvent(
|
|
|
17
21
|
surfaceId: Int,
|
|
18
22
|
viewId: Int,
|
|
19
23
|
private val payload: WritableMap
|
|
20
|
-
) : Event<
|
|
24
|
+
) : Event<OnKeyPressEvent>(surfaceId, viewId) {
|
|
21
25
|
override fun getEventName() = "onKeyPress"
|
|
22
26
|
|
|
23
27
|
override fun getEventData() = payload
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
class OnResultEvent(
|
|
31
|
+
surfaceId: Int,
|
|
32
|
+
viewId: Int,
|
|
33
|
+
private val payload: WritableMap
|
|
34
|
+
) : Event<OnResultEvent>(surfaceId, viewId) {
|
|
35
|
+
override fun getEventName() = "onResult"
|
|
36
|
+
|
|
37
|
+
override fun getEventData() = payload
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class OnFocusEvent(
|
|
41
|
+
surfaceId: Int,
|
|
42
|
+
viewId: Int
|
|
43
|
+
) : Event<OnFocusEvent>(surfaceId, viewId) {
|
|
44
|
+
override fun getEventName() = "onFocus"
|
|
45
|
+
|
|
46
|
+
override fun getEventData() = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class OnBlurEvent(
|
|
50
|
+
surfaceId: Int,
|
|
51
|
+
viewId: Int
|
|
52
|
+
) : Event<OnBlurEvent>(surfaceId, viewId) {
|
|
53
|
+
override fun getEventName() = "onBlur"
|
|
54
|
+
|
|
55
|
+
override fun getEventData() = null
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
class OnCustomKeyPressEvent(
|
|
27
59
|
surfaceId: Int,
|
|
28
60
|
viewId: Int
|
|
29
|
-
) : Event<
|
|
61
|
+
) : Event<OnCustomKeyPressEvent>(surfaceId, viewId) {
|
|
30
62
|
override fun getEventName() = "onCustomKeyEvent"
|
|
31
63
|
|
|
32
64
|
override fun getEventData() = null
|
|
33
65
|
}
|
|
34
66
|
|
|
67
|
+
object Event {
|
|
68
|
+
fun emitKeyPress(context: Context, tag: Int, key: String) {
|
|
69
|
+
val reactContext = context as ReactContext
|
|
70
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
71
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
72
|
+
val payload =
|
|
73
|
+
Arguments.createMap().apply {
|
|
74
|
+
putString("key", key)
|
|
75
|
+
}
|
|
76
|
+
val event = OnKeyPressEvent(surfaceId, tag, payload)
|
|
77
|
+
|
|
78
|
+
eventDispatcher?.dispatchEvent(event)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun emitCustomKey(context: Context, tag: Int) {
|
|
82
|
+
val reactContext = context as ReactContext
|
|
83
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
84
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
85
|
+
|
|
86
|
+
val event = OnCustomKeyPressEvent(surfaceId, tag)
|
|
87
|
+
|
|
88
|
+
eventDispatcher?.dispatchEvent(event)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun emitOnChange(context: Context, tag: Int, text: String) {
|
|
92
|
+
val reactContext = context as ReactContext
|
|
93
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
94
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
95
|
+
val payload =
|
|
96
|
+
Arguments.createMap().apply {
|
|
97
|
+
putString("text", text)
|
|
98
|
+
}
|
|
99
|
+
val event = OnChangeEvent(surfaceId, tag, payload)
|
|
100
|
+
|
|
101
|
+
eventDispatcher?.dispatchEvent(event)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fun emitOnBlurFocus(context: Context, tag: Int, isFocus: Boolean) {
|
|
105
|
+
val reactContext = context as ReactContext
|
|
106
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
107
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
108
|
+
|
|
109
|
+
val event = if (isFocus) OnFocusEvent(surfaceId, tag) else OnBlurEvent(surfaceId, tag)
|
|
110
|
+
|
|
111
|
+
eventDispatcher?.dispatchEvent(event)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun emitOnResult(context: Context, tag: Int, result: String) {
|
|
115
|
+
val reactContext = context as ReactContext
|
|
116
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
117
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, tag)
|
|
118
|
+
val payload =
|
|
119
|
+
Arguments.createMap().apply {
|
|
120
|
+
putString("result", result)
|
|
121
|
+
}
|
|
122
|
+
val event = OnResultEvent(surfaceId, tag, payload)
|
|
123
|
+
|
|
124
|
+
eventDispatcher?.dispatchEvent(event)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
35
128
|
|
|
36
129
|
|
|
@@ -5,7 +5,6 @@ import android.content.Context
|
|
|
5
5
|
import android.graphics.Color
|
|
6
6
|
import android.graphics.Typeface
|
|
7
7
|
import android.os.Build
|
|
8
|
-
import android.provider.Settings.Global.putString
|
|
9
8
|
import android.text.Editable
|
|
10
9
|
import android.text.TextWatcher
|
|
11
10
|
import android.view.KeyEvent
|
|
@@ -13,24 +12,18 @@ import android.view.inputmethod.InputMethodManager
|
|
|
13
12
|
import androidx.core.graphics.toColorInt
|
|
14
13
|
import androidx.core.view.ViewCompat
|
|
15
14
|
import androidx.core.view.WindowInsetsCompat
|
|
16
|
-
import
|
|
17
|
-
import com.facebook.react.bridge.Arguments
|
|
18
|
-
import com.facebook.react.bridge.ReactContext
|
|
19
|
-
import com.facebook.react.bridge.ReadableArray
|
|
15
|
+
import com.calculatorkeyboard.CustomKeyboardView.Companion.calculateResult
|
|
20
16
|
import com.facebook.react.bridge.ReadableMap
|
|
21
17
|
import com.facebook.react.bridge.UiThreadUtil
|
|
22
18
|
import com.facebook.react.module.annotations.ReactModule
|
|
23
19
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
24
20
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
25
21
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
26
|
-
import com.facebook.react.uimanager.UIManagerHelper
|
|
27
22
|
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
28
23
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
29
|
-
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
30
24
|
import com.facebook.react.viewmanagers.NativeInputCalculatorManagerDelegate
|
|
31
25
|
import com.facebook.react.viewmanagers.NativeInputCalculatorManagerInterface
|
|
32
26
|
import com.facebook.react.views.textinput.ReactEditText
|
|
33
|
-
import com.facebook.react.views.textinput.ReactTextInputManager
|
|
34
27
|
|
|
35
28
|
@ReactModule(name = InputCalculatorViewManager.REACT_CLASS)
|
|
36
29
|
class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInputCalculatorManagerInterface<ReactEditText> {
|
|
@@ -67,12 +60,15 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
67
60
|
val atEnd = wasFocused && view.selectionStart == e.length && view.selectionEnd == e.length
|
|
68
61
|
e.replace(0, e.length, newText)
|
|
69
62
|
if (!wasFocused || atEnd) view.setSelection(newText.length)
|
|
63
|
+
keyboardView?.reformatAndKeepSelection(editText)
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
override fun setTextAttributes(view: ReactEditText?, value: ReadableMap?) {
|
|
74
68
|
val fontWeightStr = value?.getString("fontWeight")
|
|
75
69
|
val fontSize = value?.getDouble("fontSize")
|
|
70
|
+
val placeHolder = value?.getString("placeholder")
|
|
71
|
+
val placeHolderTextColor = value?.getString("placeholderTextColor")
|
|
76
72
|
|
|
77
73
|
fontWeightStr?.let { weightStr ->
|
|
78
74
|
val weight = when (weightStr) {
|
|
@@ -102,6 +98,12 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
102
98
|
fontSize?.let {
|
|
103
99
|
view?.textSize = it.toFloat()
|
|
104
100
|
}
|
|
101
|
+
placeHolder?.let {
|
|
102
|
+
view?.setPlaceholder(placeHolder)
|
|
103
|
+
}
|
|
104
|
+
placeHolderTextColor?.let {
|
|
105
|
+
view?.setHintTextColor(it.toColorInt())
|
|
106
|
+
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
|
|
@@ -145,6 +147,8 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
145
147
|
editText = CalculatorEditText(context).apply {
|
|
146
148
|
showSoftInputOnFocus = false
|
|
147
149
|
isFocusableInTouchMode = true
|
|
150
|
+
isSingleLine = true
|
|
151
|
+
|
|
148
152
|
setOnTouchListener { v, event ->
|
|
149
153
|
if (event.action == android.view.MotionEvent.ACTION_DOWN) {
|
|
150
154
|
showSoftInputOnFocus = false
|
|
@@ -172,10 +176,15 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
172
176
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
173
177
|
|
|
174
178
|
override fun afterTextChanged(s: Editable?) {
|
|
175
|
-
emitOnChange(context, s.toString())
|
|
179
|
+
Event.emitOnChange(context, editText.id, s.toString())
|
|
180
|
+
Event.emitOnResult(context, editText.id, editText.calculateResult())
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
})
|
|
184
|
+
|
|
185
|
+
setOnFocusChangeListener { view, hasFocus ->
|
|
186
|
+
Event.emitOnBlurFocus(context, editText.id, hasFocus)
|
|
187
|
+
}
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
keyboardView = CustomKeyboardView(context, editText).apply {
|
|
@@ -230,7 +239,7 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
230
239
|
|
|
231
240
|
private fun disableSystemImeFor(v: ReactEditText) {
|
|
232
241
|
v.showSoftInputOnFocus = false
|
|
233
|
-
if (
|
|
242
|
+
if (Build.VERSION.SDK_INT >= 30) {
|
|
234
243
|
ViewCompat.getWindowInsetsController(v)?.hide(WindowInsetsCompat.Type.ime())
|
|
235
244
|
}
|
|
236
245
|
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
|
@@ -240,28 +249,18 @@ class InputCalculatorViewManager : SimpleViewManager<ReactEditText>(), NativeInp
|
|
|
240
249
|
v.showSoftInputOnFocus = true
|
|
241
250
|
}
|
|
242
251
|
|
|
243
|
-
private fun emitOnChange(context: Context, text: String) {
|
|
244
|
-
val reactContext = context as ReactContext
|
|
245
|
-
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
246
|
-
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, editText.id)
|
|
247
|
-
val payload =
|
|
248
|
-
Arguments.createMap().apply {
|
|
249
|
-
putString("text", text)
|
|
250
|
-
}
|
|
251
|
-
val event = OnChangeEvent(surfaceId, editText.id, payload)
|
|
252
|
-
|
|
253
|
-
eventDispatcher?.dispatchEvent(event)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
252
|
override fun getExportedCustomBubblingEventTypeConstants(): MutableMap<String, Any> {
|
|
257
253
|
val base = super.getExportedCustomBubblingEventTypeConstants()?.toMutableMap() ?: mutableMapOf()
|
|
258
254
|
base["onKeyPress"] = mapOf("phasedRegistrationNames" to mapOf("bubbled" to "onKeyPress"))
|
|
259
255
|
base["onChange"] = mapOf("phasedRegistrationNames" to mapOf("bubbled" to "onChange"))
|
|
256
|
+
base["onResult"] = mapOf("phasedRegistrationNames" to mapOf("bubbled" to "onResult"))
|
|
260
257
|
return base
|
|
261
258
|
}
|
|
262
259
|
|
|
263
260
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
|
|
264
261
|
val base = super.getExportedCustomDirectEventTypeConstants()?.toMutableMap() ?: mutableMapOf()
|
|
262
|
+
base["onBlur"] = mapOf("registrationName" to "onBlur")
|
|
263
|
+
base["onFocus"] = mapOf("registrationName" to "onFocus")
|
|
265
264
|
base["onCustomKeyEvent"] = mapOf("registrationName" to "onCustomKeyEvent")
|
|
266
265
|
return base
|
|
267
266
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import "CalculatorKeyboardView.h"
|
|
2
|
+
#import "Utils.h"
|
|
2
3
|
|
|
3
4
|
@interface CalculatorKeyboardView ()
|
|
4
5
|
@property (nonatomic, strong) NSArray<NSArray<NSString *> *> *numWithCTAKeys;
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
[subview removeFromSuperview];
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
self.backgroundColor = [
|
|
86
|
+
self.backgroundColor = [Utils colorFromHex:@"#f2f2f6"];
|
|
86
87
|
|
|
87
88
|
CGFloat buttonWidth = (UIScreen.mainScreen.bounds.size.width - _separatorWidth * 2 - 4 * _separatorWidth) / 5;
|
|
88
89
|
CGFloat buttonHeight = (240 - _separatorWidth * 2 - 3 * _separatorWidth) / 4;
|
|
@@ -143,7 +144,7 @@
|
|
|
143
144
|
|
|
144
145
|
if ([_specialKeys containsObject:key] || isMainKey) {
|
|
145
146
|
[button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
|
|
146
|
-
button.backgroundColor = [
|
|
147
|
+
button.backgroundColor = [Utils colorFromHex:@"#d8d8d8"];
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
if (isMainKey) {
|
|
@@ -181,51 +182,93 @@
|
|
|
181
182
|
self.customKeyButton.enabled = YES;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
self.customKeyButton.backgroundColor = [
|
|
185
|
-
[self.customKeyButton setTitleColor:[
|
|
185
|
+
self.customKeyButton.backgroundColor = [Utils colorFromHex:_customKeyBackground];
|
|
186
|
+
[self.customKeyButton setTitleColor:[Utils colorFromHex:_customKeyTextColor] forState:UIControlStateNormal];
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
- (void)keyPressed:(UIButton *)sender
|
|
189
190
|
{
|
|
190
191
|
NSString *key = sender.accessibilityIdentifier;
|
|
191
|
-
if (!key)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
BOOL isCustomKeyCTA = (sender.tag == 1);
|
|
196
|
-
if (isCustomKeyCTA) {
|
|
192
|
+
if (!key) return;
|
|
193
|
+
|
|
194
|
+
if (sender.tag == 1) {
|
|
197
195
|
[self.input emitCustomKey];
|
|
198
196
|
return;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
[self.input emitKeyPress:key];
|
|
202
|
-
|
|
200
|
+
|
|
201
|
+
NSString *text = self.input.getText ?: @"";
|
|
202
|
+
BOOL isEmpty = (text.length == 0);
|
|
203
|
+
BOOL isJustZero = [text isEqualToString:@"0"];
|
|
204
|
+
|
|
205
|
+
BOOL endsWithPlus = [text hasSuffix:@" + "];
|
|
206
|
+
BOOL endsWithMinus = [text hasSuffix:@" - "];
|
|
207
|
+
BOOL endsWithMul = [text hasSuffix:@" × "];
|
|
208
|
+
BOOL endsWithDiv = [text hasSuffix:@" ÷ "];
|
|
209
|
+
BOOL endsWithOperator = (endsWithPlus || endsWithMinus || endsWithMul || endsWithDiv);
|
|
210
|
+
|
|
211
|
+
BOOL endsWithSingleZeroAfterOp = [text hasSuffix:@" 0"];
|
|
212
|
+
|
|
213
|
+
BOOL (^isOpKey)(NSString *) = ^BOOL(NSString *k) {
|
|
214
|
+
return [k isEqualToString:@"+"] || [k isEqualToString:@"-"] ||
|
|
215
|
+
[k isEqualToString:@"×"] || [k isEqualToString:@"÷"];
|
|
216
|
+
};
|
|
217
|
+
void (^insert)(NSString *) = ^(NSString *s) { [self.input keyDidPress:s]; };
|
|
218
|
+
void (^replaceTrailingZeroWith)(NSString *) = ^(NSString *s) {
|
|
219
|
+
[self.input onBackSpace];
|
|
220
|
+
[self.input keyDidPress:s];
|
|
221
|
+
};
|
|
222
|
+
void (^replaceAllWith)(NSString *) = ^(NSString *s) {
|
|
223
|
+
[self.input clearText];
|
|
224
|
+
[self.input keyDidPress:s];
|
|
225
|
+
};
|
|
226
|
+
|
|
203
227
|
if ([key isEqualToString:@"AC"]) {
|
|
204
228
|
[self.input clearText];
|
|
205
|
-
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ([key isEqualToString:@"back"]) {
|
|
206
233
|
[self.input onBackSpace];
|
|
207
|
-
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if ([key isEqualToString:@"="]) {
|
|
208
238
|
[self.input calculateResult];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (isOpKey(key)) {
|
|
243
|
+
if (!isEmpty) {
|
|
244
|
+
insert([NSString stringWithFormat:@" %@ ", key]);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if ([key isEqualToString:@"0"] || [key isEqualToString:@"000"]) {
|
|
250
|
+
if (isEmpty) return;
|
|
251
|
+
|
|
252
|
+
if ([key isEqualToString:@"000"] && endsWithOperator) {
|
|
253
|
+
insert(@"0");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (endsWithSingleZeroAfterOp || isJustZero) return;
|
|
258
|
+
|
|
259
|
+
insert(key);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isJustZero) {
|
|
264
|
+
replaceAllWith(key);
|
|
265
|
+
} else if (endsWithSingleZeroAfterOp) {
|
|
266
|
+
replaceTrailingZeroWith(key);
|
|
212
267
|
} else {
|
|
213
|
-
|
|
268
|
+
insert(key);
|
|
214
269
|
}
|
|
215
270
|
}
|
|
216
271
|
|
|
217
|
-
- (UIColor *)colorFromHex:(NSString *)hexString
|
|
218
|
-
{
|
|
219
|
-
unsigned rgbValue = 0;
|
|
220
|
-
NSString *cleanHex = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""];
|
|
221
|
-
NSScanner *scanner = [NSScanner scannerWithString:cleanHex];
|
|
222
|
-
[scanner scanHexInt:&rgbValue];
|
|
223
|
-
|
|
224
|
-
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0
|
|
225
|
-
green:((rgbValue & 0x00FF00) >> 8) / 255.0
|
|
226
|
-
blue:(rgbValue & 0x0000FF) / 255.0
|
|
227
|
-
alpha:1.0];
|
|
228
|
-
}
|
|
229
272
|
|
|
230
273
|
@end
|
|
231
274
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
#import <react/renderer/components/CalculatorKeyboardSpecs/Props.h>
|
|
9
9
|
#import <react/renderer/components/CalculatorKeyboardSpecs/RCTComponentViewHelpers.h>
|
|
10
10
|
|
|
11
|
+
#import "Utils.h"
|
|
12
|
+
|
|
11
13
|
using namespace facebook::react;
|
|
12
14
|
|
|
13
15
|
@interface NativeInputCalculator () <RCTNativeInputCalculatorViewProtocol>
|
|
@@ -38,12 +40,37 @@ using namespace facebook::react;
|
|
|
38
40
|
|
|
39
41
|
// Set custom keyboard
|
|
40
42
|
_textField.inputView = _keyboardView;
|
|
41
|
-
|
|
43
|
+
[self startObservingFocusNotifications];
|
|
44
|
+
|
|
42
45
|
[self addSubview:_textField];
|
|
43
46
|
}
|
|
44
47
|
return self;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
- (void)startObservingFocusNotifications
|
|
51
|
+
{
|
|
52
|
+
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
53
|
+
[nc addObserver:self selector:@selector(_didBeginEditing:)
|
|
54
|
+
name:UITextFieldTextDidBeginEditingNotification object:_textField];
|
|
55
|
+
[nc addObserver:self selector:@selector(_didEndEditing:)
|
|
56
|
+
name:UITextFieldTextDidEndEditingNotification object:_textField];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)_didBeginEditing:(NSNotification *)notification
|
|
60
|
+
{
|
|
61
|
+
[self emitFocus];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)_didEndEditing:(NSNotification *)notification
|
|
65
|
+
{
|
|
66
|
+
[self emitBlur];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)dealloc
|
|
70
|
+
{
|
|
71
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
- (CGFloat)getBottomInset
|
|
48
75
|
{
|
|
49
76
|
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
|
@@ -68,6 +95,8 @@ using namespace facebook::react;
|
|
|
68
95
|
if (![_lastValue isEqualToString:newValue]) {
|
|
69
96
|
_textField.text = newValue;
|
|
70
97
|
_lastValue = newValue;
|
|
98
|
+
[self reformatAndKeepSelection];
|
|
99
|
+
[self notifyTextChange];
|
|
71
100
|
}
|
|
72
101
|
}
|
|
73
102
|
|
|
@@ -102,6 +131,14 @@ using namespace facebook::react;
|
|
|
102
131
|
CGFloat size = _textField.font ? _textField.font.pointSize : UIFont.systemFontSize;
|
|
103
132
|
_textField.font = [UIFont systemFontOfSize:size weight:weight];
|
|
104
133
|
}
|
|
134
|
+
|
|
135
|
+
if (oldViewProps.textAttributes.placeholder != newViewProps.textAttributes.placeholder
|
|
136
|
+
|| oldViewProps.textAttributes.placeholderTextColor != newViewProps.textAttributes.placeholderTextColor) {
|
|
137
|
+
_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:RCTNSStringFromString(newViewProps.textAttributes.placeholder)
|
|
138
|
+
attributes:@{
|
|
139
|
+
NSForegroundColorAttributeName: [Utils colorFromHex:RCTNSStringFromString(newViewProps.textAttributes.placeholderTextColor)]
|
|
140
|
+
}];
|
|
141
|
+
}
|
|
105
142
|
|
|
106
143
|
[super updateProps:props oldProps:oldProps];
|
|
107
144
|
}
|
|
@@ -143,11 +180,35 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
143
180
|
|
|
144
181
|
- (void)keyDidPress:(NSString *)key {
|
|
145
182
|
UITextRange *sel = _textField.selectedTextRange;
|
|
146
|
-
if (sel)
|
|
183
|
+
if (!sel) return;
|
|
184
|
+
|
|
185
|
+
NSArray<NSString *> *ops = @[ @" + ", @" - ", @" × ", @" ÷ " ];
|
|
186
|
+
BOOL isOperatorKey = [ops containsObject:key];
|
|
187
|
+
|
|
188
|
+
if (!sel.isEmpty) {
|
|
147
189
|
[_textField replaceRange:sel withText:key];
|
|
148
190
|
} else {
|
|
149
|
-
[_textField
|
|
191
|
+
NSInteger caret = [_textField offsetFromPosition:_textField.beginningOfDocument
|
|
192
|
+
toPosition:sel.start];
|
|
193
|
+
NSString *text = _textField.text ?: @"";
|
|
194
|
+
|
|
195
|
+
if (isOperatorKey && caret >= 3 && caret <= (NSInteger)text.length) {
|
|
196
|
+
NSString *last3 = [text substringWithRange:NSMakeRange(caret - 3, 3)];
|
|
197
|
+
if ([ops containsObject:last3]) {
|
|
198
|
+
UITextPosition *start = [_textField positionFromPosition:_textField.beginningOfDocument
|
|
199
|
+
offset:(caret - 3)];
|
|
200
|
+
UITextPosition *end = [_textField positionFromPosition:_textField.beginningOfDocument
|
|
201
|
+
offset:caret];
|
|
202
|
+
UITextRange *range = [_textField textRangeFromPosition:start toPosition:end];
|
|
203
|
+
[_textField replaceRange:range withText:key];
|
|
204
|
+
} else {
|
|
205
|
+
[_textField insertText:key];
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
[_textField insertText:key];
|
|
209
|
+
}
|
|
150
210
|
}
|
|
211
|
+
|
|
151
212
|
[self reformatAndKeepSelection];
|
|
152
213
|
[self notifyTextChange];
|
|
153
214
|
}
|
|
@@ -158,6 +219,11 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
158
219
|
[self notifyTextChange];
|
|
159
220
|
}
|
|
160
221
|
|
|
222
|
+
- (NSString *)getText
|
|
223
|
+
{
|
|
224
|
+
return _textField.text;
|
|
225
|
+
}
|
|
226
|
+
|
|
161
227
|
- (void)onBackSpace {
|
|
162
228
|
NSString *formatted = _textField.text ?: @"";
|
|
163
229
|
if (formatted.length == 0) return;
|
|
@@ -170,13 +236,44 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
170
236
|
NSInteger caretRaw = [self formattedCaretToRaw:formatted caret:caretFmt];
|
|
171
237
|
if (caretRaw <= 0) return;
|
|
172
238
|
|
|
239
|
+
NSArray<NSString *> *ops = @[ @" + ", @" - ", @" × ", @" ÷ " ];
|
|
240
|
+
NSInteger len = (NSInteger)rawBefore.length;
|
|
241
|
+
|
|
242
|
+
NSInteger delStart = MAX(caretRaw - 1, 0);
|
|
243
|
+
NSInteger delLen = 1;
|
|
244
|
+
|
|
245
|
+
BOOL (^matches)(NSInteger, NSInteger) = ^BOOL(NSInteger start, NSInteger end) {
|
|
246
|
+
if (start < 0 || end > len) return NO;
|
|
247
|
+
if (end - start != 3) return NO;
|
|
248
|
+
NSString *sub = [rawBefore substringWithRange:NSMakeRange(start, 3)];
|
|
249
|
+
for (NSString *op in ops) {
|
|
250
|
+
if ([sub isEqualToString:op]) return YES;
|
|
251
|
+
}
|
|
252
|
+
return NO;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (matches(caretRaw - 3, caretRaw)) {
|
|
256
|
+
delStart = caretRaw - 3;
|
|
257
|
+
delLen = 3;
|
|
258
|
+
}
|
|
259
|
+
else if (matches(caretRaw - 2, caretRaw + 1)) {
|
|
260
|
+
delStart = caretRaw - 2;
|
|
261
|
+
delLen = 3;
|
|
262
|
+
}
|
|
263
|
+
else if (matches(caretRaw - 1, caretRaw + 2)) {
|
|
264
|
+
delStart = caretRaw - 1;
|
|
265
|
+
delLen = 3;
|
|
266
|
+
}
|
|
267
|
+
|
|
173
268
|
NSMutableString *rawAfter = [rawBefore mutableCopy];
|
|
174
|
-
|
|
269
|
+
if (delStart >= 0 && delStart + delLen <= (NSInteger)rawAfter.length) {
|
|
270
|
+
[rawAfter deleteCharactersInRange:NSMakeRange(delStart, delLen)];
|
|
271
|
+
}
|
|
175
272
|
|
|
176
273
|
NSString *formattedAfter = [self formatNumberGroups:rawAfter];
|
|
177
274
|
_textField.text = formattedAfter;
|
|
178
275
|
|
|
179
|
-
NSInteger newCaretFmt = [self rawCaretToFormatted:
|
|
276
|
+
NSInteger newCaretFmt = [self rawCaretToFormatted:delStart inFormatted:formattedAfter];
|
|
180
277
|
UITextPosition *pos = [_textField positionFromPosition:_textField.beginningOfDocument offset:newCaretFmt];
|
|
181
278
|
if (pos) {
|
|
182
279
|
_textField.selectedTextRange = [_textField textRangeFromPosition:pos toPosition:pos];
|
|
@@ -185,8 +282,9 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
185
282
|
[self notifyTextChange];
|
|
186
283
|
}
|
|
187
284
|
|
|
188
|
-
- (
|
|
285
|
+
- (NSString *)calculate {
|
|
189
286
|
NSString *text = _textField.text ?: @"";
|
|
287
|
+
NSString *invalid = @"invalid";
|
|
190
288
|
|
|
191
289
|
text = [self stripGroupDots:text];
|
|
192
290
|
text = [text stringByReplacingOccurrencesOfString:@"×" withString:@"*"];
|
|
@@ -196,23 +294,29 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
196
294
|
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
|
|
197
295
|
if (![regex firstMatchInString:text options:0 range:NSMakeRange(0, text.length)]) {
|
|
198
296
|
NSLog(@"Invalid expression");
|
|
199
|
-
return;
|
|
297
|
+
return invalid;
|
|
200
298
|
}
|
|
201
299
|
|
|
202
300
|
@try {
|
|
203
301
|
NSExpression *expr = [NSExpression expressionWithFormat:text];
|
|
204
302
|
id val = [expr expressionValueWithObject:nil context:nil];
|
|
205
303
|
if ([val isKindOfClass:[NSNumber class]]) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
304
|
+
return [(NSNumber *)val stringValue];
|
|
305
|
+
}
|
|
306
|
+
} @catch (__unused NSException *e) { return invalid; }
|
|
307
|
+
return invalid;
|
|
308
|
+
}
|
|
209
309
|
|
|
210
|
-
|
|
211
|
-
|
|
310
|
+
- (void)calculateResult {
|
|
311
|
+
NSString *result = [self calculate];
|
|
312
|
+
if ([result isEqual: @"invalid"]) return;
|
|
313
|
+
NSString *formatted = [self formatNumberGroups:result];
|
|
314
|
+
_textField.text = formatted;
|
|
212
315
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
316
|
+
UITextPosition *end = _textField.endOfDocument;
|
|
317
|
+
_textField.selectedTextRange = [_textField textRangeFromPosition:end toPosition:end];
|
|
318
|
+
|
|
319
|
+
[self notifyTextChange];
|
|
216
320
|
}
|
|
217
321
|
|
|
218
322
|
- (void)emitCustomKey
|
|
@@ -224,6 +328,26 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
224
328
|
}
|
|
225
329
|
}
|
|
226
330
|
|
|
331
|
+
- (void)emitFocus
|
|
332
|
+
{
|
|
333
|
+
printf("emitFocus.");
|
|
334
|
+
if (_eventEmitter) {
|
|
335
|
+
auto emitter = std::static_pointer_cast<const NativeInputCalculatorEventEmitter>(_eventEmitter);
|
|
336
|
+
NativeInputCalculatorEventEmitter::OnFocus event{};
|
|
337
|
+
emitter->onFocus(event);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
- (void)emitBlur
|
|
342
|
+
{
|
|
343
|
+
printf("emitBlur.");
|
|
344
|
+
if (_eventEmitter) {
|
|
345
|
+
auto emitter = std::static_pointer_cast<const NativeInputCalculatorEventEmitter>(_eventEmitter);
|
|
346
|
+
NativeInputCalculatorEventEmitter::OnBlur event{};
|
|
347
|
+
emitter->onBlur(event);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
227
351
|
- (void)emitKeyPress:(NSString *)key
|
|
228
352
|
{
|
|
229
353
|
if (_eventEmitter) {
|
|
@@ -245,6 +369,13 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
|
|
|
245
369
|
.text = std::string([_textField.text UTF8String])
|
|
246
370
|
};
|
|
247
371
|
emitter->onChange(event);
|
|
372
|
+
|
|
373
|
+
NSString *result = [self calculate];
|
|
374
|
+
NativeInputCalculatorEventEmitter::OnResult event1{
|
|
375
|
+
.result = std::string([result UTF8String])
|
|
376
|
+
};
|
|
377
|
+
emitter->onResult(event1);
|
|
378
|
+
|
|
248
379
|
}
|
|
249
380
|
}
|
|
250
381
|
|
package/ios/Utils.h
ADDED
package/ios/Utils.mm
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Utils.mm
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Phuc on 21/10/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <UIKit/UIKit.h>
|
|
9
|
+
#import <Utils.h>
|
|
10
|
+
|
|
11
|
+
@implementation Utils
|
|
12
|
+
|
|
13
|
+
+ (UIColor *)colorFromHex:(NSString *)hexString
|
|
14
|
+
{
|
|
15
|
+
unsigned rgbValue = 0;
|
|
16
|
+
NSString *cleanHex = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""];
|
|
17
|
+
NSScanner *scanner = [NSScanner scannerWithString:cleanHex];
|
|
18
|
+
[scanner scanHexInt:&rgbValue];
|
|
19
|
+
|
|
20
|
+
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0
|
|
21
|
+
green:((rgbValue & 0x00FF00) >> 8) / 255.0
|
|
22
|
+
blue:(rgbValue & 0x0000FF) / 255.0
|
|
23
|
+
alpha:1.0];
|
|
24
|
+
}
|
|
25
|
+
@end
|
package/package.json
CHANGED
|
@@ -26,9 +26,15 @@ export type OnChangeEvent = Readonly<{
|
|
|
26
26
|
text: string;
|
|
27
27
|
}>;
|
|
28
28
|
|
|
29
|
+
export type OnResultEvent = Readonly<{
|
|
30
|
+
result: string;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
29
33
|
export type TextAttributes = Readonly<{
|
|
30
|
-
fontSize?: CodegenTypes.Float;
|
|
31
|
-
fontWeight?: string;
|
|
34
|
+
fontSize?: CodegenTypes.Float | undefined;
|
|
35
|
+
fontWeight?: string | undefined;
|
|
36
|
+
placeholder?: string | undefined;
|
|
37
|
+
placeholderTextColor?: string | undefined;
|
|
32
38
|
}>;
|
|
33
39
|
|
|
34
40
|
export interface NativeInputCalculatorProps extends ViewProps {
|
|
@@ -41,6 +47,9 @@ export interface NativeInputCalculatorProps extends ViewProps {
|
|
|
41
47
|
customKeyState?: string;
|
|
42
48
|
onChange?: CodegenTypes.BubblingEventHandler<OnChangeEvent>;
|
|
43
49
|
onKeyPress?: CodegenTypes.BubblingEventHandler<OnKeyPressEvent>;
|
|
50
|
+
onResult?: CodegenTypes.BubblingEventHandler<OnResultEvent>;
|
|
51
|
+
onBlur?: CodegenTypes.BubblingEventHandler<{}>;
|
|
52
|
+
onFocus?: CodegenTypes.BubblingEventHandler<{}>;
|
|
44
53
|
onCustomKeyEvent?: CodegenTypes.DirectEventHandler<{}>;
|
|
45
54
|
}
|
|
46
55
|
|
package/src/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TextStyle,
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
import NativeInputCalculator, {
|
|
12
|
+
OnResultEvent,
|
|
12
13
|
TextAttributes,
|
|
13
14
|
} from './InputCalculatorNativeComponent';
|
|
14
15
|
import { Commands } from './InputCalculatorNativeComponent';
|
|
@@ -20,12 +21,17 @@ type KeyPressEvent = { nativeEvent: { key: string } };
|
|
|
20
21
|
interface InputCalculatorProps extends TextInputProps {
|
|
21
22
|
text?: string | undefined;
|
|
22
23
|
mode?: Mode;
|
|
24
|
+
onBlur?: () => void;
|
|
25
|
+
onFocus?: () => void;
|
|
26
|
+
onResult?: (e: OnResultEvent) => void;
|
|
23
27
|
onKeyPress?: (e: KeyPressEvent) => void;
|
|
24
28
|
customKeyText?: string | undefined;
|
|
25
29
|
customKeyBackground?: CustomKeyBackground;
|
|
26
30
|
customKeyState?: CustomKeyState;
|
|
27
31
|
onCustomKeyEvent?: () => void;
|
|
28
32
|
style?: StyleProp<TextStyle>;
|
|
33
|
+
placeholder?: string | undefined;
|
|
34
|
+
placeholderTextColor?: string | undefined;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export type CustomKeyBackground = 'primary' | 'default' | string;
|
|
@@ -54,13 +60,17 @@ function isEmpty(obj: object | undefined | null) {
|
|
|
54
60
|
return !obj || Object.keys(obj).length === 0;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
function styleToTextAttributes(
|
|
58
64
|
style?: StyleProp<TextStyle>,
|
|
65
|
+
placeholder?: string,
|
|
66
|
+
placeholderTextColor?: string,
|
|
59
67
|
): Readonly<TextAttributes> {
|
|
60
68
|
const s = StyleSheet.flatten(style) ?? {};
|
|
61
69
|
const attrs: TextAttributes = {
|
|
62
70
|
fontSize: s.fontSize ?? undefined,
|
|
63
71
|
fontWeight: (s.fontWeight as TextAttributes['fontWeight']) ?? undefined,
|
|
72
|
+
placeholder,
|
|
73
|
+
placeholderTextColor,
|
|
64
74
|
};
|
|
65
75
|
return Object.freeze(omitUndefined(attrs));
|
|
66
76
|
}
|
|
@@ -74,6 +84,9 @@ const InputCalculator = React.forwardRef<
|
|
|
74
84
|
customKeyBackground = 'default',
|
|
75
85
|
mode = Mode.NumDefault,
|
|
76
86
|
customKeyText,
|
|
87
|
+
onBlur,
|
|
88
|
+
onFocus,
|
|
89
|
+
onResult,
|
|
77
90
|
onKeyPress,
|
|
78
91
|
customKeyState = CustomKeyState.Default,
|
|
79
92
|
onCustomKeyEvent,
|
|
@@ -117,8 +130,8 @@ const InputCalculator = React.forwardRef<
|
|
|
117
130
|
}));
|
|
118
131
|
|
|
119
132
|
const derivedTA = React.useMemo(
|
|
120
|
-
() => styleToTextAttributes(style),
|
|
121
|
-
[style],
|
|
133
|
+
() => styleToTextAttributes(style, props.placeholder, props.placeholderTextColor),
|
|
134
|
+
[style, props.placeholder, props.placeholderTextColor],
|
|
122
135
|
);
|
|
123
136
|
|
|
124
137
|
return (
|
|
@@ -128,6 +141,9 @@ const InputCalculator = React.forwardRef<
|
|
|
128
141
|
textAttributes={isEmpty(derivedTA) ? undefined : derivedTA}
|
|
129
142
|
ref={nativeRef}
|
|
130
143
|
onChange={_onChange}
|
|
144
|
+
onBlur={onBlur}
|
|
145
|
+
onFocus={onFocus}
|
|
146
|
+
onResult={onResult}
|
|
131
147
|
onKeyPress={onKeyPress}
|
|
132
148
|
value={text}
|
|
133
149
|
mode={mode}
|