UNPKG

fk-react-ui-components

Version:

Step 1 : Create a file in [ Seeds / Plants / Trees ] <br> Step 2 : It should export an Object with component name and story Component [Refer other components] <br> Step 3 : Story Component should return a react component <br> Step 3 : Created file should

509 lines (464 loc) 17.7 kB
import React from 'react'; import propTypes from './propTypes'; import { SliderWrapper, Scale, Line, Mark, Tooltip, Handle, InputWrapper, Separator } from './styles'; import InputComponent from '../FormElements/Input/Input'; import styled from 'styled-components'; const Input = styled(InputComponent)` width: 50px; outline: none; `; class Slider extends React.PureComponent { constructor(props) { super(props); this.handleRefs = []; let value = props.value; if (typeof value === 'undefined') { value = (typeof props.defaultValue !== 'undefined') ? props.defaultValue : (props.range ? [props.min, props.max] : props.min); } this.state = { value, draggingHandleIndex: null, isComponentMounted: false }; this.handleFirstInputValueChange = this.handleInputValueChange.bind(this, 0); this.handleSecondInputValueChange = this.handleInputValueChange.bind(this, 1); } /** * Set isComponentMounted in state as the marks can only be * rendered after rendering the main scale. */ componentDidMount() { this.setState({isComponentMounted: true}); } /** * Updates the selected value in the local state of this component * @param {object} nextProps */ componentWillReceiveProps(nextProps) { let value = nextProps.value; if (typeof value === 'undefined') { value = (typeof nextProps.defaultValue !== 'undefined') ? nextProps.defaultValue : (nextProps.range ? [nextProps.min, nextProps.max] : nextProps.min); } else if (nextProps.value !== this.state.value) { value = nextProps.value; } if (nextProps.range) { [0, 1].map(index => { if (this.state.value[index] < nextProps.min) { let value = [...this.state.value]; value[index] = nextProps.min; } if (this.state.value[index] > nextProps.max) { let value = [...this.state.value]; value[index] = nextProps.max; } }); } else { if (this.state.value < nextProps.min) { value = nextProps.min; } if (this.state.value > nextProps.max) { value = nextProps.max; } } if (value !== this.state.value) { this.setState({value}); } } /** * Sets the reference for the slider wrapper. * @param {object} ref */ setSliderRef = (ref) => { if (ref) { this.sliderRef = ref; } } /** * Sets the reference for the handle(s). * @param {number} index * @param {object} ref */ setHandleRef = (index, ref) => { if (ref) { this.handleRefs[index] = ref; } } handleInputValueChange = (index, event) => { let nextValue; let value = event.target.value; let minValue = this.props.min; let maxValue = this.props.max; if (this.props.range) { if (index === 0) { maxValue = this.state.value[1]; } else { minValue = this.state.value[0]; } } value = value < minValue ? minValue : value; value = value > maxValue ? maxValue : value; if (this.props.range) { nextValue = [...this.state.value]; nextValue[index] = value; } else { value = value nextValue = value; } this.setState({value: nextValue}); } /** * Called on pressing the mouse button. * Adds global mousemove and mouseup event listeners to listen the drag event. * In case of single value selection it updates the value in the local state * which moves the handle to that postion. In case of range selection this cannot * be done as there are two handles. * @param {object} event */ handleMouseDown = (event) => { if (this.props.disabled) { return; } if (!this.props.range) { const value = this.getValue(event.clientX); this.setState({value, draggingHandleIndex: 0}); this.props.onChange(value); this.addGlobalEventListeners(); } else if (this.handleRefs.indexOf(event.target) > -1) { this.setState({draggingHandleIndex: this.handleRefs.indexOf(event.target)}); this.addGlobalEventListeners(); } else { const Xmin = this.sliderRef.getBoundingClientRect().left; const clientPosX = event.clientX - Xmin; const handleDistances = this.state.value.map(val => Math.abs(this.getPositionX(val) - clientPosX) ); const handleToMove = handleDistances.indexOf(Math.min(...handleDistances)); const nextValueArray = [...this.state.value]; nextValueArray[handleToMove] = this.getValue(event.clientX); this.setState({value: nextValueArray}); this.props.onChange(nextValueArray); } } /** * Called on releasing the mouse button. * Removes global mousemove and mouseup event listeners * @param {object} event */ handleGlobalMouseUp = (event) => { this.setState({draggingHandleIndex: null}); this.removeGlobalEventListeners(); } /** * Called while dragging the mouse. * Updates the value in the local state of the component. */ handleGlobalMouseMove = (event) => { if (!this.props.range) { const value = this.getValue(event.clientX); this.setState({value}) this.props.onChange(value); } else if (this.state.draggingHandleIndex !== null) { let min = this.props.min; let max = this.props.max; if (this.state.draggingHandleIndex === 0) { max = this.state.value[1] } else if (this.state.draggingHandleIndex === 1) { min = this.state.value[0]; } const value = this.getValue(event.clientX, min, max); let nextValueArray = [...this.state.value]; nextValueArray[this.state.draggingHandleIndex] = value; this.setState({value: nextValueArray}); this.props.onChange(nextValueArray); } } /** * Adds global event listeners. */ addGlobalEventListeners = () => { document.body.addEventListener('mousemove', this.handleGlobalMouseMove); document.body.addEventListener('mouseup', this.handleGlobalMouseUp); } /** * Removes global event listeners. */ removeGlobalEventListeners = () => { document.body.removeEventListener('mousemove', this.handleGlobalMouseMove); document.body.removeEventListener('mouseup', this.handleGlobalMouseUp); } /** * Returns the distance in pixels from the left edge of the slider to a point * on the slider corresponding to a particular value. * @param {number} value * @return {number} */ getPositionX = (value) => { if (this.sliderRef && this.props.max > this.props.min) { const {left: Xmin, right: Xmax} = this.sliderRef.getBoundingClientRect(); /** In case of non linear scale simply get posX from the marker data */ if (this.props.nonLinear) { let posX; this.getMarkerData().map(data => { if (data.value === value) { posX = data.posX; } }); return posX; /** In case of linear scale posX can be calculated via interpolation */ } else { return parseInt((Xmax - Xmin)/(this.props.max - this.props.min) *(value - this.props.min)); } } else { return 0; } } /** * Helper function to get the nearest marker(step) corresponding to given value * @param {number} value * @return {number} */ getNearestMarkerValue = value => { let nearestValue; let minDistance = 0; this.getMarkerData().map(data => { const step = data.value; let distance = Math.abs(value - step); if (typeof nearestValue === 'undefined' || distance < minDistance) { nearestValue = step; minDistance = distance; } }); return nearestValue; } /** * Returns the corresponding value on the slider of any point on the document where the * user has clicked / dragged mouse. * @param {number} clientX The x coordinate of the point * @return {number} */ getValue = (clientX, min = this.props.min, max = this.props.max) => { if (this.sliderRef) { const {left: Xmin, right: Xmax} = this.sliderRef.getBoundingClientRect(); const posX = clientX - Xmin; let value; if (this.props.nonLinear) { /** In case of non linear scale value should be obtained from marker data */ let nearestValue, minDistance = 0; this.getMarkerData().map(data => { let distance = Math.abs(data.posX- posX); if (typeof nearestValue === 'undefined' || distance < minDistance) { nearestValue = data.value; minDistance = distance; } }); value = nearestValue; /** In case of linear scale value can be calculated using interpolation */ } else { value = this.props.min + (this.props.max - this.props.min)/(Xmax - Xmin)*posX; } /** Sanitize the value */ value = value > max ? max : value; value = value < min ? min : value; if (typeof this.props.steps !== 'undefined' && this.props.selectStepsOnly && !this.props.nonLinear) { value = this.getNearestMarkerValue(value); } return value; } else { return this.props.min; } } /** * Helper function to get the marker data in the required format. */ getMarkerData = () => { let marks = []; /** If steps are provided in props the use it to compute marker data */ if (this.props.steps) { if (this.props.nonLinear) { if (!this.sliderRef) { return; } let stepKeys = Object.keys(this.props.steps).sort(function(a, b) { return parseInt(a) - parseInt(b); }); if (parseInt(stepKeys[0]) !== this.props.min) { stepKeys = stepKeys.unshift(this.props.min); } if (parseInt(stepKeys[stepKeys.length - 1]) !== this.props.max) { stepKeys.push(this.props.max); } const spaceBetweenKeys = (this.props.max - this.props.min)/(stepKeys.length - 1); const {left: Xmin, right: Xmax} = this.sliderRef.getBoundingClientRect(); stepKeys.map((step, index) => { const posX = parseInt((Xmax - Xmin)/(this.props.max - this.props.min) *(spaceBetweenKeys*index - this.props.min)); marks.push({ index: marks.length, value: parseInt(step), posX: posX, tooltip: this.props.getTooltip(this.props.steps[step]) }); }); } else { Object.keys(this.props.steps).sort().map(step => { marks.push({ index: marks.length, value: parseInt(step), posX: this.getPositionX(parseInt(step)), tooltip: this.props.getTooltip(this.props.steps[step]) }); }); } /** If steps is not provided than place marks at equal distance on the linear scale */ } else { const step = Math.ceil((this.props.max - this.props.min)/10) || 1; let val = this.props.min; while (val <= this.props.max) { marks.push({ index: marks.length, value: parseInt(step), posX: this.getPositionX(val), tooltip: this.props.getTooltip(val) }); val = val + step; } } return marks; } /** * Renders the scale with the steps / marks */ renderScale = () => { const markerData = this.getMarkerData(); const value = this.state.value; return ( <Scale styles={this.props.styles.scale || {}}> { this.state.isComponentMounted ? markerData.map(mark => { return this.props.showMarkerWithoutTooltip || mark.tooltip ? [ <Mark styles={this.props.styles.mark || {}} posX={mark.posX} />, <Tooltip styles={this.props.styles.tooltip || {}} posX={mark.posX}> {mark.tooltip} </Tooltip> ] : null }) : null } { this.props.range ? <Line styles={this.props.styles.line || {}} style={{ left: this.getPositionX(value[0]), width: this.getPositionX(value[1]) - this.getPositionX(value[0]) }} /> : <Line styles={this.props.styles.line || {}} style={{ left: 0, width: this.getPositionX(value) }} /> } </Scale> ); } renderInput = () => { let { value } = this.state; if (this.props.range) { value = value.map(v => parseInt(v*100)/100); } else { value = parseInt(value*100)/100; } return ( <InputWrapper> { this.props.range ? [ <Input value={value[0]} onChange={this.handleFirstInputValueChange} type='number' disabled={this.props.disabled} />, <Separator>-</Separator>, <Input value={value[1]} onChange={this.handleSecondInputValueChange} type='number' disabled={this.props.disabled} /> ] : <Input value={value} onChange={this.handleFirstInputValueChange} type='number' disabled={this.props.disabled} /> } </InputWrapper> ) } render() { return ( <div> <SliderWrapper styles={this.props.styles.slider || {}} innerRef={this.setSliderRef} onMouseDown={this.handleMouseDown} > { this.renderScale() } { this.props.range ? this.state.value.map((value, index) => { return ( <Handle key={index} posX={this.getPositionX(value)} styles={this.props.styles.handle || {}} innerRef={ref => this.setHandleRef(index, ref)} active={this.state.draggingHandleIndex === index} /> ); }) : <Handle posX={this.getPositionX(this.state.value)} styles={this.props.styles.handle || {}} innerRef={ref => this.setHandleRef(0, ref)} active={this.state.draggingHandleIndex === 0} /> } </SliderWrapper> { this.props.showTextField ? this.renderInput() : null } </div> ); } } Slider.propTypes = propTypes; Slider.defaultProps = { disabled: false, selectStepsOnly: false, showMarkerWithoutTooltip: true, onChange: (val) => {}, styles: {}, range: false, min: 0, max: 100, getTooltip: (val) => val, showTextField: false }; export default Slider;