@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.
- package/Input/InputMoney.tsx +160 -53
- package/package.json +1 -1
package/Input/InputMoney.tsx
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
97
|
-
updateDisplayValue(cleanValue);
|
|
214
|
+
commit(cleanValue, cleanValue.length);
|
|
98
215
|
}
|
|
99
216
|
}
|
|
100
217
|
}, [value]);
|
|
101
218
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
136
|
-
setNumericValue('');
|
|
239
|
+
commit('', 0);
|
|
137
240
|
onChangeText?.('');
|
|
138
241
|
};
|
|
139
242
|
|
|
140
243
|
const _onChangeText = (text: string) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
178
|
-
setNumericValue('');
|
|
279
|
+
commit('', 0);
|
|
179
280
|
onChangeText?.('');
|
|
180
281
|
return;
|
|
181
282
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
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}
|