Ranges
Double Range Slider with floating tooltips
23 Sep 2022
Double Range Slider with floating tooltips.
HTML
SCSS
JS
<div class="double-range-container double-range-tooltips">
<div
id="double-range-tooltips"
class="double-range"
data-min="0"
data-max="5000"
data-unit="$"
data-step="10"
></div>
<input
class="double-range-hidden-input"
id="double-range-value-min"
value="200"
type="hidden"
/>
<input
class="double-range-hidden-input"
id="double-range-value-max"
value="2500"
type="hidden"
/>
</div>
.double-range {
padding-right: 15px;
margin-bottom: 80px;
height: 6px;
border-radius: 12px;
border: none;
box-shadow: none;
background: lightgrey;
.noUi-tooltip {
transform: translate(-50%, 100%);
left: 50%;
bottom: -10px;
display: block !important;
background: lightgrey;
font-size: 15px;
font-weight: 400;
font-family: Arial, Helvetica, sans-serif;
color: black;
line-height: 1.2;
padding: 11px 12px;
border: none;
border-radius: 4px;
&.hidden {
display: none !important;
}
}
.noUi-connect {
background: orange;
margin-right: -4px;
}
.noUi-handle {
background: orange;
width: 18px;
height: 18px;
border: 2px solid white;
cursor: pointer;
border-radius: 50%;
box-shadow: none;
&:before,
&:after {
content: none;
}
}
}
const doubleRange = document.getElementsByClassName(
"double-range-tooltips"
)[0];
if (doubleRange) {
const slider = doubleRange.querySelector("#double-range-tooltips");
const max = +slider.dataset.max;
const min = +slider.dataset.min;
const unit = slider.dataset?.unit || "£";
const step = +slider.dataset.step;
const inputsHidden = doubleRange.querySelectorAll(
".double-range-hidden-input"
);
const startValueMin = +inputsHidden[0].value;
const startValueMax = +inputsHidden[1].value;
// how many percentages limit from borders ???? is 8%
const borderLimit = 8;
// each step is go backward for this amount of % ???? is 5%
const borderDiff = 40 / borderLimit;
noUiSlider.create(slider, {
start: [startValueMin, startValueMax],
connect: true,
tooltips: true,
margin: 10,
step: step,
range: {
min: min,
max: max,
},
});
const tooltipsArr = slider.querySelectorAll(".noUi-tooltip");
const circlesArr = slider.querySelectorAll(".noUi-origin");
function returnTransform(element) {
return element
.replace(/[^0-9][^\d.]/g, "")
.replace(")", "")
.split(" ")
.map((str) => {
return Number(str);
});
}
function needToMerge() {
let tooltipOnePosition = tooltipsArr[0].getBoundingClientRect();
let tooltipTwoPosition = tooltipsArr[1].getBoundingClientRect();
if (
tooltipsArr[0].classList.contains("hidden") ||
tooltipsArr[1].classList.contains("hidden")
) {
return true;
}
return (
tooltipOnePosition.left +
tooltipOnePosition.width -
tooltipTwoPosition.left >
0
);
}
function resetTooltips(values) {
mergeDiff = null;
tooltipsArr.forEach((elem, index) => {
elem.textContent =
unit + Math.round(values[index]).toLocaleString("en-GB");
elem.classList.remove("hidden");
});
}
let trueTooltip = false;
let mergeDiff = null;
slider.noUiSlider.on("update", function (values, handle) {
// translate of noUISlider -> 0% is start, 100% is end
let translate = returnTransform(circlesArr[handle].style.transform)[0];
// min value of double range slider
let valueMin = returnTransform(circlesArr[0].style.transform)[0];
// max value of double range slider
let valueMax = returnTransform(circlesArr[1].style.transform)[0];
// difference between min and max value of double range slider
let difference = valueMax - valueMin;
inputsHidden[handle].value = Math.round(values[handle]);
// if tooltips are close to each other
if (needToMerge()) {
if (
!tooltipsArr[+!handle].classList.contains("hidden") &&
!tooltipsArr[handle].classList.contains("hidden")
) {
trueTooltip = handle;
mergeDiff = difference;
tooltipsArr[+!handle].classList.add("hidden");
}
if (trueTooltip) {
// limit left merged tooltip from overflowing
// borderLimit * 3 === need for 3 times faster limitation because of merged tooltip
if (translate <= -100 + borderLimit * 3) {
tooltipsArr[trueTooltip].style.transform = `translate(${
-50 -
difference +
(Math.abs(translate + 100 - borderLimit * 3) * borderDiff) / 2.5
}%,100%)`;
} else {
// position of tooltip in the middle of range
tooltipsArr[trueTooltip].style.transform = `translate(${
-50 - difference
}%,100%)`;
}
} else {
// if left tooltip is grabbed
if (translate >= -borderLimit * 4) {
// limit right merged tooltip from overflowing
tooltipsArr[trueTooltip].style.transform = `translate(${
-50 +
difference -
(Math.abs(translate + borderLimit * 3) * borderDiff) / 2.5
}%,100%)`;
} else {
tooltipsArr[trueTooltip].style.transform = `translate(${
-50 + difference
}%,100%)`;
}
}
tooltipsArr[trueTooltip].textContent = `${
unit + Math.round(values[0]).toLocaleString("en-GB")
} - ${unit + Math.round(values[1]).toLocaleString("en-GB")}`;
if (mergeDiff - difference < 0) {
mergeDiff = null;
resetTooltips(values);
}
} else {
// limit left solo tooltip from overflowing
if (translate <= -100 + borderLimit) {
tooltipsArr[0].style.transform = `translate(${
-50 + Math.abs(translate + 100 - borderLimit) * borderDiff
}%,100%)`;
} else if (translate >= -borderLimit) {
// limit right solo tooltip from overflowing
tooltipsArr[1].style.transform = `translate(${
-50 - Math.abs(translate + borderLimit) * borderDiff
}%,100%)`;
} else {
// position of tooltip in the middle of range
tooltipsArr[handle].style.transform = "translate(-50%,100%)";
}
tooltipsArr[handle].textContent = `${
unit + Math.round(values[handle]).toLocaleString("en-GB")
}`;
}
});
}
На базі плагіна noUiSlider.
В data-атрибути вносяться мінімальне, максимальне значення, валюта, крок (на скільки в одному кроці буде мінятися значення).
В data-unit вноситься валюта, якщо потрібен фунт (£), то можна просто видалити цей атрибут, дефолтне значення - фунт.
Є два сховані інпути. Один для максимального значення, другий для мінімального. Туди запусуються значення яке вибирається в слайдері.