UNPKG

17.8 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
3import _typeof from "@babel/runtime/helpers/esm/typeof";
4import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
5import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
6var _excluded = ["prefixCls", "className", "style", "min", "max", "step", "defaultValue", "value", "disabled", "readOnly", "upHandler", "downHandler", "keyboard", "controls", "stringMode", "parser", "formatter", "precision", "decimalSeparator", "onChange", "onInput", "onPressEnter", "onStep"];
7import * as React from 'react';
8import classNames from 'classnames';
9import KeyCode from "rc-util/es/KeyCode";
10import { useLayoutUpdateEffect } from "rc-util/es/hooks/useLayoutEffect";
11import { composeRef } from "rc-util/es/ref";
12import getMiniDecimal, { toFixed } from './utils/MiniDecimal';
13import StepHandler from './StepHandler';
14import { getNumberPrecision, num2str, getDecupleSteps, validateNumber } from './utils/numberUtil';
15import useCursor from './hooks/useCursor';
16import useFrame from './hooks/useFrame';
17/**
18 * We support `stringMode` which need handle correct type when user call in onChange
19 * format max or min value
20 * 1. if isInvalid return null
21 * 2. if precision is undefined, return decimal
22 * 3. format with precision
23 * I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3
24 * II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4
25 * III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4
26 * IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3
27 */
28
29var getDecimalValue = function getDecimalValue(stringMode, decimalValue) {
30 if (stringMode || decimalValue.isEmpty()) {
31 return decimalValue.toString();
32 }
33
34 return decimalValue.toNumber();
35};
36
37var getDecimalIfValidate = function getDecimalIfValidate(value) {
38 var decimal = getMiniDecimal(value);
39 return decimal.isInvalidate() ? null : decimal;
40};
41
42var InputNumber = /*#__PURE__*/React.forwardRef(function (props, ref) {
43 var _classNames;
44
45 var _props$prefixCls = props.prefixCls,
46 prefixCls = _props$prefixCls === void 0 ? 'rc-input-number' : _props$prefixCls,
47 className = props.className,
48 style = props.style,
49 min = props.min,
50 max = props.max,
51 _props$step = props.step,
52 step = _props$step === void 0 ? 1 : _props$step,
53 defaultValue = props.defaultValue,
54 value = props.value,
55 disabled = props.disabled,
56 readOnly = props.readOnly,
57 upHandler = props.upHandler,
58 downHandler = props.downHandler,
59 keyboard = props.keyboard,
60 _props$controls = props.controls,
61 controls = _props$controls === void 0 ? true : _props$controls,
62 stringMode = props.stringMode,
63 parser = props.parser,
64 formatter = props.formatter,
65 precision = props.precision,
66 decimalSeparator = props.decimalSeparator,
67 onChange = props.onChange,
68 onInput = props.onInput,
69 onPressEnter = props.onPressEnter,
70 onStep = props.onStep,
71 inputProps = _objectWithoutProperties(props, _excluded);
72
73 var inputClassName = "".concat(prefixCls, "-input");
74 var inputRef = React.useRef(null);
75
76 var _React$useState = React.useState(false),
77 _React$useState2 = _slicedToArray(_React$useState, 2),
78 focus = _React$useState2[0],
79 setFocus = _React$useState2[1];
80
81 var userTypingRef = React.useRef(false);
82 var compositionRef = React.useRef(false);
83 var shiftKeyRef = React.useRef(false); // ============================ Value =============================
84 // Real value control
85
86 var _React$useState3 = React.useState(function () {
87 return getMiniDecimal(value !== null && value !== void 0 ? value : defaultValue);
88 }),
89 _React$useState4 = _slicedToArray(_React$useState3, 2),
90 decimalValue = _React$useState4[0],
91 setDecimalValue = _React$useState4[1];
92
93 function setUncontrolledDecimalValue(newDecimal) {
94 if (value === undefined) {
95 setDecimalValue(newDecimal);
96 }
97 } // ====================== Parser & Formatter ======================
98
99 /**
100 * `precision` is used for formatter & onChange.
101 * It will auto generate by `value` & `step`.
102 * But it will not block user typing.
103 *
104 * Note: Auto generate `precision` is used for legacy logic.
105 * We should remove this since we already support high precision with BigInt.
106 *
107 * @param number Provide which number should calculate precision
108 * @param userTyping Change by user typing
109 */
110
111
112 var getPrecision = React.useCallback(function (numStr, userTyping) {
113 if (userTyping) {
114 return undefined;
115 }
116
117 if (precision >= 0) {
118 return precision;
119 }
120
121 return Math.max(getNumberPrecision(numStr), getNumberPrecision(step));
122 }, [precision, step]); // >>> Parser
123
124 var mergedParser = React.useCallback(function (num) {
125 var numStr = String(num);
126
127 if (parser) {
128 return parser(numStr);
129 }
130
131 var parsedStr = numStr;
132
133 if (decimalSeparator) {
134 parsedStr = parsedStr.replace(decimalSeparator, '.');
135 } // [Legacy] We still support auto convert `$ 123,456` to `123456`
136
137
138 return parsedStr.replace(/[^\w.-]+/g, '');
139 }, [parser, decimalSeparator]); // >>> Formatter
140
141 var inputValueRef = React.useRef('');
142 var mergedFormatter = React.useCallback(function (number, userTyping) {
143 if (formatter) {
144 return formatter(number, {
145 userTyping: userTyping,
146 input: String(inputValueRef.current)
147 });
148 }
149
150 var str = typeof number === 'number' ? num2str(number) : number; // User typing will not auto format with precision directly
151
152 if (!userTyping) {
153 var mergedPrecision = getPrecision(str, userTyping);
154
155 if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) {
156 // Separator
157 var separatorStr = decimalSeparator || '.';
158 str = toFixed(str, separatorStr, mergedPrecision);
159 }
160 }
161
162 return str;
163 }, [formatter, getPrecision, decimalSeparator]); // ========================== InputValue ==========================
164
165 /**
166 * Input text value control
167 *
168 * User can not update input content directly. It update with follow rules by priority:
169 * 1. controlled `value` changed
170 * * [SPECIAL] Typing like `1.` should not immediately convert to `1`
171 * 2. User typing with format (not precision)
172 * 3. Blur or Enter trigger revalidate
173 */
174
175 var _React$useState5 = React.useState(function () {
176 var initValue = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value;
177
178 if (decimalValue.isInvalidate() && ['string', 'number'].includes(_typeof(initValue))) {
179 return Number.isNaN(initValue) ? '' : initValue;
180 }
181
182 return mergedFormatter(decimalValue.toString(), false);
183 }),
184 _React$useState6 = _slicedToArray(_React$useState5, 2),
185 inputValue = _React$useState6[0],
186 setInternalInputValue = _React$useState6[1];
187
188 inputValueRef.current = inputValue; // Should always be string
189
190 function setInputValue(newValue, userTyping) {
191 setInternalInputValue(mergedFormatter( // Invalidate number is sometime passed by external control, we should let it go
192 // Otherwise is controlled by internal interactive logic which check by userTyping
193 // You can ref 'show limited value when input is not focused' test for more info.
194 newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), userTyping));
195 } // >>> Max & Min limit
196
197
198 var maxDecimal = React.useMemo(function () {
199 return getDecimalIfValidate(max);
200 }, [max, precision]);
201 var minDecimal = React.useMemo(function () {
202 return getDecimalIfValidate(min);
203 }, [min, precision]);
204 var upDisabled = React.useMemo(function () {
205 if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) {
206 return false;
207 }
208
209 return maxDecimal.lessEquals(decimalValue);
210 }, [maxDecimal, decimalValue]);
211 var downDisabled = React.useMemo(function () {
212 if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) {
213 return false;
214 }
215
216 return decimalValue.lessEquals(minDecimal);
217 }, [minDecimal, decimalValue]); // Cursor controller
218
219 var _useCursor = useCursor(inputRef.current, focus),
220 _useCursor2 = _slicedToArray(_useCursor, 2),
221 recordCursor = _useCursor2[0],
222 restoreCursor = _useCursor2[1]; // ============================= Data =============================
223
224 /**
225 * Find target value closet within range.
226 * e.g. [11, 28]:
227 * 3 => 11
228 * 23 => 23
229 * 99 => 28
230 */
231
232
233 var getRangeValue = function getRangeValue(target) {
234 // target > max
235 if (maxDecimal && !target.lessEquals(maxDecimal)) {
236 return maxDecimal;
237 } // target < min
238
239
240 if (minDecimal && !minDecimal.lessEquals(target)) {
241 return minDecimal;
242 }
243
244 return null;
245 };
246 /**
247 * Check value is in [min, max] range
248 */
249
250
251 var isInRange = function isInRange(target) {
252 return !getRangeValue(target);
253 };
254 /**
255 * Trigger `onChange` if value validated and not equals of origin.
256 * Return the value that re-align in range.
257 */
258
259
260 var triggerValueUpdate = function triggerValueUpdate(newValue, userTyping) {
261 var updateValue = newValue;
262 var isRangeValidate = isInRange(updateValue) || updateValue.isEmpty(); // Skip align value when trigger value is empty.
263 // We just trigger onChange(null)
264 // This should not block user typing
265
266 if (!updateValue.isEmpty() && !userTyping) {
267 // Revert value in range if needed
268 updateValue = getRangeValue(updateValue) || updateValue;
269 isRangeValidate = true;
270 }
271
272 if (!readOnly && !disabled && isRangeValidate) {
273 var numStr = updateValue.toString();
274 var mergedPrecision = getPrecision(numStr, userTyping);
275
276 if (mergedPrecision >= 0) {
277 updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision)); // When to fixed. The value may out of min & max range.
278 // 4 in [0, 3.8] => 3.8 => 4 (toFixed)
279
280 if (!isInRange(updateValue)) {
281 updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true));
282 }
283 } // Trigger event
284
285
286 if (!updateValue.equals(decimalValue)) {
287 setUncontrolledDecimalValue(updateValue);
288 onChange === null || onChange === void 0 ? void 0 : onChange(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue)); // Reformat input if value is not controlled
289
290 if (value === undefined) {
291 setInputValue(updateValue, userTyping);
292 }
293 }
294
295 return updateValue;
296 }
297
298 return decimalValue;
299 }; // ========================== User Input ==========================
300
301
302 var onNextPromise = useFrame(); // >>> Collect input value
303
304 var collectInputValue = function collectInputValue(inputStr) {
305 recordCursor(); // Update inputValue incase input can not parse as number
306
307 setInternalInputValue(inputStr); // Parse number
308
309 if (!compositionRef.current) {
310 var finalValue = mergedParser(inputStr);
311 var finalDecimal = getMiniDecimal(finalValue);
312
313 if (!finalDecimal.isNaN()) {
314 triggerValueUpdate(finalDecimal, true);
315 }
316 } // Trigger onInput later to let user customize value if they want do handle something after onChange
317
318
319 onInput === null || onInput === void 0 ? void 0 : onInput(inputStr); // optimize for chinese input experience
320 // https://github.com/ant-design/ant-design/issues/8196
321
322 onNextPromise(function () {
323 var nextInputStr = inputStr;
324
325 if (!parser) {
326 nextInputStr = inputStr.replace(/。/g, '.');
327 }
328
329 if (nextInputStr !== inputStr) {
330 collectInputValue(nextInputStr);
331 }
332 });
333 }; // >>> Composition
334
335
336 var onCompositionStart = function onCompositionStart() {
337 compositionRef.current = true;
338 };
339
340 var onCompositionEnd = function onCompositionEnd() {
341 compositionRef.current = false;
342 collectInputValue(inputRef.current.value);
343 }; // >>> Input
344
345
346 var onInternalInput = function onInternalInput(e) {
347 collectInputValue(e.target.value);
348 }; // ============================= Step =============================
349
350
351 var onInternalStep = function onInternalStep(up) {
352 var _inputRef$current;
353
354 // Ignore step since out of range
355 if (up && upDisabled || !up && downDisabled) {
356 return;
357 } // Clear typing status since it may caused by up & down key.
358 // We should sync with input value.
359
360
361 userTypingRef.current = false;
362 var stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step);
363
364 if (!up) {
365 stepDecimal = stepDecimal.negate();
366 }
367
368 var target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString());
369 var updatedValue = triggerValueUpdate(target, false);
370 onStep === null || onStep === void 0 ? void 0 : onStep(getDecimalValue(stringMode, updatedValue), {
371 offset: shiftKeyRef.current ? getDecupleSteps(step) : step,
372 type: up ? 'up' : 'down'
373 });
374 (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
375 }; // ============================ Flush =============================
376
377 /**
378 * Flush current input content to trigger value change & re-formatter input if needed
379 */
380
381
382 var flushInputValue = function flushInputValue(userTyping) {
383 var parsedValue = getMiniDecimal(mergedParser(inputValue));
384 var formatValue = parsedValue;
385
386 if (!parsedValue.isNaN()) {
387 // Only validate value or empty value can be re-fill to inputValue
388 // Reassign the formatValue within ranged of trigger control
389 formatValue = triggerValueUpdate(parsedValue, userTyping);
390 } else {
391 formatValue = decimalValue;
392 }
393
394 if (value !== undefined) {
395 // Reset back with controlled value first
396 setInputValue(decimalValue, false);
397 } else if (!formatValue.isNaN()) {
398 // Reset input back since no validate value
399 setInputValue(formatValue, false);
400 }
401 };
402
403 var onKeyDown = function onKeyDown(event) {
404 var which = event.which,
405 shiftKey = event.shiftKey;
406 userTypingRef.current = true;
407
408 if (shiftKey) {
409 shiftKeyRef.current = true;
410 } else {
411 shiftKeyRef.current = false;
412 }
413
414 if (which === KeyCode.ENTER) {
415 if (!compositionRef.current) {
416 userTypingRef.current = false;
417 }
418
419 flushInputValue(false);
420 onPressEnter === null || onPressEnter === void 0 ? void 0 : onPressEnter(event);
421 }
422
423 if (keyboard === false) {
424 return;
425 } // Do step
426
427
428 if (!compositionRef.current && [KeyCode.UP, KeyCode.DOWN].includes(which)) {
429 onInternalStep(KeyCode.UP === which);
430 event.preventDefault();
431 }
432 };
433
434 var onKeyUp = function onKeyUp() {
435 userTypingRef.current = false;
436 shiftKeyRef.current = false;
437 }; // >>> Focus & Blur
438
439
440 var onBlur = function onBlur() {
441 flushInputValue(false);
442 setFocus(false);
443 userTypingRef.current = false;
444 }; // ========================== Controlled ==========================
445 // Input by precision
446
447
448 useLayoutUpdateEffect(function () {
449 if (!decimalValue.isInvalidate()) {
450 setInputValue(decimalValue, false);
451 }
452 }, [precision]); // Input by value
453
454 useLayoutUpdateEffect(function () {
455 var newValue = getMiniDecimal(value);
456 setDecimalValue(newValue);
457 var currentParsedValue = getMiniDecimal(mergedParser(inputValue)); // When user typing from `1.2` to `1.`, we should not convert to `1` immediately.
458 // But let it go if user set `formatter`
459
460 if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) {
461 // Update value as effect
462 setInputValue(newValue, userTypingRef.current);
463 }
464 }, [value]); // ============================ Cursor ============================
465
466 useLayoutUpdateEffect(function () {
467 if (formatter) {
468 restoreCursor();
469 }
470 }, [inputValue]); // ============================ Render ============================
471
472 return /*#__PURE__*/React.createElement("div", {
473 className: classNames(prefixCls, className, (_classNames = {}, _defineProperty(_classNames, "".concat(prefixCls, "-focused"), focus), _defineProperty(_classNames, "".concat(prefixCls, "-disabled"), disabled), _defineProperty(_classNames, "".concat(prefixCls, "-readonly"), readOnly), _defineProperty(_classNames, "".concat(prefixCls, "-not-a-number"), decimalValue.isNaN()), _defineProperty(_classNames, "".concat(prefixCls, "-out-of-range"), !decimalValue.isInvalidate() && !isInRange(decimalValue)), _classNames)),
474 style: style,
475 onFocus: function onFocus() {
476 setFocus(true);
477 },
478 onBlur: onBlur,
479 onKeyDown: onKeyDown,
480 onKeyUp: onKeyUp,
481 onCompositionStart: onCompositionStart,
482 onCompositionEnd: onCompositionEnd
483 }, controls && /*#__PURE__*/React.createElement(StepHandler, {
484 prefixCls: prefixCls,
485 upNode: upHandler,
486 downNode: downHandler,
487 upDisabled: upDisabled,
488 downDisabled: downDisabled,
489 onStep: onInternalStep
490 }), /*#__PURE__*/React.createElement("div", {
491 className: "".concat(inputClassName, "-wrap")
492 }, /*#__PURE__*/React.createElement("input", _extends({
493 autoComplete: "off",
494 role: "spinbutton",
495 "aria-valuemin": min,
496 "aria-valuemax": max,
497 "aria-valuenow": decimalValue.isInvalidate() ? null : decimalValue.toString(),
498 step: step
499 }, inputProps, {
500 ref: composeRef(inputRef, ref),
501 className: inputClassName,
502 value: inputValue,
503 onChange: onInternalInput,
504 disabled: disabled,
505 readOnly: readOnly
506 }))));
507});
508InputNumber.displayName = 'InputNumber';
509export default InputNumber;
\No newline at end of file