@momo-kits/calculator-keyboard 0.150.2-beta.3 → 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.
Files changed (30) hide show
  1. package/README.md +45 -3
  2. package/android/src/main/java/com/calculatorkeyboard/CalculatorKeyboardPackage.kt +1 -1
  3. package/android/src/main/java/com/calculatorkeyboard/CustomKeyboardView.kt +225 -90
  4. package/android/src/main/java/com/calculatorkeyboard/Event.kt +36 -0
  5. package/android/src/main/java/com/calculatorkeyboard/InputCalculatorViewManager.kt +270 -0
  6. package/android/src/main/java/com/calculatorkeyboard/KeyboardOverplayHost.kt +232 -0
  7. package/ios/CalculatorKeyboardView.h +30 -0
  8. package/ios/CalculatorKeyboardView.mm +231 -0
  9. package/ios/NativeInputCalculator.h +11 -0
  10. package/ios/NativeInputCalculator.mm +369 -0
  11. package/package.json +21 -131
  12. package/react-native-calculator-keyboard.podspec +5 -4
  13. package/src/InputCalculatorNativeComponent.ts +62 -0
  14. package/src/index.tsx +104 -31
  15. package/android/src/main/java/com/calculatorkeyboard/RCTInputCalculator.kt +0 -179
  16. package/ios/CalculatorKeyboardView.swift +0 -115
  17. package/ios/InputCalculator-Bridging-Header.h +0 -23
  18. package/ios/InputCalculator.m +0 -79
  19. package/ios/InputCalculator.swift +0 -138
  20. package/ios/extension.swift +0 -23
  21. package/lib/commonjs/index.js +0 -48
  22. package/lib/commonjs/index.js.map +0 -1
  23. package/lib/module/index.js +0 -44
  24. package/lib/module/index.js.map +0 -1
  25. package/lib/typescript/commonjs/package.json +0 -1
  26. package/lib/typescript/commonjs/src/index.d.ts +0 -13
  27. package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
  28. package/lib/typescript/module/package.json +0 -1
  29. package/lib/typescript/module/src/index.d.ts +0 -13
  30. package/lib/typescript/module/src/index.d.ts.map +0 -1
