import React, { Component, createRef, HTMLAttributes, RefObject } from 'react'; import Touch, { TouchEvent, TouchEventHandler } from '../Touch/Touch'; import getClassName from '../../helpers/getClassName'; import classNames from '../../lib/classNames'; import { HasFormLabels, HasPlatform, HasRootRef, OldRef } from '../../types'; import { OnSliderResize, precisionRound } from '../Slider/Slider'; import withPlatform from '../../hoc/withPlatform'; import { canUseDOM } from '../../lib/dom'; export type Value = [number, number]; export interface RangeSliderProps extends HasRootRef, HasPlatform, Omit, 'value' | 'defaultValue' | 'onChange'>, HasFormLabels { min?: number; max?: number; step?: number; value?: Value; defaultValue?: Value; onChange?(value: Value, e: TouchEvent): void; } export interface RangeSliderState { containerLeft: number; startX: number; percentStart: number; percentEnd: number; active: 'start' | 'end' | null; containerWidth: number; } class RangeSlider extends Component { constructor(props: RangeSliderProps) { super(props); this.state = { startX: 0, containerLeft: 0, percentStart: 0, percentEnd: 0, active: null, containerWidth: 0, }; this.isControlledOutside = this.props.hasOwnProperty('value'); this.thumbStart = createRef(); this.thumbEnd = createRef(); } static defaultProps: RangeSliderProps = { min: 0, max: 100, step: 0, }; isControlledOutside: boolean; container: HTMLDivElement; thumbStart: RefObject; thumbEnd: RefObject; onStart: TouchEventHandler = (e: TouchEvent) => { const absolutePosition = this.validateAbsolute(e.startX - this.state.containerLeft); const percentPosition = this.absoluteToPecent(absolutePosition); const { percentStart, percentEnd } = this.calcPercentRange(percentPosition); this.onChange([this.percentToValue(percentStart), this.percentToValue(percentEnd)], e); if (this.isControlledOutside) { this.setState({ startX: absolutePosition }); } else { this.setState({ startX: absolutePosition, percentStart, percentEnd, }); } const target = e.originalEvent.target as HTMLElement; const thumb = target.closest('.Slider__thumb'); if (thumb === this.thumbStart.current) { this.setState({ active: 'start' }); } else if (thumb === this.thumbEnd.current) { this.setState({ active: 'end' }); } }; onMoveX: TouchEventHandler = (e: TouchEvent) => { const absolutePosition = this.validateAbsolute(this.state.startX + (e.shiftX || 0)); const percentPosition = this.absoluteToPecent(absolutePosition); const { percentStart, percentEnd } = this.calcPercentRange(percentPosition); this.onChange([this.percentToValue(percentStart), this.percentToValue(percentEnd)], e); if (!this.isControlledOutside) { this.setState({ percentStart, percentEnd, }); } e.originalEvent.preventDefault(); }; onEnd: TouchEventHandler = () => { this.setState({ active: null, }); }; onResize: OnSliderResize = (callback?: VoidFunction | Event) => { this.setState({ containerLeft: this.container.offsetLeft, containerWidth: this.container.offsetWidth, }, () => { typeof callback === 'function' && callback(); }); }; onChange(value: Value, e: TouchEvent) { this.props.onChange && this.props.onChange(value, e); } validateAbsolute(absolute: number) { let res = Math.max(0, Math.min(absolute, this.state.containerWidth)); if (this.props.step > 0) { const stepCount = (this.props.max - this.props.min) / this.props.step; const absStep = this.state.containerWidth / stepCount; res = Math.round(res / absStep) * absStep; } return res; } validatePercent({ percentStart, percentEnd }: { percentStart: number; percentEnd: number }) { return { percentStart: Math.max(0, Math.min(percentStart, 100)), percentEnd: Math.max(0, Math.min(percentEnd, 100)), }; } absoluteToPecent(absolute: number) { return absolute * 100 / this.state.containerWidth; } calcPercentRange(percent: number) { const { percentStart, percentEnd } = this.state; if (percentStart === 100) { return { percentStart: percent, percentEnd }; } else if (percentEnd === 0) { return { percentEnd: percent, percentStart }; } else if (Math.abs(percentStart - percent) <= Math.abs(percentEnd - percent)) { return { percentStart: percent, percentEnd }; } else { return { percentEnd: percent, percentStart }; } } percentToValue(percent: number) { const res = percent * (this.props.max - this.props.min) / 100 + this.props.min; if (this.props.step > 0) { const stepFloatPart = `${this.props.step}`.split('.')[1] || ''; return precisionRound(res, stepFloatPart.length); } return res; } valueToPercent([valueStart, valueEnd]: Value) { return { percentStart: (valueStart - this.props.min) * 100 / (this.props.max - this.props.min), percentEnd: (valueEnd - this.props.min) * 100 / (this.props.max - this.props.min), }; } get value(): Value { if (this.isControlledOutside) { return this.props.value; } else if (this.props.hasOwnProperty('defaultValue')) { return this.props.defaultValue; } else { return [this.props.min, this.props.max]; } } componentDidMount() { if (canUseDOM) { window.addEventListener('resize', this.onResize); this.onResize(() => { this.setState(this.validatePercent(this.valueToPercent(this.value))); }); } } componentDidUpdate(prevProps: RangeSliderProps) { if (this.isControlledOutside && prevProps.value !== this.props.value) { this.setState(this.validatePercent(this.valueToPercent(this.props.value))); } } componentWillUnmount() { window.removeEventListener('resize', this.onResize); } getRef: OldRef = (container: HTMLDivElement) => { this.container = container; const getRootRef = this.props.getRootRef; if (getRootRef) { if (typeof getRootRef === 'function') { getRootRef(container); } else { getRootRef.current = container; } } }; render() { const { className, min, max, step, value, defaultValue, onChange, getRootRef, platform, top, bottom, ...restProps } = this.props; return (
); } } export default withPlatform(RangeSlider);