I have two overlapping range inputs, this creates a multi range input effect.
I want it so that whenever a click is made on either of these, the input with the closest value to the newly clicked value, is changed. Not entirely sure how to go about this.
How could I do this?
(function() {
"use strict";
var supportsMultiple = self.HTMLInputElement && "valueLow" in HTMLInputElement.prototype;
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
self.multirange = function(input) {
if (supportsMultiple || input.classList.contains("multirange")) {
return;
}
var values = input.getAttribute("value").split(",");
var max = +input.max || 100;
var ghost = input.cloneNode();
input.classList.add("multirange", "original");
ghost.classList.add("multirange", "ghost");
input.value = values[0] || max / 2;
ghost.value = values[1] || max / 2;
input.parentNode.insertBefore(ghost, input.nextSibling);
Object.defineProperty(input, "originalValue", descriptor.get ? descriptor : {
// Dang you Safari >:(
get: function() {
return this.value;
},
set: function(v) {
this.value = v;
}
});
Object.defineProperties(input, {
valueLow: {
get: function() {
return Math.min(this.originalValue, ghost.value);
},
set: function(v) {
this.originalValue = v;
},
enumerable: true
},
valueHigh: {
get: function() {
return Math.max(this.originalValue, ghost.value);
},
set: function(v) {
ghost.value = v;
},
enumerable: true
}
});
if (descriptor.get) {
// Again, fuck you Safari
Object.defineProperty(input, "value", {
get: function() {
return this.valueLow + "," + this.valueHigh;
},
set: function(v) {
var values = v.split(",");
this.valueLow = values[0];
this.valueHigh = values[1];
},
enumerable: true
});
}
function update() {
ghost.style.setProperty("--low", input.valueLow * 100 / max + 1 + "%");
ghost.style.setProperty("--high", input.valueHigh * 100 / max - 1 + "%");
}
input.addEventListener("input", update);
ghost.addEventListener("input", update);
update();
}
multirange.init = function() {
Array.from(document.querySelectorAll("input[type=range][multiple]:not(.multirange)")).forEach(multirange);
}
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", multirange.init);
} else {
multirange.init();
}
})();
@supports (--css: variables) {
input[type="range"].multirange {
-webkit-appearance: none;
padding: 0;
margin: 0;
display: inline-block;
vertical-align: top;
width: 250px;
margin-top: 50px;
margin-left: 50px;
background: lightblue;
}
input[type="range"].multirange.original {
position: absolute;
}
input[type="range"].multirange.original::-webkit-slider-thumb {
position: relative;
z-index: 2;
}
input[type="range"].multirange.original::-moz-range-thumb {
transform: scale(1);
/* FF doesn't apply position it seems */
G z-index: 1;
}
input[type="range"].multirange::-moz-range-track {
border-color: transparent;
/* needed to switch FF to "styleable" control */
}
input[type="range"].multirange.ghost {
position: relative;
background: var(--track-background);
--track-background: linear-gradient(to right, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 45% / 100% 40%;
--range-color: hsl(190, 80%, 40%);
}
input[type="range"].multirange.ghost::-webkit-slider-runnable-track {
background: var(--track-background);
}
input[type="range"].multirange.ghost::-moz-range-track {
background: var(--track-background);
}
}
<input type="range" multiple value="10,80" />
You'll have to capture a mouse event on the element and calculate how close it is to the high marker vs. the low marker and decide which one to update based on that. Also, because these are two stacked input elements, you'll probably have to pass the event to the low range input manually.
Here's my go at creating such a function:
function passClick(evt) {
// Are the ghost and input elements inverted? (ghost is lower range)
var isInverted = input.valueLow == ghost.value;
// Find the horizontal position that was clicked (as a percentage of the element's width)
var clickPoint = evt.offsetX / this.offsetWidth;
// Map the percentage to a value in the range (note, assumes a min value of 0)
var clickValue = max * clickPoint;
// Get the distance to both high and low values in the range
var highDiff = Math.abs(input.valueHigh - clickValue);
var lowDiff = Math.abs(input.valueLow - clickValue);
if (lowDiff < highDiff && !isInverted || (isInverted && lowDiff > highDiff)) {
// The low value is closer to the click point than the high value
// We should update the low value input
var passEvent = new MouseEvent("mousedown", {screenX: evt.screenX, clientX: evt.clientX});
// Pass a new event to the low "input" element (which is obscured by the
// higher "ghost" element, and doesn't get mouse events outside the drag handle
input.dispatchEvent(passEvent);
// The higher "ghost" element should not respond to this event
evt.preventDefault();
return false;
}
else {
console.log("move ghost");
// The high value is closer to the click point than the low value
// The default behavior is appropriate, so do nuthin
}
}
ghost.addEventListener("mousedown", passClick);
I put this code immediately above the input.addEventListener("input", update);
line in your sample, and it seems to work. See my fiddle.
Some provisos though:
dispatchEvent
... like fireEvent
or something.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With