@momo-kits/foundation 0.168.1-beta.2 → 0.168.1-beta.4
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 +99 -171
- package/package.json +1 -1
package/Input/InputMoney.tsx
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
1
2
|
import React, {
|
|
2
3
|
forwardRef,
|
|
3
4
|
useContext,
|
|
4
5
|
useEffect,
|
|
5
6
|
useImperativeHandle,
|
|
6
|
-
useLayoutEffect,
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import {
|
|
11
|
+
Platform,
|
|
11
12
|
TextInput,
|
|
12
13
|
TextInputFocusEvent,
|
|
13
|
-
TextInputSelectionChangeEvent,
|
|
14
14
|
TouchableOpacity,
|
|
15
15
|
View,
|
|
16
16
|
} from 'react-native';
|
|
@@ -32,97 +32,9 @@ import {
|
|
|
32
32
|
} from './common';
|
|
33
33
|
import { InputMoneyProps } from './index';
|
|
34
34
|
import styles from './styles';
|
|
35
|
+
import { formatNumberToMoney } from './utils';
|
|
35
36
|
import SystemTextInput from './SystemTextInput';
|
|
36
37
|
|
|
37
|
-
type Selection = { start: number; end: number };
|
|
38
|
-
type Edit = { start: number; end: number; inserted: string };
|
|
39
|
-
|
|
40
|
-
const countDigits = (text: string) => text.replace(/\D/g, '').length;
|
|
41
|
-
|
|
42
|
-
const groupDigits = (digits: string) =>
|
|
43
|
-
digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
44
|
-
|
|
45
|
-
const caretFromDigitIndex = (display: string, digitIndex: number) => {
|
|
46
|
-
let count = 0;
|
|
47
|
-
for (let i = 0; i < display.length && count < digitIndex; i++) {
|
|
48
|
-
if (/\d/.test(display[i])) {
|
|
49
|
-
count += 1;
|
|
50
|
-
if (count === digitIndex) return i + 1;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return 0;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const detectEdit = (old: string, next: string, selection: Selection): Edit => {
|
|
57
|
-
const { start, end } = selection;
|
|
58
|
-
const removed = old.length - next.length;
|
|
59
|
-
const candidates: Edit[] = [
|
|
60
|
-
{
|
|
61
|
-
start,
|
|
62
|
-
end,
|
|
63
|
-
inserted: next.slice(start, next.length - (old.length - end)),
|
|
64
|
-
},
|
|
65
|
-
{ start: start - removed, end: start, inserted: '' },
|
|
66
|
-
{ start, end: start + removed, inserted: '' },
|
|
67
|
-
];
|
|
68
|
-
const match = candidates.find(
|
|
69
|
-
e =>
|
|
70
|
-
e.start >= 0 &&
|
|
71
|
-
e.end >= e.start &&
|
|
72
|
-
e.end <= old.length &&
|
|
73
|
-
old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
|
|
74
|
-
);
|
|
75
|
-
if (match) return match;
|
|
76
|
-
|
|
77
|
-
let prefix = 0;
|
|
78
|
-
while (
|
|
79
|
-
prefix < Math.min(old.length, next.length) &&
|
|
80
|
-
old[prefix] === next[prefix]
|
|
81
|
-
) {
|
|
82
|
-
prefix += 1;
|
|
83
|
-
}
|
|
84
|
-
let suffix = 0;
|
|
85
|
-
while (
|
|
86
|
-
suffix < Math.min(old.length, next.length) - prefix &&
|
|
87
|
-
old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
|
|
88
|
-
) {
|
|
89
|
-
suffix += 1;
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
start: prefix,
|
|
93
|
-
end: old.length - suffix,
|
|
94
|
-
inserted: next.slice(prefix, next.length - suffix),
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
|
|
99
|
-
const digits = display.replace(/\D/g, '');
|
|
100
|
-
let left = countDigits(display.slice(0, edit.start));
|
|
101
|
-
let removed = countDigits(display.slice(edit.start, edit.end));
|
|
102
|
-
const inserted = edit.inserted.replace(/\D/g, '');
|
|
103
|
-
if (
|
|
104
|
-
!removed &&
|
|
105
|
-
!inserted &&
|
|
106
|
-
left > 0 &&
|
|
107
|
-
display.slice(edit.start, edit.end).includes('.')
|
|
108
|
-
) {
|
|
109
|
-
left -= 1;
|
|
110
|
-
removed = 1;
|
|
111
|
-
}
|
|
112
|
-
const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
|
|
113
|
-
const trimmed = next.replace(/^0+(?=\d)/, '');
|
|
114
|
-
if ((!removed && !inserted) || trimmed.length > digitLimit) {
|
|
115
|
-
return { digits, caretDigit: left };
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
digits: trimmed,
|
|
119
|
-
caretDigit: Math.max(
|
|
120
|
-
0,
|
|
121
|
-
left + inserted.length - (next.length - trimmed.length),
|
|
122
|
-
),
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
|
|
126
38
|
const InputMoney = forwardRef(
|
|
127
39
|
(
|
|
128
40
|
{
|
|
@@ -160,14 +72,10 @@ const InputMoney = forwardRef(
|
|
|
160
72
|
const inputFontFamily = exportFontFamily('bold');
|
|
161
73
|
const [focused, setFocused] = useState(false);
|
|
162
74
|
const [displayValue, setDisplayValue] = useState('');
|
|
163
|
-
const [
|
|
164
|
-
const
|
|
165
|
-
const digitsRef = useRef('');
|
|
166
|
-
const selectionRef = useRef<Selection>({ start: 0, end: 0 });
|
|
167
|
-
const pendingCaretRef = useRef<number | null>(null);
|
|
168
|
-
const pendingSelectionRef = useRef<Selection | null>(null);
|
|
169
|
-
const editingRef = useRef(false);
|
|
75
|
+
const [numericValue, setNumericValue] = useState('');
|
|
76
|
+
const [lastDisplayValue, setLastDisplayValue] = useState('');
|
|
170
77
|
const inputRef = useRef<TextInput>(null);
|
|
78
|
+
const caretDigitsAfter = useRef<number | undefined>(undefined);
|
|
171
79
|
const componentName = 'InputMoney';
|
|
172
80
|
|
|
173
81
|
const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
|
|
@@ -177,94 +85,114 @@ const InputMoney = forwardRef(
|
|
|
177
85
|
props.accessibilityLabel,
|
|
178
86
|
);
|
|
179
87
|
|
|
180
|
-
const
|
|
88
|
+
const maxLength = (props as any)?.maxLength;
|
|
89
|
+
const hasMaxLength = typeof maxLength === 'number';
|
|
90
|
+
const digitLimit = hasMaxLength ? maxLength : Number.POSITIVE_INFINITY;
|
|
181
91
|
|
|
182
92
|
useEffect(() => {
|
|
183
93
|
if (value !== undefined && typeof value === 'string') {
|
|
184
94
|
const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
|
|
185
|
-
if (cleanValue !==
|
|
186
|
-
|
|
95
|
+
if (cleanValue !== numericValue) {
|
|
96
|
+
caretDigitsAfter.current = undefined;
|
|
97
|
+
setNumericValue(cleanValue);
|
|
98
|
+
updateDisplayValue(cleanValue);
|
|
187
99
|
}
|
|
188
100
|
}
|
|
189
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
101
|
}, [value]);
|
|
191
102
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
103
|
+
// Android moves the caret proportionally when the text is replaced, so it
|
|
104
|
+
// lands before the typed digit once a separator is added. Put it back after
|
|
105
|
+
// the same digit.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
setLastDisplayValue(displayValue);
|
|
108
|
+
const digitsAfter = caretDigitsAfter.current;
|
|
109
|
+
caretDigitsAfter.current = undefined;
|
|
110
|
+
if (Platform.OS !== 'android' || digitsAfter === undefined) return;
|
|
111
|
+
let before = parseInputText(displayValue).length - digitsAfter;
|
|
112
|
+
let caret = 0;
|
|
113
|
+
while (before > 0 && caret < displayValue.length) {
|
|
114
|
+
if (/\d/.test(displayValue[caret])) before -= 1;
|
|
115
|
+
caret += 1;
|
|
116
|
+
}
|
|
117
|
+
inputRef.current?.setSelection(caret, caret);
|
|
118
|
+
}, [displayValue]);
|
|
119
|
+
|
|
120
|
+
const updateDisplayValue = (numeric: string) => {
|
|
121
|
+
if (!numeric) {
|
|
122
|
+
setDisplayValue('');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const formatted = formatNumberToMoney(Number(numeric));
|
|
126
|
+
setDisplayValue(formatted ? `${formatted}${currency}` : '');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseInputText = (text: string): string => {
|
|
130
|
+
if (!text) return '';
|
|
131
|
+
const currencyRegex = new RegExp(` ?${currency}$`, 'g');
|
|
132
|
+
const textWithoutCurrency = text.replace(currencyRegex, '').trim();
|
|
133
|
+
return textWithoutCurrency.replace(/[^\d]/g, '');
|
|
134
|
+
};
|
|
198
135
|
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
setCommitCount(count => count + 1);
|
|
208
|
-
setDisplayValue(display);
|
|
209
|
-
return changed;
|
|
136
|
+
const updateNumericValue = (val: string) => {
|
|
137
|
+
let next = val.replace(/[^\d]/g, '').replace(/^0+(?=\d)/, '');
|
|
138
|
+
if (next.length > digitLimit) next = next.slice(0, digitLimit);
|
|
139
|
+
if (/^\d*$/.test(next)) {
|
|
140
|
+
setNumericValue(next);
|
|
141
|
+
updateDisplayValue(next);
|
|
142
|
+
onChangeText?.(next);
|
|
143
|
+
}
|
|
210
144
|
};
|
|
211
145
|
|
|
212
146
|
const onClearText = () => {
|
|
213
|
-
|
|
147
|
+
inputRef?.current?.clear();
|
|
148
|
+
inputRef.current?.setNativeProps({ text: '' });
|
|
149
|
+
setDisplayValue('');
|
|
150
|
+
setNumericValue('');
|
|
214
151
|
onChangeText?.('');
|
|
215
152
|
};
|
|
216
153
|
|
|
217
154
|
const _onChangeText = (text: string) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
editingRef.current = false;
|
|
224
|
-
});
|
|
225
|
-
if (text) {
|
|
226
|
-
const edit = detectEdit(displayRef.current, text, selectionRef.current);
|
|
227
|
-
({ digits, caretDigit } = applyEdit(
|
|
228
|
-
displayRef.current,
|
|
229
|
-
edit,
|
|
230
|
-
digitLimit,
|
|
231
|
-
));
|
|
155
|
+
if (!text) {
|
|
156
|
+
setDisplayValue('');
|
|
157
|
+
setNumericValue('');
|
|
158
|
+
onChangeText?.('');
|
|
159
|
+
return;
|
|
232
160
|
}
|
|
233
|
-
|
|
234
|
-
|
|
161
|
+
const old = lastDisplayValue;
|
|
162
|
+
let start = 0;
|
|
163
|
+
while (start < text.length && old[start] === text[start]) {
|
|
164
|
+
start += 1;
|
|
235
165
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
166
|
+
let suffix = 0;
|
|
167
|
+
while (
|
|
168
|
+
suffix < Math.min(text.length, old.length) - start &&
|
|
169
|
+
old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
|
|
170
|
+
) {
|
|
171
|
+
suffix += 1;
|
|
172
|
+
}
|
|
173
|
+
caretDigitsAfter.current = text
|
|
174
|
+
.slice(text.length - suffix)
|
|
175
|
+
.replace(/\D/g, '').length;
|
|
176
|
+
if (text.length < old.length) {
|
|
177
|
+
const removed = old.slice(start, old.length - suffix);
|
|
178
|
+
if (!/\d/.test(removed)) {
|
|
179
|
+
const index = removed.includes('.')
|
|
180
|
+
? parseInputText(old.slice(0, start)).length - 1
|
|
181
|
+
: numericValue.length - 1;
|
|
182
|
+
if (index >= 0) {
|
|
183
|
+
updateNumericValue(
|
|
184
|
+
numericValue.slice(0, index) + numericValue.slice(index + 1),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
256
188
|
}
|
|
257
|
-
}
|
|
189
|
+
}
|
|
190
|
+
const newNumericValue = parseInputText(text);
|
|
191
|
+
updateNumericValue(newNumericValue);
|
|
258
192
|
};
|
|
259
193
|
|
|
260
194
|
const _onFocus = (e: TextInputFocusEvent) => {
|
|
261
195
|
setFocused(true);
|
|
262
|
-
const caret = pendingCaretRef.current;
|
|
263
|
-
pendingCaretRef.current = null;
|
|
264
|
-
if (caret !== null && !props.selectTextOnFocus) {
|
|
265
|
-
selectionRef.current = { start: caret, end: caret };
|
|
266
|
-
inputRef.current?.setSelection(caret, caret);
|
|
267
|
-
}
|
|
268
196
|
onFocus?.(e);
|
|
269
197
|
};
|
|
270
198
|
|
|
@@ -274,13 +202,15 @@ const InputMoney = forwardRef(
|
|
|
274
202
|
};
|
|
275
203
|
|
|
276
204
|
const _setText = (text: string) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
205
|
+
caretDigitsAfter.current = undefined;
|
|
206
|
+
if (!text) {
|
|
207
|
+
setDisplayValue('');
|
|
208
|
+
setNumericValue('');
|
|
209
|
+
onChangeText?.('');
|
|
210
|
+
return;
|
|
283
211
|
}
|
|
212
|
+
const newNumericValue = parseInputText(text);
|
|
213
|
+
updateNumericValue(newNumericValue);
|
|
284
214
|
};
|
|
285
215
|
|
|
286
216
|
useImperativeHandle(ref, () => {
|
|
@@ -326,7 +256,6 @@ const InputMoney = forwardRef(
|
|
|
326
256
|
accessibilityLabel={`${componentId}`}
|
|
327
257
|
textAlignVertical="center"
|
|
328
258
|
ref={inputRef}
|
|
329
|
-
editable={props.editable !== false && !disabled}
|
|
330
259
|
accessibilityState={{
|
|
331
260
|
disabled: disabled,
|
|
332
261
|
...props.accessibilityState,
|
|
@@ -348,7 +277,6 @@ const InputMoney = forwardRef(
|
|
|
348
277
|
textBreakStrategy="highQuality"
|
|
349
278
|
value={displayValue}
|
|
350
279
|
onChangeText={_onChangeText}
|
|
351
|
-
onSelectionChange={_onSelectionChange}
|
|
352
280
|
onFocus={_onFocus}
|
|
353
281
|
onBlur={_onBlur}
|
|
354
282
|
placeholder={placeholder}
|