mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-02-20 17:52:45 +08:00
177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
import { useRef, useState, useMemo, useCallback, useEffect } from 'react'
|
|
import { cx } from '@emotion/css'
|
|
|
|
const Slider = ({
|
|
value,
|
|
min,
|
|
max,
|
|
onChange,
|
|
onlyCallOnChangeAfterDragEnded = false,
|
|
orientation = 'horizontal',
|
|
alwaysShowThumb = false,
|
|
}: {
|
|
value: number
|
|
min: number
|
|
max: number
|
|
onChange: (value: number) => void
|
|
onlyCallOnChangeAfterDragEnded?: boolean
|
|
orientation?: 'horizontal' | 'vertical'
|
|
alwaysShowTrack?: boolean
|
|
alwaysShowThumb?: boolean
|
|
}) => {
|
|
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(
|
|
(pointer: { x: number; y: number }) => {
|
|
if (!sliderRef?.current) return 0
|
|
const slider = sliderRef.current.getBoundingClientRect()
|
|
const newValue =
|
|
orientation === 'horizontal'
|
|
? ((pointer.x - slider.x) / slider.width) * max
|
|
: ((slider.height - (pointer.y - slider.y)) / slider.height) * max
|
|
if (newValue < min) return min
|
|
if (newValue > max) return max
|
|
return newValue
|
|
},
|
|
[sliderRef, max, min, orientation]
|
|
)
|
|
|
|
/**
|
|
* Handle slider click event
|
|
*/
|
|
const handleClick = useCallback(
|
|
(e: React.MouseEvent<HTMLDivElement>) =>
|
|
onChange(getNewValue({ x: e.clientX, y: e.clientY })),
|
|
[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({ x: e.clientX, y: e.clientY })
|
|
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) {
|
|
onChange(draggingValue)
|
|
}
|
|
}
|
|
document.addEventListener('pointerup', handlePointerUp)
|
|
|
|
return () => {
|
|
document.removeEventListener('pointerup', handlePointerUp)
|
|
}
|
|
}, [
|
|
isDragging,
|
|
setIsDragging,
|
|
onlyCallOnChangeAfterDragEnded,
|
|
draggingValue,
|
|
onChange,
|
|
])
|
|
|
|
/**
|
|
* Track and thumb styles
|
|
*/
|
|
const usedTrackStyle = useMemo(() => {
|
|
const percentage = `${(memoedValue / max) * 100}%`
|
|
return orientation === 'horizontal'
|
|
? { width: percentage }
|
|
: { height: percentage }
|
|
}, [max, memoedValue, orientation])
|
|
const thumbStyle = useMemo(() => {
|
|
const percentage = `${(memoedValue / max) * 100}%`
|
|
return orientation === 'horizontal'
|
|
? { left: percentage }
|
|
: { bottom: percentage }
|
|
}, [max, memoedValue, orientation])
|
|
|
|
return (
|
|
<div
|
|
className={cx(
|
|
'group relative flex items-center',
|
|
orientation === 'horizontal' && 'h-2',
|
|
orientation === 'vertical' && 'h-full w-2 flex-col'
|
|
)}
|
|
ref={sliderRef}
|
|
onClick={handleClick}
|
|
>
|
|
{/* Track */}
|
|
<div
|
|
className={cx(
|
|
'absolute overflow-hidden rounded-full bg-black/10 bg-opacity-10 dark:bg-white/10',
|
|
orientation === 'horizontal' && 'h-[3px] w-full',
|
|
orientation === 'vertical' && 'h-full w-[3px]'
|
|
)}
|
|
>
|
|
{/* Passed track */}
|
|
<div
|
|
className={cx(
|
|
'bg-black dark:bg-white',
|
|
orientation === 'horizontal' && 'h-full rounded-r-full',
|
|
orientation === 'vertical' && 'bottom-0 w-full rounded-t-full'
|
|
)}
|
|
style={usedTrackStyle}
|
|
></div>
|
|
</div>
|
|
|
|
{/* Thumb */}
|
|
<div
|
|
className={cx(
|
|
'absolute flex h-2 w-2 items-center justify-center rounded-full bg-black bg-opacity-20 transition-opacity dark:bg-white',
|
|
isDragging || alwaysShowThumb
|
|
? 'opacity-100'
|
|
: 'opacity-0 group-hover:opacity-100',
|
|
orientation === 'horizontal' && '-translate-x-1',
|
|
orientation === 'vertical' && 'translate-y-1'
|
|
)}
|
|
style={thumbStyle}
|
|
onClick={e => e.stopPropagation()}
|
|
onPointerDown={handlePointerDown}
|
|
></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Slider
|