最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Overlapping range inputs. On click change input with closest value - Stack Overflow

programmeradmin1浏览0评论

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" />

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" />

Share Improve this question edited Jun 14, 2016 at 12:26 ditto asked Jun 11, 2016 at 22:36 dittoditto 6,32710 gold badges58 silver badges91 bronze badges 6
  • Maybe I could draw your attention to this: refreshless./nouislider. Look at the example on top. I have submitted a couple of pull requests (in a branch that will at some point go stable) that enable the exact same functionality for more handles (clicking on the slider moves the handle closer to the click - even if two or three handles overlap (same position), the handle that can move will move). I am not sure whether you will consider using another library. The license is WTFPL. – xnakos Commented Jun 14, 2016 at 12:36
  • Thanks but I prefer to use native input[type=range]. – ditto Commented Jun 14, 2016 at 12:45
  • OK. Another question. In your example the left handle cannot be dragged right now, am I correct? – xnakos Commented Jun 14, 2016 at 12:49
  • Yeah, that's a bug in some browsers. I'm hoping this will fix it. – ditto Commented Jun 14, 2016 at 12:55
  • 1 Dang you safari is okay. The other safari reference in the ments is unnecessarily uncouth. Careful not to get caught out with poor ments in production code! – enhzflep Commented Jun 14, 2016 at 13:20
 |  Show 1 more ment

2 Answers 2

Reset to default 6 +100

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.

Here's something simple you could use. Although you might want to customize the style. I am altering the z-index of the slider element based upon its proximity to the cursor.
JSFiddle

HTML

<input id='a' type='range' />
<input id='b' type='range' />
<label role='info'></label>

JS

var a = document.getElementById('a');
var b = document.getElementById('b');

a.onmousemove = function(e) {
    MouseMove.call(a, e);
};
b.onmousemove = function(e) {
    MouseMove.call(b, e);
};

var MouseMove = function(eventArg) 
{

    var max = parseInt(a.max),
        min = parseInt(a.min),
        diff = max - min,
        clickPoint = eventArg.offsetX / a.offsetWidth,
        clickPointVal = parseInt(diff * clickPoint) + min;

    /* absolute distance from respective slider values */
    var da = Math.abs(a.value - clickPointVal),
        db = Math.abs(b.value - clickPointVal);

    // Making the two sliders appear above one another only when no mouse button is pressed, this condition may be removed at will
    if (!eventArg.buttons) 
    {
        if (da < db) 
        {
            a.style.zIndex = 2;
            b.style.zIndex = 1;
        } 
        else if (db < da)
        {
            b.style.zIndex = 2;
            a.style.zIndex = 1;
        }
    }
    document.querySelector('label').innerHTML = 'Red: ' + a.value + ', Green: ' + b.value + ', X: ' + eventArg.clientX;
}

CSS

input {
    margin: 0px;
    position: absolute;
    left: 0px;
}

label {
    display: inline-block;
    margin-top: 100px;
}

#a {
    z-index: 2;
}

#b {
    z-index: 1;
}
发布评论

评论列表(0)

  1. 暂无评论