@momo-kits/calculator-keyboard 0.150.2-beta.28 → 0.150.2-beta.30

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.
@@ -184,6 +184,8 @@ class CustomKeyboardView(
184
184
  "×", "+", "-", "÷" -> keyDidPress(" $key ")
185
185
  else -> editText.text?.insert(editText.selectionStart, key)
186
186
  }
187
+
188
+ reformatAndKeepSelection(editText)
187
189
  }
188
190
 
189
191
  private fun keyDidPress(key: String) {
@@ -196,26 +198,39 @@ class CustomKeyboardView(
196
198
  }
197
199
 
198
200
  private fun onBackSpace() {
199
- val start = editText.selectionStart
200
- val end = editText.selectionEnd
201
- if (start > 0) {
202
- val newText = end.let { editText.text?.replaceRange(start - 1, it, "") }
203
- editText.setText(newText)
204
- 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)
205
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))
206
218
  }
207
219
 
220
+
208
221
  private fun calculateResult() {
209
- val text = editText?.text.toString().replace("×", "*").replace("÷", "/")
222
+ val raw = editText.text?.toString().orEmpty()
223
+ val normalized = raw.replace(".", "")
224
+ .replace("×", "*")
225
+ .replace("÷", "/")
226
+
210
227
  val pattern = "^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$"
211
- val regex = Regex(pattern)
212
- if (regex.matches(text)) {
228
+ if (Regex(pattern).matches(normalized)) {
213
229
  try {
214
- val result = eval(text).toString()
215
- editText.setTextKeepState(result)
216
- } catch (e: Exception) {
217
- e.printStackTrace()
218
- }
230
+ val result = eval(normalized)?.toString() ?: return
231
+ editText.setTextKeepState(formatNumberGroups(result))
232
+ editText.setSelection(editText.text?.length ?: 0)
233
+ } catch (_: Exception) { /* ignore */ }
219
234
  } else {
220
235
  println("Invalid expression")
221
236
  }
@@ -289,4 +304,74 @@ class CustomKeyboardView(
289
304
  customKeyButton?.setTextColor(textColor)
290
305
  }
291
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
+ }
292
377
  }
@@ -63,7 +63,6 @@ using namespace facebook::react;
63
63
  const auto &oldViewProps = *std::static_pointer_cast<const NativeInputCalculatorProps>(_props);
64
64
  const auto &newViewProps = *std::static_pointer_cast<const NativeInputCalculatorProps>(props);
65
65
 
66
- // Update value
67
66
  if (oldViewProps.value != newViewProps.value) {
68
67
  NSString *newValue = RCTNSStringFromString(newViewProps.value);
69
68
  if (![_lastValue isEqualToString:newValue]) {
@@ -72,33 +71,26 @@ using namespace facebook::react;
72
71
  }
73
72
  }
74
73
 
75
- // Update mode
76
74
  if (oldViewProps.mode != newViewProps.mode) {
77
75
  _keyboardView.keyboardMode = RCTNSStringFromString(newViewProps.mode);
78
76
  }
79
77
 
80
- // Update customKeyText
81
78
  if (oldViewProps.customKeyText != newViewProps.customKeyText) {
82
79
  _keyboardView.customKeyText = RCTNSStringFromString(newViewProps.customKeyText);
83
80
  }
84
81
 
85
- // Update customKeyBackground
86
82
  if (oldViewProps.customKeyBackground != newViewProps.customKeyBackground) {
87
83
  _keyboardView.customKeyBackground = RCTNSStringFromString(newViewProps.customKeyBackground);
88
84
  }
89
85
 
90
- // Update customKeyTextColor
91
86
  if (oldViewProps.customKeyTextColor != newViewProps.customKeyTextColor) {
92
87
  _keyboardView.customKeyTextColor = RCTNSStringFromString(newViewProps.customKeyTextColor);
93
88
  }
94
89
 
95
- // Update customKeyState
96
90
  if (oldViewProps.customKeyState != newViewProps.customKeyState) {
97
91
  _keyboardView.customKeyState = RCTNSStringFromString(newViewProps.customKeyState);
98
92
  }
99
93
 
