import { createElement, forwardRef, RefForwardingComponent, useEffect, useMemo, useRef, useState } from 'rax'; import { StyledComponentProps, useStyles } from '@rax-ui/styles'; import View from 'rax-view'; import Track from './track'; import Thumb from './thumb'; import StyleProvider from './style'; import Context from './context'; import { getClosestThumbIndex, getNodeRect, getPrecision, getTouchPosition, noop, pauseEvent } from './utils'; enum Count { single = 1, double = 2, } export interface RangeProps extends StyledComponentProps { min?: number; max?: number; step?: number; value?: number | number[]; count?: Count; defaultValue?: number | number[]; disabled?: boolean; readOnly?: boolean; marks?: object; style?: object; tooltipVisible?: boolean; onChange?: (value, e) => void; onMove?: (value, e) => void; } function getInitialRatio(value: number | number[], max: number, min: number) { let ratio = value; if (!Array.isArray(ratio)) { ratio = [ratio]; } return ratio .map((item) => { if (item > max) { return max; } if (item < min) { return min; } return item; }) .reduce((prev, curr, index) => ({ ...prev, [index]: (curr - min) / (max - min) }), {}); } const Range: RefForwardingComponent = ( { min, max, count, value, defaultValue, step, disabled, marks, readOnly, style, tooltipVisible, onChange, onMove, ...others }, ref: any ) => { const [ratio, setRatio] = useState(getInitialRatio(value || defaultValue, max, min)); const [active, setActive] = useState(); const [dragging, setDragging] = useState(false); const [rangeWidth, setRangeWidth] = useState(0); const handles = count === Count.double ? { 0: useRef(), 1: useRef(), } : { 0: useRef(), }; const styles = useStyles(StyleProvider, others, (classNames) => { const statePrefix = disabled ? '--disabled' : ''; return { range: { ...classNames('range'), ...style, }, track: classNames(['range__track', `range${statePrefix}__track`]), trackSelected: classNames(['range__track--selected', `range${statePrefix}__track--selected`]), thumb: classNames(['range__thumb', `range${statePrefix}__thumb`]), thumbActive: classNames(['range__thumb', 'range__thumb--active', `range${statePrefix}__thumb`]), mark: classNames(['range__mark', `range${statePrefix}__mark`]), markActive: classNames(['range__mark', 'range__mark--active', `range${statePrefix}__mark`]), label: classNames(['range__label', `range${statePrefix}__label`]), labelActive: classNames(['range__label', 'range__label--active', `range${statePrefix}__label--active`]), tooltip: classNames('range__tooltip'), arrow: classNames('range__arrow'), }; }); function getRatioValue(val: number) { return (val * (max - min) + min).toFixed(getPrecision(step)); } const points = useMemo(() => { const data = { ...marks }; if (step) { for (let point = min; point <= max; point += step) { data[point] = point; } } return Object.keys(data) .map(parseFloat) .sort((a, b) => a - b); }, [marks, step, min, max]); function getClosestStep(val) { const diffs = points.map((point: number) => Math.abs(point - val - min)); return points[diffs.indexOf(Math.min(...diffs))]; } function getValue() { const result = Object.keys(ratio) .map((key) => +getRatioValue(ratio[key])) .sort((a, b) => a - b); if (result.length === 1) { return result[0]; } return result; } function updatePositionByTouch(e, activeIndex?: number) { getNodeRect(ref.current).then((rect: ClientRect) => { const position = getTouchPosition(e); const diff = position - rect.left; const offset = Math.max(Math.min(diff, rect.width), 0); const closestValue = getClosestStep((offset / rect.width) * (max - min)); setRatio({ ...ratio, [activeIndex]: (closestValue - min) / (max - min) }); }); } function handleTouchStart(e) { if (disabled || readOnly) { return; } const position = getTouchPosition(e); getClosestThumbIndex(position, handles).then((activeIndex) => { setDragging(true); setActive(activeIndex); updatePositionByTouch(e, activeIndex); pauseEvent(e); }); } function handleTouchMove(e) { if (!dragging) { return; } updatePositionByTouch(e, active); pauseEvent(e); if (onMove) { onMove(getValue(), e); } } function handleTouchEnd(e) { if (!dragging) { return; } setDragging(false); setActive(null); const result = getValue(); if (onChange) { onChange(result, e); } pauseEvent(e); } function adjustRatio() { const data = {}; Object.keys(ratio).forEach((key) => (data[key] = (getClosestStep(ratio[key] * (max - min)) - min) / (max - min))); setRatio(data); } useEffect(adjustRatio, []); useEffect(() => { getNodeRect(ref.current).then((rect) => { setRangeWidth(rect.width); }); }, []); const ratioValue = Object.values(ratio).sort((a: number, b: number) => a - b); return ( {Object.keys(handles).map((key) => { const isActive = `${active}` === `${key}`; return ( ); })} ); }; Range.defaultProps = { min: 0, max: 100, step: 1, count: 1, disabled: false, readOnly: false, onChange: noop, marks: {}, }; export default forwardRef(Range);