import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import Icon from '../icon'; import Button from '../button'; import Input from '../input'; import ConfigProvider from '../config-provider'; import { func, obj } from '../util'; /** NumberPicker */ class NumberPicker extends React.Component { static propTypes = { /** * 样式前缀 */ prefix: PropTypes.string, /** * 设置类型 * @enumdesc 普通, 内联 */ type: PropTypes.oneOf(['normal', 'inline']), /** * 大小 */ size: PropTypes.oneOf(['large', 'medium']), /** * 当前值 */ value: PropTypes.number, /** * 默认值 */ defaultValue: PropTypes.number, /** * 是否禁用 */ disabled: PropTypes.bool, /** * 步长 */ step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** * 保留小数点后位数 */ precision: PropTypes.number, /** * 用户是否可以输入 */ editable: PropTypes.bool, /** * 自动焦点 */ autoFocus: PropTypes.bool, /** * 数值被改变的事件 * @param {Number} value 数据 * @param {Event} e DOM事件对象 */ onChange: PropTypes.func, /** * 键盘按下 */ onKeyDown: PropTypes.func, /** * 焦点获得 */ onFocus: PropTypes.func, /** * 焦点失去 */ onBlur: PropTypes.func, /** * 数值订正后的回调 * @param {Object} obj {currentValue,oldValue:String} */ onCorrect: PropTypes.func, onDisabled: PropTypes.func, // 兼容0.x onDisabled /** * 最大值 */ max: PropTypes.number, /** * 最小值 */ min: PropTypes.number, /** * 自定义class */ className: PropTypes.string, /** * 自定义内联样式 */ style: PropTypes.object, state: PropTypes.oneOf(['error']), /** * 格式化当前值 * @param {Number} value * @return {String|Number} */ format: PropTypes.func, /** * 增加按钮的props */ upBtnProps: PropTypes.object, /** * 减少按钮的props */ downBtnProps: PropTypes.object, /** * 内联 label */ label: PropTypes.node, /** * inner after */ innerAfter: PropTypes.node, rtl: PropTypes.bool, /** * 是否为预览态 */ isPreview: PropTypes.bool, /** * 预览态模式下渲染的内容 * @param {number} value 评分值 */ renderPreview: PropTypes.func, /** * 预设屏幕宽度 */ device: PropTypes.oneOf(['phone', 'tablet', 'desktop']), }; static defaultProps = { prefix: 'next-', max: Infinity, min: -Infinity, type: 'normal', size: 'medium', step: 1, style: {}, precision: 0, editable: true, onChange: func.noop, onKeyDown: func.noop, onBlur: func.noop, onCorrect: func.noop, onDisabled: func.noop, }; constructor(props) { super(props); let value; if ('value' in props) { value = props.value; } else { value = props.defaultValue; } this.state = { value: value === undefined || value === null ? '' : value, hasFocused: false, reRender: true, }; } static getDerivedStateFromProps(nextProps, prevState) { if ( 'value' in nextProps && nextProps.value !== prevState.value && prevState.reRender ) { const { value } = nextProps; return { value: value === undefined || value === null ? '' : value, }; } return null; } onChange(value, e) { if (this.props.editable === true) { value = value.trim(); // Compatible Chinese Input Method value = value.replace('。', '.'); // ignore space if (this.state.value === value) { return; } // in case of autoCorrect ('0.'=>0, '0.0'=>0) , we have these steps if (value) { // ignore when input start form '-' if (value === '-' || this.state.value === '-') { this.setState({ value, reRender: false, }); return; } // ignore when input 0./0.0/0.00 to 0.001 // but take care of Number('')=0; if (value.match(/\.0*$/)) { this.setState({ value, reRender: false, }); return; } // ignore when value < min (because number is inputted one by one) if (!isNaN(value) && Number(value) < this.props.min) { this.setState({ value, reRender: false, }); return; } } this.setInputValue(value, e); } } /** * @param {Float} currentValue correct value * @param {String} oldValue input value */ onCorrect(currentValue, oldValue) { this.props.onCorrect({ currentValue, oldValue, }); } onKeyDown(e, ...args) { if (e.keyCode === 38) { this.up(false, e); } else if (e.keyCode === 40) { this.down(false, e); } this.props.onKeyDown(e, ...args); } onFocus(e, ...args) { const { onFocus } = this.props; this.setFocus(true); onFocus && onFocus(e, ...args); } onBlur(e, ...args) { const value = this.getCurrentValidValue(e.target.value.trim()); if (this.state.value !== value) { this.setValue(value, e); } this.setFocus(false); const { onBlur } = this.props; onBlur && onBlur(e, ...args); } getCurrentValidValue(value) { let val = value; const { props } = this; if (val === '') { val = ''; } else if (!isNaN(val)) { val = Number(val); if (val < props.min) { val = props.min; } if (val > props.max) { val = props.max; } // precision=2 and input from 1.99 to 1.999, should stay with 1.99 not 2 const strValue = `${val}`; const pointPos = strValue.indexOf('.'); const cutPos = pointPos + 1 + this.getPrecision(); if (pointPos !== -1 && strValue.length > cutPos) { val = Number(strValue.substr(0, cutPos)); } } else { val = this.state.value; } if (`${val}` !== `${value}`) { // under controled, set back to props.value if ( 'value' in this.props && `${this.props.value}` !== `${this.state.value}` ) { this.setState({ value: this.props.value, }); } this.onCorrect(val, value); } return val; } setValue(v, e, triggerType) { if (!('value' in this.props)) { this.setState({ value: v, }); } this.setState({ reRender: true, }); this.props.onChange(isNaN(v) || v === '' ? undefined : v, { ...e, triggerType, }); } setInputValue(v, e) { const value = this.getCurrentValidValue(v); if (this.state.value !== value) { this.setValue(value, e); } } setFocus(status) { const { format } = this.props; // Only trigger `setState` if `format` is settled to avoid unnecessary rendering if (typeof format === 'function') { this.setState({ hasFocused: status, }); } } getPrecision() { const { props } = this; const stepString = props.step.toString(); if (stepString.indexOf('e-') >= 0) { return parseInt(stepString.slice(stepString.indexOf('e-')), 10); } let precision = 0; if (stepString.indexOf('.') >= 0) { precision = stepString.length - stepString.indexOf('.') - 1; } return Math.max(precision, this.props.precision); } getPrecisionFactor() { const precision = this.getPrecision(); return Math.pow(10, precision); } upStep(val) { const { step, min } = this.props; const precisionFactor = this.getPrecisionFactor(); let result; if (typeof val === 'number') { result = (precisionFactor * val + precisionFactor * step) / precisionFactor; result = this.hackChrome(result); } else { result = min === -Infinity ? step : min; } return result; } downStep(val) { const { step, min } = this.props; const precisionFactor = this.getPrecisionFactor(); let result; if (typeof val === 'number') { result = (precisionFactor * val - precisionFactor * step) / precisionFactor; result = this.hackChrome(result); } else { result = min === -Infinity ? -step : min; } return result; } /** * fix bug in chrome browser * 0.28 + 0.01 = 0.29000000000000004 * 0.29 - 0.01 = 0.27999999999999997 * @param {Number} value value */ hackChrome(value) { const precision = this.getPrecision(); if (precision > 0) { return Number(Number(value).toFixed(precision)); } return value; } step(type, disabled, e) { if (e) { e.preventDefault(); } const { onDisabled, min, max } = this.props; if (disabled) { return onDisabled(e); } const { value } = this.state; if (isNaN(value)) { return; } let val = this[`${type}Step`](value); if (val > max) { val = max; } if (val < min) { val = min; } this.setValue(val, e, type); } down(disabled, e) { this.step('down', disabled, e); } up(disabled, e) { this.step('up', disabled, e); } renderValue() { const { value, hasFocused } = this.state; const { format } = this.props; return typeof format === 'function' && !hasFocused ? format(value) : value; } focus() { this.inputRef.getInstance().focus(); } saveInputRef(ref) { this.inputRef = ref; } handleMouseDown(e) { e.preventDefault(); } render() { const { device, prefix, rtl, disabled, style, className, size, max, min, autoFocus, editable, state, label, upBtnProps = {}, downBtnProps = {}, innerAfter, isPreview, renderPreview, } = this.props; const type = device === 'phone' ? 'inline' : this.props.type; const prefixCls = `${prefix}number-picker`; const cls = classNames({ [prefixCls]: true, [`${prefixCls}-${type}`]: type, [`${prefix}${size}`]: true, [className]: className, }); let upDisabled = false; let downDisabled = false; const { value } = this.state; if (!isNaN(value)) { const val = Number(value); if (val >= max) { upDisabled = true; } if (val <= min) { downDisabled = true; } } let extra = null; const innerAfterClassName = null; let addonBefore = null; let addonAfter = null; if (type === 'normal') { extra = ( ); } else { addonBefore = ( ); addonAfter = ( ); } const others = obj.pickOthers(NumberPicker.propTypes, this.props); const dataAttrs = obj.pickAttrsWith(this.props, 'data-'); const previewCls = classNames({ [`${prefix}form-preview`]: true, [className]: !!className, }); if (isPreview) { if (typeof renderPreview === 'function') { return (
{renderPreview(this.renderValue(), this.props)}
); } return (

{this.renderValue()}

); } return ( ); } } export default polyfill(NumberPicker);