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
JavaScript
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;