@@ -0,0 +1,231 @@
1
+ #import "CalculatorKeyboardView.h"
2
+
3
+ @interface CalculatorKeyboardView ()
4
+ @property (nonatomic, strong) NSArray<NSArray<NSString *> *> *numWithCTAKeys;
5
+ @property (nonatomic, strong) NSArray<NSArray<NSString *> *> *defaultKeys;
6
+ @property (nonatomic, strong) NSSet<NSString *> *specialKeys;
7
+ @property (nonatomic, weak) UIButton *customKeyButton;
8
+ @end
9
+
10
+ @implementation CalculatorKeyboardView {
11
+ CGFloat _separatorWidth;
12
+ }
13
+
14
+ - (instancetype)initWithFrame:(CGRect)frame
15
+ {
16
+ if (self = [super initWithFrame:frame]) {
17
+ _separatorWidth = 4.0;
18
+
19
+ _numWithCTAKeys = @[
20
+ @[@"1", @"2", @"3", @"÷", @"back"],
21
+ @[@"4", @"5", @"6", @"×", @"="],
22
+ @[@"7", @"8", @"9", @"-", @"Tiếp"],
23
+ @[@"000", @"0", @"+"]
24
+ ];
25
+
26
+ _defaultKeys = @[
27
+ @[@"1", @"2", @"3", @"÷", @"AC"],
28
+ @[@"4", @"5", @"6", @"×", @"back"],
29
+ @[@"7", @"8", @"9", @"-", @"="],
30
+ @[@"000", @"0", @"+"]
31
+ ];
32
+
33
+ _specialKeys = [NSSet setWithArray:@[@"=", @"-", @"×", @"÷", @"back", @"+", @"AC"]];
34
+
35
+ [self setup];
36
+ }
37
+ return self;
38
+ }
39
+
40
+ - (void)setKeyboardMode:(NSString *)keyboardMode
41
+ {
42
+ if ([_keyboardMode isEqualToString:keyboardMode]) {
43
+ return;
44
+ }
45
+ _keyboardMode = keyboardMode;
46
+ [self setup];
47
+ }
48
+
49
+ - (void)setCustomKeyText:(NSString *)customKeyText
50
+ {
51
+ _customKeyText = customKeyText;
52
+ [self updateCustomKeyTitle];
53
+ }
54
+
55
+ - (void)setCustomKeyBackground:(NSString *)customKeyBackground
56
+ {
57
+ _customKeyBackground = customKeyBackground;
58
+ [self updateCustomKeyBackground];
59
+ }
60
+
61
+ - (void)setCustomKeyTextColor:(NSString *)customKeyTextColor
62
+ {
63
+ _customKeyTextColor = customKeyTextColor;
64
+ [self updateCustomKeyBackground];
65
+ }
66
+
67
+ - (void)setCustomKeyState:(NSString *)customKeyState
68
+ {
69
+ _customKeyState = customKeyState;
70
+ [self updateCustomKeyBackground];
71
+ }
72
+
73
+ - (void)setKeyboardColor:(UIColor *)color
74
+ {
75
+ [self setup];
76
+ }
77
+
78
+ - (void)setup
79
+ {
80
+ // Remove all subviews
81
+ for (UIView *subview in self.subviews) {
82
+ [subview removeFromSuperview];
83
+ }
84
+
85
+ self.backgroundColor = [self colorFromHex:@"#f2f2f6"];
86
+
87
+ CGFloat buttonWidth = (UIScreen.mainScreen.bounds.size.width - _separatorWidth * 2 - 4 * _separatorWidth) / 5;
88
+ CGFloat buttonHeight = (240 - _separatorWidth * 2 - 3 * _separatorWidth) / 4;
89
+
90
+ // Create content view
91
+ UIView *contentView = [[UIView alloc] init];
92
+ contentView.translatesAutoresizingMaskIntoConstraints = NO;
93
+ [self addSubview:contentView];
94
+
95
+ // Set contentView constraints
96
+ [NSLayoutConstraint activateConstraints:@[
97
+ [contentView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_separatorWidth],
98
+ [contentView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-_separatorWidth],
99
+ [contentView.topAnchor constraintEqualToAnchor:self.topAnchor constant:_separatorWidth],
100
+ [contentView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-_separatorWidth]
101
+ ]];
102
+
103
+ CGFloat yOffset = 0;
104
+ NSArray<NSArray<NSString *> *> *keys = [_keyboardMode isEqualToString:@"NumDefault"] ? _defaultKeys : _numWithCTAKeys;
105
+
106
+ for (NSInteger rowIndex = 0; rowIndex < keys.count; rowIndex++) {
107
+ NSArray<NSString *> *row = keys[rowIndex];
108
+ CGFloat xOffset = 0;
109
+
110
+ for (NSInteger colIndex = 0; colIndex < row.count; colIndex++) {
111
+ NSString *key = row[colIndex];
112
+ BOOL isMainKey = (colIndex == 4 && rowIndex == 2);
113
+ BOOL isMainCTAKey = isMainKey && [_keyboardMode isEqualToString:@"NumWithCTA"];
114
+
115
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
116
+ button.backgroundColor = UIColor.whiteColor;
117
+ button.layer.cornerRadius = 8;
118
+
119
+ NSString *title = isMainCTAKey ? (_customKeyText ?: key) : key;
120
+ [button setTitle:title forState:UIControlStateNormal];
121
+ [button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
122
+ button.titleLabel.font = [UIFont systemFontOfSize:isMainCTAKey ? 18 : 24 weight:UIFontWeightMedium];
123
+ button.accessibilityIdentifier = key;
124
+ button.tag = isMainCTAKey ? 1 : 0;
125
+
126
+ CGRect buttonFrame = CGRectMake(xOffset, yOffset, buttonWidth, buttonHeight);
127
+ if (isMainKey) {
128
+ buttonFrame.size.height = buttonHeight * 2 + _separatorWidth;
129
+ }
130
+ if ([key isEqualToString:@"000"]) {
131
+ buttonFrame.size.width = buttonWidth * 2 + _separatorWidth;
132
+ }
133
+
134
+ button.frame = buttonFrame;
135
+
136
+ if ([key isEqualToString:@"back"]) {
137
+ [button setTitle:@"" forState:UIControlStateNormal];
138
+ UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightBold];
139
+ UIImage *image = [UIImage systemImageNamed:@"delete.backward" withConfiguration:config];
140
+ [button setImage:image forState:UIControlStateNormal];
141
+ button.tintColor = UIColor.blackColor;
142
+ }
143
+
144
+ if ([_specialKeys containsObject:key] || isMainKey) {
145
+ [button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
146
+ button.backgroundColor = [self colorFromHex:@"#d8d8d8"];
147
+ }
148
+
149
+ if (isMainKey) {
150
+ self.customKeyButton = button;
151
+ }
152
+
153
+ [button addTarget:self action:@selector(keyPressed:) forControlEvents:UIControlEventTouchUpInside];
154
+ [contentView addSubview:button];
155
+
156
+ xOffset += buttonFrame.size.width + _separatorWidth;
157
+ }
158
+
159
+ yOffset += buttonHeight + _separatorWidth;
160
+ }
161
+ }
162
+
163
+ - (void)updateCustomKeyTitle
164
+ {
165
+ if (!self.customKeyButton || !_customKeyText) {
166
+ return;
167
+ }
168
+ [self.customKeyButton setTitle:_customKeyText forState:UIControlStateNormal];
169
+ [self.customKeyButton setImage:nil forState:UIControlStateNormal];
170
+ }
171
+
172
+ - (void)updateCustomKeyBackground
173
+ {
174
+ if (!self.customKeyButton || !_keyboardMode || !_customKeyBackground || !_customKeyTextColor || !_customKeyState) {
175
+ return;
176
+ }
177
+
178
+ if ([_keyboardMode isEqualToString:@"numWithCTA"]) {
179
+ self.customKeyButton.enabled = ![_customKeyState isEqualToString:@"disable"];
180
+ } else {
181
+ self.customKeyButton.enabled = YES;
182
+ }
183
+
184
+ self.customKeyButton.backgroundColor = [self colorFromHex:_customKeyBackground];
185
+ [self.customKeyButton setTitleColor:[self colorFromHex:_customKeyTextColor] forState:UIControlStateNormal];
186
+ }
187
+
188
+ - (void)keyPressed:(UIButton *)sender
189
+ {
190
+ NSString *key = sender.accessibilityIdentifier;
191
+ if (!key) {
192
+ return;
193
+ }
194
+
195
+ BOOL isCustomKeyCTA = (sender.tag == 1);
196
+ if (isCustomKeyCTA) {
197
+ [self.input emitCustomKey];
198
+ return;
199
+ }
200
+
201
+ [self.input emitKeyPress:key];
202
+
203
+ if ([key isEqualToString:@"AC"]) {
204
+ [self.input clearText];
205
+ } else if ([key isEqualToString:@"back"]) {
206
+ [self.input onBackSpace];
207
+ } else if ([key isEqualToString:@"="]) {
208
+ [self.input calculateResult];
209
+ } else if ([key isEqualToString:@"+"] || [key isEqualToString:@"-"] ||
210
+ [key isEqualToString:@"÷"] || [key isEqualToString:@"×"]) {
211
+ [self.input keyDidPress:[NSString stringWithFormat:@" %@ ", key]];
212
+ } else {
213
+ [self.input keyDidPress:key];
214
+ }
215
+ }
216
+
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
+
230
+ @end
231
+
@@ -0,0 +1,11 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface NativeInputCalculator : RCTViewComponentView
7
+
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
@@ -0,0 +1,369 @@
1
+ #import "NativeInputCalculator.h"
2
+ #import "CalculatorKeyboardView.h"
3
+
4
+ #import <React/RCTConversions.h>
5
+ #import <React/RCTFabricComponentsPlugins.h>
6
+ #import <react/renderer/components/CalculatorKeyboardSpecs/ComponentDescriptors.h>
7
+ #import <react/renderer/components/CalculatorKeyboardSpecs/EventEmitters.h>
8
+ #import <react/renderer/components/CalculatorKeyboardSpecs/Props.h>
9
+ #import <react/renderer/components/CalculatorKeyboardSpecs/RCTComponentViewHelpers.h>
10
+
11
+ using namespace facebook::react;
12
+
13
+ @interface NativeInputCalculator () <RCTNativeInputCalculatorViewProtocol>
14
+ @end
15
+
16
+ @implementation NativeInputCalculator {
17
+ UITextField *_textField;
18
+ CalculatorKeyboardView *_keyboardView;
19
+ NSString *_lastValue;
20
+ }
21
+
22
+ - (instancetype)initWithFrame:(CGRect)frame
23
+ {
24
+ if (self = [super initWithFrame:frame]) {
25
+ static const auto defaultProps = std::make_shared<const NativeInputCalculatorProps>();
26
+ _props = defaultProps;
27
+
28
+ // Create text field
29
+ _textField = [[UITextField alloc] initWithFrame:self.bounds];
30
+ _textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
31
+ _textField.delegate = (id<UITextFieldDelegate>)self;
32
+
33
+ // Create keyboard view
34
+ CGFloat bottomInset = [self getBottomInset];
35
+ CGRect keyboardFrame = CGRectMake(0, 0, UIScreen.mainScreen.bounds.size.width, 240 + bottomInset);
36
+ _keyboardView = [[CalculatorKeyboardView alloc] initWithFrame:keyboardFrame];
37
+ _keyboardView.input = (id)self; // Bridge pattern
38
+
39
+ // Set custom keyboard
40
+ _textField.inputView = _keyboardView;
41
+
42
+ [self addSubview:_textField];
43
+ }
44
+ return self;
45
+ }
46
+
47
+ - (CGFloat)getBottomInset
48
+ {
49
+ UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
50
+ CGFloat bottom = window.safeAreaInsets.bottom > 0 ? 21 : 0;
51
+ return bottom;
52
+ }
53
+
54
+ #pragma mark - RCTComponentViewProtocol
55
+
56
+ + (ComponentDescriptorProvider)componentDescriptorProvider
57
+ {
58
+ return concreteComponentDescriptorProvider<NativeInputCalculatorComponentDescriptor>();
59
+ }
60
+
61
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
62
+ {
63
+ const auto &oldViewProps = *std::static_pointer_cast<const NativeInputCalculatorProps>(_props);
64
+ const auto &newViewProps = *std::static_pointer_cast<const NativeInputCalculatorProps>(props);
65
+
66
+ if (oldViewProps.value != newViewProps.value) {
67
+ NSString *newValue = RCTNSStringFromString(newViewProps.value);
68
+ if (![_lastValue isEqualToString:newValue]) {
69
+ _textField.text = newValue;
70
+ _lastValue = newValue;
71
+ }
72
+ }
73
+
74
+ if (oldViewProps.mode != newViewProps.mode) {
75
+ _keyboardView.keyboardMode = RCTNSStringFromString(newViewProps.mode);
76
+ }
77
+
78
+ if (oldViewProps.customKeyText != newViewProps.customKeyText) {
79
+ _keyboardView.customKeyText = RCTNSStringFromString(newViewProps.customKeyText);
80
+ }
81
+
82
+ if (oldViewProps.customKeyBackground != newViewProps.customKeyBackground) {
83
+ _keyboardView.customKeyBackground = RCTNSStringFromString(newViewProps.customKeyBackground);
84
+ }
85
+
86
+ if (oldViewProps.customKeyTextColor != newViewProps.customKeyTextColor) {
87
+ _keyboardView.customKeyTextColor = RCTNSStringFromString(newViewProps.customKeyTextColor);
88
+ }
89
+
90
+ if (oldViewProps.customKeyState != newViewProps.customKeyState) {
91
+ _keyboardView.customKeyState = RCTNSStringFromString(newViewProps.customKeyState);
92
+ }
93
+
94
+ if (oldViewProps.textAttributes.fontSize != newViewProps.textAttributes.fontSize > 0.0f) {
95
+ CGFloat newSize = (CGFloat)newViewProps.textAttributes.fontSize;
96
+ UIFont *current = _textField.font ?: [UIFont systemFontOfSize:UIFont.systemFontSize];
97
+ _textField.font = [current fontWithSize:newSize];
98
+ }
99
+
100
+ if (oldViewProps.textAttributes.fontWeight != newViewProps.textAttributes.fontWeight) {
101
+ UIFontWeight weight = _UIFontWeightFromString(newViewProps.textAttributes.fontWeight);
102
+ CGFloat size = _textField.font ? _textField.font.pointSize : UIFont.systemFontSize;
103
+ _textField.font = [UIFont systemFontOfSize:size weight:weight];
104
+ }
105
+
106
+ [super updateProps:props oldProps:oldProps];
107
+ }
108
+
109
+ static UIFontWeight _UIFontWeightFromString(std::string_view s) {
110
+ std::string v(s);
111
+ for (auto &c : v) c = (char)tolower((unsigned char)c);
112
+
113
+ if (v == "normal" || v == "400") return UIFontWeightRegular;
114
+ if (v == "bold" || v == "700") return UIFontWeightBold;
115
+ if (v == "100") return UIFontWeightUltraLight;
116
+ if (v == "200") return UIFontWeightThin;
117
+ if (v == "300") return UIFontWeightLight;
118
+ if (v == "500") return UIFontWeightMedium;
119
+ if (v == "600") return UIFontWeightSemibold;
120
+ if (v == "800") return UIFontWeightHeavy;
121
+ if (v == "900") return UIFontWeightBlack;
122
+ return UIFontWeightRegular;
123
+ }
124
+
125
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
126
+ {
127
+ RCTNativeInputCalculatorHandleCommand(self, commandName, args);
128
+ }
129
+
130
+ #pragma mark - RCTNativeInputCalculatorViewProtocol
131
+
132
+ - (void)focus
133
+ {
134
+ [_textField becomeFirstResponder];
135
+ }
136
+
137
+ - (void)blur
138
+ {
139
+ [_textField resignFirstResponder];
140
+ }
141
+
142
+ #pragma mark - Keyboard callbacks (called by CalculatorKeyboardView)
143
+
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];
152
+ [self notifyTextChange];
153
+ }
154
+
155
+ - (void)clearText
156
+ {
157
+ _textField.text = @"";
158
+ [self notifyTextChange];
159
+ }
160
+
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];
183
+ }
184
+
185
+ [self notifyTextChange];
186
+ }
187
+
188
+ - (void)calculateResult {
189
+ NSString *text = _textField.text ?: @"";
190
+
191
+ text = [self stripGroupDots:text];
192
+ text = [text stringByReplacingOccurrencesOfString:@"×" withString:@"*"];
193
+ text = [text stringByReplacingOccurrencesOfString:@"÷" withString:@"/"];
194
+
195
+ NSString *pattern = @"^\\s*(-?\\d+(\\.\\d+)?\\s*[-+*/]\\s*)*-?\\d+(\\.\\d+)?\\s*$";
196
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
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];
212
+
213
+ [self notifyTextChange];
214
+ }
215
+ } @catch (__unused NSException *e) { }
216
+ }
217
+
218
+ - (void)emitCustomKey
219
+ {
220
+ if (_eventEmitter) {
221
+ auto emitter = std::static_pointer_cast<const NativeInputCalculatorEventEmitter>(_eventEmitter);
222
+ NativeInputCalculatorEventEmitter::OnCustomKeyEvent event{};
223
+ emitter->onCustomKeyEvent(event);
224
+ }
225
+ }
226
+
227
+ - (void)emitKeyPress:(NSString *)key
228
+ {
229
+ if (_eventEmitter) {
230
+ auto emitter = std::static_pointer_cast<const NativeInputCalculatorEventEmitter>(_eventEmitter);
231
+ NativeInputCalculatorEventEmitter::OnKeyPress event{
232
+ .key = std::string([key UTF8String])
233
+ };
234
+ emitter->onKeyPress(event);
235
+ }
236
+ }
237
+
238
+ - (void)notifyTextChange
239
+ {
240
+ _lastValue = _textField.text;
241
+
242
+ if (_eventEmitter) {
243
+ auto emitter = std::static_pointer_cast<const NativeInputCalculatorEventEmitter>(_eventEmitter);
244
+ NativeInputCalculatorEventEmitter::OnChange event{
245
+ .text = std::string([_textField.text UTF8String])
246
+ };
247
+ emitter->onChange(event);
248
+ }
249
+ }
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
+
363
+ @end
364
+
365
+ Class<RCTNativeInputCalculatorViewProtocol> NativeInputCalculatorCls(void)
366
+ {
367
+ return NativeInputCalculator.class;
368
+ }
369
+