@re-flex/ui
Version:
Re-Flex ui library
255 lines (254 loc) • 9.56 kB
JavaScript
import React from "react";
const getBoundingClientRect = (element) => {
const rect = element.getBoundingClientRect();
return {
left: Math.ceil(rect.left),
top: Math.ceil(rect.bottom),
width: Math.ceil(rect.width),
height: Math.ceil(rect.height),
};
};
const sortNumList = (arr) => [...arr].sort((a, b) => Number(a) - Number(b));
const useGetLatest = (val) => {
const ref = React.useRef(val);
ref.current = val;
return React.useCallback(() => ref.current, []);
};
const linearInterpolator = {
getPercentageForValue: (val, min, max) => {
return Math.max(0, Math.min(100, ((val - min) / (max - min)) * 100));
},
getValueForClient: (client, trackDims, min, max, mode) => {
const { left, width, top, height } = trackDims;
const percentageValue = mode === "vertical" ? (top - client) / height : (client - left) / width;
const value = (max - min) * percentageValue;
return value + min;
},
};
function useRanger({ trackElRef, interpolator = linearInterpolator, tickSize = 10, values, min, max, ticks: controlledTicks, steps, onChange, onDrag, stepSize, mode, }) {
const [activeHandleIndex, setActiveHandleIndex] = React.useState();
const [tempValues, setTempValues] = React.useState();
const getLatest = useGetLatest({
activeHandleIndex,
onChange,
onDrag,
values,
tempValues,
});
const axis = mode === "vertical" ? "clientY" : "clientX";
const getValueForClient = React.useCallback((client) => {
if (!trackElRef.current)
return;
const trackDims = getBoundingClientRect(trackElRef.current);
return interpolator.getValueForClient(client, trackDims, min, max, mode);
}, [interpolator, max, min, trackElRef]);
const getNextStep = React.useCallback((val, direction) => {
if (steps) {
let currIndex = steps.indexOf(val);
let nextIndex = currIndex + direction;
if (nextIndex >= 0 && nextIndex < steps.length) {
return steps[nextIndex];
}
else {
return val;
}
}
else {
let nextVal = val + stepSize * direction;
if (nextVal >= min && nextVal <= max) {
return nextVal;
}
else {
return val;
}
}
}, [max, min, stepSize, steps]);
const roundToStep = React.useCallback((val) => {
let left = min;
let right = max;
if (steps) {
steps.forEach((step) => {
if (step <= val && step > left) {
left = step;
}
if (step >= val && step < right) {
right = step;
}
});
}
else {
while (left < val && left + stepSize < val) {
left += stepSize;
}
right = Math.min(left + stepSize, max);
}
if (val - left < right - val) {
return left;
}
return right;
}, [max, min, stepSize, steps]);
const handleDrag = React.useCallback((e) => {
const { activeHandleIndex, onDrag } = getLatest();
let client = 0;
if (e instanceof TouchEvent) {
client = e.changedTouches[0][axis];
}
else {
client = e[axis];
}
const newValue = getValueForClient(client);
const newRoundedValue = roundToStep(newValue);
const newValues = [
...values.slice(0, activeHandleIndex),
newRoundedValue,
...values.slice(activeHandleIndex + 1),
];
if (onDrag) {
onDrag(newValues);
}
else {
setTempValues(newValues);
}
}, [getLatest, getValueForClient, roundToStep, values]);
const handleKeyDown = React.useCallback((e, i) => {
const { values, onChange = (e) => { } } = getLatest();
if (e.keyCode === 37 || e.keyCode === 39) {
setActiveHandleIndex(i);
const direction = e.keyCode === 37 ? -1 : 1;
const newValue = getNextStep(values[i], direction);
const newValues = [
...values.slice(0, i),
newValue,
...values.slice(i + 1),
];
const sortedValues = sortNumList(newValues);
onChange(sortedValues);
}
}, [getLatest, getNextStep]);
const handlePress = React.useCallback((e, i) => {
setActiveHandleIndex(i);
const handleRelease = () => {
const { tempValues, values, onChange = (e) => { }, onDrag = () => { }, } = getLatest();
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("touchmove", handleDrag);
document.removeEventListener("mouseup", handleRelease);
document.removeEventListener("touchend", handleRelease);
const sortedValues = sortNumList(tempValues || values);
onChange(sortedValues);
onDrag(sortedValues);
setActiveHandleIndex(null);
setTempValues(undefined);
};
document.addEventListener("mousemove", handleDrag);
document.addEventListener("touchmove", handleDrag);
document.addEventListener("mouseup", handleRelease);
document.addEventListener("touchend", handleRelease);
}, [getLatest, handleDrag]);
const getPercentageForValue = React.useCallback((val) => interpolator.getPercentageForValue(val, min, max), [interpolator, max, min]);
const ticks = React.useMemo(() => {
let ticks = controlledTicks || steps;
if (!ticks) {
ticks = [min];
while (ticks[ticks.length - 1] < max - tickSize) {
ticks.push(ticks[ticks.length - 1] + tickSize);
}
ticks.push(max);
}
return ticks.map((value, i) => ({
value,
getTickProps: ({ key = i, style = {}, ...rest } = {}) => ({
key,
style: {
[mode === "vertical" ? "bottom" : "left"]: `${getPercentageForValue(value)}%`,
...style,
},
...rest,
}),
}));
}, [controlledTicks, getPercentageForValue, max, min, steps, tickSize, ,]);
const segments = React.useMemo(() => {
const sortedValues = sortNumList(tempValues || values);
return [...sortedValues, max].map((value, i) => ({
value,
getSegmentProps: ({ key = i, style = {}, ...rest } = {}) => {
const from = getPercentageForValue(sortedValues[i - 1] ? sortedValues[i - 1] : min);
const to = getPercentageForValue(value) - from;
return {
key,
style: {
[mode === "vertical" ? "bottom" : "left"]: `${from}%`,
[mode === "vertical" ? "height" : "width"]: `${to}%`,
...style,
},
...rest,
};
},
}));
}, [getPercentageForValue, max, min, tempValues, values]);
const handles = React.useMemo(() => (tempValues || values).map((value, i) => ({
value,
active: i === activeHandleIndex,
getHandleProps: ({ key = i, onKeyDown, onMouseDown, onTouchStart, } = {}) => ({
key,
onKeyDown: (e) => {
e.persist();
handleKeyDown(e, i);
if (onKeyDown)
onKeyDown(e);
},
onMouseDown: (e) => {
e.persist();
handlePress(e, i);
if (onMouseDown)
onMouseDown(e);
},
onTouchStart: (e) => {
e.persist();
handlePress(e, i);
if (onTouchStart)
onTouchStart(e);
},
style: {
[mode === "vertical" ? "left" : "bottom"]: "50%",
[mode === "vertical" ? "bottom" : "left"]: `${getPercentageForValue(value)}%`,
zIndex: i === activeHandleIndex ? "1" : "0",
},
}),
})), [
activeHandleIndex,
getPercentageForValue,
handleKeyDown,
handlePress,
min,
max,
tempValues,
values,
]);
const onClickTrack = (event) => {
if (event.currentTarget.getAttribute("role") === "button" ||
event.currentTarget.classList.value.includes("reflex-slider-thumb"))
return;
event.persist();
const client = event.type === "touchmove" ? event.changedTouches[0][axis] : event[axis];
const trackDims = getBoundingClientRect(trackElRef.current);
const value = interpolator.getValueForClient(client, trackDims, min, max, mode);
if (values.length === 2) {
const [findChangingValueIndex, preValue] = values
.map((val, i) => [i, Math.abs(value - val)])
.sort((a, b) => (a[1] < b[1] ? -1 : 1))[0];
values[findChangingValueIndex] = value;
onChange(values);
}
else {
onChange([value]);
}
};
return {
activeHandleIndex,
ticks,
segments,
handles,
onClickTrack,
};
}
export default useRanger;