@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
508 lines (506 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.InputNumber = void 0;
var _noop2 = _interopRequireDefault(require("lodash/noop"));
var _isString2 = _interopRequireDefault(require("lodash/isString"));
var _isNaN2 = _interopRequireDefault(require("lodash/isNaN"));
var _react = _interopRequireDefault(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _input = _interopRequireDefault(require("../input"));
var _object = require("@douyinfe/semi-foundation/lib/cjs/utils/object");
var _isNullOrUndefined = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/isNullOrUndefined"));
var _isBothNaN = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/isBothNaN"));
var _foundation = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/inputNumber/foundation"));
var _baseComponent = _interopRequireDefault(require("../_base/baseComponent"));
var _constants = require("@douyinfe/semi-foundation/lib/cjs/inputNumber/constants");
var _semiIcons = require("@douyinfe/semi-icons");
require("@douyinfe/semi-foundation/lib/cjs/inputNumber/inputNumber.css");
var _localeConsumer = _interopRequireDefault(require("../locale/localeConsumer"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
var __rest = void 0 && (void 0).__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
/* eslint-disable jsx-a11y/no-static-element-interactions */
class InputNumber extends _baseComponent.default {
get adapter() {
var _this = this;
return Object.assign(Object.assign({}, super.adapter), {
setValue: (value, cb) => this.setState({
value
}, cb),
setNumber: (number, cb) => this.setState({
number
}, cb),
setFocusing: (focusing, cb) => this.setState({
focusing
}, cb),
setHovering: hovering => this.setState({
hovering
}),
notifyChange: function () {
return _this.props.onChange(...arguments);
},
notifyNumberChange: function () {
return _this.props.onNumberChange(...arguments);
},
notifyBlur: e => this.props.onBlur(e),
notifyFocus: e => this.props.onFocus(e),
notifyUpClick: (value, e) => this.props.onUpClick(value, e),
notifyDownClick: (value, e) => this.props.onDownClick(value, e),
notifyKeyDown: e => this.props.onKeyDown(e),
registerGlobalEvent: (eventName, handler) => {
if (eventName && typeof handler === 'function') {
this.adapter.unregisterGlobalEvent(eventName);
this.adapter.setCache(eventName, handler);
document.addEventListener(eventName, handler);
}
},
unregisterGlobalEvent: eventName => {
if (eventName) {
const handler = this.adapter.getCache(eventName);
document.removeEventListener(eventName, handler);
this.adapter.setCache(eventName, null);
}
},
getInputCharacter: index => {
return this.inputNode.value[index];
},
recordCursorPosition: () => {
// Record position
try {
if (this.inputNode) {
this.cursorStart = this.inputNode.selectionStart;
this.cursorEnd = this.inputNode.selectionEnd;
this.currentValue = this.inputNode.value;
this.cursorBefore = this.inputNode.value.substring(0, this.cursorStart);
this.cursorAfter = this.inputNode.value.substring(this.cursorEnd);
}
} catch (e) {
console.warn(e);
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
},
restoreByAfter: str => {
if ((0, _isNullOrUndefined.default)(str)) {
return false;
}
const fullStr = this.inputNode.value;
const index = fullStr.lastIndexOf(str);
if (index === -1) {
return false;
}
if (index + str.length === fullStr.length) {
this.adapter.fixCaret(index, index);
return true;
}
return false;
},
restoreCursor: function () {
let str = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _this.cursorAfter;
if ((0, _isNullOrUndefined.default)(str)) {
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, (_, start) => {
const partStr = str.substring(start);
return _this.adapter.restoreByAfter(partStr);
});
},
fixCaret: (start, end) => {
if (start === undefined || end === undefined || !this.inputNode || !this.inputNode.value) {
return;
}
try {
const currentStart = this.inputNode.selectionStart;
const currentEnd = this.inputNode.selectionEnd;
if (start !== currentStart || end !== currentEnd) {
this.inputNode.setSelectionRange(start, end);
}
} catch (e) {
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
},
setClickUpOrDown: value => {
this.clickUpOrDown = value;
},
updateStates: (states, callback) => {
this.setState(states, callback);
}
});
}
constructor(props) {
super(props);
this.setInputRef = node => {
const {
forwardedRef
} = this.props;
this.inputNode = node;
if (forwardedRef && typeof forwardedRef === 'object') {
forwardedRef.current = node;
} else if (typeof forwardedRef === 'function') {
forwardedRef(node);
}
};
this.handleInputFocus = e => this.foundation.handleInputFocus(e);
this.handleInputChange = (value, event) => this.foundation.handleInputChange(value, event);
this.handleInputBlur = e => this.foundation.handleInputBlur(e);
this.handleInputKeyDown = e => this.foundation.handleInputKeyDown(e);
this.handleInputMouseEnter = e => this.foundation.handleInputMouseEnter(e);
this.handleInputMouseLeave = e => this.foundation.handleInputMouseLeave(e);
this.handleInputMouseMove = e => this.foundation.handleInputMouseMove(e);
this.handleUpClick = e => this.foundation.handleUpClick(e);
this.handleDownClick = e => this.foundation.handleDownClick(e);
this.handleMouseUp = e => this.foundation.handleMouseUp(e);
this.handleMouseLeave = e => this.foundation.handleMouseLeave(e);
this.renderButtons = () => {
const {
prefixCls,
disabled,
innerButtons,
max,
min
} = this.props;
const {
hovering,
focusing,
number
} = this.state;
const notAllowedUp = disabled ? disabled : number === max;
const notAllowedDown = disabled ? disabled : number === min;
const suffixChildrenCls = (0, _classnames.default)(`${prefixCls}-number-suffix-btns`, {
[`${prefixCls}-number-suffix-btns-inner`]: innerButtons,
[`${prefixCls}-number-suffix-btns-inner-hover`]: innerButtons && hovering && !focusing
});
const upClassName = (0, _classnames.default)(`${prefixCls}-number-button`, `${prefixCls}-number-button-up`, {
[`${prefixCls}-number-button-up-disabled`]: disabled,
[`${prefixCls}-number-button-up-not-allowed`]: notAllowedUp
});
const downClassName = (0, _classnames.default)(`${prefixCls}-number-button`, `${prefixCls}-number-button-down`, {
[`${prefixCls}-number-button-down-disabled`]: disabled,
[`${prefixCls}-number-button-down-not-allowed`]: notAllowedDown
});
return /*#__PURE__*/_react.default.createElement("div", {
className: suffixChildrenCls
}, /*#__PURE__*/_react.default.createElement("span", {
className: upClassName,
onMouseDown: notAllowedUp ? _noop2.default : this.handleUpClick,
onMouseUp: this.handleMouseUp,
onMouseLeave: this.handleMouseLeave
}, /*#__PURE__*/_react.default.createElement(_semiIcons.IconChevronUp, {
size: "extra-small"
})), /*#__PURE__*/_react.default.createElement("span", {
className: downClassName,
onMouseDown: notAllowedDown ? _noop2.default : this.handleDownClick,
onMouseUp: this.handleMouseUp,
onMouseLeave: this.handleMouseLeave
}, /*#__PURE__*/_react.default.createElement(_semiIcons.IconChevronDown, {
size: "extra-small"
})));
};
this.renderSuffix = () => {
const {
innerButtons,
suffix
} = this.props;
const {
hovering,
focusing
} = this.state;
if (innerButtons && (hovering || focusing)) {
const buttons = this.renderButtons();
return buttons;
}
return suffix;
};
this.state = {
value: '',
number: null,
focusing: Boolean(props.autofocus) || false,
hovering: false
};
this.inputNode = null;
this.foundation = new _foundation.default(this.adapter);
this.clickUpOrDown = false;
}
componentDidUpdate(prevProps) {
const {
value,
preventScroll
} = this.props;
const {
focusing
} = this.state;
let newValue;
/**
* To determine whether the front and back are equal
* NaN need to check whether both are NaN
*/
if (value !== prevProps.value && !(0, _isBothNaN.default)(value, prevProps.value)) {
if ((0, _isNullOrUndefined.default)(value) || value === '') {
newValue = '';
this.foundation.updateStates({
value: newValue,
number: null
});
} else {
let valueStr = value;
if (typeof value === 'number') {
valueStr = this.foundation.doFormat(value);
}
const parsedNum = this.foundation.doParse(valueStr, false, true, true);
const toNum = typeof value === 'number' ? value : this.foundation.doParse(valueStr, false, false, false);
/**
* focusing 状态为输入状态,输入状态的受控值要特殊处理
* 如:
* - 输入合法值
* 123 => input value 也应该是 123,同时需要设置 number 为 123
* - 输入非法值,只设置 input value,不设置非法的number
* abc => input value 这时是 abc,但失焦后会进行格式化
* 100(超出范围) => input value 应该是 100,但不设置 number
*
* 保持输入态有三种方式
* 1. 输入框输入
* - 输入可以解析为合法数字,input value根据输入值确定,失焦时更新input value
* - 输入不可解析为合法数字,进行格式化后显示在input框
* 2. 键盘点击上下按钮(input value根据受控值进行更改)
* 3. keepFocus+鼠标点击上下按钮(input value根据受控值进行更改)
*
* The focusing state is the input state, and the controlled value of the input state needs special treatment
* For example:
* - input legal value
* 123 = > input value should also be 123, and the number should be set to 123
* - input illegal value, only set the input value, do not set the illegal number
* abc = > input value This is abc at this time, but it will be formatted after being out of focus
* 100 (out of range) = > input value should be 100, but no number
*
* There are three ways to maintain the input state
* 1. input box input
* - input can be resolved into legal numbers, input value is determined according to the input value, and input value is updated when out of focus
* - input cannot be resolved into legal numbers, and it will be displayed in the input box after formatting
* 2. Keyboard click on the up and down button (input value is changed according to the controlled value)
* 3.keepFocus + mouse click on the up and down button (input value is changed according to the controlled value)
*/
if (focusing) {
if (this.foundation.isValidNumber(parsedNum) && parsedNum !== this.state.number) {
const obj = {
number: parsedNum
};
/**
* If you are clicking the button, it will automatically format once
* We need to set the status to false after trigger focus event
*/
if (this.clickUpOrDown) {
obj.value = this.foundation.doFormat(obj.number, true);
newValue = obj.value;
}
this.foundation.updateStates(obj, () => this.adapter.restoreCursor());
} else if (!(0, _isNaN2.default)(toNum)) {
// Update input content when controlled input is illegal and not NaN
newValue = this.foundation.doFormat(toNum, false);
this.foundation.updateStates({
value: newValue
});
} else {
// Update input content when controlled input NaN
this.foundation.updateStates({
value: valueStr
});
}
} else if (this.foundation.isValidNumber(parsedNum)) {
newValue = this.foundation.doFormat(parsedNum, true, true);
this.foundation.updateStates({
number: parsedNum,
value: newValue
});
} else {
// Invalid digital analog blurring effect instead of controlled failure
newValue = '';
this.foundation.updateStates({
number: null,
value: newValue
});
}
}
if (newValue && (0, _isString2.default)(newValue) && newValue !== String(this.props.value)) {
if (this.foundation._isCurrency()) {
// 仅在解析后的数值而不是格式化的字符串变化时 notifyChange
// notifyChange only when the parsed value changes, not the formatted string
const parsedNewValue = this.foundation.doParse(newValue);
const parsedPropValue = typeof this.props.value === 'string' ? this.foundation.doParse(this.props.value) : this.props.value;
if (parsedNewValue !== parsedPropValue) {
this.foundation.notifyChange(newValue, null);
}
} else {
this.foundation.notifyChange(newValue, null);
}
}
}
if (!this.clickUpOrDown) {
return;
}
if (this.props.keepFocus && this.state.focusing) {
if (document.activeElement !== this.inputNode) {
this.inputNode.focus({
preventScroll
});
}
}
}
render() {
const _a = this.props,
{
disabled,
className,
prefixCls,
min,
max,
step,
shiftStep,
precision,
formatter,
parser,
forwardedRef,
onUpClick,
onDownClick,
pressInterval,
pressTimeout,
suffix,
size,
hideButtons,
innerButtons,
style,
onNumberChange,
keepFocus,
defaultValue
} = _a,
rest = __rest(_a, ["disabled", "className", "prefixCls", "min", "max", "step", "shiftStep", "precision", "formatter", "parser", "forwardedRef", "onUpClick", "onDownClick", "pressInterval", "pressTimeout", "suffix", "size", "hideButtons", "innerButtons", "style", "onNumberChange", "keepFocus", "defaultValue"]);
const {
value,
number
} = this.state;
const inputNumberCls = (0, _classnames.default)(className, `${prefixCls}-number`, {
[`${prefixCls}-number-size-${size}`]: size
});
const buttons = this.renderButtons();
const ariaProps = {
'aria-disabled': disabled,
step
};
if (number) {
ariaProps['aria-valuenow'] = number;
}
if (max !== Infinity) {
ariaProps['aria-valuemax'] = max;
}
if (min !== -Infinity) {
ariaProps['aria-valuemin'] = min;
}
const input = /*#__PURE__*/_react.default.createElement("div", {
className: inputNumberCls,
style: style,
onMouseMove: e => this.handleInputMouseMove(e),
onMouseEnter: e => this.handleInputMouseEnter(e),
onMouseLeave: e => this.handleInputMouseLeave(e)
}, /*#__PURE__*/_react.default.createElement(_input.default, Object.assign({
role: "spinbutton"
}, ariaProps, rest, {
size: size,
disabled: disabled,
ref: this.setInputRef,
value: value,
onFocus: this.handleInputFocus,
onChange: this.handleInputChange,
onBlur: this.handleInputBlur,
onKeyDown: this.handleInputKeyDown,
suffix: this.renderSuffix()
})), hideButtons || innerButtons ? null : buttons);
return input;
}
}
exports.InputNumber = InputNumber;
InputNumber.propTypes = {
'aria-label': _propTypes.default.string,
'aria-labelledby': _propTypes.default.string,
'aria-invalid': _propTypes.default.bool,
'aria-errormessage': _propTypes.default.string,
'aria-describedby': _propTypes.default.string,
'aria-required': _propTypes.default.bool,
autofocus: _propTypes.default.bool,
clearIcon: _propTypes.default.node,
className: _propTypes.default.string,
defaultValue: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.string]),
disabled: _propTypes.default.bool,
formatter: _propTypes.default.func,
forwardedRef: _propTypes.default.any,
hideButtons: _propTypes.default.bool,
innerButtons: _propTypes.default.bool,
insetLabel: _propTypes.default.node,
insetLabelId: _propTypes.default.string,
keepFocus: _propTypes.default.bool,
max: _propTypes.default.number,
min: _propTypes.default.number,
parser: _propTypes.default.func,
precision: _propTypes.default.number,
prefixCls: _propTypes.default.string,
pressInterval: _propTypes.default.number,
pressTimeout: _propTypes.default.number,
preventScroll: _propTypes.default.bool,
shiftStep: _propTypes.default.number,
showCurrencySymbol: _propTypes.default.bool,
step: _propTypes.default.number,
style: _propTypes.default.object,
suffix: _propTypes.default.any,
value: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.string]),
onBlur: _propTypes.default.func,
onChange: _propTypes.default.func,
onDownClick: _propTypes.default.func,
onKeyDown: _propTypes.default.func,
onNumberChange: _propTypes.default.func,
onUpClick: _propTypes.default.func
};
InputNumber.defaultProps = {
forwardedRef: _noop2.default,
innerButtons: false,
keepFocus: false,
max: Infinity,
min: -Infinity,
prefixCls: _constants.cssClasses.PREFIX,
pressInterval: _constants.numbers.DEFAULT_PRESS_TIMEOUT,
pressTimeout: _constants.numbers.DEFAULT_PRESS_TIMEOUT,
shiftStep: _constants.numbers.DEFAULT_SHIFT_STEP,
showCurrencySymbol: true,
size: _constants.strings.DEFAULT_SIZE,
step: _constants.numbers.DEFAULT_STEP,
onBlur: _noop2.default,
onChange: _noop2.default,
onDownClick: _noop2.default,
onFocus: _noop2.default,
onKeyDown: _noop2.default,
onNumberChange: _noop2.default,
onUpClick: _noop2.default
};
var _default = exports.default = (0, _object.forwardStatics)(/*#__PURE__*/_react.default.forwardRef(function SemiInputNumber(props, ref) {
return /*#__PURE__*/_react.default.createElement(_localeConsumer.default, {
componentName: "InputNumber"
}, (locale, localeCode, dateFnsLocale, currency) => (/*#__PURE__*/_react.default.createElement(InputNumber, Object.assign({
localeCode: localeCode,
defaultCurrency: currency
}, props, {
forwardedRef: ref
}))));
}), InputNumber);