UNPKG

react-range

Version:

Range input. Slides in all directions.

395 lines (394 loc) 18.2 kB
"use strict"; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useThumbOverlap = exports.assertUnreachable = exports.voidFn = exports.getTrackBackground = exports.replaceAt = exports.schd = exports.translate = exports.getClosestThumbIndex = exports.translateThumbs = exports.getPaddingAndBorder = exports.getMargin = exports.checkInitialOverlap = exports.checkValuesAgainstBoundaries = exports.checkBoundaries = exports.isVertical = exports.relativeValue = exports.normalizeValue = exports.isStepDivisible = exports.isTouchEvent = exports.getStepDecimals = void 0; var react_1 = require("react"); var types_1 = require("./types"); var getStepDecimals = function (step) { var decimals = step.toString().split('.')[1]; return decimals ? decimals.length : 0; }; exports.getStepDecimals = getStepDecimals; function isTouchEvent(event) { return ((event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length)); } exports.isTouchEvent = isTouchEvent; function isStepDivisible(min, max, step) { var res = (max - min) / step; var precision = 8; var roundedRes = Number(res.toFixed(precision)); return parseInt(roundedRes.toString(), 10) === roundedRes; } exports.isStepDivisible = isStepDivisible; function normalizeValue(value, index, min, max, step, allowOverlap, values) { var BIG_NUM = 10e10; value = Math.round(value * BIG_NUM) / BIG_NUM; if (!allowOverlap) { var prev = values[index - 1]; var next = values[index + 1]; if (prev && prev > value) return prev; if (next && next < value) return next; } if (value > max) return max; if (value < min) return min; // `remainder` is a difference between the given value and a full step value // that is closest lower to the given value and is in the range between the min value // and the given value var remainder = Math.floor(value * BIG_NUM - min * BIG_NUM) % Math.floor(step * BIG_NUM); var closestLowerNum = Math.floor(value * BIG_NUM - Math.abs(remainder)); var rounded = remainder === 0 ? value : closestLowerNum / BIG_NUM; // Values with a remainder `< step/2` are rounded to the closest lower value // while values with a remainder `= > step/2` are rounded to the closest bigger value var res = Math.abs(remainder / BIG_NUM) < step / 2 ? rounded : rounded + step; var decimalPlaces = (0, exports.getStepDecimals)(step); return parseFloat(res.toFixed(decimalPlaces)); } exports.normalizeValue = normalizeValue; function relativeValue(value, min, max) { return (value - min) / (max - min); } exports.relativeValue = relativeValue; function isVertical(direction) { return direction === types_1.Direction.Up || direction === types_1.Direction.Down; } exports.isVertical = isVertical; function checkBoundaries(value, min, max) { if (min >= max) { throw new RangeError("min (".concat(min, ") is equal/bigger than max (").concat(max, ")")); } if (value < min) { throw new RangeError("value (".concat(value, ") is smaller than min (").concat(min, ")")); } if (value > max) { throw new RangeError("value (".concat(value, ") is bigger than max (").concat(max, ")")); } } exports.checkBoundaries = checkBoundaries; function checkValuesAgainstBoundaries(value, min, max) { if (value < min) { // set selectedValue to min return min; } if (value > max) { // set selectedValue to max return max; } else { return value; } } exports.checkValuesAgainstBoundaries = checkValuesAgainstBoundaries; function checkInitialOverlap(values) { if (values.length < 2) return; if (!values.slice(1).every(function (item, i) { return values[i] <= item; })) { throw new RangeError("values={[".concat(values, "]} needs to be sorted when allowOverlap={false}")); } } exports.checkInitialOverlap = checkInitialOverlap; function getMargin(element) { var style = window.getComputedStyle(element); return { top: parseInt(style['margin-top'], 10), bottom: parseInt(style['margin-bottom'], 10), left: parseInt(style['margin-left'], 10), right: parseInt(style['margin-right'], 10) }; } exports.getMargin = getMargin; function getPaddingAndBorder(element) { var style = window.getComputedStyle(element); return { top: parseInt(style['padding-top'], 10) + parseInt(style['border-top-width'], 10), bottom: parseInt(style['padding-bottom'], 10) + parseInt(style['border-bottom-width'], 10), left: parseInt(style['padding-left'], 10) + parseInt(style['border-left-width'], 10), right: parseInt(style['padding-right'], 10) + parseInt(style['border-right-width'], 10), }; } exports.getPaddingAndBorder = getPaddingAndBorder; function translateThumbs(elements, offsets, rtl) { var inverter = rtl ? -1 : 1; elements.forEach(function (element, index) { return translate(element, inverter * offsets[index].x, offsets[index].y); }); } exports.translateThumbs = translateThumbs; /** * Util function for calculating the index of the thumb that is closes to a given position * @param thumbs - array of Thumb element to calculate the distance from * @param clientX - target x position (mouse/touch) * @param clientY - target y position (mouse/touch) * @param direction - the direction of the track */ function getClosestThumbIndex(thumbs, clientX, clientY, direction) { var thumbIndex = 0; var minThumbDistance = getThumbDistance(thumbs[0], clientX, clientY, direction); for (var i = 1; i < thumbs.length; i++) { var thumbDistance = getThumbDistance(thumbs[i], clientX, clientY, direction); if (thumbDistance < minThumbDistance) { minThumbDistance = thumbDistance; thumbIndex = i; } } return thumbIndex; } exports.getClosestThumbIndex = getClosestThumbIndex; function translate(element, x, y) { element.style.transform = "translate(".concat(x, "px, ").concat(y, "px)"); } exports.translate = translate; // adapted from https://github.com/alexreardon/raf-schd var schd = function (fn) { var lastArgs = []; var frameId = null; var wrapperFn = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } lastArgs = args; if (frameId) { return; } frameId = requestAnimationFrame(function () { frameId = null; fn.apply(void 0, lastArgs); }); }; return wrapperFn; }; exports.schd = schd; function replaceAt(values, index, value) { var ret = values.slice(0); ret[index] = value; return ret; } exports.replaceAt = replaceAt; function getTrackBackground(_a) { var values = _a.values, colors = _a.colors, min = _a.min, max = _a.max, _b = _a.direction, direction = _b === void 0 ? types_1.Direction.Right : _b, _c = _a.rtl, rtl = _c === void 0 ? false : _c; if (rtl && direction === types_1.Direction.Right) { direction = types_1.Direction.Left; } else if (rtl && types_1.Direction.Left) { direction = types_1.Direction.Right; } // sort values ascending var progress = values.slice(0).sort(function (a, b) { return a - b; }).map(function (value) { return ((value - min) / (max - min)) * 100; }); var middle = progress.reduce(function (acc, point, index) { return "".concat(acc, ", ").concat(colors[index], " ").concat(point, "%, ").concat(colors[index + 1], " ").concat(point, "%"); }, ''); return "linear-gradient(".concat(direction, ", ").concat(colors[0], " 0%").concat(middle, ", ").concat(colors[colors.length - 1], " 100%)"); } exports.getTrackBackground = getTrackBackground; function voidFn() { } exports.voidFn = voidFn; function assertUnreachable(x) { throw new Error("Didn't expect to get here"); } exports.assertUnreachable = assertUnreachable; /** * Util function for grabbing the true largest width of a thumb * including the label * @param thumbEl - Thumb element to grab the largest width from * @param value - Thumb value, not label value * @param separator - Label separator value */ var getThumbWidth = function (thumbEl, value, separator, decimalPlaces, valueToLabel) { if (valueToLabel === void 0) { valueToLabel = function (value) { return value; }; } var width = Math.ceil(__spreadArray([thumbEl], Array.from(thumbEl.children), true).reduce(function (width, el) { var elWidth = Math.ceil(el.getBoundingClientRect().width); /** * If a label contains a merged label value, it won't return the true * label width for that Thumb. Clone the label and change the value * to that individual Thumb value in order to grab the true width. */ if (el.innerText && el.innerText.includes(separator) && el.childElementCount === 0) { var elClone = el.cloneNode(true); elClone.innerHTML = valueToLabel(value.toFixed(decimalPlaces)); elClone.style.visibility = 'hidden'; document.body.appendChild(elClone); elWidth = Math.ceil(elClone.getBoundingClientRect().width); document.body.removeChild(elClone); } return elWidth > width ? elWidth : width; }, thumbEl.getBoundingClientRect().width)); return width; }; /** * Bulk of logic for thumb overlaps * Consider a scenario with 5 thumbs; * Thumb 1 overlaps with thumb 0 and thumb 2 * Thumb 2 overlaps with thumb 3 * We need an array that contains [0, 1, 2, 3] * The function needs to return the directly overlapping thumbs * and all thumbs overlapping linked to those and so on * @param index - Thumb index calculating overlaps for * @param offsets - Current Array of Thumb offsets for Range * @param thumbs - Array of Thumb elements * @param values - Array of Thumb values * @param separator - String separator for merged label values * @returns overlaps - Array of all overlapping thumbs from the index */ var getOverlaps = function (index, offsets, thumbs, values, separator, decimalPlaces, valueToLabel) { if (valueToLabel === void 0) { valueToLabel = function (value) { return value; }; } var overlaps = []; /** * Recursive function for building the overlaps Array * If an overlap is found, find the overlaps for that overlap * @param thumbIndex current Thumb index to find overlaps from */ var buildOverlaps = function (thumbIndex) { var thumbXWidth = getThumbWidth(thumbs[thumbIndex], values[thumbIndex], separator, decimalPlaces, valueToLabel); var thumbX = offsets[thumbIndex].x; /** * Iterate through the Thumb offsets, if there is a match * add the thumbIndex and siblingIndex to the overlaps Array * * Then build overlaps from the overlapping siblingIndex */ offsets.forEach(function (_a, siblingIndex) { var siblingX = _a.x; var siblingWidth = getThumbWidth(thumbs[siblingIndex], values[siblingIndex], separator, decimalPlaces, valueToLabel); if (thumbIndex !== siblingIndex && ((thumbX >= siblingX && thumbX <= siblingX + siblingWidth) || (thumbX + thumbXWidth >= siblingX && thumbX + thumbXWidth <= siblingX + siblingWidth))) { if (!overlaps.includes(siblingIndex)) { overlaps.push(thumbIndex); overlaps.push(siblingIndex); overlaps = __spreadArray(__spreadArray([], overlaps, true), [thumbIndex, siblingIndex], false); buildOverlaps(siblingIndex); } } }); }; buildOverlaps(index); // Sort and remove duplicates from the built overlaps return Array.from(new Set(overlaps.sort())); }; /** * A custom React Hook for calculating whether a thumb overlaps * another and whether labels could/should merge. * @param rangeRef - React ref value of Range component * @param values - current Range values Array * @param index - thumb index * @param step - step value, used to calculate the number of decimal places * @param separator - string to separate thumb values * @returns label value + styling for thumb label */ var useThumbOverlap = function (rangeRef, values, index, step, separator, valueToLabel) { if (step === void 0) { step = 0.1; } if (separator === void 0) { separator = ' - '; } if (valueToLabel === void 0) { valueToLabel = function (value) { return value; }; } var decimalPlaces = (0, exports.getStepDecimals)(step); // Create initial label style and value. Label value defaults to thumb value var _a = (0, react_1.useState)({}), labelStyle = _a[0], setLabelStyle = _a[1]; var _b = (0, react_1.useState)(valueToLabel(values[index].toFixed(decimalPlaces))), labelValue = _b[0], setLabelValue = _b[1]; // When the rangeRef or values change, update the Thumb label values and styling (0, react_1.useEffect)(function () { if (rangeRef) { var thumbs = rangeRef.getThumbs(); if (thumbs.length < 1) return; var newStyle = {}; var offsets_1 = rangeRef.getOffsets(); /** * Get any overlaps for the given Thumb index. This must return all linked * Thumbs. So if there are 4 Thumbs and Thumbs 2, 3 and 4 overlap. If we are * getting the overlaps for Thumb 1 and it overlaps only Thumb 2, we must get * 2, 3 and 4 also. */ var overlaps = getOverlaps(index, offsets_1, thumbs, values, separator, decimalPlaces, valueToLabel); // Set a default label value of the Thumb value var labelValue_1 = valueToLabel(values[index].toFixed(decimalPlaces)); /** * If there are overlaps for the Thumb, we need to calculate the correct * Label value along with the relevant styling. We only want to show a Label * for the left most Thumb in an overlapping set. * All other Thumbs will be set to display: none. */ if (overlaps.length) { /** * Get an Array of the offsets for the overlapping Thumbs * This is so we can determine if the Thumb we are looking at * is the left most thumb in an overlapping set */ var offsetsX = overlaps.reduce(function (a, c, i, s) { return a.length ? __spreadArray(__spreadArray([], a, true), [offsets_1[s[i]].x], false) : [offsets_1[s[i]].x]; }, []); /** * If our Thumb is the left most Thumb, we can build a Label value * and set the style for that Label */ if (Math.min.apply(Math, offsetsX) === offsets_1[index].x) { /** * First calculate the Label value. To do this, * grab all the values for the Thumbs in our overlaps. * Then convert that to a Set and sort it whilst removing duplicates. */ var labelValues_1 = []; overlaps.forEach(function (thumb) { labelValues_1.push(values[thumb].toFixed(decimalPlaces)); }); /** * Update the labelValue with the resulting Array * joined by our defined separator */ labelValue_1 = Array.from(new Set(labelValues_1.sort(function (a, b) { return parseFloat(a) - parseFloat(b); }))) .map(valueToLabel) .join(separator); /** * Lastly, build the label styling. The label styling will * position the label and apply a transform so that it's centered. * We want the center point between the left edge of the left most Thumb * and the right edge of the right most Thumb. */ var first = Math.min.apply(Math, offsetsX); var last = Math.max.apply(Math, offsetsX); var lastWidth = thumbs[overlaps[offsetsX.indexOf(last)]].getBoundingClientRect().width; newStyle.left = "".concat(Math.abs(first - (last + lastWidth)) / 2, "px"); newStyle.transform = 'translate(-50%, 0)'; } else { // If the Thumb isn't the left most Thumb, hide the Label! newStyle.visibility = 'hidden'; } } // Update the label value and style setLabelValue(labelValue_1); setLabelStyle(newStyle); } }, [rangeRef, values]); return [labelValue, labelStyle]; }; exports.useThumbOverlap = useThumbOverlap; /** * Util function for calculating the distance of the center of a thumb * form a given mouse/touch target's position * @param thumbEl - Thumb element to calculate the distance from * @param clientX - target x position (mouse/touch) * @param clientY - target y position (mouse/touch) * @param direction - the direction of the track */ function getThumbDistance(thumbEl, clientX, clientY, direction) { var _a = thumbEl.getBoundingClientRect(), left = _a.left, top = _a.top, width = _a.width, height = _a.height; return isVertical(direction) ? Math.abs(clientY - (top + height / 2)) : Math.abs(clientX - (left + width / 2)); }