diff options
author | bobzel <zzzman@gmail.com> | 2025-03-10 16:13:04 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-03-10 16:13:04 -0400 |
commit | b7989dded8bb001876de6cbca59bf77935f0daf7 (patch) | |
tree | 0dba0665674db7bb84770833df0a4100d0520701 /packages/components/src/components/Slider/Slider.tsx | |
parent | 4979415d4604d280e81a162bf9a9d39c731d3738 (diff) | |
parent | 5bf944035c0ba94ad15245416f51ca0329a51bde (diff) |
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'packages/components/src/components/Slider/Slider.tsx')
-rw-r--r-- | packages/components/src/components/Slider/Slider.tsx | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/packages/components/src/components/Slider/Slider.tsx b/packages/components/src/components/Slider/Slider.tsx new file mode 100644 index 000000000..5af945383 --- /dev/null +++ b/packages/components/src/components/Slider/Slider.tsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import { Colors, getFontSize, getHeight, Size, getFormLabelSize, isDark, INumberProps } from '../../global'; +import './Slider.scss'; + +export interface ISliderProps extends INumberProps { + multithumb: boolean; + autorangeMinVal?: number; // minimimum value that min can have when autoranging + autorangeMinSize?: number; // minimum difference between min and max when autoranging + autorange?: number; // automatically adjust min/max to be +/- autorange/2 around the current value when the thumb is 15% from the min/max, or when the multithumbs are within 20% of the range and the range is bigger than autorange + endNumber?: number; + setEndNumber?: (newVal: number) => void; + setFinalNumber?: (newVal: number) => void; + setFinalEndNumber?: (newVal: number) => void; + decimals?: number; + step?: number; + minDiff?: number; +} + +let lastVal = 0; // bcz: WHY do I have to do this?? the pointerdown event locks in the value of 'valLoc' when it's created so need some other way to get the current value to that old handler... +let lastEndVal = 0; + +export const Slider = (props: ISliderProps) => { + const [width, setWidth] = useState<number>(100); + const [valLoc, setNumberLoc] = useState<number>(props.number ?? props.min + (props.max - props.min) / 2); + const [endNumberLoc, setEndNumberLoc] = useState<number>(props.endNumber ?? props.min + (2 * (props.max - props.min)) / 3); + const [min, setMin] = useState<number>(props.min); + const [max, setMax] = useState<number>(props.max); + const { + formLabel, + formLabelPlacement, + multithumb, + autorange, + autorangeMinVal, + autorangeMinSize, + decimals, + step = 1, + number = valLoc, + endNumber = endNumberLoc, + minDiff = (max - min) / 20, + size = Size.SMALL, + height, + unit, + onPointerDown, + setNumber, + setEndNumber, + setFinalNumber, + setFinalEndNumber, + color = Colors.MEDIUM_BLUE, + fillWidth, + } = props; + + const toDecimal = (num: number) => (decimals !== undefined ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : num); + + const getLeftPos = (locVal: number) => { + const dragger = getHeight(+(height || 0), size); + return ((locVal - min) / (max - min)) * (width - dragger); + }; + + const getValueLabel = (locVal: number): JSX.Element => { + return ( + <div + className="rs-label-container" + style={{ + left: `${getLeftPos(locVal)}px`, + background: color, + color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, + fontSize: getFontSize(size), + height: getHeight(+(height || 0), size), + width: getHeight(+(height || 0), size), + top: 0, + }}> + <span className="rs-label">{toDecimal(locVal)}</span> + </div> + ); + }; + const checkAutorange = () => { + if (autorange) { + const minval = multithumb ? Math.min(lastVal, lastEndVal) : lastVal; + const maxval = multithumb ? Math.max(lastVal, lastEndVal) : lastVal; + const autosize = Math.max(autorangeMinSize ?? 0, autorange ?? maxval - minval) / 2; + if (Math.abs((minval - min) / (max - min)) < 0.15 || Math.abs((max - maxval) / (max - min)) < 0.15 || (multithumb && maxval - minval < (max - min) / 5 && autosize < max - min)) { + const newminval = autorangeMinVal !== undefined && minval - autosize < autorangeMinVal ? autorangeMinVal : minval - autosize; + setMin(newminval); + setMax(newminval !== minval ? Math.max(maxval + autosize, newminval + autosize) : maxval + autosize); + } + } + }; + + const valSlider = (which: string, val: number, onchange: (val: number) => void, setFinal: () => void) => { + const valPointerup = () => { + document.removeEventListener('pointerup', valPointerup, true); + setFinal(); + checkAutorange(); + }; + return ( + <div key={which} className={`range-slider ${size}`}> + {getValueLabel(val)} + <input + className={`rs-range ${size}`} + type="range" + color={color} + min={min} + max={max} + step={step} + value={val} + onPointerDown={() => document.addEventListener('pointerup', valPointerup, true)} + onChange={e => { + onchange(+e.target.value); + e.stopPropagation(); + }} + /> + </div> + ); + }; + const onchange = (val: number) => { + // eslint-disable-next-line no-param-reassign + if (autorangeMinVal && val < autorangeMinVal) val = autorangeMinVal; + setNumber?.((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val))); + setNumberLoc((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val))); + }; + const onendchange = (val: number) => { + setEndNumber?.((lastEndVal = Math.max(number + (minDiff ?? 0), val))); + setEndNumberLoc((lastEndVal = Math.max(number + (minDiff ?? 0), val))); + }; + const ValSlider: (JSX.Element | null)[] = [!multithumb ? null : valSlider('end', endNumberLoc, onendchange, () => setFinalEndNumber?.(lastEndVal)), valSlider('start', valLoc, onchange, () => setFinalNumber?.(lastVal))]; + + const slider = ( + <div + className="slider-wrapper" + onPointerEnter={() => { + lastVal = valLoc; + lastEndVal = endNumberLoc; + }} + style={{ + padding: `5px 0px ${getHeight(+(height || 0), size)}px 0px`, + width: fillWidth ? '100%' : 'fit-content', + }}> + <div + className="slider-container" + ref={r => { + r && new ResizeObserver(() => setWidth(+(r?.clientWidth ?? 100))).observe(r); + setWidth(+(r?.clientWidth ?? 100)); + }} + style={{ height: getHeight(+(height || 0), size) }} + onPointerDown={onPointerDown}> + {ValSlider} + <div + className="selected-range" + style={{ + height: getHeight(+(height || 0), size) / 10, + background: multithumb ? Colors.LIGHT_GRAY : color, + }} + /> + <div + className="range" + style={{ + height: getHeight(+(height || 0), size) / 10, + width: getLeftPos(endNumber) - getLeftPos(number), + left: getLeftPos(number) + getHeight(+(height || 0), size), + display: multithumb ? undefined : 'none', + background: color, + }} + /> + <div className="box-minmax" style={{ fontSize: getFontSize(size), color }}> + <span> + {toDecimal(min)} + {unit} + </span> + <span> + {toDecimal(max)} + {unit} + </span> + </div> + </div> + </div> + ); + + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className="formLabel" style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + {slider} + </div> + ) : ( + slider + ); +}; |