@momo-kits/foundation 0.165.2-beta.1 → 0.165.2-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.
Files changed (2) hide show
  1. package/Input/InputMoney.tsx +160 -53
  2. package/package.json +1 -1
@@ -9,8 +9,10 @@ import React, {
9
9
  } from 'react';
10
10
  import {
11
11
  NativeSyntheticEvent,
12
+ Platform,
12
13
  TextInput,
13
14
  TextInputFocusEventData,
15
+ TextInputSelectionChangeEventData,
14
16
  TouchableOpacity,
15
17
  View,
16
18
  } from 'react-native';
@@ -32,9 +34,124 @@ import {
32
34
  } from './common';
33
35
  import { InputMoneyProps } from './index';
34
36
  import styles from './styles';
35
- import { formatNumberToMoney } from './utils';
36
37
  import SystemTextInput from './SystemTextInput';
37
38
 
39
+ type Selection = { start: number; end: number };
40
+ type Edit = { start: number; end: number; inserted: string };
41
+
42
+ const countDigits = (text: string) => text.replace(/\D/g, '').length;
43
+
44
+ const groupDigits = (digits: string) =>
45
+ digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
46
+
47
+ // Caret position right after the `digitIndex`-th digit of `display`.
48
+ const caretFromDigitIndex = (display: string, digitIndex: number) => {
49
+ let count = 0;
50
+ for (let i = 0; i < display.length && count < digitIndex; i++) {
51
+ if (/\d/.test(display[i])) {
52
+ count += 1;
53
+ if (count === digitIndex) return i + 1;
54
+ }
55
+ }
56
+ return 0;
57
+ };
58
+
59
+ // Finds which range of `old` was replaced to produce `next`. `selection` is
60
+ // the pre-edit selection on iOS and already the post-edit caret on Android,
61
+ // where onSelectionChange fires before onChangeText.
62
+ const detectEdit = (old: string, next: string, selection: Selection): Edit => {
63
+ const { start, end } = selection;
64
+ const removed = old.length - next.length;
65
+ const preEdit: Edit[] = [
66
+ {
67
+ start,
68
+ end,
69
+ inserted: next.slice(start, next.length - (old.length - end)),
70
+ },
71
+ { start: start - removed, end: start, inserted: '' },
72
+ { start, end: start + removed, inserted: '' },
73
+ ];
74
+ const postEdit: Edit[] = [];
75
+ const tail = next.length - start;
76
+ if (start === end && tail <= old.length) {
77
+ let prefix = 0;
78
+ while (
79
+ prefix < Math.min(start, old.length - tail) &&
80
+ old[prefix] === next[prefix]
81
+ ) {
82
+ prefix += 1;
83
+ }
84
+ postEdit.push({
85
+ start: prefix,
86
+ end: old.length - tail,
87
+ inserted: next.slice(prefix, start),
88
+ });
89
+ }
90
+ const candidates =
91
+ Platform.OS === 'android'
92
+ ? [...postEdit, ...preEdit]
93
+ : [...preEdit, ...postEdit];
94
+ const match = candidates.find(
95
+ e =>
96
+ e.start >= 0 &&
97
+ e.end >= e.start &&
98
+ e.end <= old.length &&
99
+ old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
100
+ );
101
+ if (match) return match;
102
+
103
+ let prefix = 0;
104
+ while (
105
+ prefix < Math.min(old.length, next.length) &&
106
+ old[prefix] === next[prefix]
107
+ ) {
108
+ prefix += 1;
109
+ }
110
+ let suffix = 0;
111
+ while (
112
+ suffix < Math.min(old.length, next.length) - prefix &&
113
+ old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
114
+ ) {
115
+ suffix += 1;
116
+ }
117
+ return {
118
+ start: prefix,
119
+ end: old.length - suffix,
120
+ inserted: next.slice(prefix, next.length - suffix),
121
+ };
122
+ };
123
+
124
+ // Applies `edit` to the digits of `display`. Separators and currency are never
125
+ // deleted: removing a separator deletes the digit before it instead, removing
126
+ // only the currency leaves the digits untouched.
127
+ const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
128
+ const digits = display.replace(/\D/g, '');
129
+ let left = countDigits(display.slice(0, edit.start));
130
+ let removed = countDigits(display.slice(edit.start, edit.end));
131
+ const inserted = edit.inserted.replace(/\D/g, '');
132
+ if (
133
+ !removed &&
134
+ !inserted &&
135
+ left > 0 &&
136
+ display.slice(edit.start, edit.end).includes('.')
137
+ ) {
138
+ left -= 1;
139
+ removed = 1;
140
+ }
141
+ const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
142
+ const trimmed = next.replace(/^0+(?=\d)/, '');
143
+ if ((!removed && !inserted) || trimmed.length > digitLimit) {
144
+ return { digits, caretDigit: left };
145
+ }
146
+ return {
147
+ digits: trimmed,
148
+ caretDigit: Math.max(
149
+ 0,
150
+ left + inserted.length - (next.length - trimmed.length),
151
+ ),
152
+ };
153
+ };
154
+
38
155
  const InputMoney = forwardRef(
39
156
  (
40
157
  {
@@ -73,7 +190,8 @@ const InputMoney = forwardRef(
73
190
  const [focused, setFocused] = useState(false);
74
191
  const [displayValue, setDisplayValue] = useState('');
75
192
  const [numericValue, setNumericValue] = useState('');
76
- const [lastDisplayValue, setLastDisplayValue] = useState('');
193
+ const [selection, setSelection] = useState<Selection>();
194
+ const selectionRef = useRef<Selection>({ start: 0, end: 0 });
77
195
  const inputRef = useRef<TextInput>(null);
78
196
  const componentName = 'InputMoney';
79
197
 
@@ -91,25 +209,21 @@ const InputMoney = forwardRef(
91
209
 
92
210
  useEffect(() => {
93
211
  if (value !== undefined && typeof value === 'string') {
94
- const cleanValue = value.replace(/\D/g, '');
212
+ const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
95
213
  if (cleanValue !== numericValue) {
96
- setNumericValue(cleanValue);
97
- updateDisplayValue(cleanValue);
214
+ commit(cleanValue, cleanValue.length);
98
215
  }
99
216
  }
100
217
  }, [value]);
101
218
 
102
- useEffect(() => {
103
- setLastDisplayValue(displayValue);
104
- }, [displayValue]);
105
-
106
- const updateDisplayValue = (numeric: string) => {
107
- if (!numeric) {
108
- setDisplayValue('');
109
- return;
110
- }
111
- const formatted = formatNumberToMoney(Number(numeric));
112
- setDisplayValue(formatted ? `${formatted}${currency}` : '');
219
+ const commit = (digits: string, caretDigit: number) => {
220
+ const display = digits ? `${groupDigits(digits)}${currency}` : '';
221
+ const caret = caretFromDigitIndex(display, caretDigit);
222
+ selectionRef.current = { start: caret, end: caret };
223
+ setSelection({ start: caret, end: caret });
224
+ setDisplayValue(display);
225
+ setNumericValue(digits);
226
+ return digits !== numericValue;
113
227
  };
114
228
 
115
229
  const parseInputText = (text: string): string => {
@@ -119,47 +233,35 @@ const InputMoney = forwardRef(
119
233
  return textWithoutCurrency.replace(/[^\d]/g, '');
120
234
  };
121
235
 
122
- const updateNumericValue = (val: string) => {
123
- let next = val.replace(/[^\d]/g, '');
124
- if (next.length > digitLimit) next = next.slice(0, digitLimit);
125
- if (/^\d*$/.test(next)) {
126
- setNumericValue(next);
127
- updateDisplayValue(next);
128
- onChangeText?.(next);
129
- }
130
- };
131
-
132
236
  const onClearText = () => {
133
237
  inputRef?.current?.clear();
134
238
  inputRef.current?.setNativeProps({ text: '' });
135
- setDisplayValue('');
136
- setNumericValue('');
239
+ commit('', 0);
137
240
  onChangeText?.('');
138
241
  };
139
242
 
140
243
  const _onChangeText = (text: string) => {
141
- if (!text) {
142
- setDisplayValue('');
143
- setNumericValue('');
144
- onChangeText?.('');
145
- return;
146
- }
147
- if (text.length < lastDisplayValue.length) {
148
- const lastChar = lastDisplayValue.charAt(lastDisplayValue.length - 1);
149
- const isRemovingCurrency =
150
- lastDisplayValue.endsWith(currency) || lastChar === ' ';
151
- const isRemovingDot = lastChar === '.';
152
- if (isRemovingCurrency || isRemovingDot) {
153
- const currentNumericValue = numericValue;
154
- if (currentNumericValue.length > 0) {
155
- const newNumericValue = currentNumericValue.slice(0, -1);
156
- updateNumericValue(newNumericValue);
157
- return;
158
- }
159
- }
244
+ const { digits, caretDigit } = text
245
+ ? applyEdit(
246
+ displayValue,
247
+ detectEdit(displayValue, text, selectionRef.current),
248
+ digitLimit,
249
+ )
250
+ : { digits: '', caretDigit: 0 };
251
+ if (commit(digits, caretDigit)) {
252
+ onChangeText?.(digits);
160
253
  }
161
- const newNumericValue = parseInputText(text);
162
- updateNumericValue(newNumericValue);
254
+ };
255
+
256
+ // Keeps the caret out of the currency suffix.
257
+ const _onSelectionChange = (
258
+ e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
259
+ ) => {
260
+ const { start, end } = e.nativeEvent.selection;
261
+ selectionRef.current = { start, end };
262
+ const max = displayValue ? displayValue.length - currency.length : 0;
263
+ setSelection({ start: Math.min(start, max), end: Math.min(end, max) });
264
+ props.onSelectionChange?.(e);
163
265
  };
164
266
 
165
267
  const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
@@ -174,13 +276,16 @@ const InputMoney = forwardRef(
174
276
 
175
277
  const _setText = (text: string) => {
176
278
  if (!text) {
177
- setDisplayValue('');
178
- setNumericValue('');
279
+ commit('', 0);
179
280
  onChangeText?.('');
180
281
  return;
181
282
  }
182
- const newNumericValue = parseInputText(text);
183
- updateNumericValue(newNumericValue);
283
+ const digits = parseInputText(text)
284
+ .replace(/^0+(?=\d)/, '')
285
+ .slice(0, digitLimit);
286
+ if (commit(digits, digits.length)) {
287
+ onChangeText?.(digits);
288
+ }
184
289
  };
185
290
 
186
291
  useImperativeHandle(ref, () => {
@@ -247,6 +352,8 @@ const InputMoney = forwardRef(
247
352
  textBreakStrategy="highQuality"
248
353
  value={displayValue}
249
354
  onChangeText={_onChangeText}
355
+ selection={selection}
356
+ onSelectionChange={_onSelectionChange}
250
357
  onFocus={_onFocus}
251
358
  onBlur={_onBlur}
252
359
  placeholder={placeholder}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.165.2-beta.1",
3
+ "version": "0.165.2-beta.2",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},