import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties'; import _extends from 'babel-runtime/helpers/extends'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; import _inherits from 'babel-runtime/helpers/inherits'; import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import KeyCode from 'rc-util/es/KeyCode'; import InputHandler from './InputHandler'; function noop() {} function preventDefault(e) { e.preventDefault(); } function defaultParser(input) { return input.replace(/[^\w\.-]+/g, ''); } /** * When click and hold on a button - the speed of auto changin the value. */ var SPEED = 200; /** * When click and hold on a button - the delay before auto changin the value. */ var DELAY = 600; /** * Max Safe Integer -- on IE this is not available, so manually set the number in that case. * The reason this is used, instead of Infinity is because numbers above the MSI are unstable */ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1; var isValidProps = function isValidProps(value) { return value !== undefined && value !== null; }; var isEqual = function isEqual(oldValue, newValue) { return newValue === oldValue || typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue); }; var InputNumber = function (_React$Component) { _inherits(InputNumber, _React$Component); function InputNumber(props) { _classCallCheck(this, InputNumber); var _this = _possibleConstructorReturn(this, _React$Component.call(this, props)); _initialiseProps.call(_this); var value = void 0; if ('value' in props) { value = props.value; } else { value = props.defaultValue; } _this.state = { focused: props.autoFocus }; var validValue = _this.getValidValue(_this.toNumber(value)); _this.state = _extends({}, _this.state, { inputValue: _this.toPrecisionAsStep(validValue), value: validValue }); return _this; } InputNumber.prototype.componentDidMount = function componentDidMount() { this.componentDidUpdate(); }; InputNumber.prototype.componentDidUpdate = function componentDidUpdate(prevProps) { var _props = this.props, value = _props.value, onChange = _props.onChange, max = _props.max, min = _props.min; var focused = this.state.focused; // Don't trigger in componentDidMount if (prevProps) { if (!isEqual(prevProps.value, value) || !isEqual(prevProps.max, max) || !isEqual(prevProps.min, min)) { var validValue = focused ? value : this.getValidValue(value); var nextInputValue = void 0; if (this.pressingUpOrDown) { nextInputValue = validValue; } else if (this.inputting) { nextInputValue = this.rawInput; } else { nextInputValue = this.toPrecisionAsStep(validValue); } this.setState({ // eslint-disable-line value: validValue, inputValue: nextInputValue }); } // Trigger onChange when max or min change // https://github.com/ant-design/ant-design/issues/11574 var nextValue = 'value' in this.props ? value : this.state.value; // ref: null < 20 === true // https://github.com/ant-design/ant-design/issues/14277 if ('max' in this.props && prevProps.max !== max && typeof nextValue === 'number' && nextValue > max && onChange) { onChange(max); } if ('min' in this.props && prevProps.min !== min && typeof nextValue === 'number' && nextValue < min && onChange) { onChange(min); } } // Restore cursor try { // Firefox set the input cursor after it get focused. // This caused that if an input didn't init with the selection, // set will cause cursor not correct when first focus. // Safari will focus input if set selection. We need skip this. if (this.cursorStart !== undefined && this.state.focused) { // In most cases, the string after cursor is stable. // We can move the cursor before it if ( // If not match full str, try to match part of str !this.partRestoreByAfter(this.cursorAfter) && this.state.value !== this.props.value) { // If not match any of then, let's just keep the position // TODO: Logic should not reach here, need check if happens var pos = this.cursorStart + 1; // If not have last string, just position to the end if (!this.cursorAfter) { pos = this.input.value.length; } else if (this.lastKeyCode === KeyCode.BACKSPACE) { pos = this.cursorStart - 1; } else if (this.lastKeyCode === KeyCode.DELETE) { pos = this.cursorStart; } this.fixCaret(pos, pos); } else if (this.currentValue === this.input.value) { // Handle some special key code switch (this.lastKeyCode) { case KeyCode.BACKSPACE: this.fixCaret(this.cursorStart - 1, this.cursorStart - 1); break; case KeyCode.DELETE: this.fixCaret(this.cursorStart + 1, this.cursorStart + 1); break; default: // Do nothing } } } } catch (e) {} // Do nothing // Reset last key this.lastKeyCode = null; // pressingUpOrDown is true means that someone just click up or down button if (!this.pressingUpOrDown) { return; } if (this.props.focusOnUpDown && this.state.focused) { if (document.activeElement !== this.input) { this.focus(); } } this.pressingUpOrDown = false; }; InputNumber.prototype.componentWillUnmount = function componentWillUnmount() { this.stop(); }; InputNumber.prototype.getCurrentValidValue = function getCurrentValidValue(value) { var val = value; if (val === '') { val = ''; } else if (!this.isNotCompleteNumber(parseFloat(val, 10))) { val = this.getValidValue(val); } else { val = this.state.value; } return this.toNumber(val); }; InputNumber.prototype.getRatio = function getRatio(e) { var ratio = 1; if (e.metaKey || e.ctrlKey) { ratio = 0.1; } else if (e.shiftKey) { ratio = 10; } return ratio; }; InputNumber.prototype.getValueFromEvent = function getValueFromEvent(e) { // optimize for chinese input expierence // https://github.com/ant-design/ant-design/issues/8196 var value = e.target.value.trim().replace(/。/g, '.'); if (isValidProps(this.props.decimalSeparator)) { value = value.replace(this.props.decimalSeparator, '.'); } return value; }; InputNumber.prototype.getValidValue = function getValidValue(value) { var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props.min; var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.props.max; var val = parseFloat(value, 10); // https://github.com/ant-design/ant-design/issues/7358 if (isNaN(val)) { return value; } if (val < min) { val = min; } if (val > max) { val = max; } return val; }; InputNumber.prototype.setValue = function setValue(v, callback) { // trigger onChange var precision = this.props.precision; var newValue = this.isNotCompleteNumber(parseFloat(v, 10)) ? null : parseFloat(v, 10); var _state = this.state, _state$value = _state.value, value = _state$value === undefined ? null : _state$value, _state$inputValue = _state.inputValue, inputValue = _state$inputValue === undefined ? null : _state$inputValue; // https://github.com/ant-design/ant-design/issues/7363 // https://github.com/ant-design/ant-design/issues/16622 var newValueInString = typeof newValue === 'number' ? newValue.toFixed(precision) : '' + newValue; var changed = newValue !== value || newValueInString !== '' + inputValue; if (!('value' in this.props)) { this.setState({ value: newValue, inputValue: this.toPrecisionAsStep(v) }, callback); } else { // always set input value same as value this.setState({ inputValue: this.toPrecisionAsStep(this.state.value) }, callback); } if (changed) { this.props.onChange(newValue); } return newValue; }; InputNumber.prototype.getPrecision = function getPrecision(value) { if (isValidProps(this.props.precision)) { return this.props.precision; } var valueString = value.toString(); if (valueString.indexOf('e-') >= 0) { return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10); } var precision = 0; if (valueString.indexOf('.') >= 0) { precision = valueString.length - valueString.indexOf('.') - 1; } return precision; }; // step={1.0} value={1.51} // press + // then value should be 2.51, rather than 2.5 // if this.props.precision is undefined // https://github.com/react-component/input-number/issues/39 InputNumber.prototype.getMaxPrecision = function getMaxPrecision(currentValue) { var ratio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var _props2 = this.props, precision = _props2.precision, step = _props2.step; if (isValidProps(precision)) { return precision; } var ratioPrecision = this.getPrecision(ratio); var stepPrecision = this.getPrecision(step); var currentValuePrecision = this.getPrecision(currentValue); if (!currentValue) { return ratioPrecision + stepPrecision; } return Math.max(currentValuePrecision, ratioPrecision + stepPrecision); }; InputNumber.prototype.getPrecisionFactor = function getPrecisionFactor(currentValue) { var ratio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var precision = this.getMaxPrecision(currentValue, ratio); return Math.pow(10, precision); }; InputNumber.prototype.fixCaret = function fixCaret(start, end) { if (start === undefined || end === undefined || !this.input || !this.input.value) { return; } try { var currentStart = this.input.selectionStart; var currentEnd = this.input.selectionEnd; if (start !== currentStart || end !== currentEnd) { this.input.setSelectionRange(start, end); } } catch (e) { // Fix error in Chrome: // Failed to read the 'selectionStart' property from 'HTMLInputElement' // http://stackoverflow.com/q/21177489/3040605 } }; InputNumber.prototype.focus = function focus() { this.input.focus(); this.recordCursorPosition(); }; InputNumber.prototype.blur = function blur() { this.input.blur(); }; InputNumber.prototype.select = function select() { this.input.select(); }; InputNumber.prototype.formatWrapper = function formatWrapper(num) { // http://2ality.com/2012/03/signedzero.html // https://github.com/ant-design/ant-design/issues/9439 if (this.props.formatter) { return this.props.formatter(num); } return num; }; InputNumber.prototype.toPrecisionAsStep = function toPrecisionAsStep(num) { if (this.isNotCompleteNumber(num) || num === '') { return num; } var precision = Math.abs(this.getMaxPrecision(num)); if (!isNaN(precision)) { return Number(num).toFixed(precision); } return num.toString(); }; // '1.' '1x' 'xx' '' => are not complete numbers InputNumber.prototype.isNotCompleteNumber = function isNotCompleteNumber(num) { return isNaN(num) || num === '' || num === null || num && num.toString().indexOf('.') === num.toString().length - 1; }; InputNumber.prototype.toNumber = function toNumber(num) { var precision = this.props.precision; var focused = this.state.focused; // num.length > 16 => This is to prevent input of large numbers var numberIsTooLarge = num && num.length > 16 && focused; if (this.isNotCompleteNumber(num) || numberIsTooLarge) { return num; } if (isValidProps(precision)) { return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); } return Number(num); }; InputNumber.prototype.upStep = function upStep(val, rat) { var step = this.props.step; var precisionFactor = this.getPrecisionFactor(val, rat); var precision = Math.abs(this.getMaxPrecision(val, rat)); var result = ((precisionFactor * val + precisionFactor * step * rat) / precisionFactor).toFixed(precision); return this.toNumber(result); }; InputNumber.prototype.downStep = function downStep(val, rat) { var step = this.props.step; var precisionFactor = this.getPrecisionFactor(val, rat); var precision = Math.abs(this.getMaxPrecision(val, rat)); var result = ((precisionFactor * val - precisionFactor * step * rat) / precisionFactor).toFixed(precision); return this.toNumber(result); }; InputNumber.prototype.step = function step(type, e) { var _this2 = this; var ratio = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; var recursive = arguments[3]; this.stop(); if (e) { e.persist(); e.preventDefault(); } var props = this.props; if (props.disabled) { return; } var value = this.getCurrentValidValue(this.state.inputValue) || 0; if (this.isNotCompleteNumber(value)) { return; } var val = this[type + 'Step'](value, ratio); var outOfRange = val > props.max || val < props.min; if (val > props.max) { val = props.max; } else if (val < props.min) { val = props.min; } this.setValue(val); this.setState({ focused: true }); if (outOfRange) { return; } this.autoStepTimer = setTimeout(function () { _this2[type](e, ratio, true); }, recursive ? SPEED : DELAY); }; InputNumber.prototype.render = function render() { var _classNames; var props = _extends({}, this.props); var prefixCls = props.prefixCls, disabled = props.disabled, readOnly = props.readOnly, useTouch = props.useTouch, autoComplete = props.autoComplete, upHandler = props.upHandler, downHandler = props.downHandler, rest = _objectWithoutProperties(props, ['prefixCls', 'disabled', 'readOnly', 'useTouch', 'autoComplete', 'upHandler', 'downHandler']); var classes = classNames((_classNames = {}, _classNames[prefixCls] = true, _classNames[props.className] = !!props.className, _classNames[prefixCls + '-disabled'] = disabled, _classNames[prefixCls + '-focused'] = this.state.focused, _classNames)); var upDisabledClass = ''; var downDisabledClass = ''; var value = this.state.value; if (value || value === 0) { if (!isNaN(value)) { var val = Number(value); if (val >= props.max) { upDisabledClass = prefixCls + '-handler-up-disabled'; } if (val <= props.min) { downDisabledClass = prefixCls + '-handler-down-disabled'; } } else { upDisabledClass = prefixCls + '-handler-up-disabled'; downDisabledClass = prefixCls + '-handler-down-disabled'; } } var dataOrAriaAttributeProps = {}; for (var key in props) { if (props.hasOwnProperty(key) && (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role')) { dataOrAriaAttributeProps[key] = props[key]; } } var editable = !props.readOnly && !props.disabled; // focus state, show input value // unfocus state, show valid value var inputDisplayValue = this.getInputDisplayValue(); var upEvents = void 0; var downEvents = void 0; if (useTouch) { upEvents = { onTouchStart: editable && !upDisabledClass ? this.up : noop, onTouchEnd: this.stop }; downEvents = { onTouchStart: editable && !downDisabledClass ? this.down : noop, onTouchEnd: this.stop }; } else { upEvents = { onMouseDown: editable && !upDisabledClass ? this.up : noop, onMouseUp: this.stop, onMouseLeave: this.stop }; downEvents = { onMouseDown: editable && !downDisabledClass ? this.down : noop, onMouseUp: this.stop, onMouseLeave: this.stop }; } var isUpDisabled = !!upDisabledClass || disabled || readOnly; var isDownDisabled = !!downDisabledClass || disabled || readOnly; // ref for test return React.createElement( 'div', { className: classes, style: props.style, title: props.title, onMouseEnter: props.onMouseEnter, onMouseLeave: props.onMouseLeave, onMouseOver: props.onMouseOver, onMouseOut: props.onMouseOut }, React.createElement( 'div', { className: prefixCls + '-handler-wrap' }, React.createElement( InputHandler, _extends({ ref: this.saveUp, disabled: isUpDisabled, prefixCls: prefixCls, unselectable: 'unselectable' }, upEvents, { role: 'button', 'aria-label': 'Increase Value', 'aria-disabled': !!isUpDisabled, className: prefixCls + '-handler ' + prefixCls + '-handler-up ' + upDisabledClass }), upHandler || React.createElement('span', { unselectable: 'unselectable', className: prefixCls + '-handler-up-inner', onClick: preventDefault }) ), React.createElement( InputHandler, _extends({ ref: this.saveDown, disabled: isDownDisabled, prefixCls: prefixCls, unselectable: 'unselectable' }, downEvents, { role: 'button', 'aria-label': 'Decrease Value', 'aria-disabled': !!isDownDisabled, className: prefixCls + '-handler ' + prefixCls + '-handler-down ' + downDisabledClass }), downHandler || React.createElement('span', { unselectable: 'unselectable', className: prefixCls + '-handler-down-inner', onClick: preventDefault }) ) ), React.createElement( 'div', { className: prefixCls + '-input-wrap' }, React.createElement('input', _extends({ role: 'spinbutton', 'aria-valuemin': props.min, 'aria-valuemax': props.max, 'aria-valuenow': value, required: props.required, type: props.type, placeholder: props.placeholder, onClick: props.onClick, onMouseUp: this.onMouseUp, className: prefixCls + '-input', tabIndex: props.tabIndex, autoComplete: autoComplete, onFocus: this.onFocus, onBlur: this.onBlur, onKeyDown: editable ? this.onKeyDown : noop, onKeyUp: editable ? this.onKeyUp : noop, autoFocus: props.autoFocus, maxLength: props.maxLength, readOnly: props.readOnly, disabled: props.disabled, max: props.max, min: props.min, step: props.step, name: props.name, title: props.title, id: props.id, onChange: this.onChange, ref: this.saveInput, value: inputDisplayValue, pattern: props.pattern, inputMode: props.inputMode }, dataOrAriaAttributeProps)) ) ); }; return InputNumber; }(React.Component); InputNumber.propTypes = { value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), focusOnUpDown: PropTypes.bool, autoFocus: PropTypes.bool, onChange: PropTypes.func, onPressEnter: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, prefixCls: PropTypes.string, tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), disabled: PropTypes.bool, onFocus: PropTypes.func, onBlur: PropTypes.func, readOnly: PropTypes.bool, max: PropTypes.number, min: PropTypes.number, step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), upHandler: PropTypes.node, downHandler: PropTypes.node, useTouch: PropTypes.bool, formatter: PropTypes.func, parser: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, onMouseOver: PropTypes.func, onMouseOut: PropTypes.func, onMouseUp: PropTypes.func, precision: PropTypes.number, required: PropTypes.bool, pattern: PropTypes.string, decimalSeparator: PropTypes.string, inputMode: PropTypes.string }; InputNumber.defaultProps = { focusOnUpDown: true, useTouch: false, prefixCls: 'rc-input-number', min: -MAX_SAFE_INTEGER, step: 1, style: {}, onChange: noop, onKeyDown: noop, onPressEnter: noop, onFocus: noop, onBlur: noop, parser: defaultParser, required: false, autoComplete: 'off' }; var _initialiseProps = function _initialiseProps() { var _this3 = this; this.onKeyDown = function (e) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } var _props3 = _this3.props, onKeyDown = _props3.onKeyDown, onPressEnter = _props3.onPressEnter; if (e.keyCode === KeyCode.UP) { var ratio = _this3.getRatio(e); _this3.up(e, ratio); _this3.stop(); } else if (e.keyCode === KeyCode.DOWN) { var _ratio = _this3.getRatio(e); _this3.down(e, _ratio); _this3.stop(); } else if (e.keyCode === KeyCode.ENTER && onPressEnter) { onPressEnter(e); } // Trigger user key down _this3.recordCursorPosition(); _this3.lastKeyCode = e.keyCode; if (onKeyDown) { onKeyDown.apply(undefined, [e].concat(args)); } }; this.onKeyUp = function (e) { for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } var onKeyUp = _this3.props.onKeyUp; _this3.stop(); _this3.recordCursorPosition(); // Trigger user key up if (onKeyUp) { onKeyUp.apply(undefined, [e].concat(args)); } }; this.onChange = function (e) { var onChange = _this3.props.onChange; if (_this3.state.focused) { _this3.inputting = true; } _this3.rawInput = _this3.props.parser(_this3.getValueFromEvent(e)); _this3.setState({ inputValue: _this3.rawInput }); onChange(_this3.toNumber(_this3.rawInput)); // valid number or invalid string }; this.onMouseUp = function () { var onMouseUp = _this3.props.onMouseUp; _this3.recordCursorPosition(); if (onMouseUp) { onMouseUp.apply(undefined, arguments); } }; this.onFocus = function () { var _props4; _this3.setState({ focused: true }); (_props4 = _this3.props).onFocus.apply(_props4, arguments); }; this.onBlur = function () { var onBlur = _this3.props.onBlur; _this3.inputting = false; _this3.setState({ focused: false }); var value = _this3.getCurrentValidValue(_this3.state.inputValue); var newValue = _this3.setValue(value); if (onBlur) { var originValue = _this3.input.value; var inputValue = _this3.getInputDisplayValue({ focus: false, value: newValue }); _this3.input.value = inputValue; onBlur.apply(undefined, arguments); _this3.input.value = originValue; } }; this.getInputDisplayValue = function (state) { var _ref = state || _this3.state, focused = _ref.focused, inputValue = _ref.inputValue, value = _ref.value; var inputDisplayValue = void 0; if (focused) { inputDisplayValue = inputValue; } else { inputDisplayValue = _this3.toPrecisionAsStep(value); } if (inputDisplayValue === undefined || inputDisplayValue === null) { inputDisplayValue = ''; } var inputDisplayValueFormat = _this3.formatWrapper(inputDisplayValue); if (isValidProps(_this3.props.decimalSeparator)) { inputDisplayValueFormat = inputDisplayValueFormat.toString().replace('.', _this3.props.decimalSeparator); } return inputDisplayValueFormat; }; this.recordCursorPosition = function () { // Record position try { _this3.cursorStart = _this3.input.selectionStart; _this3.cursorEnd = _this3.input.selectionEnd; _this3.currentValue = _this3.input.value; _this3.cursorBefore = _this3.input.value.substring(0, _this3.cursorStart); _this3.cursorAfter = _this3.input.value.substring(_this3.cursorEnd); } catch (e) { // Fix error in Chrome: // Failed to read the 'selectionStart' property from 'HTMLInputElement' // http://stackoverflow.com/q/21177489/3040605 } }; this.restoreByAfter = function (str) { if (str === undefined) return false; var fullStr = _this3.input.value; var index = fullStr.lastIndexOf(str); if (index === -1) return false; var prevCursorPos = _this3.cursorBefore.length; if (_this3.lastKeyCode === KeyCode.DELETE && _this3.cursorBefore.charAt(prevCursorPos - 1) === str[0]) { _this3.fixCaret(prevCursorPos, prevCursorPos); return true; } if (index + str.length === fullStr.length) { _this3.fixCaret(index, index); return true; } return false; }; this.partRestoreByAfter = function (str) { if (str === undefined) return false; // For loop from full str to the str with last char to map. e.g. 123 // -> 123 // -> 23 // -> 3 return Array.prototype.some.call(str, function (_, start) { var partStr = str.substring(start); return _this3.restoreByAfter(partStr); }); }; this.stop = function () { if (_this3.autoStepTimer) { clearTimeout(_this3.autoStepTimer); } }; this.down = function (e, ratio, recursive) { _this3.pressingUpOrDown = true; _this3.step('down', e, ratio, recursive); }; this.up = function (e, ratio, recursive) { _this3.pressingUpOrDown = true; _this3.step('up', e, ratio, recursive); }; this.saveUp = function (node) { _this3.upHandler = node; }; this.saveDown = function (node) { _this3.downHandler = node; }; this.saveInput = function (node) { _this3.input = node; }; }; export default InputNumber;