react-number-field
Version:
React Number Field
404 lines (305 loc) • 9.79 kB
JavaScript
'use strict';
var React = require('react')
var Field = require('react-field')
var assign = require('object-assign')
var IS = require('i-s')
import { findDOMNode } from 'react-dom'
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() {
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(){
return {
defaultValue: this.props.defaultValue
}
},
getSelectedRange: function(dom){
return {
start: getSelectionStart(dom),
end : getSelectionEnd(dom)
}
},
getValidationFn: function(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(){
var props = this.prepareProps(this.props)
const fieldProps = assign({}, props, { ref: 'field' })
const Factory = props.factory || Field
return <Factory {...fieldProps} />
},
prepareProps: function(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(event){
this.setState({
focused: true
})
;(this.props.onFocus || emptyFn)(event)
},
handleBlur: function() {
this.setState({
focused: false
})
if (this.isSpinning()){
this.stopSpin()
}
;(this.props.onBlur || emptyFn)(event)
},
handleChange: function(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)(...arguments)
},
getValue: function() {
var value = this.props.value === undefined?
this.state.defaultValue:
this.props.value
return value
},
prepareValue: function(props, state) {
return this.getValue()
},
handleKeyDown: function(event) {
var key = event.key
if (!key){
return
}
var name = 'handle' + toUpperFirst(key) + 'KeyDown'
if (this[name]){
this[name](event)
}
},
handleArrowDownKeyDown: function(event){
this.handleArrowKeySpin(-1, event)
},
handleArrowUpKeyDown: function(event){
this.handleArrowKeySpin(1, event)
},
handleArrowKeySpin: function(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() {
this.props.spinOnArrowKeys && this.stopSpin()
global.removeEventListener('keyup', this.onSpinKeyUp)
},
handleKeyPress: function(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() {
return findDOMNode(this.refs.field)
},
isFocused: function() {
return !!this.state.focused
},
getStepValue: function(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(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(){
clearInterval(this.spinIntervalId)
this.spinIntervalId = null
},
startSpin: function(direction, config){
if (this.spinIntervalId){
clearInterval(this.spinIntervalId)
}
this.spinIntervalId = setInterval(this.stepTo.bind(this, direction, config), this.props.stepDelay)
},
isSpinning: function() {
return this.spinIntervalId != null
},
handleWheel: function(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(value, event) {
this.handleChange(value, event)
},
focus: function(value, event) {
findDOMNode(this.refs.field).focus()
}
})