react-number-field
Version:
React Number Field
385 lines (288 loc) • 10 kB
JavaScript
'use strict';
var _reactDom = require('react-dom');
var React = require('react');
var Field = require('react-field');
var assign = require('object-assign');
var IS = require('i-s');
var getSelectionStart = require('./getSelectionStart');
var getSelectionEnd = require('./getSelectionEnd');
function emptyFn() {}
var isNumeric = IS.numeric;
var isInt = IS.int;
var isFloat = IS.float;
function toUpperFirst(str) {
return str ? str.charAt(0).toUpperCase() + str.substring(1) : '';
}
function noDot(value) {
value = value + '';
return value.indexOf('.') === -1;
}
/**
* Returns true if the given value is >= than #minValue
* @param {Number/String} value
* @return {Boolean}
*/
function isMinValueRespected(value, props) {
var minValue = props.minValue;
if (minValue == null || value === '') {
return true;
}
return value >= minValue;
}
/**
* Returns true if the given value is <= than #maxValue
* @param {Number/String} value
* @return {Boolean}
*/
function isMaxValueRespected(value, props) {
var maxValue = props.maxValue;
if (maxValue == null || value === '') {
return true;
}
return value <= maxValue;
}
function checkNumeric(value, props) {
if (value === '') {
return true;
}
if (props.numbersOnly) {
var numeric = isNumeric(value);
return numeric || props.allowNegative && value === '-' || props.allowFloat && value === '.' || props.allowNegative && props.allowFloat && value == '-.';
}
}
function checkFloat(value, props) {
if (props.allowFloat === false) {
return noDot(value) && isNumeric(value) && isInt(value * 1);
}
}
function checkPositive(value, props) {
if (props.allowNegative === false) {
return isNumeric(value) && value * 1 >= 0;
}
}
module.exports = React.createClass({
displayName: 'ReactNumberField',
propTypes: {
onInvalid: React.PropTypes.func,
validate: React.PropTypes.func,
stepDelay: React.PropTypes.number,
minValue: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]),
maxValue: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string])
},
getDefaultProps: function getDefaultProps() {
return {
spinOnArrowKeys: true,
numbersOnly: true,
minValue: null,
maxValue: null,
step: 1,
shiftStep: 10,
requireFocusOnStep: true,
stepOnWheel: true,
allowNegative: true,
allowFloat: true,
stepDelay: 40,
validations: [checkNumeric, checkFloat, checkPositive, isMinValueRespected, isMaxValueRespected]
};
},
getInitialState: function getInitialState() {
return {
defaultValue: this.props.defaultValue
};
},
getSelectedRange: function getSelectedRange(dom) {
return {
start: getSelectionStart(dom),
end: getSelectionEnd(dom)
};
},
getValidationFn: function getValidationFn(props) {
var validations = [].concat(props.validations);
if (typeof props.validate === 'function') {
validations.push(props.validate);
}
return function (value, properties) {
return validations.reduce(function (a, b) {
return a && b(value, properties) !== false;
}, true);
};
},
render: function render() {
var props = this.prepareProps(this.props);
var fieldProps = assign({}, props, { ref: 'field' });
var Factory = props.factory || Field;
return React.createElement(Factory, fieldProps);
},
prepareProps: function prepareProps(thisProps) {
var props = assign({}, thisProps);
props.value = this.prepareValue(props, this.state);
var validationFn = this.validationFn = this.getValidationFn(thisProps);
props.onWheel = this.handleWheel;
props.onKeyPress = this.handleKeyPress.bind(this, props);
props.onKeyDown = this.handleKeyDown;
props.validate = validationFn;
props.onChange = this.handleChange;
props.onBlur = this.handleBlur;
props.onFocus = this.handleFocus;
return props;
},
handleFocus: function handleFocus(event) {
this.setState({
focused: true
});(this.props.onFocus || emptyFn)(event);
},
handleBlur: function handleBlur() {
this.setState({
focused: false
});
if (this.isSpinning()) {
this.stopSpin();
}
;(this.props.onBlur || emptyFn)(event);
},
handleChange: function handleChange(value, event) {
if (value && value.target) {
event = value;
value = event.target.value;
}
if (this.props.value === undefined) {
this.setState({
defaultValue: value
});
}
;(this.props.onChange || emptyFn).apply(undefined, arguments);
},
getValue: function getValue() {
var value = this.props.value === undefined ? this.state.defaultValue : this.props.value;
return value;
},
prepareValue: function prepareValue(props, state) {
return this.getValue();
},
handleKeyDown: function handleKeyDown(event) {
var key = event.key;
if (!key) {
return;
}
var name = 'handle' + toUpperFirst(key) + 'KeyDown';
if (this[name]) {
this[name](event);
}
},
handleArrowDownKeyDown: function handleArrowDownKeyDown(event) {
this.handleArrowKeySpin(-1, event);
},
handleArrowUpKeyDown: function handleArrowUpKeyDown(event) {
this.handleArrowKeySpin(1, event);
},
handleArrowKeySpin: function handleArrowKeySpin(direction, event) {
if (this.isSpinning()) {
event.preventDefault();
return;
}
if (this.props.spinOnArrowKeys && !this.isSpinning()) {
this.startSpin(direction, {
shiftKey: event.shiftKey,
event: event
});
event.preventDefault();
global.addEventListener('keyup', this.onSpinKeyUp);
}
},
onSpinKeyUp: function onSpinKeyUp() {
this.props.spinOnArrowKeys && this.stopSpin();
global.removeEventListener('keyup', this.onSpinKeyUp);
},
handleKeyPress: function handleKeyPress(props, event) {
var validationFn = this.validationFn;
if (!event.which || event.which == 8 /*backspace*/) {
//not a printable character
return;
}
var input = event.target;
var value = props.value + '';
var range = this.getSelectedRange(input);
var beforeValue = value.substring(0, range.start);
var afterValue = value.substring(range.end);
var key = String.fromCharCode(event.which || event.keyCode);
var newValue = beforeValue + key + afterValue;
var preventInvalid = true;
var valid;
if (newValue !== '') {
valid = validationFn(newValue, props, key);
if (!valid && typeof props.onInvalid === 'function' && props.onInvalid(newValue, props, event) !== false) {
preventInvalid = false;
}
if (!valid && preventInvalid) {
event.preventDefault();
}
}
},
getInput: function getInput() {
return (0, _reactDom.findDOMNode)(this.refs.field);
},
isFocused: function isFocused() {
return !!this.state.focused;
},
getStepValue: function getStepValue(props, direction, config) {
config = config || {};
var value = this.getValue();
var stepValue = props.step;
if (value === '') {
return 0;
}
if (typeof value == 'string') {
value = parseFloat(value);
}
if (config.shiftKey && props.shiftStep) {
stepValue = props.shiftStep;
}
return value + direction * stepValue;
},
stepTo: function stepTo(direction, config) {
config = config || {};
var props = this.props;
var validationFn = this.validationFn;
if (props.step != null) {
var stepFn = typeof props.stepFn === 'function' ? props.stepFn : this.getStepValue;
var value = stepFn(props, direction, config);
var valid = validationFn(value, props);
if (valid) {
this.notify(value, config.event);
}
}
return value;
},
stopSpin: function stopSpin() {
clearInterval(this.spinIntervalId);
this.spinIntervalId = null;
},
startSpin: function startSpin(direction, config) {
if (this.spinIntervalId) {
clearInterval(this.spinIntervalId);
}
this.spinIntervalId = setInterval(this.stepTo.bind(this, direction, config), this.props.stepDelay);
},
isSpinning: function isSpinning() {
return this.spinIntervalId != null;
},
handleWheel: function handleWheel(event) {
var props = this.props;
if ((props.requireFocusOnStep && this.isFocused() || !props.requireFocusOnStep) && props.stepOnWheel && props.step) {
event.preventDefault();
var nativeEvent = event.nativeEvent;
var y = nativeEvent.wheelDeltaY || nativeEvent.wheelDelta || -nativeEvent.deltaY;
y = y < 0 ? -1 : 1;
this.stepTo(y, {
shiftKey: event.shiftKey,
event: event
});
}
},
notify: function notify(value, event) {
this.handleChange(value, event);
},
focus: function focus(value, event) {
(0, _reactDom.findDOMNode)(this.refs.field).focus();
}
});