import { useState, useRef, CSSProperties, ReactNode, useMemo, useEffect, SyntheticEvent, } from 'react' import classNames from 'classnames' import { useStrike, UseStrikeConfig, useEvent } from '../../use' import { minmax, mround } from '../../utils' import { PAN_END, PAN_MOVE, PAN_START } from '../../strike' import { CommonComponentProps } from '../../utils/types' import './slider.scss' type RangeValue = [number, number] export interface SliderBaseProps extends CommonComponentProps { className?: string style?: CSSProperties min?: number max?: number step?: number vertical?: boolean disabled?: boolean pieceColor?: string trackColor?: string trackSize?: string thumbColor?: string thumbSize?: string start?: (value: number) => ReactNode end?: (value: number) => ReactNode } export interface SliderSingleProps extends SliderBaseProps { range?: false value?: number defaultValue?: number onAfterChange?: (value: number) => void onChange?: (value: number) => void } export interface SliderRangeProps extends SliderBaseProps { range?: true value?: RangeValue defaultValue?: RangeValue onAfterChange?: (value: RangeValue) => void onChange?: (value: RangeValue) => void } export type SliderProps = SliderSingleProps | SliderRangeProps const VERTICAL = 'vertical' const HORIZONTAL = 'horizontal' export function Slider(props: SliderProps) { const { className, value, defaultValue, min = 0, max = 100, step = 1, range = false, vertical = false, disabled = false, pieceColor = '', trackColor = '', trackSize = '', thumbColor = '', thumbSize = '', start, end, onAfterChange, onChange, ...restProps } = props const [isDown, setIsDown] = useState(false) const trackRef = useRef(null) const trackSizeRef = useRef(0) const downCoord = useRef(0) const downRatio = useRef(0) const oldRatio = useRef(0) const isNearStart = useRef(false) const getInitValue = (index: number) => { return () => { const val = range ? value ?? defaultValue ?? [min, min] : [min, value ?? defaultValue ?? min] return minmax(mround((val as RangeValue)[index], step), min, max) } } const [startValue, setStartValue] = useState(getInitValue(0)) const [endValue, setEndValue] = useState(getInitValue(1)) const downValue = useRef({ start: startValue, end: endValue, }) const oldValue = useRef({ start: startValue, end: endValue, }) const currRatio = useRef({ start: 0, end: 0, }) // 受控 useEffect(() => { if (value == null) return let [startValue, endValue] = (range ? value : [min, value]) as RangeValue startValue = minmax(mround(startValue, step), min, max) endValue = minmax(mround(endValue, step), min, max) Object.assign(oldValue.current, { start: startValue, end: endValue, }) setStartValue(startValue) setEndValue(endValue) }, [value, range, min, max]) const [startRatio, endRatio] = useMemo(() => { const total = max - min let startRatio = (startValue - min) / total let endRatio = (endValue - min) / total Object.assign(currRatio.current, { start: startRatio, end: endRatio, }) return [startRatio, endRatio] }, [startValue, endValue, min, max]) const handlePanStart = useEvent(({ x, y }) => { if (disabled) return Object.assign(downValue.current, oldValue.current) let rect = (trackRef.current as unknown as Element).getBoundingClientRect() let size = (trackSizeRef.current = vertical ? rect.height : rect.width) let rectCoord = vertical ? rect.top : rect.left let clientCoord = (downCoord.current = vertical ? y : x) let { start: startRatio, end: endRatio } = currRatio.current let offset = clientCoord - rectCoord if (range) { let startDist = Math.abs(offset - startRatio * size) let endDist = Math.abs(offset - endRatio * size) isNearStart.current = startDist < endDist } downRatio.current = offset / size oldRatio.current = isNearStart.current ? startRatio : endRatio setIsDown(true) }) const handleRatio = (ratio: number) => { ratio = minmax(mround(ratio, step / (max - min)), 0, 1) let { start: startRatio, end: endRatio } = currRatio.current if (range) { if (isNearStart.current) { if (ratio >= endRatio) { startRatio = endRatio endRatio = ratio isNearStart.current = false } else { startRatio = ratio } } else { if (ratio < startRatio) { endRatio = startRatio startRatio = ratio isNearStart.current = true } else { endRatio = ratio } } } else { endRatio = ratio } const total = max - min const ratioToValue = (ratio: number) => minmax(mround(ratio * total + min, step), min, max) const startValue = ratioToValue(startRatio) const endValue = ratioToValue(endRatio) if ( oldValue.current.start !== startValue || oldValue.current.end !== endValue ) { Object.assign(oldValue.current, { start: startValue, end: endValue, }) // 非受控 if (value == null) { setStartValue(startValue) setEndValue(endValue) } onChange?.( (range ? [startValue, endValue] : endValue) as RangeValue & number ) } } const handlePanMove = useEvent(({ x, y }) => { if (disabled) return let clientCoord = vertical ? y : x let ratio = (clientCoord - downCoord.current) / trackSizeRef.current + oldRatio.current handleRatio(ratio) }) const handlePanEnd = useEvent(() => { if (disabled) return setIsDown(false) const { start: startValue, end: endValue } = downValue.current if ( oldValue.current.start !== startValue || oldValue.current.end !== endValue ) { onAfterChange?.( (range ? [startValue, endValue] : endValue) as RangeValue & number ) } }) const strikeConfig = { pan: true, direction: vertical ? VERTICAL : HORIZONTAL, lockDirection: false, } as UseStrikeConfig const startThumbBinding = useStrike((strike) => { strike.on(PAN_START, handlePanStart) strike.on(PAN_MOVE, handlePanMove) strike.on(PAN_END, handlePanEnd) }, strikeConfig) const endThumbBinding = useStrike((strike) => { strike.on(PAN_START, handlePanStart) strike.on(PAN_MOVE, handlePanMove) strike.on(PAN_END, handlePanEnd) }, strikeConfig) const handleSliderClick = useEvent((event) => { if (disabled) return const { clientX, clientY } = event handlePanStart({ x: clientX, y: clientY, }) handleRatio(downRatio.current) handlePanEnd() }) const trackStyle = { width: vertical ? trackSize : '', height: !vertical ? trackSize : '', backgroundColor: trackColor, } let startPercent = startRatio * 100 + '%' let endPercent = (endRatio - startRatio) * 100 + '%' const pieceStyle = { [vertical ? 'top' : 'left']: startPercent, [vertical ? 'height' : 'width']: endPercent, backgroundColor: pieceColor, } const thumbStyle = { width: thumbSize, height: thumbSize, backgroundColor: thumbColor, } const sliderClass = classNames( 's-slider', { 's-slider-is-down': isDown, 's-slider-vertical': vertical, 's-slider-disabled': disabled, }, className ) const thumbElement = () => (
) const stopPropagation = (event: SyntheticEvent) => event.stopPropagation() return (