100
- // ===== textAttributes (fontSize, fontWeight) =====
101
- // Codegen should have a prop like: std::optional<folly::dynamic> textAttributes;
102
94
  if (oldViewProps.textAttributes.fontSize != newViewProps.textAttributes.fontSize > 0.0f) {
103
95
  CGFloat newSize = (CGFloat)newViewProps.textAttributes.fontSize;
104
96
  UIFont *current = _textField.font ?: [UIFont systemFontOfSize:UIFont.systemFontSize];
@@ -149,9 +141,14 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
149
141
 
150
142
  #pragma mark - Keyboard callbacks (called by CalculatorKeyboardView)
151
143
 
152
- - (void)keyDidPress:(NSString *)key
153
- {
154
- [_textField insertText:key];
144
+ - (void)keyDidPress:(NSString *)key {
145
+ UITextRange *sel = _textField.selectedTextRange;
146
+ if (sel) {
147
+ [_textField replaceRange:sel withText:key];
148
+ } else {
149
+ [_textField insertText:key];
150
+ }
151
+ [self reformatAndKeepSelection];
155
152
  [self notifyTextChange];
156
153
  }
157
154
 
@@ -161,31 +158,61 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
161
158
  [self notifyTextChange];
162
159
  }
163
160
 
164
- - (void)onBackSpace
165
- {
166
- if (_textField.text.length > 0) {
167
- _textField.text = [_textField.text substringToIndex:_textField.text.length - 1];
168
- [self notifyTextChange];
161
+ - (void)onBackSpace {
162
+ NSString *formatted = _textField.text ?: @"";
163
+ if (formatted.length == 0) return;
164
+
165
+ UITextRange *selRange = _textField.selectedTextRange;
166
+ NSInteger caretFmt = (NSInteger)[_textField offsetFromPosition:_textField.beginningOfDocument
167
+ toPosition:selRange.start];
168
+
169
+ NSString *rawBefore = [self stripGroupDots:formatted];
170
+ NSInteger caretRaw = [self formattedCaretToRaw:formatted caret:caretFmt];
171
+ if (caretRaw <= 0) return;
172
+
173
+ NSMutableString *rawAfter = [rawBefore mutableCopy];
174
+ [rawAfter deleteCharactersInRange:NSMakeRange(caretRaw - 1, 1)];
175
+
176
+ NSString *formattedAfter = [self formatNumberGroups:rawAfter];
177
+ _textField.text = formattedAfter;
178
+
179
+ NSInteger newCaretFmt = [self rawCaretToFormatted:(caretRaw - 1) inFormatted:formattedAfter];
180
+ UITextPosition *pos = [_textField positionFromPosition:_textField.beginningOfDocument offset:newCaretFmt];
181
+ if (pos) {
182
+ _textField.selectedTextRange = [_textField textRangeFromPosition:pos toPosition:pos];
169
183
  }
184
+
185
+ [self notifyTextChange];
170
186
  }
171
187
 
172
- - (void)calculateResult
173
- {
174
- NSString *text = [_textField.text stringByReplacingOccurrencesOfString:@"×" withString:@"*"];
188
+ - (void)calculateResult {
189
+ NSString *text = _textField.text ?: @"";
190
+
191
+ text = [self stripGroupDots:text];
192
+ text = [text stringByReplacingOccurrencesOfString:@"×" withString:@"*"];
175
193
  text = [text stringByReplacingOccurrencesOfString:@"÷" withString:@"/"];
176
194
 
177
195
  NSString *pattern = @"^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$";
178
196
  NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
179
- NSRange range = NSMakeRange(0, text.length);
197
+ if (![regex firstMatchInString:text options:0 range:NSMakeRange(0, text.length)]) {
198
+ NSLog(@"Invalid expression");
199
+ return;
200
+ }
201
+
202
+ @try {
203
+ NSExpression *expr = [NSExpression expressionWithFormat:text];
204
+ id val = [expr expressionValueWithObject:nil context:nil];
205
+ if ([val isKindOfClass:[NSNumber class]]) {
206
+ NSString *result = [(NSNumber *)val stringValue];
207
+ NSString *formatted = [self formatNumberGroups:result];
208
+ _textField.text = formatted;
209
+
210
+ UITextPosition *end = _textField.endOfDocument;
211
+ _textField.selectedTextRange = [_textField textRangeFromPosition:end toPosition:end];
180
212
 
181
- if ([regex firstMatchInString:text options:0 range:range]) {
182
- NSExpression *expression = [NSExpression expressionWithFormat:text];
183
- id result = [expression expressionValueWithObject:nil context:nil];
184
- if ([result isKindOfClass:[NSNumber class]]) {
185
- _textField.text = [result stringValue];
186
213
  [self notifyTextChange];
187
214
  }
188
- }
215
+ } @catch (__unused NSException *e) { }
189
216
  }
190
217
 
191
218
  - (void)emitCustomKey
@@ -221,6 +248,118 @@ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
221
248
  }
222
249
  }
223
250
 
251
+ #pragma mark - Thousand grouping helpers (strip/format + caret mapping)
252
+
253
+ - (BOOL)isGroupDotAt:(NSInteger)i inString:(NSString *)s {
254
+ if (i < 0 || i >= (NSInteger)s.length) return NO;
255
+ unichar c = [s characterAtIndex:i];
256
+ if (c != '.') return NO;
257
+ BOOL leftIsDigit = (i - 1 >= 0) && [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i-1]];
258
+ BOOL rightIsDigit = (i + 1 < (NSInteger)s.length) && [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i+1]];
259
+ return leftIsDigit && rightIsDigit;
260
+ }
261
+
262
+ - (NSString *)stripGroupDots:(NSString *)input {
263
+ if (input.length == 0) return input;
264
+ NSMutableString *out = [NSMutableString stringWithCapacity:input.length];
265
+ for (NSInteger i = 0; i < (NSInteger)input.length; i++) {
266
+ unichar c = [input characterAtIndex:i];
267
+ if (c == '.' && [self isGroupDotAt:i inString:input]) {
268
+ // skip
269
+ } else {
270
+ [out appendFormat:@"%C", c];
271
+ }
272
+ }
273
+ return out;
274
+ }
275
+
276
+ - (NSString *)formatNumberGroups:(NSString *)input {
277
+ NSString *noSep = [self stripGroupDots:input];
278
+ if (noSep.length == 0) return noSep;
279
+
280
+ NSError *err = nil;
281
+ NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:@"\\d+" options:0 error:&err];
282
+ if (err) return noSep;
283
+
284
+ NSMutableString *result = [noSep mutableCopy];
285
+ __block NSInteger delta = 0;
286
+ [re enumerateMatchesInString:noSep options:0 range:NSMakeRange(0, noSep.length) usingBlock:^(NSTextCheckingResult * _Nullable match, NSMatchingFlags flags, BOOL * _Nonnull stop) {
287
+ if (!match) return;
288
+ NSRange mr = match.range;
289
+ mr.location += delta;
290
+
291
+ NSString *digits = [result substringWithRange:mr];
292
+
293
+ NSMutableString *rev = [NSMutableString stringWithCapacity:digits.length];
294
+ for (NSInteger i = digits.length - 1; i >= 0; i--) {
295
+ [rev appendFormat:@"%C", [digits characterAtIndex:i]];
296
+ }
297
+
298
+ NSMutableArray<NSString *> *chunks = [NSMutableArray array];
299
+ for (NSInteger i = 0; i < (NSInteger)rev.length; i += 3) {
300
+ NSRange r = NSMakeRange(i, MIN(3, (NSInteger)rev.length - i));
301
+ [chunks addObject:[rev substringWithRange:r]];
302
+ }
303
+ NSString *joined = [chunks componentsJoinedByString:@"."];
304
+
305
+ NSMutableString *final = [NSMutableString stringWithCapacity:joined.length];
306
+ for (NSInteger i = joined.length - 1; i >= 0; i--) {
307
+ [final appendFormat:@"%C", [joined characterAtIndex:i]];
308
+ }
309
+
310
+ [result replaceCharactersInRange:mr withString:final];
311
+ delta += (final.length - mr.length);
312
+ }];
313
+
314
+ return result;
315
+ }
316
+
317
+ - (NSInteger)formattedCaretToRaw:(NSString *)formatted caret:(NSInteger)caretFmt {
318
+ NSInteger rawIdx = 0;
319
+ NSInteger limit = MIN(caretFmt, (NSInteger)formatted.length);
320
+ for (NSInteger i = 0; i < limit; i++) {
321
+ unichar c = [formatted characterAtIndex:i];
322
+ if (!(c == '.' && [self isGroupDotAt:i inString:formatted])) {
323
+ rawIdx++;
324
+ }
325
+ }
326
+ return rawIdx;
327
+ }
328
+
329
+ - (NSInteger)rawCaretToFormatted:(NSInteger)rawCaret inFormatted:(NSString *)formatted {
330
+ NSInteger rawSeen = 0;
331
+ for (NSInteger i = 0; i < (NSInteger)formatted.length; i++) {
332
+ unichar c = [formatted characterAtIndex:i];
333
+ if (!(c == '.' && [self isGroupDotAt:i inString:formatted])) {
334
+ if (rawSeen == rawCaret) return i;
335
+ rawSeen++;
336
+ }
337
+ }
338
+ return (NSInteger)formatted.length;
339
+ }
340
+
341
+ - (void)reformatAndKeepSelection {
342
+ NSString *formattedBefore = _textField.text ?: @"";
343
+
344
+ UITextRange *selRange = _textField.selectedTextRange;
345
+ NSInteger caretFmtBefore = (NSInteger)[_textField offsetFromPosition:_textField.beginningOfDocument
346
+ toPosition:selRange.start];
347
+
348
+ NSInteger caretRaw = [self formattedCaretToRaw:formattedBefore caret:caretFmtBefore];
349
+ NSString *formattedAfter = [self formatNumberGroups:formattedBefore];
350
+
351
+ if (![formattedAfter isEqualToString:formattedBefore]) {
352
+ _textField.text = formattedAfter;
353
+ }
354
+
355
+ NSInteger caretFmtAfter = [self rawCaretToFormatted:caretRaw inFormatted:formattedAfter];
356
+ UITextPosition *pos = [_textField positionFromPosition:_textField.beginningOfDocument offset:caretFmtAfter];
357
+ if (pos) {
358
+ _textField.selectedTextRange = [_textField textRangeFromPosition:pos toPosition:pos];
359
+ }
360
+ }
361
+
362
+
224
363
  @end
225
364
 
226
365
  Class<RCTNativeInputCalculatorViewProtocol> NativeInputCalculatorCls(void)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/calculator-keyboard",
3
- "version": "0.150.2-beta.28",
3
+ "version": "0.150.2-beta.30",
4
4
  "description": "react native calculator keyboard",
5
5
  "main": "./src/index.tsx",
6
6
  "files": [