UNPKG

diva-mobile-numeric-input

Version:
572 lines (558 loc) 18.2 kB
import React, { Component } from "react"; import { View, TextInput, StyleSheet, Text, TouchableOpacity, } from "react-native"; import Icon from "react-native-vector-icons/Ionicons"; import Button from "../Button"; import PropTypes from "prop-types"; import { create, PREDEF_RES } from "react-native-pixel-perfect"; let calcSize = create(PREDEF_RES.iphone7.px); export default class NumericInput extends Component { constructor(props) { super(props); const noInitSent = props.initValue !== 0 && !props.initValue; this.state = { value: noInitSent ? (props.value ? props.value : 0) : props.initValue, lastValid: noInitSent ? (props.value ? props.value : 0) : props.initValue, stringValue: (noInitSent ? props.value ? props.value : 0 : props.initValue ).toString(), }; this.ref = null; this.changeQuantityButtonRef = React.createRef(null); } // this.props refers to the new props componentDidUpdate() { const initSent = !(this.props.initValue !== 0 && !this.props.initValue); // compare the new value (props.initValue) with the existing/old one (this.state.value) if (this.props.initValue !== this.state.value && initSent) { this.setState({ value: this.props.initValue, lastValid: this.props.initValue, stringValue: this.props.initValue.toString(), }); } } updateBaseResolution = (width, height) => { calcSize = create({ width, height }); }; inc = () => { if (this.props.leadingCallback) { this.props.leadingCallback(); } if ( this.props.timeBetweenPress && Date.now() - this.changeQuantityButtonRef.current < this.props.timeBetweenPress ) { return; } this.changeQuantityButtonRef.current = Date.now(); let value = this.props.value && typeof this.props.value === "number" ? this.props.value : this.state.value; if ( this.props.maxValue === null || value + this.props.step < this.props.maxValue ) { value = (value + this.props.step).toFixed(12); value = this.props.valueType === "real" ? parseFloat(value) : parseInt(value); this.setState({ value, stringValue: value.toString() }); } else if (this.props.maxValue !== null) { this.props.onLimitReached(true, "Reached Maximum Value!"); value = this.props.maxValue; this.setState({ value, stringValue: value.toString() }); } if (value !== this.props.value) this.props.onChange && this.props.onChange(Number(value)); }; dec = () => { if (this.props.leadingCallback) { this.props.leadingCallback(); } if ( this.props.timeBetweenPress && Date.now() - this.changeQuantityButtonRef.current < this.props.timeBetweenPress ) { return; } this.changeQuantityButtonRef.current = Date.now(); let value = this.props.value && typeof this.props.value === "number" ? this.props.value : this.state.value; if ( this.props.minValue === null || value - this.props.step > this.props.minValue ) { value = (value - this.props.step).toFixed(12); value = this.props.valueType === "real" ? parseFloat(value) : parseInt(value); } else if (this.props.minValue !== null) { this.props.onLimitReached(false, "Reached Minimum Value!"); value = this.props.minValue; } if (value !== this.props.value) this.props.onChange && this.props.onChange(Number(value)); this.setState({ value, stringValue: value.toString() }); }; isLegalValue = (value, mReal, mInt) => value === "" || (((this.props.valueType === "real" && mReal(value)) || (this.props.valueType !== "real" && mInt(value))) && (this.props.maxValue === null || parseFloat(value) <= this.props.maxValue) && (this.props.minValue === null || parseFloat(value) >= this.props.minValue)); realMatch = (value) => value && value.match(/-?\d+(\.(\d+)?)?/) && value.match(/-?\d+(\.(\d+)?)?/)[0] === value.match(/-?\d+(\.(\d+)?)?/).input; intMatch = (value) => value && value.match(/-?\d+/) && value.match(/-?\d+/)[0] === value.match(/-?\d+/).input; onChange = (value) => { let currValue = typeof this.props.value === "number" ? this.props.value : this.state.value; if ( (value.length === 1 && value === "-") || (value.length === 2 && value === "0-") ) { this.setState({ stringValue: "-" }); return; } if ( (value.length === 1 && value === ".") || (value.length === 2 && value === "0.") ) { this.setState({ stringValue: "0." }); return; } if (value.charAt(value.length - 1) === ".") { this.setState({ stringValue: value }); return; } let legal = this.isLegalValue(value, this.realMatch, this.intMatch); if (legal) { this.setState({ lastValid: value }); } if (!legal && !this.props.validateOnBlur) { if (this.ref) { this.ref.blur(); setTimeout(() => { this.ref.clear(); setTimeout(() => { this.props.onChange && this.props.onChange(currValue - 1); this.setState({ value: currValue - 1 }, () => { this.setState({ value: currValue, legal }); this.props.onChange && this.props.onChange(currValue); }); }, 10); }, 15); setTimeout(() => this.ref.focus(), 20); } } else if (!legal && this.props.validateOnBlur) { this.setState({ stringValue: value }); let parsedValue = this.props.valueType === "real" ? parseFloat(value) : parseInt(value); parsedValue = isNaN(parsedValue) ? 0 : parsedValue; if (parsedValue !== this.props.value) this.props.onChange && this.props.onChange(parsedValue); this.setState({ value: parsedValue, legal, stringValue: parsedValue.toString(), }); } else { this.setState({ stringValue: value }); let parsedValue = this.props.valueType === "real" ? parseFloat(value) : parseInt(value); parsedValue = isNaN(parsedValue) ? 0 : parsedValue; if (parsedValue !== this.props.value) this.props.onChange && this.props.onChange(parsedValue); this.setState({ value: parsedValue, legal, stringValue: parsedValue.toString(), }); } }; onBlur = () => { let match = this.state.stringValue.match(/-?[0-9]\d*(\.\d+)?/); let legal = match && match[0] === match.input && (this.props.maxValue === null || parseFloat(this.state.stringValue) <= this.props.maxValue) && (this.props.minValue === null || parseFloat(this.state.stringValue) >= this.props.minValue); if (!legal) { if ( this.props.minValue !== null && parseFloat(this.state.stringValue) <= this.props.minValue ) { this.props.onLimitReached(true, "Reached Minimum Value!"); } if ( this.props.maxValue !== null && parseFloat(this.state.stringValue) >= this.props.maxValue ) { this.props.onLimitReached(false, "Reached Maximum Value!"); } if (this.ref) { this.ref.blur(); setTimeout(() => { this.ref.clear(); setTimeout(() => { this.props.onChange && this.props.onChange(this.state.lastValid); this.setState({ value: this.state.lastValid }, () => { this.setState({ value: this.state.lastValid, stringValue: this.state.lastValid.toString(), }); this.props.onChange && this.props.onChange(this.state.lastValid); }); }, 10); }, 15); setTimeout(() => this.ref.focus(), 50); } } this.props.onBlur && this.props.onBlur(); }; onFocus = () => { this.setState({ lastValid: this.state.value }); this.props.onFocus && this.props.onFocus(); }; render() { const editable = this.props.editable; const sepratorWidth = typeof this.props.separatorWidth === "undefined" ? this.props.sepratorWidth : this.props.separatorWidth; //supporting old property name sepratorWidth const borderColor = this.props.borderColor; const iconStyle = [style.icon, this.props.iconStyle]; const totalWidth = this.props.totalWidth; const totalHeight = this.props.totalHeight ? this.props.totalHeight : totalWidth * 0.4; const inputWidth = this.props.type === "up-down" ? totalWidth * 0.6 : totalWidth * 0.4; const borderRadiusTotal = totalHeight * 0.18; const fontSize = totalHeight * 0.38; const textColor = this.props.textColor; const maxReached = this.state.value === this.props.maxValue; const minReached = this.state.value === this.props.minValue; const inputContainerStyle = this.props.type === "up-down" ? [ style.inputContainerUpDown, { width: totalWidth, height: totalHeight, borderColor: borderColor, }, this.props.rounded ? { borderRadius: borderRadiusTotal } : {}, this.props.containerStyle, ] : [ style.inputContainerPlusMinus, { width: totalWidth, height: totalHeight, borderColor: borderColor, }, this.props.rounded ? { borderRadius: borderRadiusTotal } : {}, this.props.containerStyle, ]; const inputStyle = this.props.type === "up-down" ? [ style.inputUpDown, { width: inputWidth, height: totalHeight, fontSize: fontSize, color: textColor, borderRightWidth: 2, borderRightColor: borderColor, }, this.props.inputStyle, ] : [ style.inputPlusMinus, { width: inputWidth, height: totalHeight, fontSize: fontSize, color: textColor, borderRightWidth: sepratorWidth, borderLeftWidth: sepratorWidth, borderLeftColor: borderColor, borderRightColor: borderColor, }, this.props.inputStyle, ]; const upDownStyle = [ { alignItems: "center", width: totalWidth - inputWidth, backgroundColor: this.props.upDownButtonsBackgroundColor, borderRightWidth: 1, borderRightColor: borderColor, }, this.props.rounded ? { borderTopRightRadius: borderRadiusTotal, borderBottomRightRadius: borderRadiusTotal, } : {}, ]; const rightButtonStyle = [ { position: "absolute", zIndex: -1, right: 0, height: totalHeight - 2, justifyContent: "center", alignItems: "center", borderWidth: 0, backgroundColor: this.props.rightButtonBackgroundColor, width: (totalWidth - inputWidth) / 2, }, this.props.rounded ? { borderTopRightRadius: borderRadiusTotal, borderBottomRightRadius: borderRadiusTotal, } : {}, ]; const leftButtonStyle = [ { position: "absolute", zIndex: -1, left: 0, height: totalHeight - 2, justifyContent: "center", alignItems: "center", backgroundColor: this.props.leftButtonBackgroundColor, width: (totalWidth - inputWidth) / 2, borderWidth: 0, }, this.props.rounded ? { borderTopLeftRadius: borderRadiusTotal, borderBottomLeftRadius: borderRadiusTotal, } : {}, ]; const inputWraperStyle = { alignSelf: "center", borderLeftColor: borderColor, borderLeftWidth: sepratorWidth, borderRightWidth: sepratorWidth, borderRightColor: borderColor, }; if (this.props.type === "up-down") return ( <View style={inputContainerStyle}> <TextInput {...this.props.extraTextInputProps} editable={editable} returnKeyType="done" underlineColorAndroid="rgba(0,0,0,0)" keyboardType="numeric" value={this.state.stringValue} onChangeText={this.onChange} style={inputStyle} ref={(ref) => (this.ref = ref)} onBlur={this.onBlur} onFocus={this.onFocus} /> <View style={upDownStyle}> <Button onPress={this.inc} style={{ flex: 1, width: "100%", alignItems: "center" }} > <Icon name="ios-arrow-up" size={fontSize} style={[ ...iconStyle, maxReached ? this.props.reachMaxIncIconStyle : {}, minReached ? this.props.reachMinIncIconStyle : {}, ]} /> </Button> <Button onPress={this.dec} style={{ flex: 1, width: "100%", alignItems: "center" }} > <Icon name="ios-arrow-down" size={fontSize} style={[ ...iconStyle, maxReached ? this.props.reachMaxDecIconStyle : {}, minReached ? this.props.reachMinDecIconStyle : {}, ]} /> </Button> </View> </View> ); else return ( <View style={inputContainerStyle}> <Button onPress={this.dec} style={leftButtonStyle} accessibilityLabel={"productQuantityDecrement"} > <Icon name="remove" size={fontSize} style={[ ...iconStyle, maxReached ? this.props.reachMaxDecIconStyle : {}, minReached ? this.props.reachMinDecIconStyle : {}, ]} /> </Button> <TouchableOpacity style={[inputWraperStyle]}> <Text style={[inputStyle, { textAlignVertical: "center" }]} ref={(ref) => (this.ref = ref)} onPress={() => { this.props?.inputOnPress ? this.props.inputOnPress() : null; }} > {this.state.stringValue} </Text> </TouchableOpacity> <Button onPress={this.inc} style={rightButtonStyle} accessibilityLabel={"productQuantityIncrement"} > <Icon name="add" size={fontSize} style={[ ...iconStyle, maxReached ? this.props.reachMaxIncIconStyle : {}, minReached ? this.props.reachMinIncIconStyle : {}, ]} /> </Button> </View> ); } } const style = StyleSheet.create({ seprator: { backgroundColor: "grey", height: calcSize(80), }, inputContainerUpDown: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", borderColor: "grey", borderWidth: 1, }, inputContainerPlusMinus: { flexDirection: "row", alignItems: "center", justifyContent: "center", borderWidth: 1, }, inputUpDown: { textAlign: "center", padding: 0, }, inputPlusMinus: { textAlign: "center", padding: 0, }, icon: { fontWeight: "900", backgroundColor: "rgba(0,0,0,0)", }, upDown: { alignItems: "center", paddingRight: calcSize(15), }, }); NumericInput.propTypes = { iconSize: PropTypes.number, borderColor: PropTypes.string, iconStyle: PropTypes.any, totalWidth: PropTypes.number, totalHeight: PropTypes.number, sepratorWidth: PropTypes.number, type: PropTypes.oneOf(["up-down", "plus-minus"]), valueType: PropTypes.oneOf(["real", "integer"]), rounded: PropTypes.any, textColor: PropTypes.string, containerStyle: PropTypes.any, inputStyle: PropTypes.any, initValue: PropTypes.number, onChange: PropTypes.func.isRequired, onLimitReached: PropTypes.func, value: PropTypes.number, minValue: PropTypes.number, maxValue: PropTypes.number, step: PropTypes.number, upDownButtonsBackgroundColor: PropTypes.string, rightButtonBackgroundColor: PropTypes.string, leftButtonBackgroundColor: PropTypes.string, editable: PropTypes.bool, reachMaxIncIconStyle: PropTypes.any, reachMaxDecIconStyle: PropTypes.any, reachMinIncIconStyle: PropTypes.any, reachMinDecIconStyle: PropTypes.any, extraTextInputProps: PropTypes.any, }; NumericInput.defaultProps = { iconSize: calcSize(30), borderColor: "#d4d4d4", iconStyle: {}, totalWidth: calcSize(220), sepratorWidth: 1, type: "plus-minus", rounded: false, textColor: "black", containerStyle: {}, inputStyle: {}, initValue: null, valueType: "integer", value: null, minValue: null, maxValue: null, step: 1, upDownButtonsBackgroundColor: "white", rightButtonBackgroundColor: "white", leftButtonBackgroundColor: "white", editable: true, validateOnBlur: true, reachMaxIncIconStyle: {}, reachMaxDecIconStyle: {}, reachMinIncIconStyle: {}, reachMinDecIconStyle: {}, onLimitReached: (isMax, msg) => {}, extraTextInputProps: {}, };