react-numeric-input
Version:
Number input component that can replace the native number input which is not yet very well supported and where it is, it does not have the same appearance across the browsers. Additionally this component offers more flexible options and can be used for an
1,153 lines (1,030 loc) • 37.4 kB
JSX
// @flow
import React, { Component } from "react"
import PropTypes from 'prop-types'
const KEYCODE_UP = 38;
const KEYCODE_DOWN = 40;
const IS_BROWSER = typeof document != 'undefined';
const RE_NUMBER = /^[+-]?((\.\d+)|(\d+(\.\d+)?))$/;
const RE_INCOMPLETE_NUMBER = /^([+-]|\.0*|[+-]\.0*|[+-]?\d+\.)?$/;
/**
* Just a simple helper to provide support for older IEs. This is not exactly a
* polyfill for classList.add but it does what we need with minimal effort.
* Works with single className only!
*/
function addClass(element, className) {
if (element.classList) {
return element.classList.add(className)
}
if (!element.className.search(new RegExp("\\b" + className + "\\b"))) {
element.className = " " + className
}
}
/**
* Just a simple helper to provide support for older IEs. This is not exactly a
* polyfill for classList.remove but it does what we need with minimal effort.
* Works with single className only!
*/
function removeClass(element, className) {
if (element.className) {
if (element.classList) {
return element.classList.remove(className)
}
element.className = element.className.replace(
new RegExp("\\b" + className + "\\b", "g"),
""
)
}
}
/**
* Lookup the object.prop and returns it. If it happens to be a function,
* executes it with args and returns it's return value. It the prop does not
* exist on the object, or if it equals undefined, or if it is a function that
* returns undefined the defaultValue will be returned instead.
* @param {Object} object The object to look into
* @param {String} prop The property name
* @param {*} defaultValue The default value
* @param {*[]} args Any additional arguments to pass to the
* function (if the prop is a function).
* @return {*} Whatever happens to be the return value
*/
function access(object, prop, defaultValue, ...args) {
let result = object[prop];
if (typeof result == "function") {
result = result(...args);
}
return result === undefined ? defaultValue : result;
}
/* eslint-disable */
type NumericInputState = {
selectionStart?: number | null;
selectionEnd ?: number | null;
btnDownHover ?: boolean;
btnDownActive ?: boolean;
btnUpHover ?: boolean;
btnUpActive ?: boolean;
value ?: number | null;
stringValue ?: string;
}
/*eslint-enable*/
class NumericInput extends Component
{
static propTypes = {
step : PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
min : PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
max : PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
precision : PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
maxLength : PropTypes.number,
parse : PropTypes.func,
format : PropTypes.func,
className : PropTypes.string,
disabled : PropTypes.bool,
readOnly : PropTypes.bool,
required : PropTypes.bool,
snap : PropTypes.bool,
noValidate : PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
style : PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
noStyle : PropTypes.bool,
type : PropTypes.string,
pattern : PropTypes.string,
onFocus : PropTypes.func,
onBlur : PropTypes.func,
onKeyDown : PropTypes.func,
onChange : PropTypes.func,
onInvalid : PropTypes.func,
onValid : PropTypes.func,
onInput : PropTypes.func,
onSelect : PropTypes.func,
size : PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
value : PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
defaultValue : PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
strict : PropTypes.bool,
componentClass: PropTypes.string,
mobile(props, propName) {
let prop = props[propName]
if (prop !== true && prop !== false && prop !== 'auto' &&
typeof prop != 'function') {
return new Error(
'The "mobile" prop must be true, false, "auto" or a function'
);
}
}
};
/**
* The default behavior is to start from 0, use step of 1 and display
* integers
*/
static defaultProps = {
step : 1,
min : Number.MIN_SAFE_INTEGER || -9007199254740991,
max : Number.MAX_SAFE_INTEGER || 9007199254740991,
precision : null,
parse : null,
format : null,
mobile : 'auto',
strict : false,
componentClass: "input",
style : {}
};
/**
* This are the default styles that act as base for all the component
* instances. One can modify this object to change the default styles
* of all the widgets on the page.
*/
static style = {
// The wrapper (span)
wrap: {
position: 'relative',
display : 'inline-block'
},
'wrap.hasFormControl': {
display : 'block'
},
// The increase button arrow (i)
arrowUp: {
position : 'absolute',
top : '50%',
left : '50%',
width : 0,
height : 0,
borderWidth: '0 0.6ex 0.6ex 0.6ex',
borderColor: 'transparent transparent rgba(0, 0, 0, 0.7)',
borderStyle: 'solid',
margin : '-0.3ex 0 0 -0.56ex'
},
// The decrease button arrow (i)
arrowDown: {
position : 'absolute',
top : '50%',
left : '50%',
width : 0,
height : 0,
borderWidth: '0.6ex 0.6ex 0 0.6ex',
borderColor: 'rgba(0, 0, 0, 0.7) transparent transparent',
borderStyle: 'solid',
margin : '-0.3ex 0 0 -0.56ex'
},
// The vertical segment of the plus sign (for mobile only)
plus: {
position : 'absolute',
top : '50%',
left : '50%',
width : 2,
height : 10,
background : 'rgba(0,0,0,.7)',
margin : '-5px 0 0 -1px'
},
// The horizontal segment of the plus/minus signs (for mobile only)
minus: {
position : 'absolute',
top : '50%',
left : '50%',
width : 10,
height : 2,
background : 'rgba(0,0,0,.7)',
margin : '-1px 0 0 -5px'
},
// Common styles for the up/down buttons (b)
btn: {
position : 'absolute',
right : 2,
width : '2.26ex',
borderColor: 'rgba(0,0,0,.1)',
borderStyle: 'solid',
textAlign : 'center',
cursor : 'default',
transition : 'all 0.1s',
background : 'rgba(0,0,0,.1)',
boxShadow : '-1px -1px 3px rgba(0,0,0,.1) inset,' +
'1px 1px 3px rgba(255,255,255,.7) inset'
},
btnUp: {
top : 2,
bottom : '50%',
borderRadius: '2px 2px 0 0',
borderWidth : '1px 1px 0 1px'
},
'btnUp.mobile': {
width : '3.3ex',
bottom : 2,
boxShadow : 'none',
borderRadius : 2,
borderWidth : 1
},
btnDown: {
top : '50%',
bottom : 2,
borderRadius: '0 0 2px 2px',
borderWidth : '0 1px 1px 1px'
},
'btnDown.mobile': {
width : '3.3ex',
bottom : 2,
left : 2,
top : 2,
right : 'auto',
boxShadow : 'none',
borderRadius : 2,
borderWidth : 1
},
'btn:hover': {
background: 'rgba(0,0,0,.2)'
},
'btn:active': {
background: 'rgba(0,0,0,.3)',
boxShadow : '0 1px 3px rgba(0,0,0,.2) inset,' +
'-1px -1px 4px rgba(255,255,255,.5) inset'
},
'btn:disabled': {
opacity: 0.5,
boxShadow: 'none',
cursor: 'not-allowed'
},
// The input (input[type="text"])
input: {
paddingRight: '3ex',
boxSizing : 'border-box',
fontSize : 'inherit'
},
// The input with bootstrap class
'input:not(.form-control)': {
border : '1px solid #ccc',
borderRadius : 2,
paddingLeft : 4,
display : 'block',
WebkitAppearance : 'none',
lineHeight : 'normal'
},
'input.mobile': {
paddingLeft :' 3.4ex',
paddingRight: '3.4ex',
textAlign : 'center'
},
'input:focus': {},
'input:disabled': {
color : 'rgba(0, 0, 0, 0.3)',
textShadow: '0 1px 0 rgba(255, 255, 255, 0.8)'
}
};
/**
* When click and hold on a button - the speed of auto changing the value.
* This is a static property and can be modified if needed.
*/
static SPEED = 50;
/**
* When click and hold on a button - the delay before auto changing the value.
* This is a static property and can be modified if needed.
*/
static DELAY = 500;
/**
* The constant indicating up direction (or increasing in general)
*/
static DIRECTION_UP = "up";
/**
* The constant indicating down direction (or decreasing in general)
*/
static DIRECTION_DOWN = "down";
/**
* The step timer
* @type {Number}
*/
_timer: number;
/**
* This holds the last known validation error. We need to compare that with
* new errors and detect validation changes...
* @type {[type]}
*/
_valid: string;
_isStrict: boolean;
_ignoreValueChange: boolean;
_isMounted: boolean;
_inputFocus: boolean;
onTouchEnd: Function;
refsWrapper: Object;
refsInput: Object;
/**
* The state of the component
* @type {Object}
*/
state: NumericInputState;
/**
* The stop method (need to declare it here to use it in the constructor)
* @type {Function}
*/
stop: Function;
/**
* Set the initial state and bind this.stop to the instance.
*/
constructor(...args: Array<any>)
{
super(...args);
this._isStrict = !!this.props.strict;
this.state = {
btnDownHover : false,
btnDownActive : false,
btnUpHover : false,
btnUpActive : false,
stringValue : "",
...this._propsToState(this.props)
};
this.stop = this.stop.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.refsInput = {};
this.refsWrapper = {};
}
_propsToState(props: Object) {
let out:NumericInputState = {};
if (props.hasOwnProperty("value")) {
out.stringValue = String(
props.value || props.value === 0 ? props.value : ''
).trim();
out.value = out.stringValue !== '' ?
this._parse(props.value) :
null
}
else if (!this._isMounted && props.hasOwnProperty("defaultValue")) {
out.stringValue = String(
props.defaultValue || props.defaultValue === 0 ? props.defaultValue : ''
).trim()
out.value = props.defaultValue !== '' ?
this._parse(props.defaultValue) :
null
}
return out;
}
/**
* Special care is taken for the "value" prop:
* - If not provided - set it to null
* - If the prop is a number - use it as is
* - Otherwise:
* 1. Convert it to string (falsy values become "")
* 2. Then trim it.
* 3. Then parse it to number (delegating to this.props.parse if any)
*/
componentWillReceiveProps(props: Object): void
{
this._isStrict = !!props.strict;
let nextState = this._propsToState(props)
if (Object.keys(nextState).length) {
this._ignoreValueChange = true
this.setState(nextState, () => {
this._ignoreValueChange = false
})
}
}
/**
* Save the input selection right before rendering
*/
componentWillUpdate(): void
{
this.saveSelection()
}
/**
* After the component has been rendered into the DOM, do whatever is
* needed to "reconnect" it to the outer world, i.e. restore selection,
* call some of the callbacks, validate etc.
*/
componentDidUpdate(prevProps: Object, prevState: Object): void
{
// Call the onChange if needed. This is placed here because there are
// many reasons for changing the value and this is the common place
// that can capture them all
if (!this._ignoreValueChange // no onChange if re-rendered with different value prop
&& prevState.value !== this.state.value // no onChange if the value remains the same
&& (!isNaN(this.state.value) || this.state.value === null) // only if changing to number or null
) {
this._invokeEventCallback("onChange", this.state.value, this.refsInput.value, this.refsInput)
}
// focus the input is needed (for example up/down buttons set
// this._inputFocus to true)
if (this._inputFocus) {
this.refsInput.focus()
// Restore selectionStart (if any)
if (this.state.selectionStart || this.state.selectionStart === 0) {
this.refsInput.selectionStart = this.state.selectionStart
}
// Restore selectionEnd (if any)
if (this.state.selectionEnd || this.state.selectionEnd === 0) {
this.refsInput.selectionEnd = this.state.selectionEnd
}
}
this.checkValidity()
}
/**
* This is used to clear the timer if any
*/
componentWillUnmount(): void
{
this._isMounted = false
this.stop();
}
/**
* Adds getValueAsNumber and setValue methods to the input DOM element.
*/
componentDidMount(): void
{
this._isMounted = true
this.refsInput.getValueAsNumber = () => this.state.value || 0
this.refsInput.setValue = (value) => {
this.setState({
value: this._parse(value),
stringValue: value
})
}
// This is a special case! If the component has the "autoFocus" prop
// and the browser did focus it we have to pass that to the onFocus
if (!this._inputFocus && IS_BROWSER && document.activeElement === this.refsInput) {
this._inputFocus = true
this.refsInput.focus()
this._invokeEventCallback("onFocus", {
target: this.refsInput,
type : "focus"
})
}
this.checkValidity()
}
/**
* Saves the input selection in the state so that it can be restored after
* updates
*/
saveSelection(): void
{
this.state.selectionStart = this.refsInput.selectionStart
this.state.selectionEnd = this.refsInput.selectionEnd
}
/**
* Unless noValidate is set to true, the component will check the
* existing validation state (if any) and will toggle the "has-error"
* CSS class on the wrapper
*/
checkValidity(): void
{
let valid, validationError = ""
let supportsValidation = !!this.refsInput.checkValidity
// noValidate
let noValidate = !!(
this.props.noValidate && this.props.noValidate != "false"
)
this.refsInput.noValidate = noValidate
// If "noValidate" is set or "checkValidity" is not supported then
// consider the element valid. Otherwise consider it invalid and
// make some additional checks below
valid = noValidate || !supportsValidation
if (valid) {
validationError = ""
}
else {
// In some browsers once a pattern is set it cannot be removed. The
// browser sets it to "" instead which results in validation
// failures...
if (this.refsInput.pattern === "") {
this.refsInput.pattern = this.props.required ? ".+" : ".*"
}
// Now check validity
if (supportsValidation) {
this.refsInput.checkValidity()
valid = this.refsInput.validity.valid
if (!valid) {
validationError = this.refsInput.validationMessage
}
}
// Some browsers might fail to validate maxLength
if (valid && supportsValidation && this.props.maxLength) {
if (this.refsInput.value.length > this.props.maxLength) {
validationError = "This value is too long"
}
}
}
validationError = validationError || (
valid ? "" : this.refsInput.validationMessage || "Unknown Error"
)
let validStateChanged = this._valid !== validationError
this._valid = validationError
if (validationError) {
addClass(this.refsWrapper, "has-error")
if (validStateChanged) {
this._invokeEventCallback(
"onInvalid",
validationError,
this.state.value,
this.refsInput.value
)
}
}
else {
removeClass(this.refsWrapper, "has-error")
if (validStateChanged) {
this._invokeEventCallback(
"onValid",
this.state.value,
this.refsInput.value
)
}
}
}
/**
* Used internally to parse the argument x to it's numeric representation.
* If the argument cannot be converted to finite number returns 0; If a
* "precision" prop is specified uses it round the number with that
* precision (no fixed precision here because the return value is float, not
* string).
*/
_toNumber(x: any): number
{
let n = parseFloat(x);
if (isNaN(n) || !isFinite(n)) {
n = 0;
}
if (this._isStrict) {
let precision = access(this.props, "precision", null, this);
let q = Math.pow(10, precision === null ? 10 : precision);
let _min = +access(this.props, "min", NumericInput.defaultProps.min, this);
let _max = +access(this.props, "max", NumericInput.defaultProps.max, this);
n = Math.min( Math.max(n, _min), _max );
n = Math.round( n * q ) / q;
}
return n;
}
/**
* This is used internally to parse any string into a number. It will
* delegate to this.props.parse function if one is provided. Otherwise it
* will just use parseFloat.
* @private
*/
_parse(x: string): number
{
x = String(x);
if (typeof this.props.parse == 'function') {
return parseFloat(this.props.parse(x));
}
return parseFloat(x);
}
/**
* This is used internally to format a number to it's display representation.
* It will invoke the this.props.format function if one is provided.
* @private
*/
_format(n: number): string
{
let _n = this._toNumber(n);
let precision = access(this.props, "precision", null, this);
if (precision !== null) {
_n = n.toFixed(precision);
}
_n += "";
if (this.props.format) {
return this.props.format(_n);
}
return _n;
}
/**
* The internal method that actually sets the new value on the input
* @private
*/
_step(n: number, callback?: Function): boolean
{
let _isStrict = this._isStrict;
this._isStrict = true;
let _step = +access(
this.props,
"step",
NumericInput.defaultProps.step,
this,
(
n > 0 ?
NumericInput.DIRECTION_UP :
NumericInput.DIRECTION_DOWN
)
);
let _n = this._toNumber((this.state.value || 0) + _step * n);
if (this.props.snap) {
_n = Math.round(_n / _step) * _step
}
this._isStrict = _isStrict;
if (_n !== this.state.value) {
this.setState({ value: _n, stringValue: _n + "" }, callback);
return true;
}
return false;
}
/**
* This binds the Up/Down arrow key listeners
*/
_onKeyDown(...args: Array<any>): void
{
args[0].persist()
this._invokeEventCallback("onKeyDown", ...args)
let e = args[0]
if (!e.isDefaultPrevented()) {
if (e.keyCode === KEYCODE_UP) {
e.preventDefault();
this._step(e.ctrlKey || e.metaKey ? 0.1 : e.shiftKey ? 10 : 1);
}
else if (e.keyCode === KEYCODE_DOWN) {
e.preventDefault();
this._step(e.ctrlKey || e.metaKey ? -0.1 : e.shiftKey ? -10 : -1);
}
else {
let value = this.refsInput.value, length = value.length;
if (e.keyCode === 8) { // backspace
if (this.refsInput.selectionStart == this.refsInput.selectionEnd &&
this.refsInput.selectionEnd > 0 &&
value.length &&
value.charAt(this.refsInput.selectionEnd - 1) === ".")
{
e.preventDefault();
this.refsInput.selectionStart = this.refsInput.selectionEnd = this.refsInput.selectionEnd - 1;
}
}
else if (e.keyCode === 46) { // delete
if (this.refsInput.selectionStart == this.refsInput.selectionEnd &&
this.refsInput.selectionEnd < length + 1 &&
value.length &&
value.charAt(this.refsInput.selectionEnd) === ".")
{
e.preventDefault();
this.refsInput.selectionStart = this.refsInput.selectionEnd = this.refsInput.selectionEnd + 1;
}
}
}
}
}
/**
* Stops the widget from auto-changing by clearing the timer (if any)
*/
stop(): void
{
if ( this._timer ) {
clearTimeout( this._timer );
}
}
/**
* Increments the value with one step and the enters a recursive calls
* after DELAY. This is bound to the mousedown event on the "up" button
* and will be stopped on mouseout/mouseup.
* @param {Boolean} _recursive The method is passing this to itself while
* it is in recursive mode.
* @return void
*/
increase(_recursive: boolean = false, callback?: Function): void
{
this.stop();
this._step(1, callback);
let _max = +access(this.props, "max", NumericInput.defaultProps.max, this);
if (isNaN(this.state.value) || +this.state.value < _max) {
this._timer = setTimeout(() => {
this.increase(true);
}, _recursive ? NumericInput.SPEED : NumericInput.DELAY);
}
}
/**
* Decrements the value with one step and the enters a recursive calls
* after DELAY. This is bound to the mousedown event on the "down" button
* and will be stopped on mouseout/mouseup.
* @param {Boolean} _recursive The method is passing this to itself while
* it is in recursive mode.
* @return void
*/
decrease(_recursive: boolean = false, callback?: Function): void
{
this.stop();
this._step(-1, callback);
let _min = +access(this.props, "min", NumericInput.defaultProps.min, this);
if (isNaN(this.state.value) || +this.state.value > _min) {
this._timer = setTimeout(() => {
this.decrease(true);
}, _recursive ? NumericInput.SPEED : NumericInput.DELAY);
}
}
/**
* Handles the mousedown event on the up/down buttons. Changes The
* internal value and sets up a delay for auto increment/decrement
* (until mouseup or mouseleave)
*/
onMouseDown(dir: "up"|"down", callback?: Function): void
{
if (dir == 'down') {
this.decrease(false, callback);
}
else if (dir == 'up') {
this.increase(false, callback);
}
}
/**
* Handles the touchstart event on the up/down buttons. Changes The
* internal value and DOES NOT sets up a delay for auto increment/decrement.
* Note that this calls e.preventDefault() so the event is not used for
* creating a virtual mousedown after it
*/
onTouchStart(dir: "up"|"down", e: Event): void
{
e.preventDefault();
if (dir == 'down') {
this.decrease();
}
else if (dir == 'up') {
this.increase();
}
}
onTouchEnd(e: Event): void
{
e.preventDefault();
this.stop();
}
/**
* Helper method to invoke event callback functions if they are provided
* in the props.
* @param {String} callbackName The name of the function prop
* @param {*[]} args Any additional argument are passed thru
*/
_invokeEventCallback(callbackName: string, ...args: Array<any>): void
{
if (typeof this.props[callbackName] == "function") {
this.props[callbackName].call(null, ...args);
}
}
/**
* Renders an input wrapped in relative span and up/down buttons
* @return {Component}
*/
render()
{
let props = this.props
let state = this.state
let css = {}
let {
// These are ignored in rendering
step, min, max, precision, parse, format, mobile, snap, componentClass,
value, type, style, defaultValue, onInvalid, onValid, strict, noStyle,
// The rest are passed to the input
...rest
} = this.props;
noStyle = noStyle || style === false;
// Build the styles
for (let x in NumericInput.style) {
css[x] = Object.assign(
{},
NumericInput.style[x],
style ? style[x] || {} : {}
);
}
let hasFormControl = props.className && (/\bform-control\b/).test(
props.className
)
if (mobile == 'auto') {
mobile = IS_BROWSER && 'ontouchstart' in document
}
if (typeof mobile == "function") {
mobile = mobile.call(this)
}
mobile = !!mobile
let attrs = {
wrap : {
style : noStyle ? null : css.wrap,
className : 'react-numeric-input',
ref: (e)=>{if(e!=null && e!= undefined){this.refsWrapper=e;}},
onMouseUp : undefined,
onMouseLeave: undefined
},
input : {
ref: (e)=>{if(e!=null && e!= undefined){this.refsInput=e;}},
type: 'text',
style: noStyle ? null : Object.assign(
{},
css.input,
!hasFormControl ?
css['input:not(.form-control)'] :
{},
this._inputFocus ? css['input:focus'] : {}
),
...rest
},
btnUp: {
onMouseEnter: undefined,
onMouseDown : undefined,
onMouseUp : undefined,
onMouseLeave: undefined,
onTouchStart: undefined,
onTouchEnd : undefined,
style: noStyle ? null : Object.assign(
{},
css.btn,
css.btnUp,
props.disabled || props.readOnly ?
css['btn:disabled'] :
state.btnUpActive ?
css['btn:active'] :
state.btnUpHover ?
css['btn:hover'] :
{}
)
},
btnDown: {
onMouseEnter: undefined,
onMouseDown : undefined,
onMouseUp : undefined,
onMouseLeave: undefined,
onTouchStart: undefined,
onTouchEnd : undefined,
style: noStyle ? null : Object.assign(
{},
css.btn,
css.btnDown,
props.disabled || props.readOnly ?
css['btn:disabled'] :
state.btnDownActive ?
css['btn:active'] :
state.btnDownHover ?
css['btn:hover'] :
{}
)
}
};
let stringValue = String(
// if state.stringValue is set and not empty
state.stringValue ||
// else if state.value is set and not null|undefined
(state.value || state.value === 0 ? state.value : "") ||
// or finally use ""
""
);
let loose = !this._isStrict && (this._inputFocus || !this._isMounted)
// incomplete number
if (loose && RE_INCOMPLETE_NUMBER.test(stringValue)) {
attrs.input.value = stringValue;
}
// Not a number and not empty (loose mode only)
else if (loose && stringValue && !RE_NUMBER.test(stringValue)) {
attrs.input.value = stringValue;
}
// number
else if (state.value || state.value === 0) {
attrs.input.value = this._format(state.value);
}
// empty
else {
attrs.input.value = "";
}
if (hasFormControl && !noStyle) {
Object.assign(attrs.wrap.style, css['wrap.hasFormControl'])
}
// mobile
if (mobile && !noStyle) {
Object.assign(attrs.input .style, css['input.mobile' ])
Object.assign(attrs.btnUp .style, css['btnUp.mobile' ])
Object.assign(attrs.btnDown.style, css['btnDown.mobile'])
}
// Attach event listeners if the widget is not disabled
if (!props.disabled && !props.readOnly) {
Object.assign(attrs.wrap, {
onMouseUp : this.stop,
onMouseLeave : this.stop
});
Object.assign(attrs.btnUp, {
onTouchStart: this.onTouchStart.bind(this, 'up'),
onTouchEnd: this.onTouchEnd,
onMouseEnter: () => {
this.setState({
btnUpHover : true
});
},
onMouseLeave: () => {
this.stop();
this.setState({
btnUpHover : false,
btnUpActive: false
});
},
onMouseUp: () => {
this.setState({
btnUpHover : true,
btnUpActive : false
});
},
onMouseDown: (...args) => {
args[0].preventDefault();
args[0].persist();
this._inputFocus = true;
this.setState({
btnUpHover : true,
btnUpActive : true
}, () => {
this._invokeEventCallback("onFocus", ...args)
this.onMouseDown('up');
});
}
});
Object.assign(attrs.btnDown, {
onTouchStart: this.onTouchStart.bind(this, 'down'),
onTouchEnd: this.onTouchEnd,
onMouseEnter: () => {
this.setState({
btnDownHover : true
});
},
onMouseLeave: () => {
this.stop();
this.setState({
btnDownHover : false,
btnDownActive: false
});
},
onMouseUp: () => {
this.setState({
btnDownHover : true,
btnDownActive : false
});
},
onMouseDown: (...args) => {
args[0].preventDefault();
args[0].persist();
this._inputFocus = true;
this.setState({
btnDownHover : true,
btnDownActive : true
}, () => {
this._invokeEventCallback("onFocus", ...args)
this.onMouseDown('down');
});
}
});
Object.assign(attrs.input, {
onChange : e => {
const original = e.target.value;
let val = this._parse(original)
if (isNaN(val)) {
val = null
}
this.setState({
value: this._isStrict ? this._toNumber(val) : val,
stringValue: original
})
},
onKeyDown: this._onKeyDown.bind(this),
onInput: (...args) => {
this.saveSelection()
this._invokeEventCallback("onInput", ...args)
},
onSelect: (...args) => {
this.saveSelection()
this._invokeEventCallback("onSelect", ...args)
},
onFocus: (...args) => {
args[0].persist();
this._inputFocus = true;
const val = this._parse(args[0].target.value);
this.setState({
value: val,
stringValue: val || val === 0 ? val + "" : ""
}, () => {
this._invokeEventCallback("onFocus", ...args)
});
},
onBlur: (...args) => {
let _isStrict = this._isStrict
this._isStrict = true
args[0].persist();
this._inputFocus = false;
const val = this._parse(args[0].target.value);
this.setState({
value: val
}, () => {
this._invokeEventCallback("onBlur", ...args)
this._isStrict = _isStrict
});
}
});
}
else {
if (!noStyle && props.disabled) {
Object.assign(attrs.input.style, css['input:disabled'])
}
}
const InputTag = componentClass || 'input';
if (mobile) {
return (
<span {...attrs.wrap}>
<InputTag {...attrs.input}/>
<b {...attrs.btnUp}>
<i style={ noStyle ? null : css.minus }/>
<i style={ noStyle ? null : css.plus }/>
</b>
<b {...attrs.btnDown}>
<i style={ noStyle ? null : css.minus }/>
</b>
</span>
)
}
return (
<span {...attrs.wrap}>
<InputTag {...attrs.input}/>
<b {...attrs.btnUp}>
<i style={ noStyle ? null : css.arrowUp }/>
</b>
<b {...attrs.btnDown}>
<i style={ noStyle ? null : css.arrowDown }/>
</b>
</span>
);
}
}
module.exports = NumericInput;