2022-05-12 02:45:43 +08:00
|
|
|
import { useRef, useState, useMemo, useCallback, useEffect } from 'react'
|
|
|
|
import cx from 'classnames'
|
|
|
|
|
2022-03-13 14:40:38 +08:00
|
|
|
const Slider = ({
|
|
|
|
value,
|
|
|
|
min,
|
|
|
|
max,
|
|
|
|
onChange,
|
|
|
|
onlyCallOnChangeAfterDragEnded = false,
|
|
|
|
orientation = 'horizontal',
|
|
|
|
}: {
|
|
|
|
value: number
|
|
|
|
min: number
|
|
|
|
max: number
|
|
|
|
onChange: (value: number) => void
|
|
|
|
onlyCallOnChangeAfterDragEnded?: boolean
|
|
|
|
orientation?: 'horizontal' | 'vertical'
|
|
|
|
}) => {
|
|
|
|
const sliderRef = useRef<HTMLInputElement>(null)
|
|
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
|
|
const [draggingValue, setDraggingValue] = useState(value)
|
|
|
|
const memoedValue = useMemo(
|
|
|
|
() =>
|
|
|
|
isDragging && onlyCallOnChangeAfterDragEnded ? draggingValue : value,
|
|
|
|
[isDragging, draggingValue, value, onlyCallOnChangeAfterDragEnded]
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the value of the slider based on the position of the pointer
|
|
|
|
*/
|
|
|
|
const getNewValue = useCallback(
|
|
|
|
(val: number) => {
|
|
|
|
if (!sliderRef?.current) return 0
|
|
|
|
const sliderWidth = sliderRef.current.getBoundingClientRect().width
|
|
|
|
const newValue = (val / sliderWidth) * max
|
|
|
|
if (newValue < min) return min
|
|
|
|
if (newValue > max) return max
|
|
|
|
return newValue
|
|
|
|
},
|
|
|
|
[sliderRef, max, min]
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle slider click event
|
|
|
|
*/
|
|
|
|
const handleClick = useCallback(
|
|
|
|
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
|
|
onChange(getNewValue(e.clientX))
|
|
|
|
},
|
|
|
|
[getNewValue, onChange]
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle pointer down event
|
|
|
|
*/
|
|
|
|
const handlePointerDown = () => {
|
|
|
|
setIsDragging(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle pointer move events
|
|
|
|
*/
|
|
|
|
useEffect(() => {
|
|
|
|
const handlePointerMove = (e: { clientX: number; clientY: number }) => {
|
|
|
|
if (!isDragging) return
|
|
|
|
const newValue = getNewValue(e.clientX)
|
|
|
|
onlyCallOnChangeAfterDragEnded
|
|
|
|
? setDraggingValue(newValue)
|
|
|
|
: onChange(newValue)
|
|
|
|
}
|
|
|
|
document.addEventListener('pointermove', handlePointerMove)
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('pointermove', handlePointerMove)
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
isDragging,
|
|
|
|
onChange,
|
|
|
|
setDraggingValue,
|
|
|
|
onlyCallOnChangeAfterDragEnded,
|
|
|
|
getNewValue,
|
|
|
|
])
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle pointer up events
|
|
|
|
*/
|
|
|
|
useEffect(() => {
|
|
|
|
const handlePointerUp = () => {
|
|
|
|
if (!isDragging) return
|
|
|
|
setIsDragging(false)
|
|
|
|
if (onlyCallOnChangeAfterDragEnded) {
|
|
|
|
console.log('draggingValue', draggingValue)
|
|
|
|
onChange(draggingValue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
document.addEventListener('pointerup', handlePointerUp)
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('pointerup', handlePointerUp)
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
isDragging,
|
|
|
|
setIsDragging,
|
|
|
|
onlyCallOnChangeAfterDragEnded,
|
|
|
|
draggingValue,
|
|
|
|
onChange,
|
|
|
|
])
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Track and thumb styles
|
|
|
|
*/
|
|
|
|
const usedTrackStyle = useMemo(
|
|
|
|
() => ({ width: `${(memoedValue / max) * 100}%` }),
|
|
|
|
[max, memoedValue]
|
|
|
|
)
|
|
|
|
const thumbStyle = useMemo(
|
|
|
|
() => ({
|
|
|
|
left: `${(memoedValue / max) * 100}%`,
|
|
|
|
transform: `translateX(-10px)`,
|
|
|
|
}),
|
|
|
|
[max, memoedValue]
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2022-03-17 19:30:43 +08:00
|
|
|
className='group flex h-2 -translate-y-[3px] items-center'
|
2022-03-13 14:40:38 +08:00
|
|
|
ref={sliderRef}
|
|
|
|
onClick={handleClick}
|
|
|
|
>
|
|
|
|
{/* Track */}
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
|
2022-03-13 14:40:38 +08:00
|
|
|
|
|
|
|
{/* Passed track */}
|
|
|
|
<div
|
2022-05-12 02:45:43 +08:00
|
|
|
className={cx(
|
2022-03-13 14:40:38 +08:00
|
|
|
'absolute h-[2px] group-hover:bg-brand-500',
|
2022-03-23 01:21:22 +08:00
|
|
|
isDragging ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-500'
|
2022-03-13 14:40:38 +08:00
|
|
|
)}
|
|
|
|
style={usedTrackStyle}
|
|
|
|
></div>
|
|
|
|
|
|
|
|
{/* Thumb */}
|
|
|
|
<div
|
2022-05-12 02:45:43 +08:00
|
|
|
className={cx(
|
2022-03-13 14:40:38 +08:00
|
|
|
'absolute flex h-5 w-5 items-center justify-center rounded-full bg-brand-500 bg-opacity-20 transition-opacity ',
|
|
|
|
isDragging ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
|
|
)}
|
|
|
|
style={thumbStyle}
|
|
|
|
onClick={e => e.stopPropagation()}
|
|
|
|
onPointerDown={handlePointerDown}
|
|
|
|
>
|
2022-03-17 19:30:43 +08:00
|
|
|
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
|
2022-03-13 14:40:38 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Slider
|