150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
/* eslint-disable react-hooks/exhaustive-deps */
|
|
import clsx from "clsx";
|
|
import {
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
MouseEvent as ReactMouseEvent,
|
|
TouchEvent as ReactTouchEvent,
|
|
} from "react";
|
|
|
|
interface IMultiRangeSlider {
|
|
min: number;
|
|
max: number;
|
|
currentMin: number;
|
|
currentMax: number;
|
|
offset: number;
|
|
disabled?: boolean;
|
|
label: string;
|
|
onChange: (value: [number, number]) => void;
|
|
}
|
|
|
|
function MultiRangeSlider({
|
|
currentMax,
|
|
currentMin,
|
|
max,
|
|
min,
|
|
onChange,
|
|
offset,
|
|
label,
|
|
disabled = false,
|
|
}: IMultiRangeSlider) {
|
|
const [current, setCurrent] = useState<"min" | "max" | null>(null);
|
|
|
|
const rangeRef = useRef<HTMLDivElement>(null);
|
|
|
|
function calculateValue(clientX: number, isMin: boolean) {
|
|
if (!rangeRef.current) return;
|
|
|
|
const rect = rangeRef.current.getBoundingClientRect();
|
|
const percentage = (clientX - rect.x) / rect.width;
|
|
const newValue = min + percentage * (max - min);
|
|
|
|
return isMin
|
|
? Math.max(Math.min(newValue, currentMax - offset), min)
|
|
: Math.min(Math.max(newValue, currentMin + offset), max);
|
|
}
|
|
|
|
const [value, setValue] = useState<[number, number]>([
|
|
currentMin,
|
|
currentMax,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
setValue([currentMin, currentMax]);
|
|
}, [currentMin, currentMax]);
|
|
|
|
function handleChange(
|
|
e: MouseEvent | TouchEvent | ReactMouseEvent | ReactTouchEvent
|
|
) {
|
|
if (!current || disabled) return;
|
|
|
|
const { clientX } = "touches" in e ? e.touches[0] : e;
|
|
const newValue = calculateValue(clientX, current === "min");
|
|
|
|
if (newValue !== undefined) {
|
|
if (current === "min") setValue([newValue, currentMax]);
|
|
else setValue([currentMin, newValue]);
|
|
}
|
|
}
|
|
|
|
function handleMouseUp() {
|
|
setCurrent(null);
|
|
onChange(value.map(Math.ceil) as [number, number]);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!current) return;
|
|
document.addEventListener("mousemove", handleChange as EventListener);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
document.addEventListener("mouseleave", handleMouseUp);
|
|
document.addEventListener("touchmove", handleChange as EventListener);
|
|
document.addEventListener("touchend", handleMouseUp);
|
|
document.addEventListener("touchcancel", handleMouseUp);
|
|
return () => {
|
|
document.removeEventListener("mousemove", handleChange as EventListener);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
document.removeEventListener("mouseleave", handleMouseUp);
|
|
document.removeEventListener("touchmove", handleChange as EventListener);
|
|
document.removeEventListener("touchend", handleMouseUp);
|
|
document.removeEventListener("touchcancel", handleMouseUp);
|
|
};
|
|
}, [current, value]);
|
|
|
|
const getThumbStyle = (newValue: number) => ({
|
|
left: `${((newValue - min) / (max - min)) * 100}%`,
|
|
});
|
|
|
|
return (
|
|
<div className="2xl:space-y-[0.556vw] space-y-2">
|
|
<p className="text-s text-[#0D1922]/70">{label}</p>
|
|
<div className="bg-white/80 2xl:rounded-[0.833vw] rounded-xl relative 2xl:px-[1.111vw] 2xl:py-[0.972vw] px-4 py-3.5 flex justify-between 2xl:ring-[0.069vw] ring-1 ring-[#E2E2DC]">
|
|
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
|
|
{Intl.NumberFormat("en").format(Math.ceil(value[0]))}
|
|
</p>
|
|
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
|
|
{Intl.NumberFormat("en").format(Math.ceil(value[1]))}
|
|
</p>
|
|
<div className="absolute bottom-0 left-0 w-full 2xl:px-[1.111vw] px-4 translate-y-1/2">
|
|
<div
|
|
className="relative flex 2xl:h-[1.111vw] h-4"
|
|
ref={rangeRef}
|
|
onMouseMove={
|
|
handleChange as React.MouseEventHandler<HTMLDivElement>
|
|
}
|
|
onTouchMove={
|
|
handleChange as React.TouchEventHandler<HTMLDivElement>
|
|
}
|
|
>
|
|
<div
|
|
style={{
|
|
width: `${((value[1] - value[0]) / (max - min)) * 100}%`,
|
|
left: `${((value[0] - min) / (max - min)) * 100}%`,
|
|
}}
|
|
className={clsx(
|
|
"2xl:h-[0.139vw] h-0.5 self-center relative",
|
|
disabled ? "bg-[#D2D2D2]" : "bg-[#00BED7]"
|
|
)}
|
|
/>
|
|
{["min", "max"].map((type) => (
|
|
<div
|
|
key={type}
|
|
onMouseDown={() => setCurrent(type as "min" | "max")}
|
|
onTouchStart={() => setCurrent(type as "min" | "max")}
|
|
style={getThumbStyle(type === "min" ? value[0] : value[1])}
|
|
className={clsx(
|
|
"rounded-full 2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 absolute bottom-0 -translate-x-1/2",
|
|
current === type ? "cursor-grabbing" : "cursor-grab",
|
|
disabled ? "bg-[#D2D2D2] !cursor-default" : "bg-[#00BED7]"
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default MultiRangeSlider;
|