numeric-input-react 1.0.21 → 1.0.23
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/numeric-input.d.ts +2 -16
- package/dist/numeric-input.d.ts.map +1 -1
- package/dist/numeric-input.js +14 -638
- package/dist/numeric-input.js.map +1 -1
- package/dist/numeric-input.types.d.ts +15 -0
- package/dist/numeric-input.types.d.ts.map +1 -0
- package/dist/numeric-input.types.js +2 -0
- package/dist/numeric-input.types.js.map +1 -0
- package/dist/numeric-input.utils.d.ts +27 -0
- package/dist/numeric-input.utils.d.ts.map +1 -0
- package/dist/numeric-input.utils.js +143 -0
- package/dist/numeric-input.utils.js.map +1 -0
- package/dist/use-numeric-input.d.ts +28 -0
- package/dist/use-numeric-input.d.ts.map +1 -0
- package/dist/use-numeric-input.js +420 -0
- package/dist/use-numeric-input.js.map +1 -0
- package/package.json +17 -3
package/dist/numeric-input.js
CHANGED
|
@@ -1,645 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
const convertFullWidthToHalfWidth = (str) => {
|
|
9
|
-
return str
|
|
10
|
-
.replace(/[0-9]/g, (char) => {
|
|
11
|
-
// Convert full-width numbers (0-9) to half-width (0-9)
|
|
12
|
-
return String.fromCharCode(char.charCodeAt(0) - 0xfee0);
|
|
13
|
-
})
|
|
14
|
-
.replace(/[.]/g, '.') // Convert full-width period (.) to half-width (.)
|
|
15
|
-
.replace(/[,]/g, ',') // Convert full-width comma (,) to half-width (,)
|
|
16
|
-
.replace(/[-]/g, '-') // Convert full-width minus (-, U+FF0D) to half-width (-)
|
|
17
|
-
.replace(/[ー]/g, '-') // Convert katakana long vowel mark (ー, U+30FC) to minus (-) when used as minus
|
|
18
|
-
.replace(/[−]/g, '-'); // Convert mathematical minus sign (−, U+2212) to half-width (-)
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Escapes special regex characters in a string
|
|
22
|
-
*/
|
|
23
|
-
const escapeRegex = (str) => {
|
|
24
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Normalizes the input string by removing invalid characters
|
|
28
|
-
* and ensuring proper decimal point handling
|
|
29
|
-
*/
|
|
30
|
-
const normalizeNumericInput = (input, allowDecimal, allowNegative, maxLength) => {
|
|
31
|
-
let normalized = input;
|
|
32
|
-
// Remove all characters except digits, decimal point, and optionally minus sign
|
|
33
|
-
const allowedChars = allowDecimal
|
|
34
|
-
? allowNegative
|
|
35
|
-
? /[^0-9.\-]/g
|
|
36
|
-
: /[^0-9.]/g
|
|
37
|
-
: allowNegative
|
|
38
|
-
? /[^0-9\-]/g
|
|
39
|
-
: /[^0-9]/g;
|
|
40
|
-
normalized = normalized.replace(allowedChars, '');
|
|
41
|
-
// Handle negative sign: only allow at the start
|
|
42
|
-
if (allowNegative) {
|
|
43
|
-
const minusCount = (normalized.match(/-/g) || []).length;
|
|
44
|
-
if (minusCount > 1) {
|
|
45
|
-
// Keep only the first minus sign
|
|
46
|
-
normalized = normalized.replace(/-/g, (match, offset) => {
|
|
47
|
-
return offset === 0 ? match : '';
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// If minus is not at the start, move it to the start
|
|
51
|
-
if (normalized.includes('-') && !normalized.startsWith('-')) {
|
|
52
|
-
normalized = `-${normalized.replace(/-/g, '')}`;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
normalized = normalized.replace(/-/g, '');
|
|
57
|
-
}
|
|
58
|
-
// Handle decimal point: only allow one, and only if decimals are allowed
|
|
59
|
-
if (allowDecimal) {
|
|
60
|
-
const decimalCount = (normalized.match(/\./g) || []).length;
|
|
61
|
-
if (decimalCount > 1) {
|
|
62
|
-
// Keep only the first decimal point
|
|
63
|
-
const firstDecimalIndex = normalized.indexOf('.');
|
|
64
|
-
normalized =
|
|
65
|
-
normalized.slice(0, firstDecimalIndex + 1) +
|
|
66
|
-
normalized.slice(firstDecimalIndex + 1).replace(/\./g, '');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
normalized = normalized.replace(/\./g, '');
|
|
71
|
-
}
|
|
72
|
-
// Apply maxLength if specified
|
|
73
|
-
// maxLength should only apply to digits, not to minus sign or decimal point
|
|
74
|
-
if (maxLength) {
|
|
75
|
-
// Count only digits in the normalized string
|
|
76
|
-
const digitCount = (normalized.match(/\d/g) || []).length;
|
|
77
|
-
if (digitCount > maxLength) {
|
|
78
|
-
// Remove excess digits from the end, preserving minus sign and decimal point
|
|
79
|
-
const hasMinus = normalized.startsWith('-');
|
|
80
|
-
let result = hasMinus ? '-' : '';
|
|
81
|
-
let digitsSeen = 0;
|
|
82
|
-
let decimalAdded = false;
|
|
83
|
-
// Start from after minus sign if present
|
|
84
|
-
for (let i = hasMinus ? 1 : 0; i < normalized.length; i++) {
|
|
85
|
-
const char = normalized[i];
|
|
86
|
-
if (/\d/.test(char)) {
|
|
87
|
-
// Only keep digits up to maxLength
|
|
88
|
-
if (digitsSeen < maxLength) {
|
|
89
|
-
result += char;
|
|
90
|
-
digitsSeen++;
|
|
91
|
-
}
|
|
92
|
-
// Skip excess digits
|
|
93
|
-
}
|
|
94
|
-
else if (char === '.' && !decimalAdded) {
|
|
95
|
-
// Only keep decimal point if we have at least one digit and haven't added one yet
|
|
96
|
-
if (digitsSeen > 0) {
|
|
97
|
-
result += char;
|
|
98
|
-
decimalAdded = true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
normalized = result;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return normalized;
|
|
106
|
-
};
|
|
107
|
-
function NumericInput({ value, className, separator, onValueChange, onCompositionStart, onCompositionEnd, onBlur, maxLength, allowDecimal = false, allowNegative = false, minValue, maxValue, maxDecimalPlaces, ...props }) {
|
|
108
|
-
// Validate min/max values
|
|
109
|
-
if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
|
|
110
|
-
console.warn('NumericInput: minValue should be less than or equal to maxValue');
|
|
111
|
-
}
|
|
112
|
-
const isComposing = useRef(false);
|
|
113
|
-
const inputRef = useRef(null);
|
|
114
|
-
// Store the raw input value during IME composition
|
|
115
|
-
const [composingValue, setComposingValue] = useState('');
|
|
116
|
-
// Track if we've already processed the value from composition end
|
|
117
|
-
const hasProcessedComposition = useRef(false);
|
|
118
|
-
// Store the raw input string to preserve leading zeros
|
|
119
|
-
const [rawInputValue, setRawInputValue] = useState('');
|
|
120
|
-
const formatValue = useCallback((numValue) => {
|
|
121
|
-
if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
|
|
122
|
-
return '';
|
|
123
|
-
}
|
|
124
|
-
const valueStr = numValue.toString();
|
|
125
|
-
// If no separator, return as is
|
|
126
|
-
if (!separator) {
|
|
127
|
-
return valueStr;
|
|
128
|
-
}
|
|
129
|
-
// Split into integer and decimal parts
|
|
130
|
-
const [integerPart, decimalPart] = valueStr.split('.');
|
|
131
|
-
// Format integer part with separator (thousands separator)
|
|
132
|
-
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
|
133
|
-
// Combine with decimal part if exists
|
|
134
|
-
return decimalPart !== undefined
|
|
135
|
-
? `${formattedInteger}.${decimalPart}`
|
|
136
|
-
: formattedInteger;
|
|
137
|
-
}, [separator]);
|
|
138
|
-
const handleValueChange = useCallback((inputValue, skipCompositionCheck = false) => {
|
|
139
|
-
// During IME composition, update the composing value for display
|
|
140
|
-
// Don't convert full-width to half-width yet - wait for composition end
|
|
141
|
-
if (!skipCompositionCheck && isComposing.current) {
|
|
142
|
-
setComposingValue(inputValue);
|
|
143
|
-
// Store raw input value (could be full-width) for later processing
|
|
144
|
-
setRawInputValue(inputValue);
|
|
145
|
-
// Still notify parent but don't process the value
|
|
146
|
-
onValueChange({
|
|
147
|
-
value: 0,
|
|
148
|
-
formattedValue: inputValue,
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// Convert full-width Japanese characters to half-width
|
|
153
|
-
let rawValue = convertFullWidthToHalfWidth(inputValue);
|
|
154
|
-
// Remove scientific notation (e.g., "1e10", "1E10")
|
|
155
|
-
// This prevents unexpected number conversions
|
|
156
|
-
rawValue = rawValue.replace(/[eE]/g, '');
|
|
157
|
-
// Normalize the input (remove invalid chars, handle decimals, negatives)
|
|
158
|
-
rawValue = normalizeNumericInput(rawValue, allowDecimal, allowNegative, maxLength);
|
|
159
|
-
// Limit decimal places if specified
|
|
160
|
-
if (maxDecimalPlaces !== undefined && allowDecimal) {
|
|
161
|
-
const decimalIndex = rawValue.indexOf('.');
|
|
162
|
-
if (decimalIndex !== -1) {
|
|
163
|
-
const integerPart = rawValue.slice(0, decimalIndex);
|
|
164
|
-
const decimalPart = rawValue.slice(decimalIndex + 1);
|
|
165
|
-
if (decimalPart.length > maxDecimalPlaces) {
|
|
166
|
-
rawValue = `${integerPart}.${decimalPart.slice(0, maxDecimalPlaces)}`;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Handle empty input first (before processing leading zeros)
|
|
171
|
-
if (rawValue === '') {
|
|
172
|
-
setRawInputValue('');
|
|
173
|
-
onValueChange({
|
|
174
|
-
value: 0,
|
|
175
|
-
formattedValue: '',
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Handle only minus sign (half-width or full-width converted): preserve it if allowNegative is true
|
|
180
|
-
if (rawValue === '-') {
|
|
181
|
-
if (allowNegative) {
|
|
182
|
-
setRawInputValue('-');
|
|
183
|
-
onValueChange({
|
|
184
|
-
value: 0,
|
|
185
|
-
formattedValue: '-',
|
|
186
|
-
});
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
// If negative is not allowed, treat as empty
|
|
191
|
-
setRawInputValue('');
|
|
192
|
-
onValueChange({
|
|
193
|
-
value: 0,
|
|
194
|
-
formattedValue: '',
|
|
195
|
-
});
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Remove leading zeros except for single "0" or "0." patterns
|
|
200
|
-
// Only allow "0", "-0", "0.", "-0." to keep leading zero
|
|
201
|
-
// For cases like "01", "0123", "09999", "00.1" → remove leading zeros
|
|
202
|
-
const shouldKeepSingleZero = rawValue === '0' ||
|
|
203
|
-
rawValue === '-0' ||
|
|
204
|
-
rawValue === '0.' ||
|
|
205
|
-
rawValue === '-0.';
|
|
206
|
-
if (!shouldKeepSingleZero) {
|
|
207
|
-
// Remove leading zeros (but keep the minus sign if present)
|
|
208
|
-
if (rawValue.startsWith('-')) {
|
|
209
|
-
const withoutMinus = rawValue.slice(1);
|
|
210
|
-
// Split by decimal point to handle cases like "00.1"
|
|
211
|
-
if (withoutMinus.includes('.')) {
|
|
212
|
-
const [integerPart, decimalPart] = withoutMinus.split('.');
|
|
213
|
-
const cleanedInteger = integerPart.replace(/^0+/, '');
|
|
214
|
-
// If cleanedInteger is empty and there's a decimal part, keep "0"
|
|
215
|
-
if (cleanedInteger === '' && decimalPart) {
|
|
216
|
-
rawValue = `-0.${decimalPart}`;
|
|
217
|
-
}
|
|
218
|
-
else if (cleanedInteger === '') {
|
|
219
|
-
rawValue = '-0';
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
rawValue = `-${cleanedInteger}.${decimalPart}`;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
const withoutLeadingZeros = withoutMinus.replace(/^0+/, '');
|
|
227
|
-
rawValue =
|
|
228
|
-
withoutLeadingZeros === '' ? '-0' : `-${withoutLeadingZeros}`;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Split by decimal point to handle cases like "00.1"
|
|
233
|
-
if (rawValue.includes('.')) {
|
|
234
|
-
const [integerPart, decimalPart] = rawValue.split('.');
|
|
235
|
-
const cleanedInteger = integerPart.replace(/^0+/, '');
|
|
236
|
-
// If cleanedInteger is empty and there's a decimal part, keep "0"
|
|
237
|
-
if (cleanedInteger === '' && decimalPart) {
|
|
238
|
-
rawValue = `0.${decimalPart}`;
|
|
239
|
-
}
|
|
240
|
-
else if (cleanedInteger === '') {
|
|
241
|
-
rawValue = '0';
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
rawValue = `${cleanedInteger}.${decimalPart}`;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
const cleaned = rawValue.replace(/^0+/, '');
|
|
249
|
-
rawValue = cleaned === '' ? '0' : cleaned;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// Store the raw input value to preserve single "0" only
|
|
254
|
-
setRawInputValue(rawValue);
|
|
255
|
-
// Convert to number
|
|
256
|
-
const valueAsNumber = Number(rawValue);
|
|
257
|
-
// Handle invalid numbers
|
|
258
|
-
if (Number.isNaN(valueAsNumber) || !Number.isFinite(valueAsNumber)) {
|
|
259
|
-
setRawInputValue('');
|
|
260
|
-
onValueChange({
|
|
261
|
-
value: 0,
|
|
262
|
-
formattedValue: '',
|
|
263
|
-
});
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
// Handle value exceeding MAX_SAFE_INTEGER
|
|
267
|
-
if (Math.abs(valueAsNumber) > Number.MAX_SAFE_INTEGER) {
|
|
268
|
-
const clampedValue = valueAsNumber > 0 ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
|
|
269
|
-
const clampedString = clampedValue.toString();
|
|
270
|
-
setRawInputValue(clampedString);
|
|
271
|
-
onValueChange({
|
|
272
|
-
value: clampedValue,
|
|
273
|
-
formattedValue: formatValue(clampedValue),
|
|
274
|
-
});
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
// Only preserve single "0" or "0." patterns (not multiple leading zeros like "01", "0123")
|
|
278
|
-
const isSingleZero = rawValue === '0' ||
|
|
279
|
-
rawValue === '-0' ||
|
|
280
|
-
rawValue.startsWith('0.') ||
|
|
281
|
-
rawValue.startsWith('-0.');
|
|
282
|
-
// Check if the value ends with a decimal point (e.g., "2.", "-2.", "123.")
|
|
283
|
-
// This allows users to continue typing decimal digits
|
|
284
|
-
const endsWithDecimalPoint = allowDecimal && rawValue.endsWith('.') && !rawValue.endsWith('..');
|
|
285
|
-
// Apply min/max validation only for complete numbers (not intermediate typing states)
|
|
286
|
-
// Allow intermediate values while typing (e.g., allow "1000" if max is 100, user might be typing "100")
|
|
287
|
-
let finalValue = valueAsNumber;
|
|
288
|
-
let finalRawValue = rawValue;
|
|
289
|
-
let shouldClamp = false;
|
|
290
|
-
// Only clamp if the value is complete (not ending with decimal point and not a single zero pattern)
|
|
291
|
-
if (!isSingleZero && !endsWithDecimalPoint) {
|
|
292
|
-
if (minValue !== undefined && finalValue < minValue) {
|
|
293
|
-
finalValue = minValue;
|
|
294
|
-
finalRawValue = minValue.toString();
|
|
295
|
-
shouldClamp = true;
|
|
296
|
-
}
|
|
297
|
-
if (maxValue !== undefined && finalValue > maxValue) {
|
|
298
|
-
finalValue = maxValue;
|
|
299
|
-
finalRawValue = maxValue.toString();
|
|
300
|
-
shouldClamp = true;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// If clamped, update rawInputValue
|
|
304
|
-
if (shouldClamp) {
|
|
305
|
-
setRawInputValue(finalRawValue);
|
|
306
|
-
}
|
|
307
|
-
// If it's a single zero pattern or ends with decimal point, use the raw value for display
|
|
308
|
-
if (isSingleZero || endsWithDecimalPoint) {
|
|
309
|
-
// Use the raw value as-is to preserve single "0" or trailing decimal point
|
|
310
|
-
onValueChange({
|
|
311
|
-
value: finalValue,
|
|
312
|
-
formattedValue: shouldClamp ? formatValue(finalValue) : rawValue,
|
|
313
|
-
});
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
// Valid number without leading zeros - format and return
|
|
317
|
-
onValueChange({
|
|
318
|
-
value: finalValue,
|
|
319
|
-
formattedValue: formatValue(finalValue),
|
|
320
|
-
});
|
|
321
|
-
}, [
|
|
322
|
-
allowDecimal,
|
|
323
|
-
allowNegative,
|
|
324
|
-
maxLength,
|
|
325
|
-
onValueChange,
|
|
326
|
-
formatValue,
|
|
327
|
-
separator,
|
|
3
|
+
import { useNumericInput } from './use-numeric-input';
|
|
4
|
+
const NumericInput = ({ value, maxValue, minValue, separator, maxLength, className, maxDecimalPlaces, allowDecimal = false, allowNegative = false, onBlur, onValueChange, onCompositionEnd, onCompositionStart, ...props }) => {
|
|
5
|
+
const { inputRef, inputMode, displayValue, hasProcessedComposition, handleBlur, handleValueChange, handleCompositionEnd, handleCompositionStart, } = useNumericInput({
|
|
6
|
+
value,
|
|
328
7
|
minValue,
|
|
329
8
|
maxValue,
|
|
9
|
+
separator,
|
|
10
|
+
maxLength,
|
|
11
|
+
allowDecimal,
|
|
12
|
+
allowNegative,
|
|
330
13
|
maxDecimalPlaces,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
setComposingValue(e.currentTarget.value);
|
|
337
|
-
// Handle custom onCompositionStart
|
|
338
|
-
if (onCompositionStart) {
|
|
339
|
-
onCompositionStart(e);
|
|
340
|
-
}
|
|
341
|
-
}, [onCompositionStart]);
|
|
342
|
-
const handleCompositionEnd = useCallback((e) => {
|
|
343
|
-
isComposing.current = false;
|
|
344
|
-
const finalValue = e.currentTarget.value;
|
|
345
|
-
// Clear the composing value
|
|
346
|
-
setComposingValue('');
|
|
347
|
-
// Mark that we've processed composition to prevent duplicate processing in onChange
|
|
348
|
-
hasProcessedComposition.current = true;
|
|
349
|
-
// Handle custom onCompositionEnd
|
|
350
|
-
if (onCompositionEnd) {
|
|
351
|
-
onCompositionEnd(e);
|
|
352
|
-
}
|
|
353
|
-
// Process the value after composition ends
|
|
354
|
-
// Convert full-width to half-width and preserve minus sign if needed
|
|
355
|
-
// Use requestAnimationFrame to ensure it happens after any pending onChange events
|
|
356
|
-
requestAnimationFrame(() => {
|
|
357
|
-
// Convert full-width to half-width before processing
|
|
358
|
-
const convertedValue = convertFullWidthToHalfWidth(finalValue);
|
|
359
|
-
// If the converted value is just a minus sign, preserve it
|
|
360
|
-
if (allowNegative && convertedValue === '-') {
|
|
361
|
-
setRawInputValue('-');
|
|
362
|
-
onValueChange({
|
|
363
|
-
value: 0,
|
|
364
|
-
formattedValue: '-',
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
// Process normally
|
|
369
|
-
handleValueChange(convertedValue, true);
|
|
370
|
-
}
|
|
371
|
-
// Reset flag after processing
|
|
372
|
-
hasProcessedComposition.current = false;
|
|
373
|
-
});
|
|
374
|
-
}, [onCompositionEnd, handleValueChange, allowNegative]);
|
|
375
|
-
const handleBlur = useCallback((e) => {
|
|
376
|
-
// Check if we need to preserve minus sign before processing
|
|
377
|
-
// Check both half-width and full-width minus in rawInputValue and e.target.value
|
|
378
|
-
// Also check katakana long vowel mark (ー) and mathematical minus sign (−) which can be used as minus
|
|
379
|
-
const currentValue = e.target.value;
|
|
380
|
-
const isCurrentValueMinus = currentValue === '-' || currentValue === '-' || currentValue === 'ー' || currentValue === '−';
|
|
381
|
-
const isRawInputMinus = rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−';
|
|
382
|
-
const shouldPreserveMinus = allowNegative && (isRawInputMinus || isCurrentValueMinus);
|
|
383
|
-
// If still composing when blur happens, force end composition
|
|
384
|
-
if (isComposing.current) {
|
|
385
|
-
isComposing.current = false;
|
|
386
|
-
const finalValue = e.target.value;
|
|
387
|
-
setComposingValue('');
|
|
388
|
-
hasProcessedComposition.current = true;
|
|
389
|
-
// Convert full-width to half-width before processing
|
|
390
|
-
const convertedValue = convertFullWidthToHalfWidth(finalValue);
|
|
391
|
-
// If the converted value is just a minus sign, preserve it
|
|
392
|
-
if (allowNegative && convertedValue === '-') {
|
|
393
|
-
setRawInputValue('-');
|
|
394
|
-
onValueChange({
|
|
395
|
-
value: 0,
|
|
396
|
-
formattedValue: '-',
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
// Process the value immediately
|
|
401
|
-
handleValueChange(convertedValue, true);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
else if (composingValue !== '') {
|
|
405
|
-
// If there's a composing value but not composing, process it
|
|
406
|
-
// Convert full-width to half-width before processing
|
|
407
|
-
const convertedValue = convertFullWidthToHalfWidth(composingValue);
|
|
408
|
-
// If the converted value is just a minus sign, preserve it
|
|
409
|
-
if (allowNegative && convertedValue === '-') {
|
|
410
|
-
setRawInputValue('-');
|
|
411
|
-
onValueChange({
|
|
412
|
-
value: 0,
|
|
413
|
-
formattedValue: '-',
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
handleValueChange(convertedValue, true);
|
|
418
|
-
}
|
|
419
|
-
setComposingValue('');
|
|
420
|
-
}
|
|
421
|
-
else if (!hasProcessedComposition.current && e.target.value) {
|
|
422
|
-
// If we haven't processed composition and there's a value, process it
|
|
423
|
-
// Convert full-width to half-width before processing
|
|
424
|
-
const convertedValue = convertFullWidthToHalfWidth(e.target.value);
|
|
425
|
-
// Process the value - handleValueChange will preserve minus sign if present
|
|
426
|
-
handleValueChange(convertedValue, true);
|
|
427
|
-
}
|
|
428
|
-
// Apply min/max validation on blur for any intermediate values
|
|
429
|
-
// This ensures values are clamped even if user was typing an out-of-range value
|
|
430
|
-
// But preserve intermediate states like "-" (minus sign only, half-width or full-width)
|
|
431
|
-
if (rawInputValue !== '') {
|
|
432
|
-
// Preserve minus sign only if allowNegative is true - skip clamp validation
|
|
433
|
-
// Check half-width, full-width minus, katakana long vowel mark (ー), and mathematical minus sign (−)
|
|
434
|
-
const isMinusOnly = allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−');
|
|
435
|
-
if (!isMinusOnly) {
|
|
436
|
-
// Convert to half-width for number conversion
|
|
437
|
-
const convertedValue = convertFullWidthToHalfWidth(rawInputValue);
|
|
438
|
-
const numValue = Number(convertedValue);
|
|
439
|
-
if (!Number.isNaN(numValue) && Number.isFinite(numValue)) {
|
|
440
|
-
let clampedValue = numValue;
|
|
441
|
-
let shouldUpdate = false;
|
|
442
|
-
if (minValue !== undefined && clampedValue < minValue) {
|
|
443
|
-
clampedValue = minValue;
|
|
444
|
-
shouldUpdate = true;
|
|
445
|
-
}
|
|
446
|
-
if (maxValue !== undefined && clampedValue > maxValue) {
|
|
447
|
-
clampedValue = maxValue;
|
|
448
|
-
shouldUpdate = true;
|
|
449
|
-
}
|
|
450
|
-
if (shouldUpdate) {
|
|
451
|
-
const clampedString = clampedValue.toString();
|
|
452
|
-
setRawInputValue(clampedString);
|
|
453
|
-
onValueChange({
|
|
454
|
-
value: clampedValue,
|
|
455
|
-
formattedValue: formatValue(clampedValue),
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// If we need to preserve minus sign (only when value is just minus, no numbers), ensure it's still set as half-width
|
|
462
|
-
// Check both current rawInputValue and the value from input element
|
|
463
|
-
// Only preserve if the value is just a minus sign, not if it has numbers (those are handled by handleValueChange)
|
|
464
|
-
if (shouldPreserveMinus) {
|
|
465
|
-
// Check if rawInputValue is just a minus sign (not a number with minus)
|
|
466
|
-
const isJustMinus = rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−';
|
|
467
|
-
if (isJustMinus) {
|
|
468
|
-
// Convert any full-width minus to half-width
|
|
469
|
-
const finalMinusValue = '-';
|
|
470
|
-
if (rawInputValue !== finalMinusValue) {
|
|
471
|
-
setRawInputValue(finalMinusValue);
|
|
472
|
-
onValueChange({
|
|
473
|
-
value: 0,
|
|
474
|
-
formattedValue: finalMinusValue,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
// If rawInputValue has numbers (e.g., "-123"), handleValueChange already processed it correctly
|
|
479
|
-
}
|
|
480
|
-
// Reset the flag
|
|
481
|
-
hasProcessedComposition.current = false;
|
|
482
|
-
// Call custom onBlur if provided
|
|
483
|
-
if (onBlur) {
|
|
484
|
-
onBlur(e);
|
|
485
|
-
}
|
|
486
|
-
}, [composingValue, onBlur, handleValueChange, rawInputValue, minValue, maxValue, formatValue, allowNegative]);
|
|
487
|
-
// Reset rawInputValue when value prop changes externally (e.g., form reset)
|
|
488
|
-
useEffect(() => {
|
|
489
|
-
if (value === null || value === undefined || value === '') {
|
|
490
|
-
// Preserve "-", "-", "ー", or "−" if allowNegative is true and user is typing negative number
|
|
491
|
-
if (allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−')) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
setRawInputValue('');
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
// Convert value to number if it's a string
|
|
498
|
-
// Escape separator for regex if it exists
|
|
499
|
-
const numValue = typeof value === 'string'
|
|
500
|
-
? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
|
|
501
|
-
: Number(value);
|
|
502
|
-
// If the value is 0, preserve rawInputValue if it's "0", "-0", "0.", "-0.", "-", "-", "ー", or "−"
|
|
503
|
-
// Also preserve negative numbers when allowNegative is true (user might be typing)
|
|
504
|
-
// Otherwise, if value prop is 0 (controlled from outside), set rawInputValue to "0" to display it
|
|
505
|
-
if (numValue === 0) {
|
|
506
|
-
const isSingleZero = rawInputValue === '0' ||
|
|
507
|
-
rawInputValue === '-0' ||
|
|
508
|
-
rawInputValue === '-' ||
|
|
509
|
-
rawInputValue === '-' ||
|
|
510
|
-
rawInputValue === 'ー' ||
|
|
511
|
-
rawInputValue === '−' ||
|
|
512
|
-
rawInputValue.startsWith('0.') ||
|
|
513
|
-
rawInputValue.startsWith('-0.');
|
|
514
|
-
// Check if rawInputValue is a negative number (preserve it when allowNegative is true)
|
|
515
|
-
if (allowNegative && rawInputValue !== '') {
|
|
516
|
-
const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
|
|
517
|
-
const rawAsNumber = Number(convertedRawValue);
|
|
518
|
-
// If it's a valid negative number, preserve it
|
|
519
|
-
if (!Number.isNaN(rawAsNumber) && Number.isFinite(rawAsNumber) && rawAsNumber < 0) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (!isSingleZero) {
|
|
524
|
-
// If value prop is 0 from outside, we should display "0"
|
|
525
|
-
// Set rawInputValue to "0" so it can be displayed
|
|
526
|
-
setRawInputValue('0');
|
|
527
|
-
}
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
// For non-zero values, check if the numeric value matches what we'd get from rawInputValue
|
|
531
|
-
// But preserve intermediate states like "-", "-", or "ー" (minus sign only)
|
|
532
|
-
// Also preserve negative numbers that start with minus sign when allowNegative is true
|
|
533
|
-
if (rawInputValue !== '') {
|
|
534
|
-
// Preserve minus sign only if allowNegative is true (half-width, full-width, katakana, and mathematical minus)
|
|
535
|
-
if (allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−')) {
|
|
536
|
-
// Don't clear rawInputValue if it's just a minus sign
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
// Convert to half-width for number comparison
|
|
540
|
-
const convertedRawValue = convertFullWidthToHalfWidth(rawInputValue);
|
|
541
|
-
const rawAsNumber = Number(convertedRawValue);
|
|
542
|
-
// If rawInputValue starts with minus and allowNegative is true, preserve it
|
|
543
|
-
// This handles cases where user is typing negative numbers and value prop might not match yet
|
|
544
|
-
if (allowNegative && convertedRawValue.startsWith('-')) {
|
|
545
|
-
// Always preserve negative numbers when allowNegative is true
|
|
546
|
-
// Only clear if value prop is a positive number that clearly doesn't match
|
|
547
|
-
// (e.g., rawInputValue is "-123" but numValue is 123 - signs differ)
|
|
548
|
-
if (rawAsNumber === numValue) {
|
|
549
|
-
// They match, keep rawInputValue
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
else if (numValue > 0 && Math.abs(rawAsNumber) === numValue) {
|
|
553
|
-
// Value prop is positive but rawInputValue is negative with same absolute value
|
|
554
|
-
// This means parent explicitly set a positive value, so clear rawInputValue
|
|
555
|
-
setRawInputValue('');
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
// In all other cases (numValue is 0, negative, or doesn't match), preserve rawInputValue
|
|
559
|
-
// This ensures user's typing is not lost
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
// For non-negative values, check if they match
|
|
565
|
-
if (rawAsNumber !== numValue) {
|
|
566
|
-
// Value changed externally, clear rawInputValue
|
|
567
|
-
setRawInputValue('');
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}, [value, separator, rawInputValue, allowNegative]);
|
|
572
|
-
// Format the display value
|
|
573
|
-
const displayValue = useMemo(() => {
|
|
574
|
-
// If currently composing, use the composing value (this allows IME input to display)
|
|
575
|
-
if (composingValue !== '') {
|
|
576
|
-
return composingValue;
|
|
577
|
-
}
|
|
578
|
-
// If rawInputValue is empty, check if we should display the value prop
|
|
579
|
-
// This handles both: value prop from outside, and user deleting content
|
|
580
|
-
if (rawInputValue === '') {
|
|
581
|
-
if (value === null || value === undefined || value === '') {
|
|
582
|
-
return '';
|
|
583
|
-
}
|
|
584
|
-
// Convert value to number if it's a string
|
|
585
|
-
// Escape separator for regex if it exists
|
|
586
|
-
const numValue = typeof value === 'string'
|
|
587
|
-
? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
|
|
588
|
-
: Number(value);
|
|
589
|
-
// If value is 0 and rawInputValue is empty, show "0" (value prop from outside)
|
|
590
|
-
// This allows displaying 0 when it's passed as a prop
|
|
591
|
-
// Note: If user deletes content, onValueChange is called with formattedValue: '',
|
|
592
|
-
// and parent should update value prop to null/undefined/'' to hide "0"
|
|
593
|
-
if (numValue === 0) {
|
|
594
|
-
return '0';
|
|
595
|
-
}
|
|
596
|
-
// For non-zero values, format and display them
|
|
597
|
-
if (!Number.isNaN(numValue) && Number.isFinite(numValue)) {
|
|
598
|
-
return formatValue(numValue);
|
|
599
|
-
}
|
|
600
|
-
return '';
|
|
601
|
-
}
|
|
602
|
-
// If we have a raw input value with single zero, minus sign only, or ending with decimal point, use it for display
|
|
603
|
-
if (rawInputValue !== '') {
|
|
604
|
-
const isSingleZero = rawInputValue === '0' ||
|
|
605
|
-
rawInputValue === '-0' ||
|
|
606
|
-
rawInputValue.startsWith('0.') ||
|
|
607
|
-
rawInputValue.startsWith('-0.');
|
|
608
|
-
// Check half-width, full-width minus, katakana long vowel mark (ー), and mathematical minus sign (−)
|
|
609
|
-
const isMinusOnly = allowNegative && (rawInputValue === '-' || rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−');
|
|
610
|
-
const endsWithDecimalPoint = allowDecimal &&
|
|
611
|
-
rawInputValue.endsWith('.') &&
|
|
612
|
-
!rawInputValue.endsWith('..');
|
|
613
|
-
if (isSingleZero || isMinusOnly || endsWithDecimalPoint) {
|
|
614
|
-
// If it's full-width minus, katakana long vowel mark, or mathematical minus sign, convert to half-width for display
|
|
615
|
-
if (rawInputValue === '-' || rawInputValue === 'ー' || rawInputValue === '−') {
|
|
616
|
-
return '-';
|
|
617
|
-
}
|
|
618
|
-
return rawInputValue;
|
|
619
|
-
}
|
|
620
|
-
// If rawInputValue is not empty and doesn't match special cases, use it to calculate display value
|
|
621
|
-
// This handles the case where user is typing but value prop hasn't been updated yet
|
|
622
|
-
const rawAsNumber = Number(rawInputValue);
|
|
623
|
-
if (!Number.isNaN(rawAsNumber) && Number.isFinite(rawAsNumber)) {
|
|
624
|
-
return formatValue(rawAsNumber);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
if (value === null || value === undefined || value === '') {
|
|
628
|
-
return '';
|
|
629
|
-
}
|
|
630
|
-
// Convert value to number if it's a string
|
|
631
|
-
// Escape separator for regex if it exists
|
|
632
|
-
const numValue = typeof value === 'string'
|
|
633
|
-
? Number(value.replace(new RegExp(`[${separator ? escapeRegex(separator) : ''}]`, 'g'), ''))
|
|
634
|
-
: Number(value);
|
|
635
|
-
if (Number.isNaN(numValue)) {
|
|
636
|
-
return '';
|
|
637
|
-
}
|
|
638
|
-
// Format and return the value
|
|
639
|
-
return formatValue(numValue);
|
|
640
|
-
}, [value, formatValue, separator, composingValue, rawInputValue, allowNegative, allowDecimal]);
|
|
641
|
-
// Determine appropriate inputMode for mobile keyboards
|
|
642
|
-
const inputMode = allowDecimal ? 'decimal' : 'numeric';
|
|
14
|
+
onBlur,
|
|
15
|
+
onValueChange,
|
|
16
|
+
onCompositionEnd,
|
|
17
|
+
onCompositionStart,
|
|
18
|
+
});
|
|
643
19
|
return (_jsx("input", { ref: inputRef, type: "text", inputMode: inputMode, value: displayValue, className: className, onCompositionEnd: handleCompositionEnd, onCompositionStart: handleCompositionStart, onBlur: handleBlur, onChange: (e) => {
|
|
644
20
|
// Skip onChange if we just processed composition to avoid duplicate processing
|
|
645
21
|
// This prevents duplicate when composition end and onChange fire in quick succession
|
|
@@ -648,6 +24,6 @@ function NumericInput({ value, className, separator, onValueChange, onCompositio
|
|
|
648
24
|
}
|
|
649
25
|
handleValueChange(e.target.value);
|
|
650
26
|
}, ...props }));
|
|
651
|
-
}
|
|
27
|
+
};
|
|
652
28
|
export { NumericInput };
|
|
653
29
|
//# sourceMappingURL=numeric-input.js.map
|