diff --git a/src/components/construction/Slider/Slider.module.scss b/src/components/construction/Slider/Slider.module.scss new file mode 100644 index 0000000..ee6fc87 --- /dev/null +++ b/src/components/construction/Slider/Slider.module.scss @@ -0,0 +1,104 @@ +.slider { + $color: #09c; + width: 100%; + position: relative; + padding: 10px 15px; + user-select: none; + + .labelContainer { + display: flex; + justify-content: space-between; + font-weight: bold; + } + + .sliderContainer { + position: relative; + width: 100%; + height: 30px; + cursor: pointer; + + .trackContainer { + width: 100%; + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + height: 5px; + border-radius: 5px; + cursor: pointer; + overflow: hidden; + + .tracks { + position: relative; + width: 100%; + height: 5px; + + .inactiveTrackContainer { + top: 0; + left: 0; + position: absolute; + background-color: rgba($color, 0.5); + width: 100%; + height: 5px; + } + + .activeTrackContainer { + top: 0; + left: 0; + position: absolute; + background-color: $color; + height: 5px; + } + } + + .ticksContainer { + width: 100%; + height: 5px; + z-index: 1; + position: absolute; + top: 50%; + transform: translateY(-50%); + + .ticksList { + position: relative; + width: 100%; + height: 5px; + + .tick { + background: #000; + position: absolute; + width: 2px; + height: 2px; + border-radius: 2px; + top: 50%; + transform: translateY(-50%); + + &.active { + background: #fff; + } + } + } + } + } + + .thumb { + position: absolute; + top: 50%; + left: 0; + transform: translate(-15px, -50%); + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + + .thumbPoint { + background-color: $color; + width: 20px; + height: 20px; + border-radius: 20px; + cursor: pointer; + } + } + } +} diff --git a/src/components/construction/Slider/Slider.tsx b/src/components/construction/Slider/Slider.tsx new file mode 100644 index 0000000..885e10d --- /dev/null +++ b/src/components/construction/Slider/Slider.tsx @@ -0,0 +1,127 @@ +import styles from './Slider.module.scss' +import {useEffect, useRef, useState} from "react"; +import classNames from "@/helpers/classNames"; + +interface Props { + value: number; + min: number; + max: number; + step?: number; + label?: string; + showTicks?: boolean; + displayValueFn?: (value: number) => string; + onChange?: (value: number) => void; +} + +export default function (props: Props) { + const [isMouseDown, setIsMouseDown] = useState(false); + const [value, setValue] = useState(props.value || 0); + + const thumbRef = useRef(null); + const trackContainerRef = useRef(null); + const activeTrackContainerRef = useRef(null); + const getStep = () => props.step || 1; + const getShowTicks = () => props.showTicks || false; + const getTicks = () => { + const step = getStep(); + + const ticks: [position: number, active: boolean][] = []; + for (let i = 0; i < (props.max - props.min); i += step) { + const posPercent = (i) / (props.max - props.min) * 100; + ticks.push([posPercent, i <= value]) + } + return ticks; + } + const round = (n: number, to: number): number => Math.round(n / to) * to; + const getStepFractionalDigitsCount = (): number => { + return (String(+getStep()).split('.')[1] || '').length + } + const getValue = (xPosition: number): number => { + const step = getStep(); + + const {width} = (trackContainerRef.current as HTMLElement).getBoundingClientRect(); + + const percent = (xPosition / width) * 100; + + const maxRange = props.max - props.min; + + let value = ((percent / 100) * maxRange) + props.min; + value = round(value, step).toFixed(getStepFractionalDigitsCount()) as number; + return value; + } + const getPositionByValue = (val: number): number => { + return val / (props.max - props.min) * 100; + } + + const getXMousePosition = e => { + const {clientX} = e; + const {left, width} = (trackContainerRef.current as HTMLElement).getBoundingClientRect(); + let x = clientX - left; + if (x < 0) { + x = 0 + } + if (x > width) { + x = width + } + return x; + } + const setThumbPosition = e => { + const x = getXMousePosition(e); + const val = getValue(x) + setValue(val); + const position = getPositionByValue(val) + thumbRef.current.style.left = `${position}%`; + activeTrackContainerRef.current.style.width = `${position}%`; + props.onChange?.( val) + } + const mouseDown = e => { + setIsMouseDown(true); + setThumbPosition(e); + } + const mouseUp = () => { + setIsMouseDown(false); + } + const mouseMove = e => { + if (!isMouseDown) { + return; + } + setThumbPosition(e); + } + + useEffect(() => { + const val = props.value; + setValue(val); + const position = getPositionByValue(val) + thumbRef.current.style.left = `${position}%`; + activeTrackContainerRef.current.style.width = `${position}%`; + }, [props.value]) + return ( +
+
+ {props.label || ''} + {props.displayValueFn?.(value) || value} +
+
+
+
+
+
+
+ {getShowTicks() ?
+
+ {getTicks().map(([pos, active]) => +
)} +
+
: ''} +
+
+
+
+
+
+ ) +}