import React from 'react'; import PropTypes from 'prop-types'; const MARGIN_LR = 8; // Left/ Right margin /** * Horizontal Slider */ export default class Slider extends React.Component { static defaultProps = { axis: false, min: 0, max: 1, scale: 1 // SVG render scale }; static propTypes = { axis: PropTypes.bool, axisUnit: PropTypes.string,// units (T, M, etc.) max: PropTypes.number, min: PropTypes.number, onChange: PropTypes.func.isRequired,// function which determins percent value onResize: PropTypes.func, percent: PropTypes.number.isRequired,// value of slider scale: PropTypes.number }; /** * Constructor * @param {Object} props React Component properties */ constructor(props) { super(props); this._down = this._down.bind(this); this._move = this._move.bind(this); this._up = this._up.bind(this); this._keyup = this._keyup.bind(this); this._keydown = this._keydown.bind(this); this._touchstart = this._touchstart.bind(this); this._touchend = this._touchend.bind(this); this._updatePercent = this._updatePercent.bind(this); this._updateDimensions = this._updateDimensions.bind(this); this.state = { width: 0 }; } /** * On Mouse/Touch down handler * @param {SyntheticEvent} event Event */ _down(event) { let rect = event.currentTarget.getBoundingClientRect(); this.left = rect.left; this.width = rect.width; this._move(event); this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); } /** * Update the slider percentage on move * @param {SyntheticEvent} event Event */ _move(event) { if(this.width !== null && this.left != null) { let clientX = event.touches ? event.touches[0].clientX : event.clientX; event.preventDefault(); this._updatePercent(clientX - this.left, this.width); } } /** * On Mouse/Touch up handler * @param {Event} event DOM Event */ _up(event) { this.sliderInputBox.sliderVal.focus(); clearTimeout(this.touchStartTimer); event.preventDefault(); this.left = null; this.width = null; } /** * Key up handler for keyboard. * display the number field then set focus to it * when "Enter" key is pressed * @param {Event} event Keyboard event */ _keyup(event) { switch (event.key) { case 'Enter': event.preventDefault(); this.sliderInputBox._setDisplay('block'); return; default: return; } } /** * Key down handler * increment slider position by +/- 1 when right/left arrow key is pressed or held * @param {Event} event Keyboard even */ _keydown(event) { let newVal = this.props.percent * this.props.max; switch (event.key) { case 'ArrowRight': newVal += 1; if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); return; case 'ArrowLeft': newVal -= 1; if (newVal >= 0) this.props.onChange(newVal / this.props.max); return; default: return; } } /** * Touch start handler * @param {Event} event DOM Event * */ _touchstart(event) { this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); } /** * Touch end handler * @param {Event} event DOM Event * */ _touchend(event) { this.sliderInputBox.sliderVal.focus(); clearTimeout(this.touchStartTimer); } /** * Determine if the user is still dragging * @param {SyntheticEvent} event Event */ _enter(event) { if(event.buttons !== 1) { this.left = null; this.width = null; } } /** * Update the slider percentage * @param {number} pos Slider drag position * @param {number} width Slider width * @param {Event} event DOM Event */ _updatePercent(pos, width) { this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); } /** * Update dimenions from rendered DOM */ _updateDimensions() { this.setState({ outerWidth: this.node.getBoundingClientRect().width }); } /** * Add listeners when about to mount */ componentWillMount() { if (this.props.onResize) { this.resizeListener = this.props.onResize(this._updateDimensions); } } /** * Trigger DOM updates on mount */ componentDidMount() { this._updateDimensions(); } /** * Remove listeners on unmount */ componentWillUnmount() { if (this.resizeListener) { this.resizeListener.remove(); } } /** * Render the slider * @return {React.Component} The slider */ render() { let outerWidth = this.state.outerWidth; let { axis, axisUnit, min, max, scale } = this.props; let style = { width: '100%', height: axis ? '2.5em' : '1.5em', boxSizing: 'border-box' }; if (!outerWidth) { return this.node = node} />; } let margin = MARGIN_LR * scale; let width = outerWidth - (margin * 2); let pctPos = width * this.props.percent; return
this.node = node} tabIndex="0"> {axis && {min + axisUnit} {(min + max / 2) + axisUnit} {max + axisUnit} } this.sliderInputBox = tb} onChange={this.props.onChange} percent={this.props.percent} axisUnit={this.props.axisUnit} scale={this.props.scale} max={this.props.max} />
; } } /** * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) **/ class TextInputBox extends React.Component { static propTypes = { axisUnit: PropTypes.string,// units (T, M, etc.) max: PropTypes.number, onChange: PropTypes.func.isRequired,// function which determins percent value percent: PropTypes.number.isRequired,// value of slider scale: PropTypes.number }; /** * Determine if the user is still dragging * @param {Object} props React Component properties */ constructor(props) { super(props); this._handleFocus = this._handleFocus.bind(this); this._handleBlur = this._handleBlur.bind(this); this._handleChange = this._handleChange.bind(this); this._keyup = this._keyup.bind(this); this.state = this._getInitialState(); } /** * Update input value if slider changes will change props/state * @param {Object} nextProps React Component properites * @param {Object} nextState React Component state values */ componentWillReceiveProps(nextProps, nextState) { let nextValue = nextProps.percent * nextProps.max; // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { this.setState({ inputValue: nextValue }); } } /** * Update slider textbox visibility/values if changes are made to slider * @param {Object} prevProps React Component properites * @param {Object} prevState React Component state values */ componentDidUpdate(prevProps, prevState) { if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); } if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { // they chose a different module this.setState({ inputValue: this.props.max }); } if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { this.props.onChange(this.state.inputValue / this.props.max); } } /** * Set initial state for the textbox. * We may want to rethink this to * try and make it a stateless component * @returns {object} React state object with initial values set */ _getInitialState() { return { divStyle: { display:'none' }, inputStyle: { width:'4em' }, labelStyle: { marginLeft: '.1em' }, maxLength:5, size:5, min:0, tabIndex:-1, type:'number', readOnly: true, inputValue: this.props.percent * this.props.max }; } /** * * @param {string} val block or none */ _setDisplay(val) { this.setState({ divStyle: { display:val } }); } /** * Update the input value * when textbox gets focus */ _handleFocus() { this.setState({ inputValue:this._getValue() }); } /** * Update inputValue when textbox loses focus */ _handleBlur() { this._setDisplay('none'); if (this.state.inputValue !== '') { this.props.onChange(this.state.inputValue / this.props.max); } else { this.setState({ inputValue: this.props.percent * this.props.max }); } } /** * Get the value in the text box * @returns {number} inputValue Value of the input box */ _getValue() { return this.state.inputValue; } /** * Update and set limits on input box * values depending on what user * has selected * * @param {SyntheticEvent} event ReactJs onChange event */ _handleChange(event) { if (event.target.value < 0) { this.setState({ inputValue: 0 }); } else if (event.target.value <= this.props.max) { this.setState({ inputValue: event.target.value }); } else { this.setState({ inputValue: this.props.max }); } } /** * Key up handler for input field. * If user hits Enter key, blur/close the input field * @param {Event} event Keyboard event */ _keyup(event) { switch (event.key) { case 'Enter': this.sliderVal.blur(); return; default: return; } } /** * Get the value in the text box * @return {React.Component} Text Input component for Slider */ render() { let { axisUnit, onChange, percent, scale } = this.props; return
{this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/>{this.props.axisUnit}
; } }