@momo-kits/foundation 0.167.1-beta.1 → 0.167.1-beta.11
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 +176 -51
- package/package.json +1 -1
package/Input/InputMoney.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, {
|
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
6
|
useImperativeHandle,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from 'react';
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
NativeSyntheticEvent,
|
|
12
13
|
TextInput,
|
|
13
14
|
TextInputFocusEventData,
|
|
15
|
+
TextInputSelectionChangeEventData,
|
|
14
16
|
TouchableOpacity,
|
|
15
17
|
View,
|
|
16
18
|
} from 'react-native';
|
|
@@ -32,9 +34,101 @@ 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
|
+
const detectEdit = (old: string, next: string, selection: Selection): Edit => {
|
|
60
|
+
const { start, end } = selection;
|
|
61
|
+
const removed = old.length - next.length;
|
|
62
|
+
const candidates: Edit[] = [
|
|
63
|
+
{
|
|
64
|
+
start,
|
|
65
|
+
end,
|
|
66
|
+
inserted: next.slice(start, next.length - (old.length - end)),
|
|
67
|
+
},
|
|
68
|
+
{ start: start - removed, end: start, inserted: '' },
|
|
69
|
+
{ start, end: start + removed, inserted: '' },
|
|
70
|
+
];
|
|
71
|
+
const match = candidates.find(
|
|
72
|
+
e =>
|
|
73
|
+
e.start >= 0 &&
|
|
74
|
+
e.end >= e.start &&
|
|
75
|
+
e.end <= old.length &&
|
|
76
|
+
old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
|
|
77
|
+
);
|
|
78
|
+
if (match) return match;
|
|
79
|
+
|
|
80
|
+
let prefix = 0;
|
|
81
|
+
while (
|
|
82
|
+
prefix < Math.min(old.length, next.length) &&
|
|
83
|
+
old[prefix] === next[prefix]
|
|
84
|
+
) {
|
|
85
|
+
prefix += 1;
|
|
86
|
+
}
|
|
87
|
+
let suffix = 0;
|
|
88
|
+
while (
|
|
89
|
+
suffix < Math.min(old.length, next.length) - prefix &&
|
|
90
|
+
old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
|
|
91
|
+
) {
|
|
92
|
+
suffix += 1;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
start: prefix,
|
|
96
|
+
end: old.length - suffix,
|
|
97
|
+
inserted: next.slice(prefix, next.length - suffix),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Applies `edit` to the digits of `display`. Separators and currency are never
|
|
102
|
+
// deleted: removing a separator deletes the digit before it instead, removing
|
|
103
|
+
// only the currency leaves the digits untouched.
|
|
104
|
+
const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
|
|
105
|
+
const digits = display.replace(/\D/g, '');
|
|
106
|
+
let left = countDigits(display.slice(0, edit.start));
|
|
107
|
+
let removed = countDigits(display.slice(edit.start, edit.end));
|
|
108
|
+
const inserted = edit.inserted.replace(/\D/g, '');
|
|
109
|
+
if (
|
|
110
|
+
!removed &&
|
|
111
|
+
!inserted &&
|
|
112
|
+
left > 0 &&
|
|
113
|
+
display.slice(edit.start, edit.end).includes('.')
|
|
114
|
+
) {
|
|
115
|
+
left -= 1;
|
|
116
|
+
removed = 1;
|
|
117
|
+
}
|
|
118
|
+
const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
|
|
119
|
+
const trimmed = next.replace(/^0+(?=\d)/, '');
|
|
120
|
+
if ((!removed && !inserted) || trimmed.length > digitLimit) {
|
|
121
|
+
return { digits, caretDigit: left };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
digits: trimmed,
|
|
125
|
+
caretDigit: Math.max(
|
|
126
|
+
0,
|
|
127
|
+
left + inserted.length - (next.length - trimmed.length),
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
38
132
|
const InputMoney = forwardRef(
|
|
39
133
|
(
|
|
40
134
|
{
|
|
@@ -73,7 +167,13 @@ const InputMoney = forwardRef(
|
|
|
73
167
|
const [focused, setFocused] = useState(false);
|
|
74
168
|
const [displayValue, setDisplayValue] = useState('');
|
|
75
169
|
const [numericValue, setNumericValue] = useState('');
|
|
76
|
-
const [
|
|
170
|
+
const [commitCount, setCommitCount] = useState(0);
|
|
171
|
+
const displayRef = useRef('');
|
|
172
|
+
const selectionRef = useRef<Selection>({ start: 0, end: 0 });
|
|
173
|
+
// Caret to push to native once the committed text is rendered.
|
|
174
|
+
const pendingCaretRef = useRef<number | null>(null);
|
|
175
|
+
const pendingSelectionRef = useRef<Selection | null>(null);
|
|
176
|
+
const editingRef = useRef(false);
|
|
77
177
|
const inputRef = useRef<TextInput>(null);
|
|
78
178
|
const componentName = 'InputMoney';
|
|
79
179
|
|
|
@@ -91,25 +191,36 @@ const InputMoney = forwardRef(
|
|
|
91
191
|
|
|
92
192
|
useEffect(() => {
|
|
93
193
|
if (value !== undefined && typeof value === 'string') {
|
|
94
|
-
const cleanValue = value.replace(/\D/g, '');
|
|
194
|
+
const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
|
|
95
195
|
if (cleanValue !== numericValue) {
|
|
96
|
-
|
|
97
|
-
updateDisplayValue(cleanValue);
|
|
196
|
+
commit(cleanValue, cleanValue.length);
|
|
98
197
|
}
|
|
99
198
|
}
|
|
100
199
|
}, [value]);
|
|
101
200
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
201
|
+
// Always set the caret imperatively: a controlled `selection` prop is only
|
|
202
|
+
// sent to native when it differs from the last native selection, so it is
|
|
203
|
+
// skipped whenever a stale selection event already reported the same value.
|
|
204
|
+
useLayoutEffect(() => {
|
|
205
|
+
const caret = pendingCaretRef.current;
|
|
206
|
+
if (caret === null) return;
|
|
207
|
+
pendingCaretRef.current = null;
|
|
208
|
+
if (inputRef.current?.isFocused()) {
|
|
209
|
+
inputRef.current.setSelection(caret, caret);
|
|
110
210
|
}
|
|
111
|
-
|
|
112
|
-
|
|
211
|
+
}, [commitCount]);
|
|
212
|
+
|
|
213
|
+
const commit = (digits: string, caretDigit: number) => {
|
|
214
|
+
const display = digits ? `${groupDigits(digits)}${currency}` : '';
|
|
215
|
+
const caret = caretFromDigitIndex(display, caretDigit);
|
|
216
|
+
const changed = digits !== numericValue;
|
|
217
|
+
displayRef.current = display;
|
|
218
|
+
selectionRef.current = { start: caret, end: caret };
|
|
219
|
+
pendingCaretRef.current = caret;
|
|
220
|
+
setCommitCount(count => count + 1);
|
|
221
|
+
setDisplayValue(display);
|
|
222
|
+
setNumericValue(digits);
|
|
223
|
+
return changed;
|
|
113
224
|
};
|
|
114
225
|
|
|
115
226
|
const parseInputText = (text: string): string => {
|
|
@@ -119,47 +230,57 @@ const InputMoney = forwardRef(
|
|
|
119
230
|
return textWithoutCurrency.replace(/[^\d]/g, '');
|
|
120
231
|
};
|
|
121
232
|
|
|
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
233
|
const onClearText = () => {
|
|
133
234
|
inputRef?.current?.clear();
|
|
134
235
|
inputRef.current?.setNativeProps({ text: '' });
|
|
135
|
-
|
|
136
|
-
setNumericValue('');
|
|
236
|
+
commit('', 0);
|
|
137
237
|
onChangeText?.('');
|
|
138
238
|
};
|
|
139
239
|
|
|
140
240
|
const _onChangeText = (text: string) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
241
|
+
let digits = '';
|
|
242
|
+
let caretDigit = 0;
|
|
243
|
+
pendingSelectionRef.current = null;
|
|
244
|
+
editingRef.current = true;
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
editingRef.current = false;
|
|
247
|
+
});
|
|
248
|
+
if (text) {
|
|
249
|
+
const edit = detectEdit(displayRef.current, text, selectionRef.current);
|
|
250
|
+
({ digits, caretDigit } = applyEdit(
|
|
251
|
+
displayRef.current,
|
|
252
|
+
edit,
|
|
253
|
+
digitLimit,
|
|
254
|
+
));
|
|
146
255
|
}
|
|
147
|
-
if (
|
|
148
|
-
|
|
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
|
-
}
|
|
256
|
+
if (commit(digits, caretDigit)) {
|
|
257
|
+
onChangeText?.(digits);
|
|
160
258
|
}
|
|
161
|
-
|
|
162
|
-
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Keeps the caret out of the currency suffix.
|
|
262
|
+
const _onSelectionChange = (
|
|
263
|
+
e: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
|
|
264
|
+
) => {
|
|
265
|
+
props.onSelectionChange?.(e);
|
|
266
|
+
if (editingRef.current) return;
|
|
267
|
+
const selection = e.nativeEvent.selection;
|
|
268
|
+
pendingSelectionRef.current = selection;
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (pendingSelectionRef.current !== selection) return;
|
|
271
|
+
pendingSelectionRef.current = null;
|
|
272
|
+
const { start, end } = selection;
|
|
273
|
+
const display = displayRef.current;
|
|
274
|
+
const max = display ? display.length - currency.length : 0;
|
|
275
|
+
const clamped = {
|
|
276
|
+
start: Math.min(start, max),
|
|
277
|
+
end: Math.min(end, max),
|
|
278
|
+
};
|
|
279
|
+
selectionRef.current = clamped;
|
|
280
|
+
if (clamped.start !== start || clamped.end !== end) {
|
|
281
|
+
inputRef.current?.setSelection(clamped.start, clamped.end);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
163
284
|
};
|
|
164
285
|
|
|
165
286
|
const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
@@ -174,13 +295,16 @@ const InputMoney = forwardRef(
|
|
|
174
295
|
|
|
175
296
|
const _setText = (text: string) => {
|
|
176
297
|
if (!text) {
|
|
177
|
-
|
|
178
|
-
setNumericValue('');
|
|
298
|
+
commit('', 0);
|
|
179
299
|
onChangeText?.('');
|
|
180
300
|
return;
|
|
181
301
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
302
|
+
const digits = parseInputText(text)
|
|
303
|
+
.replace(/^0+(?=\d)/, '')
|
|
304
|
+
.slice(0, digitLimit);
|
|
305
|
+
if (commit(digits, digits.length)) {
|
|
306
|
+
onChangeText?.(digits);
|
|
307
|
+
}
|
|
184
308
|
};
|
|
185
309
|
|
|
186
310
|
useImperativeHandle(ref, () => {
|
|
@@ -247,6 +371,7 @@ const InputMoney = forwardRef(
|
|
|
247
371
|
textBreakStrategy="highQuality"
|
|
248
372
|
value={displayValue}
|
|
249
373
|
onChangeText={_onChangeText}
|
|
374
|
+
onSelectionChange={_onSelectionChange}
|
|
250
375
|
onFocus={_onFocus}
|
|
251
376
|
onBlur={_onBlur}
|
|
252
377
|
placeholder={placeholder}
|