727 lines
16 KiB
JavaScript
Executable File
727 lines
16 KiB
JavaScript
Executable File
import React, { Component } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import * as valueTransformer from './value-transformer';
|
|
import rangePropType from './range-prop-type';
|
|
import valuePropType from './value-prop-type';
|
|
import Slider from './slider';
|
|
import Track from './track';
|
|
import { captialize, distanceTo, isDefined, isObject, length } from '../utils';
|
|
import styled from 'styled-components';
|
|
import remcalc from 'remcalc';
|
|
|
|
export const RangeStyled = styled.div`
|
|
position: relative;
|
|
min-height: ${remcalc(10)};
|
|
`;
|
|
|
|
/**
|
|
* A React component that allows users to input numeric values within a range
|
|
* by dragging its sliders.
|
|
*/
|
|
export default class InputRange extends Component {
|
|
/**
|
|
* @ignore
|
|
* @override
|
|
* @return {Object}
|
|
*/
|
|
static get propTypes() {
|
|
return {
|
|
ariaLabelledby: PropTypes.string,
|
|
ariaControls: PropTypes.string,
|
|
classNames: PropTypes.objectOf(PropTypes.string),
|
|
disabled: PropTypes.bool,
|
|
draggableTrack: PropTypes.bool,
|
|
formatLabel: PropTypes.func,
|
|
maxValue: rangePropType,
|
|
minValue: rangePropType,
|
|
name: PropTypes.string,
|
|
onChangeStart: PropTypes.func,
|
|
onChange: PropTypes.func,
|
|
onChangeComplete: PropTypes.func,
|
|
step: PropTypes.number,
|
|
value: valuePropType
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
* @override
|
|
* @return {Object}
|
|
*/
|
|
static get defaultProps() {
|
|
return {
|
|
disabled: false,
|
|
maxValue: 10,
|
|
minValue: 0,
|
|
step: 1
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {Object} props
|
|
* @param {string} [props.ariaLabelledby]
|
|
* @param {string} [props.ariaControls]
|
|
* @param {InputRangeClassNames} [props.classNames]
|
|
* @param {boolean} [props.disabled = false]
|
|
* @param {Function} [props.formatLabel]
|
|
* @param {number|Range} [props.maxValue = 10]
|
|
* @param {number|Range} [props.minValue = 0]
|
|
* @param {string} [props.name]
|
|
* @param {string} props.onChange
|
|
* @param {Function} [props.onChangeComplete]
|
|
* @param {Function} [props.onChangeStart]
|
|
* @param {number} [props.step = 1]
|
|
* @param {number|Range} props.value
|
|
*/
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
/**
|
|
* @private
|
|
* @type {?number}
|
|
*/
|
|
this.startValue = null;
|
|
|
|
/**
|
|
* @private
|
|
* @type {?Component}
|
|
*/
|
|
this.node = null;
|
|
|
|
/**
|
|
* @private
|
|
* @type {?Component}
|
|
*/
|
|
this.trackNode = null;
|
|
|
|
/**
|
|
* @private
|
|
* @type {bool}
|
|
*/
|
|
this.isSliderDragging = false;
|
|
|
|
this.handleSliderDrag = this.handleSliderDrag.bind(this);
|
|
this.handleTrackDrag = this.handleTrackDrag.bind(this);
|
|
this.handleTrackMouseDown = this.handleTrackMouseDown.bind(this);
|
|
this.handleInteractionStart = this.handleInteractionStart.bind(this);
|
|
this.handleInteractionEnd = this.handleInteractionEnd.bind(this);
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
this.handleKeyUp = this.handleKeyUp.bind(this);
|
|
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
* @override
|
|
* @return {void}
|
|
*/
|
|
componentWillUnmount() {
|
|
this.removeDocumentMouseUpListener();
|
|
this.removeDocumentTouchEndListener();
|
|
}
|
|
|
|
/**
|
|
* Return the bounding rect of the track
|
|
* @private
|
|
* @return {ClientRect}
|
|
*/
|
|
getTrackClientRect() {
|
|
return this.trackNode && this.trackNode.getClientRect();
|
|
}
|
|
|
|
/**
|
|
* Return the slider key closest to a point
|
|
* @private
|
|
* @param {Point} position
|
|
* @return {string}
|
|
*/
|
|
getKeyByPosition(position) {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const positions = valueTransformer.getPositionsFromValues(
|
|
values,
|
|
this.props.minValue,
|
|
this.props.maxValue,
|
|
this.getTrackClientRect()
|
|
);
|
|
|
|
if (this.isMultiValue()) {
|
|
const distanceToMin = distanceTo(position, positions.min);
|
|
const distanceToMax = distanceTo(position, positions.max);
|
|
|
|
if (distanceToMin < distanceToMax) {
|
|
return 'min';
|
|
}
|
|
}
|
|
|
|
return 'max';
|
|
}
|
|
|
|
/**
|
|
* Return all the slider keys
|
|
* @private
|
|
* @return {string[]}
|
|
*/
|
|
getKeys() {
|
|
if (this.isMultiValue()) {
|
|
return ['min', 'max'];
|
|
}
|
|
|
|
return ['max'];
|
|
}
|
|
|
|
/**
|
|
* Return true if the difference between the new and the current value is
|
|
* greater or equal to the step amount of the component
|
|
* @private
|
|
* @param {Range} values
|
|
* @return {boolean}
|
|
*/
|
|
hasStepDifference(values) {
|
|
const currentValues = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
|
|
return (
|
|
length(values.min, currentValues.min) >= this.props.step ||
|
|
length(values.max, currentValues.max) >= this.props.step
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return true if the component accepts a min and max value
|
|
* @private
|
|
* @return {boolean}
|
|
*/
|
|
isMultiValue() {
|
|
return isObject(this.props.value);
|
|
}
|
|
|
|
/**
|
|
* Return true if the range is within the max and min value of the component
|
|
* @private
|
|
* @param {Range} values
|
|
* @return {boolean}
|
|
*/
|
|
isWithinRange(values) {
|
|
if (this.isMultiValue()) {
|
|
return (
|
|
values.min >= this.props.minValue &&
|
|
values.max <= this.props.maxValue &&
|
|
values.min < values.max
|
|
);
|
|
}
|
|
|
|
return (
|
|
values.max >= this.props.minValue && values.max <= this.props.maxValue
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return true if the new value should trigger a render
|
|
* @private
|
|
* @param {Range} values
|
|
* @return {boolean}
|
|
*/
|
|
shouldUpdate(values) {
|
|
return this.isWithinRange(values) && this.hasStepDifference(values);
|
|
}
|
|
|
|
/**
|
|
* Update the position of a slider
|
|
* @private
|
|
* @param {string} key
|
|
* @param {Point} position
|
|
* @return {void}
|
|
*/
|
|
updatePosition(key, position) {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const positions = valueTransformer.getPositionsFromValues(
|
|
values,
|
|
this.props.minValue,
|
|
this.props.maxValue,
|
|
this.getTrackClientRect()
|
|
);
|
|
|
|
positions[key] = position;
|
|
|
|
this.updatePositions(positions);
|
|
}
|
|
|
|
/**
|
|
* Update the positions of multiple sliders
|
|
* @private
|
|
* @param {Object} positions
|
|
* @param {Point} positions.min
|
|
* @param {Point} positions.max
|
|
* @return {void}
|
|
*/
|
|
updatePositions(positions) {
|
|
const values = {
|
|
min: valueTransformer.getValueFromPosition(
|
|
positions.min,
|
|
this.props.minValue,
|
|
this.props.maxValue,
|
|
this.getTrackClientRect()
|
|
),
|
|
max: valueTransformer.getValueFromPosition(
|
|
positions.max,
|
|
this.props.minValue,
|
|
this.props.maxValue,
|
|
this.getTrackClientRect()
|
|
)
|
|
};
|
|
|
|
const transformedValues = {
|
|
min: valueTransformer.getStepValueFromValue(values.min, this.props.step),
|
|
max: valueTransformer.getStepValueFromValue(values.max, this.props.step)
|
|
};
|
|
|
|
this.updateValues(transformedValues);
|
|
}
|
|
|
|
/**
|
|
* Update the value of a slider
|
|
* @private
|
|
* @param {string} key
|
|
* @param {number} value
|
|
* @return {void}
|
|
*/
|
|
updateValue(key, value) {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
|
|
values[key] = value;
|
|
|
|
this.updateValues(values);
|
|
}
|
|
|
|
/**
|
|
* Update the values of multiple sliders
|
|
* @private
|
|
* @param {Range|number} values
|
|
* @return {void}
|
|
*/
|
|
updateValues(values) {
|
|
if (!this.shouldUpdate(values)) {
|
|
return;
|
|
}
|
|
|
|
this.props.onChange(this.isMultiValue() ? values : values.max);
|
|
}
|
|
|
|
/**
|
|
* Increment the value of a slider by key name
|
|
* @private
|
|
* @param {string} key
|
|
* @return {void}
|
|
*/
|
|
incrementValue(key) {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const value = values[key] + this.props.step;
|
|
|
|
this.updateValue(key, value);
|
|
}
|
|
|
|
/**
|
|
* Decrement the value of a slider by key name
|
|
* @private
|
|
* @param {string} key
|
|
* @return {void}
|
|
*/
|
|
decrementValue(key) {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const value = values[key] - this.props.step;
|
|
|
|
this.updateValue(key, value);
|
|
}
|
|
|
|
/**
|
|
* Listen to mouseup event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
addDocumentMouseUpListener() {
|
|
this.removeDocumentMouseUpListener();
|
|
this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
|
|
}
|
|
|
|
/**
|
|
* Listen to touchend event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
addDocumentTouchEndListener() {
|
|
this.removeDocumentTouchEndListener();
|
|
this.node.ownerDocument.addEventListener('touchend', this.handleTouchEnd);
|
|
}
|
|
|
|
/**
|
|
* Stop listening to mouseup event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
removeDocumentMouseUpListener() {
|
|
this.node && this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
|
|
}
|
|
|
|
/**
|
|
* Stop listening to touchend event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
removeDocumentTouchEndListener() {
|
|
this.node.ownerDocument.removeEventListener(
|
|
'touchend',
|
|
this.handleTouchEnd
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle any "mousemove" event received by the slider
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @param {string} key
|
|
* @return {void}
|
|
*/
|
|
handleSliderDrag(event, key) {
|
|
if (this.props.disabled) {
|
|
return;
|
|
}
|
|
|
|
const position = valueTransformer.getPositionFromEvent(
|
|
event,
|
|
this.getTrackClientRect()
|
|
);
|
|
this.isSliderDragging = true;
|
|
requestAnimationFrame(() => this.updatePosition(key, position));
|
|
}
|
|
|
|
/**
|
|
* Handle any "mousemove" event received by the track
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @return {void}
|
|
*/
|
|
handleTrackDrag(event, prevEvent) {
|
|
if (
|
|
this.props.disabled ||
|
|
!this.props.draggableTrack ||
|
|
this.isSliderDragging
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const { maxValue, minValue, value: { max, min } } = this.props;
|
|
|
|
const position = valueTransformer.getPositionFromEvent(
|
|
event,
|
|
this.getTrackClientRect()
|
|
);
|
|
const value = valueTransformer.getValueFromPosition(
|
|
position,
|
|
minValue,
|
|
maxValue,
|
|
this.getTrackClientRect()
|
|
);
|
|
const stepValue = valueTransformer.getStepValueFromValue(
|
|
value,
|
|
this.props.step
|
|
);
|
|
|
|
const prevPosition = valueTransformer.getPositionFromEvent(
|
|
prevEvent,
|
|
this.getTrackClientRect()
|
|
);
|
|
const prevValue = valueTransformer.getValueFromPosition(
|
|
prevPosition,
|
|
minValue,
|
|
maxValue,
|
|
this.getTrackClientRect()
|
|
);
|
|
const prevStepValue = valueTransformer.getStepValueFromValue(
|
|
prevValue,
|
|
this.props.step
|
|
);
|
|
|
|
const offset = prevStepValue - stepValue;
|
|
|
|
const transformedValues = {
|
|
min: min - offset,
|
|
max: max - offset
|
|
};
|
|
|
|
this.updateValues(transformedValues);
|
|
}
|
|
|
|
/**
|
|
* Handle any "mousedown" event received by the track
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @param {Point} position
|
|
* @return {void}
|
|
*/
|
|
|
|
handleTrackMouseDown(event, position) {
|
|
if (this.props.disabled) {
|
|
return;
|
|
}
|
|
|
|
const { maxValue, minValue, value: { max, min } } = this.props;
|
|
|
|
event.preventDefault();
|
|
|
|
const value = valueTransformer.getValueFromPosition(
|
|
position,
|
|
minValue,
|
|
maxValue,
|
|
this.getTrackClientRect()
|
|
);
|
|
const stepValue = valueTransformer.getStepValueFromValue(
|
|
value,
|
|
this.props.step
|
|
);
|
|
|
|
if (!this.props.draggableTrack || stepValue > max || stepValue < min) {
|
|
this.updatePosition(this.getKeyByPosition(position), position);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the start of any mouse/touch event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
|
|
handleInteractionStart() {
|
|
if (this.props.onChangeStart) {
|
|
this.props.onChangeStart(this.props.value);
|
|
}
|
|
|
|
if (this.props.onChangeComplete && !isDefined(this.startValue)) {
|
|
this.startValue = this.props.value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the end of any mouse/touch event
|
|
* @private
|
|
* @return {void}
|
|
*/
|
|
handleInteractionEnd() {
|
|
if (this.isSliderDragging) {
|
|
this.isSliderDragging = false;
|
|
}
|
|
|
|
if (!this.props.onChangeComplete || !isDefined(this.startValue)) {
|
|
return;
|
|
}
|
|
|
|
if (this.startValue !== this.props.value) {
|
|
this.props.onChangeComplete(this.props.value);
|
|
}
|
|
|
|
this.startValue = null;
|
|
}
|
|
|
|
/**
|
|
* Handle any "keydown" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @return {void}
|
|
*/
|
|
|
|
handleKeyDown(event) {
|
|
this.handleInteractionStart(event);
|
|
}
|
|
|
|
/**
|
|
* Handle any "keyup" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @return {void}
|
|
*/
|
|
handleKeyUp(event) {
|
|
this.handleInteractionEnd(event);
|
|
}
|
|
|
|
/**
|
|
* Handle any "mousedown" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @return {void}
|
|
*/
|
|
|
|
handleMouseDown(event) {
|
|
this.handleInteractionStart(event);
|
|
this.addDocumentMouseUpListener();
|
|
}
|
|
|
|
/**
|
|
* Handle any "mouseup" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
*/
|
|
|
|
handleMouseUp(event) {
|
|
this.handleInteractionEnd(event);
|
|
this.removeDocumentMouseUpListener();
|
|
}
|
|
|
|
/**
|
|
* Handle any "touchstart" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
* @return {void}
|
|
*/
|
|
|
|
handleTouchStart(event) {
|
|
this.handleInteractionStart(event);
|
|
this.addDocumentTouchEndListener();
|
|
}
|
|
|
|
/**
|
|
* Handle any "touchend" event received by the component
|
|
* @private
|
|
* @param {SyntheticEvent} event
|
|
*/
|
|
handleTouchEnd(event) {
|
|
this.handleInteractionEnd(event);
|
|
this.removeDocumentTouchEndListener();
|
|
}
|
|
|
|
/**
|
|
* Return JSX of sliders
|
|
* @private
|
|
* @return {JSX.Element}
|
|
*/
|
|
renderSliders() {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const percentages = valueTransformer.getPercentagesFromValues(
|
|
values,
|
|
this.props.minValue,
|
|
this.props.maxValue
|
|
);
|
|
|
|
return this.getKeys().map(key => {
|
|
const value = values[key];
|
|
const percentage = percentages[key];
|
|
|
|
let { maxValue, minValue } = this.props;
|
|
|
|
if (key === 'min') {
|
|
maxValue = values.max;
|
|
} else {
|
|
minValue = values.min;
|
|
}
|
|
|
|
const slider = (
|
|
<Slider
|
|
ariaLabelledby={this.props.ariaLabelledby}
|
|
ariaControls={this.props.ariaControls}
|
|
classNames={this.props.classNames}
|
|
formatLabel={this.props.formatLabel}
|
|
key={key}
|
|
greyed={this.props.greyed}
|
|
maxValue={maxValue}
|
|
minValue={minValue}
|
|
onSliderDrag={this.handleSliderDrag}
|
|
percentage={percentage}
|
|
type={key}
|
|
value={value}
|
|
/>
|
|
);
|
|
|
|
return slider;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return JSX of hidden inputs
|
|
* @private
|
|
* @return {JSX.Element}
|
|
*/
|
|
renderHiddenInputs() {
|
|
if (!this.props.name) {
|
|
return [];
|
|
}
|
|
|
|
const isMultiValue = this.isMultiValue();
|
|
const values = valueTransformer.getValueFromProps(this.props, isMultiValue);
|
|
|
|
return this.getKeys().map(key => {
|
|
const value = values[key];
|
|
const name = isMultiValue
|
|
? `${this.props.name}${captialize(key)}`
|
|
: this.props.name;
|
|
|
|
return <input key={key} type="hidden" name={name} value={value} />;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
* @override
|
|
* @return {JSX.Element}
|
|
*/
|
|
render() {
|
|
const values = valueTransformer.getValueFromProps(
|
|
this.props,
|
|
this.isMultiValue()
|
|
);
|
|
const percentages = valueTransformer.getPercentagesFromValues(
|
|
values,
|
|
this.props.minValue,
|
|
this.props.maxValue
|
|
);
|
|
|
|
return (
|
|
<RangeStyled
|
|
aria-disabled={this.props.disabled}
|
|
innerRef={node => {
|
|
this.node = node;
|
|
}}
|
|
onKeyDown={this.handleKeyDown}
|
|
onKeyUp={this.handleKeyUp}
|
|
onMouseDown={this.handleMouseDown}
|
|
onTouchStart={this.handleTouchStart}
|
|
>
|
|
<Track
|
|
classNames={this.props.classNames}
|
|
draggableTrack={this.props.draggableTrack}
|
|
ref={trackNode => {
|
|
this.trackNode = trackNode;
|
|
}}
|
|
percentages={percentages}
|
|
onTrackDrag={this.handleTrackDrag}
|
|
onTrackMouseDown={this.handleTrackMouseDown}
|
|
>
|
|
{this.renderSliders()}
|
|
</Track>
|
|
|
|
{this.renderHiddenInputs()}
|
|
</RangeStyled>
|
|
);
|
|
}
|
|
}
|