import React, { Component, FC, createRef, HTMLAttributes, RefCallback, useCallback, useState } from 'react';
import Touch, { TouchEvent, TouchEventHandler } from '../Touch/Touch';
import { getClassName } from '../../helpers/getClassName';
import { classNames } from '../../lib/classNames';
import { HasPlatform, HasRootRef } from '../../types';
import { withPlatform } from '../../hoc/withPlatform';
import { setRef } from '../../lib/utils';
import { rescale, clamp } from '../../helpers/math';
import { withAdaptivity, AdaptivityProps } from '../../hoc/withAdaptivity';
export type Value = [number, number];
export interface RangeSliderProps extends
HasRootRef,
HasPlatform,
Omit, 'value' | 'defaultValue' | 'onChange'>,
AdaptivityProps {
min?: number;
max?: number;
step?: number;
value?: Value;
defaultValue?: Value;
disabled?: boolean;
onChange?(value: Value, e: TouchEvent): void;
}
class RangeSliderDumb extends Component {
dragging: false | 'start' | 'end' = false;
startX = 0;
containerWidth = 0;
container: HTMLDivElement;
thumbStart = createRef();
thumbEnd = createRef();
onStart: TouchEventHandler = (e: TouchEvent) => {
if (this.props.disabled) {
return;
}
const boundingRect = this.container.getBoundingClientRect();
this.containerWidth = boundingRect.width;
const absolutePosition = e.startX - boundingRect.left;
const value = this.offsetToValue(absolutePosition);
this.dragging = this.snapDirection(value, e.originalEvent.target);
this.startX = absolutePosition;
this.props.onChange(this.updateRange(value), e);
};
onMove: TouchEventHandler = (e: TouchEvent) => {
if (this.props.disabled) {
return;
}
const value = this.offsetToValue(this.startX + (e.shiftX || 0));
this.props.onChange(this.updateRange(value), e);
e.originalEvent.preventDefault();
};
onEnd: TouchEventHandler = () => {
if (this.props.disabled) {
return;
}
this.dragging = false;
};
updateRange(value: number): Value {
if (this.props.disabled) {
return this.props.value;
}
const [start, end] = this.props.value;
const { dragging } = this;
if (dragging === 'start') {
if (value > end) {
// "перехватиться", если перетянули за конец
this.dragging = 'end';
return [end, value];
}
return [value, end];
}
if (dragging === 'end') {
if (value < start) {
// "перехватиться", если перетянули за начало
this.dragging = 'start';
return [value, start];
}
return [start, value];
}
return this.props.value;
};
offsetToValue(absolute: number) {
const { min, max, step } = this.props;
return rescale(absolute, [0, this.containerWidth], [min, max], { step });
}
snapDirection(value: number, target: EventTarget) {
if (target === this.thumbStart.current) {
return 'start';
}
if (target === this.thumbEnd.current) {
return 'end';
}
const [start, end] = this.props.value;
return Math.abs(start - value) <= Math.abs(end - value) ? 'start' : 'end';
}
getRef: RefCallback = (container) => {
this.container = container;
setRef(container, this.props.getRootRef);
};
render() {
const { className, min, max, step, value, defaultValue,
onChange, getRootRef, platform, sizeY, disabled, ...restProps } = this.props;
const percentStart = (value[0] - min) / (max - min) * 100;
const percentEnd = (value[1] - min) / (max - min) * 100;
return (
);
}
}
const RangeSlider: FC = ({ onChange, defaultValue, ...props }) => {
const isControlled = Boolean(props.value);
const [localValue, setValue] = useState(defaultValue || [props.min, props.max] as Value);
const [start, end] = props.value || localValue;
const value = [clamp(start, props.min, props.max), clamp(end, props.min, props.max)] as Value;
const handleChange: RangeSliderProps['onChange'] = useCallback((nextValue, event) => {
if (props.disabled || value[0] === nextValue[0] && value[1] === nextValue[1]) {
return;
}
!isControlled && setValue(nextValue);
onChange && onChange(nextValue, event);
}, [onChange, isControlled, value]);
return ;
};
RangeSlider.defaultProps = {
min: 0,
max: 100,
step: 0,
};
export default withAdaptivity(withPlatform(RangeSlider), {
sizeY: true,
});