import _extends from "@babel/runtime/helpers/esm/extends"; import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; import _typeof from "@babel/runtime/helpers/esm/typeof"; import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"; var _excluded = ["prefixCls", "className", "style", "min", "max", "step", "defaultValue", "value", "disabled", "readOnly", "upHandler", "downHandler", "keyboard", "controls", "classNames", "stringMode", "parser", "formatter", "precision", "decimalSeparator", "onChange", "onInput", "onPressEnter", "onStep"], _excluded2 = ["disabled", "style", "prefixCls", "value", "prefix", "suffix", "addonBefore", "addonAfter", "classes", "className", "classNames"]; import getMiniDecimal, { getNumberPrecision, num2str, toFixed, validateNumber } from '@rc-component/mini-decimal'; import clsx from 'classnames'; import { BaseInput } from 'rc-input'; import { useLayoutUpdateEffect } from "rc-util/es/hooks/useLayoutEffect"; import { composeRef } from "rc-util/es/ref"; import * as React from 'react'; import useCursor from "./hooks/useCursor"; import StepHandler from "./StepHandler"; import { getDecupleSteps } from "./utils/numberUtil"; import { triggerFocus } from "rc-input/es/utils/commonUtils"; import useFrame from "./hooks/useFrame"; /** * We support `stringMode` which need handle correct type when user call in onChange * format max or min value * 1. if isInvalid return null * 2. if precision is undefined, return decimal * 3. format with precision * I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3 * II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4 * III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4 * IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3 */ var getDecimalValue = function getDecimalValue(stringMode, decimalValue) { if (stringMode || decimalValue.isEmpty()) { return decimalValue.toString(); } return decimalValue.toNumber(); }; var getDecimalIfValidate = function getDecimalIfValidate(value) { var decimal = getMiniDecimal(value); return decimal.isInvalidate() ? null : decimal; }; var InternalInputNumber = /*#__PURE__*/React.forwardRef(function (props, ref) { var _clsx; var _props$prefixCls = props.prefixCls, prefixCls = _props$prefixCls === void 0 ? 'rc-input-number' : _props$prefixCls, className = props.className, style = props.style, min = props.min, max = props.max, _props$step = props.step, step = _props$step === void 0 ? 1 : _props$step, defaultValue = props.defaultValue, value = props.value, disabled = props.disabled, readOnly = props.readOnly, upHandler = props.upHandler, downHandler = props.downHandler, keyboard = props.keyboard, _props$controls = props.controls, controls = _props$controls === void 0 ? true : _props$controls, classNames = props.classNames, stringMode = props.stringMode, parser = props.parser, formatter = props.formatter, precision = props.precision, decimalSeparator = props.decimalSeparator, onChange = props.onChange, onInput = props.onInput, onPressEnter = props.onPressEnter, onStep = props.onStep, inputProps = _objectWithoutProperties(props, _excluded); var inputClassName = "".concat(prefixCls, "-input"); var inputRef = React.useRef(null); var _React$useState = React.useState(false), _React$useState2 = _slicedToArray(_React$useState, 2), focus = _React$useState2[0], setFocus = _React$useState2[1]; var userTypingRef = React.useRef(false); var compositionRef = React.useRef(false); var shiftKeyRef = React.useRef(false); // ============================ Value ============================= // Real value control var _React$useState3 = React.useState(function () { return getMiniDecimal(value !== null && value !== void 0 ? value : defaultValue); }), _React$useState4 = _slicedToArray(_React$useState3, 2), decimalValue = _React$useState4[0], setDecimalValue = _React$useState4[1]; function setUncontrolledDecimalValue(newDecimal) { if (value === undefined) { setDecimalValue(newDecimal); } } // ====================== Parser & Formatter ====================== /** * `precision` is used for formatter & onChange. * It will auto generate by `value` & `step`. * But it will not block user typing. * * Note: Auto generate `precision` is used for legacy logic. * We should remove this since we already support high precision with BigInt. * * @param number Provide which number should calculate precision * @param userTyping Change by user typing */ var getPrecision = React.useCallback(function (numStr, userTyping) { if (userTyping) { return undefined; } if (precision >= 0) { return precision; } return Math.max(getNumberPrecision(numStr), getNumberPrecision(step)); }, [precision, step]); // >>> Parser var mergedParser = React.useCallback(function (num) { var numStr = String(num); if (parser) { return parser(numStr); } var parsedStr = numStr; if (decimalSeparator) { parsedStr = parsedStr.replace(decimalSeparator, '.'); } // [Legacy] We still support auto convert `$ 123,456` to `123456` return parsedStr.replace(/[^\w.-]+/g, ''); }, [parser, decimalSeparator]); // >>> Formatter var inputValueRef = React.useRef(''); var mergedFormatter = React.useCallback(function (number, userTyping) { if (formatter) { return formatter(number, { userTyping: userTyping, input: String(inputValueRef.current) }); } var str = typeof number === 'number' ? num2str(number) : number; // User typing will not auto format with precision directly if (!userTyping) { var mergedPrecision = getPrecision(str, userTyping); if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) { // Separator var separatorStr = decimalSeparator || '.'; str = toFixed(str, separatorStr, mergedPrecision); } } return str; }, [formatter, getPrecision, decimalSeparator]); // ========================== InputValue ========================== /** * Input text value control * * User can not update input content directly. It updates with follow rules by priority: * 1. controlled `value` changed * * [SPECIAL] Typing like `1.` should not immediately convert to `1` * 2. User typing with format (not precision) * 3. Blur or Enter trigger revalidate */ var _React$useState5 = React.useState(function () { var initValue = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value; if (decimalValue.isInvalidate() && ['string', 'number'].includes(_typeof(initValue))) { return Number.isNaN(initValue) ? '' : initValue; } return mergedFormatter(decimalValue.toString(), false); }), _React$useState6 = _slicedToArray(_React$useState5, 2), inputValue = _React$useState6[0], setInternalInputValue = _React$useState6[1]; inputValueRef.current = inputValue; // Should always be string function setInputValue(newValue, userTyping) { setInternalInputValue(mergedFormatter( // Invalidate number is sometime passed by external control, we should let it go // Otherwise is controlled by internal interactive logic which check by userTyping // You can ref 'show limited value when input is not focused' test for more info. newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), userTyping)); } // >>> Max & Min limit var maxDecimal = React.useMemo(function () { return getDecimalIfValidate(max); }, [max, precision]); var minDecimal = React.useMemo(function () { return getDecimalIfValidate(min); }, [min, precision]); var upDisabled = React.useMemo(function () { if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) { return false; } return maxDecimal.lessEquals(decimalValue); }, [maxDecimal, decimalValue]); var downDisabled = React.useMemo(function () { if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) { return false; } return decimalValue.lessEquals(minDecimal); }, [minDecimal, decimalValue]); // Cursor controller var _useCursor = useCursor(inputRef.current, focus), _useCursor2 = _slicedToArray(_useCursor, 2), recordCursor = _useCursor2[0], restoreCursor = _useCursor2[1]; // ============================= Data ============================= /** * Find target value closet within range. * e.g. [11, 28]: * 3 => 11 * 23 => 23 * 99 => 28 */ var getRangeValue = function getRangeValue(target) { // target > max if (maxDecimal && !target.lessEquals(maxDecimal)) { return maxDecimal; } // target < min if (minDecimal && !minDecimal.lessEquals(target)) { return minDecimal; } return null; }; /** * Check value is in [min, max] range */ var isInRange = function isInRange(target) { return !getRangeValue(target); }; /** * Trigger `onChange` if value validated and not equals of origin. * Return the value that re-align in range. */ var triggerValueUpdate = function triggerValueUpdate(newValue, userTyping) { var updateValue = newValue; var isRangeValidate = isInRange(updateValue) || updateValue.isEmpty(); // Skip align value when trigger value is empty. // We just trigger onChange(null) // This should not block user typing if (!updateValue.isEmpty() && !userTyping) { // Revert value in range if needed updateValue = getRangeValue(updateValue) || updateValue; isRangeValidate = true; } if (!readOnly && !disabled && isRangeValidate) { var numStr = updateValue.toString(); var mergedPrecision = getPrecision(numStr, userTyping); if (mergedPrecision >= 0) { updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision)); // When to fixed. The value may out of min & max range. // 4 in [0, 3.8] => 3.8 => 4 (toFixed) if (!isInRange(updateValue)) { updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true)); } } // Trigger event if (!updateValue.equals(decimalValue)) { setUncontrolledDecimalValue(updateValue); onChange === null || onChange === void 0 ? void 0 : onChange(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue)); // Reformat input if value is not controlled if (value === undefined) { setInputValue(updateValue, userTyping); } } return updateValue; } return decimalValue; }; // ========================== User Input ========================== var onNextPromise = useFrame(); // >>> Collect input value var collectInputValue = function collectInputValue(inputStr) { recordCursor(); // Update inputValue in case input can not parse as number // Refresh ref value immediately since it may used by formatter inputValueRef.current = inputStr; setInternalInputValue(inputStr); // Parse number if (!compositionRef.current) { var finalValue = mergedParser(inputStr); var finalDecimal = getMiniDecimal(finalValue); if (!finalDecimal.isNaN()) { triggerValueUpdate(finalDecimal, true); } } // Trigger onInput later to let user customize value if they want to handle something after onChange onInput === null || onInput === void 0 ? void 0 : onInput(inputStr); // optimize for chinese input experience // https://github.com/ant-design/ant-design/issues/8196 onNextPromise(function () { var nextInputStr = inputStr; if (!parser) { nextInputStr = inputStr.replace(/。/g, '.'); } if (nextInputStr !== inputStr) { collectInputValue(nextInputStr); } }); }; // >>> Composition var onCompositionStart = function onCompositionStart() { compositionRef.current = true; }; var onCompositionEnd = function onCompositionEnd() { compositionRef.current = false; collectInputValue(inputRef.current.value); }; // >>> Input var onInternalInput = function onInternalInput(e) { collectInputValue(e.target.value); }; // ============================= Step ============================= var onInternalStep = function onInternalStep(up) { var _inputRef$current; // Ignore step since out of range if (up && upDisabled || !up && downDisabled) { return; } // Clear typing status since it may be caused by up & down key. // We should sync with input value. userTypingRef.current = false; var stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step); if (!up) { stepDecimal = stepDecimal.negate(); } var target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString()); var updatedValue = triggerValueUpdate(target, false); onStep === null || onStep === void 0 ? void 0 : onStep(getDecimalValue(stringMode, updatedValue), { offset: shiftKeyRef.current ? getDecupleSteps(step) : step, type: up ? 'up' : 'down' }); (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus(); }; // ============================ Flush ============================= /** * Flush current input content to trigger value change & re-formatter input if needed. * This will always flush input value for update. * If it's invalidate, will fallback to last validate value. */ var flushInputValue = function flushInputValue(userTyping) { var parsedValue = getMiniDecimal(mergedParser(inputValue)); var formatValue = parsedValue; if (!parsedValue.isNaN()) { // Only validate value or empty value can be re-fill to inputValue // Reassign the formatValue within ranged of trigger control formatValue = triggerValueUpdate(parsedValue, userTyping); } else { formatValue = triggerValueUpdate(decimalValue, userTyping); } if (value !== undefined) { // Reset back with controlled value first setInputValue(decimalValue, false); } else if (!formatValue.isNaN()) { // Reset input back since no validate value setInputValue(formatValue, false); } }; // Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari) var onBeforeInput = function onBeforeInput() { userTypingRef.current = true; }; var onKeyDown = function onKeyDown(event) { var key = event.key, shiftKey = event.shiftKey; userTypingRef.current = true; shiftKeyRef.current = shiftKey; if (key === 'Enter') { if (!compositionRef.current) { userTypingRef.current = false; } flushInputValue(false); onPressEnter === null || onPressEnter === void 0 ? void 0 : onPressEnter(event); } if (keyboard === false) { return; } // Do step if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) { onInternalStep(key === 'Up' || key === 'ArrowUp'); event.preventDefault(); } }; var onKeyUp = function onKeyUp() { userTypingRef.current = false; shiftKeyRef.current = false; }; // >>> Focus & Blur var onBlur = function onBlur() { flushInputValue(false); setFocus(false); userTypingRef.current = false; }; // ========================== Controlled ========================== // Input by precision useLayoutUpdateEffect(function () { if (!decimalValue.isInvalidate()) { setInputValue(decimalValue, false); } }, [precision]); // Input by value useLayoutUpdateEffect(function () { var newValue = getMiniDecimal(value); setDecimalValue(newValue); var currentParsedValue = getMiniDecimal(mergedParser(inputValue)); // When user typing from `1.2` to `1.`, we should not convert to `1` immediately. // But let it go if user set `formatter` if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) { // Update value as effect setInputValue(newValue, userTypingRef.current); } }, [value]); // ============================ Cursor ============================ useLayoutUpdateEffect(function () { if (formatter) { restoreCursor(); } }, [inputValue]); // ============================ Render ============================ return /*#__PURE__*/React.createElement("div", { className: clsx(prefixCls, classNames === null || classNames === void 0 ? void 0 : classNames.input, className, (_clsx = {}, _defineProperty(_clsx, "".concat(prefixCls, "-focused"), focus), _defineProperty(_clsx, "".concat(prefixCls, "-disabled"), disabled), _defineProperty(_clsx, "".concat(prefixCls, "-readonly"), readOnly), _defineProperty(_clsx, "".concat(prefixCls, "-not-a-number"), decimalValue.isNaN()), _defineProperty(_clsx, "".concat(prefixCls, "-out-of-range"), !decimalValue.isInvalidate() && !isInRange(decimalValue)), _clsx)), style: style, onFocus: function onFocus() { setFocus(true); }, onBlur: onBlur, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onCompositionStart: onCompositionStart, onCompositionEnd: onCompositionEnd, onBeforeInput: onBeforeInput }, controls && /*#__PURE__*/React.createElement(StepHandler, { prefixCls: prefixCls, upNode: upHandler, downNode: downHandler, upDisabled: upDisabled, downDisabled: downDisabled, onStep: onInternalStep }), /*#__PURE__*/React.createElement("div", { className: "".concat(inputClassName, "-wrap") }, /*#__PURE__*/React.createElement("input", _extends({ autoComplete: "off", role: "spinbutton", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": decimalValue.isInvalidate() ? null : decimalValue.toString(), step: step }, inputProps, { ref: composeRef(inputRef, ref), className: inputClassName, value: inputValue, onChange: onInternalInput, disabled: disabled, readOnly: readOnly })))); }); var InputNumber = /*#__PURE__*/React.forwardRef(function (props, ref) { var disabled = props.disabled, style = props.style, prefixCls = props.prefixCls, value = props.value, prefix = props.prefix, suffix = props.suffix, addonBefore = props.addonBefore, addonAfter = props.addonAfter, classes = props.classes, className = props.className, classNames = props.classNames, rest = _objectWithoutProperties(props, _excluded2); var inputFocusRef = React.useRef(null); var focus = function focus(option) { if (inputFocusRef.current) { triggerFocus(inputFocusRef.current, option); } }; return /*#__PURE__*/React.createElement(BaseInput, { inputElement: /*#__PURE__*/React.createElement(InternalInputNumber, _extends({ prefixCls: prefixCls, disabled: disabled, classNames: classNames, ref: composeRef(inputFocusRef, ref) }, rest)), className: className, triggerFocus: focus, prefixCls: prefixCls, value: value, disabled: disabled, style: style, prefix: prefix, suffix: suffix, addonAfter: addonAfter, addonBefore: addonBefore, classes: classes, classNames: classNames, components: { affixWrapper: 'div', groupWrapper: 'div', wrapper: 'div', groupAddon: 'div' } }); }); InputNumber.displayName = 'InputNumber'; export default InputNumber;