@momo-kits/foundation 0.168.1-beta.1 → 0.168.1-beta.3

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 +62 -173
  2. package/package.json +1 -1
@@ -4,14 +4,12 @@ import React, {
4
4
  useContext,
5
5
  useEffect,
6
6
  useImperativeHandle,
7
- useLayoutEffect,
8
7
  useRef,
9
8
  useState,
10
9
  } from 'react';
11
10
  import {
12
11
  TextInput,
13
12
  TextInputFocusEvent,
14
- TextInputSelectionChangeEvent,
15
13
  TouchableOpacity,
16
14
  View,
17
15
  } from 'react-native';
@@ -33,101 +31,9 @@ import {
33
31
  } from './common';
34
32
  import { InputMoneyProps } from './index';
35
33
  import styles from './styles';
34
+ import { formatNumberToMoney } from './utils';
36
35
  import SystemTextInput from './SystemTextInput';
37
36
 
38
- type Selection = { start: number; end: number };
39
- type Edit = { start: number; end: number; inserted: string };
40
-
41
- const countDigits = (text: string) => text.replace(/\D/g, '').length;
42
-
43
- const groupDigits = (digits: string) =>
44
- digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
45
-
46
- // Caret position right after the `digitIndex`-th digit of `display`.
47
- const caretFromDigitIndex = (display: string, digitIndex: number) => {
48
- let count = 0;
49
- for (let i = 0; i < display.length && count < digitIndex; i++) {
50
- if (/\d/.test(display[i])) {
51
- count += 1;
52
- if (count === digitIndex) return i + 1;
53
- }
54
- }
55
- return 0;
56
- };
57
-
58
- const detectEdit = (old: string, next: string, selection: Selection): Edit => {
59
- const { start, end } = selection;
60
- const removed = old.length - next.length;
61
- const candidates: Edit[] = [
62
- {
63
- start,
64
- end,
65
- inserted: next.slice(start, next.length - (old.length - end)),
66
- },
67
- { start: start - removed, end: start, inserted: '' },
68
- { start, end: start + removed, inserted: '' },
69
- ];
70
- const match = candidates.find(
71
- e =>
72
- e.start >= 0 &&
73
- e.end >= e.start &&
74
- e.end <= old.length &&
75
- old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
76
- );
77
- if (match) return match;
78
-
79
- let prefix = 0;
80
- while (
81
- prefix < Math.min(old.length, next.length) &&
82
- old[prefix] === next[prefix]
83
- ) {
84
- prefix += 1;
85
- }
86
- let suffix = 0;
87
- while (
88
- suffix < Math.min(old.length, next.length) - prefix &&
89
- old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
90
- ) {
91
- suffix += 1;
92
- }
93
- return {
94
- start: prefix,
95
- end: old.length - suffix,
96
- inserted: next.slice(prefix, next.length - suffix),
97
- };
98
- };
99
-
100
- // Applies `edit` to the digits of `display`. Separators and currency are never
101
- // deleted: removing a separator deletes the digit before it instead, removing
102
- // only the currency leaves the digits untouched.
103
- const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
104
- const digits = display.replace(/\D/g, '');
105
- let left = countDigits(display.slice(0, edit.start));
106
- let removed = countDigits(display.slice(edit.start, edit.end));
107
- const inserted = edit.inserted.replace(/\D/g, '');
108
- if (
109
- !removed &&
110
- !inserted &&
111
- left > 0 &&
112
- display.slice(edit.start, edit.end).includes('.')
113
- ) {
114
- left -= 1;
115
- removed = 1;
116
- }
117
- const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
118
- const trimmed = next.replace(/^0+(?=\d)/, '');
119
- if ((!removed && !inserted) || trimmed.length > digitLimit) {
120
- return { digits, caretDigit: left };
121
- }
122
- return {
123
- digits: trimmed,
124
- caretDigit: Math.max(
125
- 0,
126
- left + inserted.length - (next.length - trimmed.length),
127
- ),
128
- };
129
- };
130
-
131
37
  const InputMoney = forwardRef(
132
38
  (
133
39
  {
@@ -166,13 +72,7 @@ const InputMoney = forwardRef(
166
72
  const [focused, setFocused] = useState(false);
167
73
  const [displayValue, setDisplayValue] = useState('');
168
74
  const [numericValue, setNumericValue] = useState('');
169
- const [commitCount, setCommitCount] = useState(0);
170
- const displayRef = useRef('');
171
- const selectionRef = useRef<Selection>({ start: 0, end: 0 });
172
- // Caret to push to native once the committed text is rendered.
173
- const pendingCaretRef = useRef<number | null>(null);
174
- const pendingSelectionRef = useRef<Selection | null>(null);
175
- const editingRef = useRef(false);
75
+ const [lastDisplayValue, setLastDisplayValue] = useState('');
176
76
  const inputRef = useRef<TextInput>(null);
177
77
  const componentName = 'InputMoney';
178
78
 
@@ -191,34 +91,23 @@ const InputMoney = forwardRef(
191
91
  if (value !== undefined && typeof value === 'string') {
192
92
  const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
193
93
  if (cleanValue !== numericValue) {
194
- commit(cleanValue, cleanValue.length);
94
+ setNumericValue(cleanValue);
95
+ updateDisplayValue(cleanValue);
195
96
  }
196
97
  }
197
98
  }, [value]);
198
99
 
199
- // Always set the caret imperatively: a controlled `selection` prop is only
200
- // sent to native when it differs from the last native selection, so it is
201
- // skipped whenever a stale selection event already reported the same value.
202
- useLayoutEffect(() => {
203
- const caret = pendingCaretRef.current;
204
- if (caret === null) return;
205
- pendingCaretRef.current = null;
206
- if (inputRef.current?.isFocused()) {
207
- inputRef.current.setSelection(caret, caret);
208
- }
209
- }, [commitCount]);
100
+ useEffect(() => {
101
+ setLastDisplayValue(displayValue);
102
+ }, [displayValue]);
210
103
 
211
- const commit = (digits: string, caretDigit: number) => {
212
- const display = digits ? `${groupDigits(digits)}${currency}` : '';
213
- const caret = caretFromDigitIndex(display, caretDigit);
214
- const changed = digits !== numericValue;
215
- displayRef.current = display;
216
- selectionRef.current = { start: caret, end: caret };
217
- pendingCaretRef.current = caret;
218
- setCommitCount(count => count + 1);
219
- setDisplayValue(display);
220
- setNumericValue(digits);
221
- return changed;
104
+ const updateDisplayValue = (numeric: string) => {
105
+ if (!numeric) {
106
+ setDisplayValue('');
107
+ return;
108
+ }
109
+ const formatted = formatNumberToMoney(Number(numeric));
110
+ setDisplayValue(formatted ? `${formatted}${currency}` : '');
222
111
  };
223
112
 
224
113
  const parseInputText = (text: string): string => {
@@ -228,55 +117,59 @@ const InputMoney = forwardRef(
228
117
  return textWithoutCurrency.replace(/[^\d]/g, '');
229
118
  };
230
119
 
120
+ const updateNumericValue = (val: string) => {
121
+ let next = val.replace(/[^\d]/g, '').replace(/^0+(?=\d)/, '');
122
+ if (next.length > digitLimit) next = next.slice(0, digitLimit);
123
+ if (/^\d*$/.test(next)) {
124
+ setNumericValue(next);
125
+ updateDisplayValue(next);
126
+ onChangeText?.(next);
127
+ }
128
+ };
129
+
231
130
  const onClearText = () => {
232
131
  inputRef?.current?.clear();
233
132
  inputRef.current?.setNativeProps({ text: '' });
234
- commit('', 0);
133
+ setDisplayValue('');
134
+ setNumericValue('');
235
135
  onChangeText?.('');
236
136
  };
237
137
 
238
138
  const _onChangeText = (text: string) => {
239
- let digits = '';
240
- let caretDigit = 0;
241
- pendingSelectionRef.current = null;
242
- editingRef.current = true;
243
- setTimeout(() => {
244
- editingRef.current = false;
245
- });
246
- if (text) {
247
- const edit = detectEdit(displayRef.current, text, selectionRef.current);
248
- ({ digits, caretDigit } = applyEdit(
249
- displayRef.current,
250
- edit,
251
- digitLimit,
252
- ));
253
- }
254
- if (commit(digits, caretDigit)) {
255
- onChangeText?.(digits);
139
+ if (!text) {
140
+ setDisplayValue('');
141
+ setNumericValue('');
142
+ onChangeText?.('');
143
+ return;
256
144
  }
257
- };
258
-
259
- // Keeps the caret out of the currency suffix.
260
- const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
261
- props.onSelectionChange?.(e);
262
- if (editingRef.current) return;
263
- const selection = e.nativeEvent.selection;
264
- pendingSelectionRef.current = selection;
265
- setTimeout(() => {
266
- if (pendingSelectionRef.current !== selection) return;
267
- pendingSelectionRef.current = null;
268
- const { start, end } = selection;
269
- const display = displayRef.current;
270
- const max = display ? display.length - currency.length : 0;
271
- const clamped = {
272
- start: Math.min(start, max),
273
- end: Math.min(end, max),
274
- };
275
- selectionRef.current = clamped;
276
- if (clamped.start !== start || clamped.end !== end) {
277
- inputRef.current?.setSelection(clamped.start, clamped.end);
145
+ if (text.length < lastDisplayValue.length) {
146
+ const old = lastDisplayValue;
147
+ let start = 0;
148
+ while (start < text.length && old[start] === text[start]) {
149
+ start += 1;
150
+ }
151
+ let suffix = 0;
152
+ while (
153
+ suffix < text.length - start &&
154
+ old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
155
+ ) {
156
+ suffix += 1;
278
157
  }
279
- });
158
+ const removed = old.slice(start, old.length - suffix);
159
+ if (!/\d/.test(removed)) {
160
+ const index = removed.includes('.')
161
+ ? parseInputText(old.slice(0, start)).length - 1
162
+ : numericValue.length - 1;
163
+ if (index >= 0) {
164
+ updateNumericValue(
165
+ numericValue.slice(0, index) + numericValue.slice(index + 1),
166
+ );
167
+ }
168
+ return;
169
+ }
170
+ }
171
+ const newNumericValue = parseInputText(text);
172
+ updateNumericValue(newNumericValue);
280
173
  };
281
174
 
282
175
  const _onFocus = (e: TextInputFocusEvent) => {
@@ -291,16 +184,13 @@ const InputMoney = forwardRef(
291
184
 
292
185
  const _setText = (text: string) => {
293
186
  if (!text) {
294
- commit('', 0);
187
+ setDisplayValue('');
188
+ setNumericValue('');
295
189
  onChangeText?.('');
296
190
  return;
297
191
  }
298
- const digits = parseInputText(text)
299
- .replace(/^0+(?=\d)/, '')
300
- .slice(0, digitLimit);
301
- if (commit(digits, digits.length)) {
302
- onChangeText?.(digits);
303
- }
192
+ const newNumericValue = parseInputText(text);
193
+ updateNumericValue(newNumericValue);
304
194
  };
305
195
 
306
196
  useImperativeHandle(ref, () => {
@@ -367,7 +257,6 @@ const InputMoney = forwardRef(
367
257
  textBreakStrategy="highQuality"
368
258
  value={displayValue}
369
259
  onChangeText={_onChangeText}
370
- onSelectionChange={_onSelectionChange}
371
260
  onFocus={_onFocus}
372
261
  onBlur={_onBlur}
373
262
  placeholder={placeholder}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.168.1-beta.1",
3
+ "version": "0.168.1-beta.3",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},