UNPKG

24.2 kBJavaScriptView Raw
1import _extends from 'babel-runtime/helpers/extends';
2import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
3import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
4import _inherits from 'babel-runtime/helpers/inherits';
5
6var _class, _temp;
7
8import React from 'react';
9import PropTypes from 'prop-types';
10import classNames from 'classnames';
11import BigNumber from 'bignumber.js';
12import { polyfill } from 'react-lifecycles-compat';
13
14import Icon from '../icon';
15import Button from '../button';
16import Input from '../input';
17import { func, obj } from '../util';
18
19var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1;
20var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -Math.pow(2, 53) + 1;
21
22var isNil = obj.isNil;
23/** NumberPicker */
24
25var NumberPicker = (_temp = _class = function (_React$Component) {
26 _inherits(NumberPicker, _React$Component);
27
28 function NumberPicker(props) {
29 _classCallCheck(this, NumberPicker);
30
31 var _this = _possibleConstructorReturn(this, _React$Component.call(this, props));
32
33 var defaultValue = props.defaultValue,
34 stringMode = props.stringMode;
35
36
37 var value = void 0;
38 if ('value' in props) {
39 value = props.value;
40 } else {
41 value = defaultValue;
42 }
43 value = value === undefined || value === null ? '' : stringMode ? '' + value : value;
44 _this.state = {
45 value: value,
46 hasFocused: false,
47 onlyDisplay: false,
48 displayValue: value,
49 max: stringMode ? Infinity : MAX_SAFE_INTEGER,
50 min: stringMode ? -Infinity : MIN_SAFE_INTEGER
51 };
52 return _this;
53 }
54
55 NumberPicker.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
56 // 用户键入非法值后render逻辑,未触发onChange,业务组件无感知,不强制受控value
57 if (prevState.onlyDisplay) {
58 return {
59 value: prevState.value,
60 displayValue: prevState.displayValue,
61 onlyDisplay: false
62 };
63 }
64
65 var state = {};
66 var value = nextProps.value,
67 stringMode = nextProps.stringMode;
68 // 一般受控render逻辑
69
70 if ('value' in nextProps && '' + nextProps.value !== '' + prevState.value) {
71 var newValue = value === undefined || value === null ? '' : stringMode ? '' + value : value;
72 state.value = newValue;
73 // 因为 Number('') === 0,所以会导致value=0赋值不生效
74 if (prevState.displayValue === '' || Number(prevState.displayValue) !== nextProps.value) {
75 state.displayValue = newValue;
76 }
77 }
78
79 // 如果是undefined或null,应该不限制最大最小值
80 var min = nextProps.min,
81 max = nextProps.max;
82
83 if ('min' in nextProps && min !== prevState.min) {
84 state.min = !isNil(min) ? min : stringMode ? Infinity : MIN_SAFE_INTEGER;
85 }
86
87 if ('max' in nextProps && max !== prevState.max) {
88 state.max = !isNil(max) ? max : stringMode ? Infinity : MAX_SAFE_INTEGER;
89 }
90
91 if (Object.keys(state).length) {
92 return state;
93 }
94
95 return null;
96 };
97
98 NumberPicker.prototype.isGreaterThan = function isGreaterThan(v1, v2) {
99 var stringMode = this.props.stringMode;
100
101 if (stringMode) return BigNumber(v1).isGreaterThan(BigNumber(v2));
102 return Number(v1) > Number(v2);
103 };
104
105 NumberPicker.prototype.correctBoundary = function correctBoundary(value) {
106 var _state = this.state,
107 max = _state.max,
108 min = _state.min;
109
110 return this.isGreaterThan(min, value) ? min : this.isGreaterThan(value, max) ? max : value;
111 };
112
113 NumberPicker.prototype.setFocus = function setFocus(status) {
114 var format = this.props.format;
115 // Only trigger `setState` if `format` is settled to avoid unnecessary rendering
116
117 if (typeof format === 'function') {
118 this.setState({
119 hasFocused: status
120 });
121 }
122 };
123
124 NumberPicker.prototype.onFocus = function onFocus(e) {
125 var onFocus = this.props.onFocus;
126
127 this.setFocus(true);
128
129 for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
130 args[_key - 1] = arguments[_key];
131 }
132
133 onFocus && onFocus.apply(undefined, [e].concat(args));
134 };
135
136 NumberPicker.prototype.onBlur = function onBlur(e) {
137 var _props = this.props,
138 editable = _props.editable,
139 stringMode = _props.stringMode;
140
141 var displayValue = '' + this.state.displayValue;
142 // 展示值合法但超出边界时,额外在Blur时触发onChange
143 // 展示值非法时,回退前一个有效值
144 if (editable === true && !isNaN(displayValue) && !this.shouldFireOnChange(displayValue) && !this.withinMinMax(displayValue)) {
145 var valueCorrected = this.correctValue(displayValue);
146 valueCorrected = stringMode ? BigNumber(valueCorrected).toFixed(this.getPrecision()) : valueCorrected;
147 if (this.state.value !== valueCorrected) {
148 this.setValue({ value: valueCorrected, e: e });
149 }
150 this.setDisplayValue({ displayValue: valueCorrected });
151 } else {
152 this.setDisplayValue({ displayValue: this.state.value });
153 }
154
155 this.setFocus(false);
156 var onBlur = this.props.onBlur;
157
158 for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
159 args[_key2 - 1] = arguments[_key2];
160 }
161
162 onBlur && onBlur.apply(undefined, [e].concat(args));
163 };
164
165 NumberPicker.prototype.withinMinMax = function withinMinMax(value) {
166 var _state2 = this.state,
167 max = _state2.max,
168 min = _state2.min;
169
170 if (isNaN(value) || this.isGreaterThan(value, max) || this.isGreaterThan(min, value)) return false;
171 return true;
172 };
173
174 NumberPicker.prototype.setDisplayValue = function setDisplayValue(_ref) {
175 var displayValue = _ref.displayValue,
176 _ref$onlyDisplay = _ref.onlyDisplay,
177 onlyDisplay = _ref$onlyDisplay === undefined ? false : _ref$onlyDisplay;
178
179 this.setState({ displayValue: displayValue, onlyDisplay: onlyDisplay });
180 };
181
182 NumberPicker.prototype.getDisplayValue = function getDisplayValue() {
183 var _state3 = this.state,
184 displayValue = _state3.displayValue,
185 hasFocused = _state3.hasFocused;
186 var format = this.props.format;
187
188
189 return typeof format === 'function' && !hasFocused ? format(displayValue) : // 避免原生input将number类型的-0,渲染为0
190 typeof displayValue === 'number' && 1 / displayValue === -Infinity ? '-0' : displayValue;
191 };
192
193 NumberPicker.prototype.shouldFireOnChange = function shouldFireOnChange(value) {
194 // 不触发onChange:a.非数字 b.超出边界的数字输入
195 if (isNaN(value) || !this.withinMinMax(value)) {
196 return false;
197 }
198 return true;
199 };
200
201 NumberPicker.prototype.onChange = function onChange(value, e) {
202 // ignore space & Compatible Chinese Input Method
203 value = value.replace('。', '.').trim();
204 var onlyDisplay = false;
205 if (this.props.editable === true && this.shouldFireOnChange(value)) {
206 var valueCorrected = this.correctValue(value);
207 if (this.state.value !== valueCorrected) {
208 this.setValue({ value: valueCorrected, e: e });
209 }
210 } else {
211 onlyDisplay = true;
212 }
213
214 // 【不应支持】如果输入为满足精度要求的纯数字,底层input.value设置为数字类型而非string
215 // if (`${valueCorrected}` === value) value = valueCorrected;
216
217 this.setDisplayValue({ displayValue: value, onlyDisplay: onlyDisplay });
218 };
219
220 NumberPicker.prototype.correctValue = function correctValue(value) {
221 var val = value;
222
223 // take care of isNaN('')=false
224 if (val !== '') {
225 // 精度订正:直接cut,不四舍五入
226 var precisionSet = this.getPrecision();
227 var precisionCurrent = value.length - value.indexOf('.') - 1;
228 var dotIndex = value.indexOf('.');
229 // precision === 0 should cut '.' for stringMode
230 var cutPosition = precisionSet !== 0 ? dotIndex + 1 + precisionSet : dotIndex + precisionSet;
231 if (dotIndex > -1 && precisionCurrent > precisionSet) val = val.substr(0, cutPosition);
232
233 // 边界订正:
234 val = this.correctBoundary(val);
235 val = this.props.stringMode ? BigNumber(val).toFixed() : Number(val);
236 }
237
238 if (isNaN(val)) val = this.state.value;
239
240 if ('' + val !== '' + value) {
241 // .0* 到 .x0* 不该触发onCorrect
242 if (!/\.[0-9]*0+$/g.test(value)) {
243 this.props.onCorrect({
244 currentValue: val,
245 oldValue: value
246 });
247 }
248 }
249
250 return val;
251 };
252
253 NumberPicker.prototype.setValue = function setValue(_ref2) {
254 var value = _ref2.value,
255 e = _ref2.e,
256 triggerType = _ref2.triggerType;
257
258 if (!('value' in this.props) || value === this.props.value) {
259 this.setState({
260 value: value
261 });
262 }
263
264 this.props.onChange(isNaN(value) || value === '' ? undefined : value, _extends({}, e, {
265 triggerType: triggerType
266 }));
267 };
268
269 NumberPicker.prototype.getPrecision = function getPrecision() {
270 var stepString = this.props.step.toString();
271 if (stepString.indexOf('e-') >= 0) {
272 return parseInt(stepString.slice(stepString.indexOf('e-')), 10);
273 }
274 var precision = 0;
275 if (stepString.indexOf('.') >= 0) {
276 precision = stepString.length - stepString.indexOf('.') - 1;
277 }
278
279 return Math.max(precision, this.props.precision);
280 };
281
282 NumberPicker.prototype.getPrecisionFactor = function getPrecisionFactor() {
283 var precision = this.getPrecision();
284 return Math.pow(10, precision);
285 };
286
287 NumberPicker.prototype.onKeyDown = function onKeyDown(e) {
288 var _props2;
289
290 if (e.keyCode === 38) {
291 this.up(false, e);
292 } else if (e.keyCode === 40) {
293 this.down(false, e);
294 }
295
296 for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
297 args[_key3 - 1] = arguments[_key3];
298 }
299
300 (_props2 = this.props).onKeyDown.apply(_props2, [e].concat(args));
301 };
302
303 NumberPicker.prototype.up = function up(disabled, e) {
304 this.step('up', disabled, e);
305 };
306
307 NumberPicker.prototype.down = function down(disabled, e) {
308 this.step('down', disabled, e);
309 };
310
311 NumberPicker.prototype.step = function step(type, disabled, e) {
312 if (e) {
313 e.preventDefault();
314 }
315
316 var onDisabled = this.props.onDisabled;
317
318 if (disabled) {
319 return onDisabled(e);
320 }
321
322 var value = this.state.value;
323 // 受控下,可能强制回填非法值
324 if (isNaN(value)) {
325 return;
326 }
327
328 if (value === '' && !this.props.stringMode) {
329 value = 0;
330 }
331
332 var val = this[type + 'Step'](value);
333 val = this.correctBoundary(val);
334 // 受控下,显示的值应为受控value
335 if (!('value' in this.props)) {
336 this.setDisplayValue({ displayValue: val });
337 }
338
339 this.setValue({ value: val, e: e, triggerType: type });
340 };
341
342 NumberPicker.prototype.upStep = function upStep(val) {
343 var _props3 = this.props,
344 step = _props3.step,
345 stringMode = _props3.stringMode;
346
347 var precisionFactor = this.getPrecisionFactor();
348 if (typeof val === 'number' && !stringMode) {
349 var result = (precisionFactor * val + precisionFactor * step) / precisionFactor;
350 return this.hackChrome(result);
351 }
352 return BigNumber(val || '0').plus(step).toFixed(this.getPrecision());
353 };
354
355 NumberPicker.prototype.downStep = function downStep(val) {
356 var _props4 = this.props,
357 step = _props4.step,
358 stringMode = _props4.stringMode;
359
360 var precisionFactor = this.getPrecisionFactor();
361 if (typeof val === 'number' && !stringMode) {
362 var result = (precisionFactor * val - precisionFactor * step) / precisionFactor;
363 return this.hackChrome(result);
364 }
365 return BigNumber(val || '0').minus(step).toFixed(this.getPrecision());
366 };
367
368 /**
369 * fix bug in chrome browser
370 * 0.28 + 0.01 = 0.29000000000000004
371 * 0.29 - 0.01 = 0.27999999999999997
372 * @param {Number} value value
373 */
374
375
376 NumberPicker.prototype.hackChrome = function hackChrome(value) {
377 var precision = this.getPrecision();
378 if (precision > 0) {
379 return Number(Number(value).toFixed(precision));
380 }
381 return value;
382 };
383
384 NumberPicker.prototype.focus = function focus() {
385 this.inputRef.getInstance().focus();
386 };
387
388 NumberPicker.prototype.saveInputRef = function saveInputRef(ref) {
389 this.inputRef = ref;
390 };
391
392 NumberPicker.prototype.getInputNode = function getInputNode() {
393 return this.inputRef;
394 };
395
396 NumberPicker.prototype.handleMouseDown = function handleMouseDown(e) {
397 e.preventDefault();
398 };
399
400 NumberPicker.prototype.render = function render() {
401 var _classNames, _classNames2;
402
403 var _props5 = this.props,
404 device = _props5.device,
405 prefix = _props5.prefix,
406 rtl = _props5.rtl,
407 disabled = _props5.disabled,
408 style = _props5.style,
409 className = _props5.className,
410 size = _props5.size,
411 autoFocus = _props5.autoFocus,
412 editable = _props5.editable,
413 state = _props5.state,
414 label = _props5.label,
415 _props5$upBtnProps = _props5.upBtnProps,
416 upBtnProps = _props5$upBtnProps === undefined ? {} : _props5$upBtnProps,
417 _props5$downBtnProps = _props5.downBtnProps,
418 downBtnProps = _props5$downBtnProps === undefined ? {} : _props5$downBtnProps,
419 innerAfter = _props5.innerAfter,
420 isPreview = _props5.isPreview,
421 renderPreview = _props5.renderPreview,
422 hasTrigger = _props5.hasTrigger,
423 alwaysShowTrigger = _props5.alwaysShowTrigger;
424 var _state4 = this.state,
425 max = _state4.max,
426 min = _state4.min;
427
428 var type = device === 'phone' || this.props.type === 'inline' ? 'inline' : 'normal';
429
430 var prefixCls = prefix + 'number-picker';
431
432 var cls = classNames((_classNames = {}, _classNames[prefixCls] = true, _classNames[prefixCls + '-' + type] = type, _classNames['' + prefix + size] = true, _classNames[prefixCls + '-show-trigger'] = alwaysShowTrigger, _classNames[prefixCls + '-no-trigger'] = !hasTrigger, _classNames[prefix + 'disabled'] = disabled, _classNames[className] = className, _classNames));
433
434 var upDisabled = false;
435 var downDisabled = false;
436 var value = this.state.value;
437 if (!isNaN(value)) {
438 if (!this.isGreaterThan(max, value)) {
439 upDisabled = true;
440 }
441 if (this.isGreaterThan(min, value) || min === value) {
442 downDisabled = true;
443 }
444 }
445
446 var extra = null,
447 innerAfterClassName = null,
448 addonBefore = null,
449 addonAfter = null;
450 if (type === 'normal') {
451 extra = React.createElement(
452 'span',
453 { className: prefixCls + '-handler' },
454 React.createElement(
455 Button,
456 _extends({}, upBtnProps, {
457 onMouseDown: this.handleMouseDown,
458 disabled: disabled,
459 className: (upBtnProps.className || '') + ' ' + (upDisabled ? 'disabled' : ''),
460 onClick: this.up.bind(this, upDisabled),
461 tabIndex: -1
462 }),
463 React.createElement(Icon, { type: 'arrow-up', className: prefixCls + '-up-icon' })
464 ),
465 React.createElement(
466 Button,
467 _extends({}, downBtnProps, {
468 onMouseDown: this.handleMouseDown,
469 disabled: disabled,
470 className: (downBtnProps.className || '') + ' ' + (downDisabled ? 'disabled' : ''),
471 onClick: this.down.bind(this, downDisabled),
472 tabIndex: -1
473 }),
474 React.createElement(Icon, { type: 'arrow-down', className: prefixCls + '-down-icon' })
475 )
476 );
477 } else {
478 addonBefore = React.createElement(
479 Button,
480 _extends({}, downBtnProps, {
481 size: size,
482 disabled: disabled,
483 className: (downBtnProps.className || '') + ' ' + (downDisabled ? 'disabled' : ''),
484 onClick: this.down.bind(this, downDisabled),
485 tabIndex: -1
486 }),
487 React.createElement(Icon, { type: 'minus', className: prefixCls + '-minus-icon' })
488 );
489 addonAfter = React.createElement(
490 Button,
491 _extends({}, upBtnProps, {
492 size: size,
493 disabled: disabled,
494 className: (upBtnProps.className || '') + ' ' + (upDisabled ? 'disabled' : ''),
495 onClick: this.up.bind(this, upDisabled),
496 tabIndex: -1
497 }),
498 React.createElement(Icon, { type: 'add', className: prefixCls + '-add-icon' })
499 );
500 }
501
502 var others = obj.pickOthers(NumberPicker.propTypes, this.props);
503 var dataAttrs = obj.pickAttrsWith(this.props, 'data-');
504
505 var previewCls = classNames((_classNames2 = {}, _classNames2[prefix + 'form-preview'] = true, _classNames2[className] = !!className, _classNames2));
506
507 if (isPreview) {
508 if (typeof renderPreview === 'function') {
509 return React.createElement(
510 'div',
511 _extends({}, others, { style: style, className: previewCls }),
512 renderPreview(this.getDisplayValue(), this.props)
513 );
514 }
515 return React.createElement(
516 'p',
517 _extends({}, others, { style: { style: style }, className: previewCls }),
518 this.getDisplayValue(),
519 '\xA0',
520 innerAfter
521 );
522 }
523
524 return React.createElement(
525 'span',
526 _extends({ className: cls, style: style, dir: rtl ? 'rtl' : undefined }, dataAttrs),
527 React.createElement(Input, _extends({}, others, {
528 hasClear: false,
529 'aria-valuemax': max,
530 'aria-valuemin': min,
531 state: state === 'error' ? 'error' : null,
532 onBlur: this.onBlur.bind(this),
533 onFocus: this.onFocus.bind(this),
534 onKeyDown: this.onKeyDown.bind(this),
535 autoFocus: autoFocus,
536 readOnly: !editable,
537 value: this.getDisplayValue(),
538 disabled: disabled,
539 size: size,
540 onChange: this.onChange.bind(this),
541 ref: this.saveInputRef.bind(this),
542 label: label,
543 innerAfter: innerAfter,
544 extra: hasTrigger ? extra : null,
545 addonBefore: addonBefore,
546 addonAfter: addonAfter,
547 composition: true
548 }))
549 );
550 };
551
552 return NumberPicker;
553}(React.Component), _class.propTypes = {
554 /**
555 * 样式前缀
556 */
557 prefix: PropTypes.string,
558 /**
559 * 设置类型(当 device 为 phone 时,NumberPicker 的类型强制为 normal,不可通过 type 修改)
560 * @enumdesc 普通, 内联
561 */
562 type: PropTypes.oneOf(['normal', 'inline']),
563 /**
564 * 大小
565 */
566 size: PropTypes.oneOf(['large', 'medium', 'small']),
567 /**
568 * 当前值
569 */
570 value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
571 /**
572 * 默认值
573 */
574 defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
575 /**
576 * 是否禁用
577 */
578 disabled: PropTypes.bool,
579 /**
580 * 步长
581 */
582 step: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
583 /**
584 * 保留小数点后位数
585 */
586 precision: PropTypes.number,
587 /**
588 * 用户是否可以输入
589 */
590 editable: PropTypes.bool,
591 /**
592 * 自动焦点
593 */
594 autoFocus: PropTypes.bool,
595 /**
596 * 数值被改变的事件
597 * @param {Number|String} value 数据
598 * @param {Event} e DOM事件对象
599 */
600 onChange: PropTypes.func,
601 /**
602 * 键盘按下
603 * @param {Event} e DOM事件对象
604 */
605 onKeyDown: PropTypes.func,
606 /**
607 * 焦点获得
608 * @param {Event} e DOM事件对象
609 */
610 onFocus: PropTypes.func,
611 /**
612 * 焦点失去
613 * @param {Event} e DOM事件对象
614 */
615 onBlur: PropTypes.func,
616 /**
617 * 数值订正后的回调
618 * @param {Object} obj {currentValue,oldValue:String}
619 */
620 onCorrect: PropTypes.func,
621 onDisabled: PropTypes.func, // 兼容0.x onDisabled
622 /**
623 * 最大值
624 */
625 max: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
626 /**
627 * 最小值
628 */
629 min: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
630 /**
631 * 自定义class
632 */
633 className: PropTypes.string,
634 /**
635 * 自定义内联样式
636 */
637 style: PropTypes.object,
638 state: PropTypes.oneOf(['error']),
639 /**
640 * 格式化当前值
641 * @param {Number} value
642 * @return {String|Number}
643 */
644 format: PropTypes.func,
645 /**
646 * 增加按钮的props
647 */
648 upBtnProps: PropTypes.object,
649 /**
650 * 减少按钮的props
651 */
652 downBtnProps: PropTypes.object,
653 /**
654 * 内联 左侧label
655 */
656 label: PropTypes.node,
657 /**
658 * 内联 右侧附加内容
659 */
660 innerAfter: PropTypes.node,
661 rtl: PropTypes.bool,
662 /**
663 * 是否为预览态
664 */
665 isPreview: PropTypes.bool,
666 /**
667 * 预览态模式下渲染的内容
668 * @param {Number|String} value 当前值
669 * @param {Object} props 传入的组件参数
670 * @returns {reactNode} Element 渲染内容
671 */
672 renderPreview: PropTypes.func,
673 /**
674 * 预设屏幕宽度
675 */
676 device: PropTypes.oneOf(['phone', 'tablet', 'desktop']),
677 /**
678 * 是否展示点击按钮
679 */
680 hasTrigger: PropTypes.bool,
681 /**
682 * 是否一直显示点击按钮(无须hover)
683 */
684 alwaysShowTrigger: PropTypes.bool,
685 /**
686 * 开启大数支持,输入输出均为string类型
687 * @version 1.24
688 */
689 stringMode: PropTypes.bool
690}, _class.defaultProps = {
691 prefix: 'next-',
692 // max: MAX_SAFE_INTEGER,
693 // min: MIN_SAFE_INTEGER,
694 type: 'normal',
695 size: 'medium',
696 step: 1,
697 style: {},
698 precision: 0,
699 editable: true,
700 onChange: func.noop,
701 onKeyDown: func.noop,
702 onBlur: func.noop,
703 onCorrect: func.noop,
704 onDisabled: func.noop,
705 hasTrigger: true,
706 alwaysShowTrigger: false,
707 stringMode: false
708}, _temp);
709NumberPicker.displayName = 'NumberPicker';
710
711
712export default polyfill(NumberPicker);
\No newline at end of file