Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overlapping range inputs. On click change input with closest value

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" />
like image 287
ditto Avatar asked Jun 11 '16 22:06

ditto


1 Answers

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:

  • I only tested in Chrome. IE might have some trouble based on how I replicated the event. It may use a mechanism other than dispatchEvent... like fireEvent or something.
  • Initially I coded it assuming that the "ghost" element always kept track of the high range. I've since updated things to invert the event dispatching when the ghost element has the lower value--but I sped through it.
like image 110
TheMadDeveloper Avatar answered Nov 14 '22 22:11

TheMadDeveloper