import React, { Component, HTMLAttributes } from 'react'; import Touch, { TouchEventHandler, TouchEvent } from '../Touch/Touch'; import classNames from '../../lib/classNames'; import getClassName from '../../helpers/getClassName'; import { HasFormLabels, HasPlatform, HasRootRef, OldRef } from '../../types'; import withPlatform from '../../hoc/withPlatform'; import { canUseDOM } from '../../lib/dom'; export interface SliderProps extends HasRootRef, HasPlatform, Omit, 'defaultValue' | 'onChange'>, HasFormLabels { min?: number; max?: number; value?: number; step?: number; onChange?(value: number, e: TouchEvent): void; defaultValue?: number; } export interface SliderState { containerLeft: number; startX: number; percentPosition: number; active: boolean; containerWidth: number; } export type OnSliderResize = (callback?: VoidFunction | Event) => void; export function precisionRound(number: number, precision: number) { let factor = Math.pow(10, precision || 1); return Math.round(number * factor) / factor; } class Slider extends Component { constructor(props: SliderProps) { super(props); this.state = { startX: 0, containerLeft: 0, percentPosition: 0, active: false, containerWidth: 0, }; this.isControlledOutside = this.props.hasOwnProperty('value'); } static defaultProps: SliderProps = { min: 0, max: 100, step: 0, }; isControlledOutside: boolean; container: HTMLDivElement; onStart: TouchEventHandler = (e: TouchEvent) => { const absolutePosition = this.validateAbsolute(e.startX - this.state.containerLeft); const percentPosition = this.absoluteToPecent(absolutePosition); this.onChange(this.percentToValue(percentPosition), e); if (this.isControlledOutside) { this.setState({ startX: absolutePosition }); } else { this.setState({ startX: absolutePosition, percentPosition, }); } const target = e.originalEvent.target as HTMLElement; this.setState({ active: !!target.closest('.Slider__thumb') }); }; onMoveX: TouchEventHandler = (e: TouchEvent) => { const absolutePosition = this.validateAbsolute(this.state.startX + (e.shiftX || 0)); const percentPosition = this.absoluteToPecent(absolutePosition); this.onChange(this.percentToValue(percentPosition), e); if (!this.isControlledOutside) { this.setState({ percentPosition }); } e.originalEvent.preventDefault(); }; onEnd: TouchEventHandler = () => { this.setState({ active: false, }); }; onResize: OnSliderResize = (callback?: VoidFunction) => { this.setState({ containerLeft: this.container.offsetLeft, containerWidth: this.container.offsetWidth, }, () => { typeof callback === 'function' && callback(); }); }; onChange(value: number, 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(percent: number) { return Math.max(0, Math.min(percent, 100)); } absoluteToPecent(absolute: number) { return absolute * 100 / this.state.containerWidth; } 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(value: number) { return (value - this.props.min) * 100 / (this.props.max - this.props.min); } get value() { if (this.isControlledOutside) { return this.props.value; } else if (this.props.hasOwnProperty('defaultValue')) { return this.props.defaultValue; } else { return this.props.min; } } componentDidMount() { if (canUseDOM) { window.addEventListener('resize', this.onResize); this.onResize(() => { this.setState({ percentPosition: this.validatePercent(this.valueToPercent(this.value)) }); }); } } componentDidUpdate(prevProps: SliderProps) { if (this.isControlledOutside && this.props.value !== prevProps.value) { this.setState({ percentPosition: 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(Slider);