HTML滑块是否支持两个输入?

166

是否可以制作一个具有两个输入值的HTML5滑块,例如选择价格范围?如果可以,该如何实现?


4
我已经为此制作了一个基于CSS的组件 - Codepen演示 - vsync
到目前为止,我相信只有一个答案是可访问的,另一个答案虽然我已经评论了它的可访问性问题,但在某种程度上也是可访问的。如果您正在阅读这篇文章,那么现在至少是2022年,我恳请您寻找一个完全可访问的解决方案(使用 ctrl+f 搜索“可访问性”)。 - maxshuty
1
增加更近期的示例,说明如何实现此操作:本地双范围滑块 - sketchyTech
11个回答

111

我已经寻找了一段时间轻量级、无依赖的双滑块(似乎为此单独导入 jQuery 很疯狂),但并没有太多选择。最终,我对 @Wildhoney 的代码 进行了一些修改,我很喜欢它。

function getVals(){
  // Get slider values
  var parent = this.parentNode;
  var slides = parent.getElementsByTagName("input");
    var slide1 = parseFloat( slides[0].value );
    var slide2 = parseFloat( slides[1].value );
  // Neither slider will clip the other, so make sure we determine which is larger
  if( slide1 > slide2 ){ var tmp = slide2; slide2 = slide1; slide1 = tmp; }
  
  var displayElement = parent.getElementsByClassName("rangeValues")[0];
      displayElement.innerHTML = slide1 + " - " + slide2;
}

window.onload = function(){
  // Initialize Sliders
  var sliderSections = document.getElementsByClassName("range-slider");
      for( var x = 0; x < sliderSections.length; x++ ){
        var sliders = sliderSections[x].getElementsByTagName("input");
        for( var y = 0; y < sliders.length; y++ ){
          if( sliders[y].type ==="range" ){
            sliders[y].oninput = getVals;
            // Manually trigger event first time to display values
            sliders[y].oninput();
          }
        }
      }
}
  section.range-slider {
    position: relative;
    width: 200px;
    height: 35px;
    text-align: center;
}

section.range-slider input {
    pointer-events: none;
    position: absolute;
    overflow: hidden;
    left: 0;
    top: 15px;
    width: 200px;
    outline: none;
    height: 18px;
    margin: 0;
    padding: 0;
}

section.range-slider input::-webkit-slider-thumb {
    pointer-events: all;
    position: relative;
    z-index: 1;
    outline: 0;
}

section.range-slider input::-moz-range-thumb {
    pointer-events: all;
    position: relative;
    z-index: 10;
    -moz-appearance: none;
    width: 9px;
}

section.range-slider input::-moz-range-track {
    position: relative;
    z-index: -1;
    background-color: rgba(0, 0, 0, 1);
    border: 0;
}
section.range-slider input:last-of-type::-moz-range-track {
    -moz-appearance: none;
    background: none transparent;
    border: 0;
}
  section.range-slider input[type=range]::-moz-focus-outer {
  border: 0;
}
<!-- This block can be reused as many times as needed -->
<section class="range-slider">
  <span class="rangeValues"></span>
  <input value="5" min="0" max="15" step="0.5" type="range">
  <input value="10" min="0" max="15" step="0.5" type="range">
</section>


很遗憾,它无法在Android 4.0.4移动Safari 4.0浏览器上运行。 :-( - erik
@erik 我没有一个好的方法来测试那些版本,但是在我的 Android 5.0 上和最新版本的 Safari 上它可以工作(Google Play 显示为 1.2,所以我对你的 4.0 感到困惑)。如果你解决了这个问题,我很乐意知道。 - Gary
1
真聪明!我想知道为什么它没有被标记为正确答案,因为它优雅地解决了问题,而且没有任何依赖。只是为了做这件事而添加jQuery显然是过度的。 - zanona
20
我非常喜欢你的方法,试图将其调整为我的用例,但当我意识到一些我拥有的设计规范(例如,将范围的“活动”部分与轨道的其余部分着色不同)无法使用此方法实现时,我不得不放弃它。因此,我寻找另一个解决方案,并找到了这个非常好的项目:http://refreshless.com/nouislider/它没有依赖关系,具有漂亮干净的API,与AMD兼容,并提供了许多选项。到目前为止,我对它感到非常满意。 - Felix Wienberg
感谢您发布此消息,这显然是使用约20行JS的最有效解决方案。我已经适应并在Chrome、Safari和iOS上成功运行,这也是我目前手头拥有的全部设备。 - SGD
显示剩余3条评论

86

48
前段时间......但是我通过将两个滑块精确叠放在一起来“伪造”一个双滑块。其中一个从最小值开始,另一个从最大值开始。我想这有点作弊,但......对我来说有效。 - frequent
9
就我个人而言,我比较喜欢ionRangeSlider,相对于jQuery UI的滑块控件:http://ionden.com/a/plugins/ion.rangeSlider/en.html - Charlotte
@frequent,我想按照你的方式模拟双滑块,但是如果我在彼此上面绘制两个元素,只有顶部的元素会接受鼠标点击。 - Ross Rogers
5
另一种方法是使用并排的输入控件“伪造”双滑块:http://www.simple.gy/blog/range-slider-two-handles/ - SimplGy
@kunambi 这些讨论似乎没有任何结果出来。:-( - Mitar
显示剩余3条评论

75

稍晚了一些,但noUiSlider避免了具有jQuery-ui依赖的问题(被接受的答案没有)。它唯一的“缺点”是仅支持IE9及以上版本,如果您需要支持旧版IE,则可能存在问题。

此外,它是免费的、开源的,可在商业项目中无限制使用。

安装方法:下载noUiSlider,将CSS和JS文件提取到站点文件系统的某个位置,然后从head链接到CSS,从body链接到JS:

<!-- In <head> -->
<link href="nouislider.min.css" rel="stylesheet">

<!-- In <body> -->
<script src="nouislider.min.js"></script>

示例用法:创建一个从0到100的滑块,并将其起始值设置为20-80。

HTML:

<div id="slider">
</div>

JS:

var slider = document.getElementById('slider');

noUiSlider.create(slider, {
    start: [20, 80],
    connect: true,
    range: {
        'min': 0,
        'max': 100
    }
});

2
哇,太棒了!!这正是我们所需要的,而且它开发得非常出色!没有使用jQuery。 - DavidTaubmann
3
我没有深入研究,但我可以用键盘来操作滑块,所以我认为现在已经实现了@ptrin :) - Edu Ruiz
很好。我在添加来自 noUISlider文档的功能,例如工具提示 时遇到了问题,添加 tooltips: [true, true] 没有任何作用。有什么想法吗?我们需要做的是链接到 nouislider.min.css 和 nouislider.min.js还是需要做更多的操作? - for_all_intensive_purposes
1
@ChrisDixon(抱歉,刚看到评论)应该足够了,也许可以尝试调试工具提示并确认它们是否已损坏。如果您能修复它们,就可以向项目发送PR ;) - dario_ramos
谢谢。对于那些使用vuejs的人来说,http://veeno.surge.sh基于nouislider,也非常好用。 - italo.portinho
显示剩余4条评论

36
当然,您可以简单地使用两个重叠的滑块,并添加一些 JavaScript(实际上不超过 5 行),以确保选择器不超过最小/最大值(就像 @Garys 的解决方案一样)。
下面附上一个简短的代码片段,包括一些 CSS3 样式以展示您可以做什么(仅在webkit中可用)。我还添加了一些标签来显示所选值。
它使用 JQuery,但 Vanillajs 版本也不是魔法。
@更新:以下代码仅为概念证明。由于有很多请求,我已经添加了 Mozilla Firefox 的可能解决方案(不更改原始代码)。在使用之前,您可能需要重构下面的代码。

    (function() {

        function addSeparator(nStr) {
            nStr += '';
            var x = nStr.split('.');
            var x1 = x[0];
            var x2 = x.length > 1 ? '.' + x[1] : '';
            var rgx = /(\d+)(\d{3})/;
            while (rgx.test(x1)) {
                x1 = x1.replace(rgx, '$1' + '.' + '$2');
            }
            return x1 + x2;
        }

        function rangeInputChangeEventHandler(e){
            var rangeGroup = $(this).attr('name'),
                minBtn = $(this).parent().children('.min'),
                maxBtn = $(this).parent().children('.max'),
                range_min = $(this).parent().children('.range_min'),
                range_max = $(this).parent().children('.range_max'),
                minVal = parseInt($(minBtn).val()),
                maxVal = parseInt($(maxBtn).val()),
                origin = $(this).context.className;

            if(origin === 'min' && minVal > maxVal-5){
                $(minBtn).val(maxVal-5);
            }
            var minVal = parseInt($(minBtn).val());
            $(range_min).html(addSeparator(minVal*1000) + ' €');


            if(origin === 'max' && maxVal-5 < minVal){
                $(maxBtn).val(5+ minVal);
            }
            var maxVal = parseInt($(maxBtn).val());
            $(range_max).html(addSeparator(maxVal*1000) + ' €');
        }

     $('input[type="range"]').on( 'input', rangeInputChangeEventHandler);
})();
body{
font-family: sans-serif;
font-size:14px;
}
input[type='range'] {
  width: 210px;
  height: 30px;
  overflow: hidden;
  cursor: pointer;
    outline: none;
}
input[type='range'],
input[type='range']::-webkit-slider-runnable-track,
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
    background: none;
}
input[type='range']::-webkit-slider-runnable-track {
  width: 200px;
  height: 1px;
  background: #003D7C;
}

input[type='range']:nth-child(2)::-webkit-slider-runnable-track{
  background: none;
}

input[type='range']::-webkit-slider-thumb {
  position: relative;
  height: 15px;
  width: 15px;
  margin-top: -7px;
  background: #fff;
  border: 1px solid #003D7C;
  border-radius: 25px;
  z-index: 1;
}


input[type='range']:nth-child(1)::-webkit-slider-thumb{
  z-index: 2;
}

.rangeslider{
    position: relative;
    height: 60px;
    width: 210px;
    display: inline-block;
    margin-top: -5px;
    margin-left: 20px;
}
.rangeslider input{
    position: absolute;
}
.rangeslider{
    position: absolute;
}

.rangeslider span{
    position: absolute;
    margin-top: 30px;
    left: 0;
}

.rangeslider .right{
   position: relative;
   float: right;
   margin-right: -5px;
}


/* Proof of concept for Firefox */
@-moz-document url-prefix() {
  .rangeslider::before{
    content:'';
    width:100%;
    height:2px;
    background: #003D7C;
    display:block;
    position: relative;
    top:16px;
  }

  input[type='range']:nth-child(1){
    position:absolute;
    top:35px !important;
    overflow:visible !important;
    height:0;
  }

  input[type='range']:nth-child(2){
    position:absolute;
    top:35px !important;
    overflow:visible !important;
    height:0;
  }
input[type='range']::-moz-range-thumb {
  position: relative;
  height: 15px;
  width: 15px;
  margin-top: -7px;
  background: #fff;
  border: 1px solid #003D7C;
  border-radius: 25px;
  z-index: 1;
}

  input[type='range']:nth-child(1)::-moz-range-thumb {
      transform: translateY(-20px);    
  }
  input[type='range']:nth-child(2)::-moz-range-thumb {
      transform: translateY(-20px);    
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<div class="rangeslider">
                                <input class="min" name="range_1" type="range" min="1" max="100" value="10" />
                                <input class="max" name="range_1" type="range" min="1" max="100" value="90" />
                                <span class="range_min light left">10.000 €</span>
                                <span class="range_max light right">90.000 €</span>
                            </div>


7
看起来很不错!但是在Firefox里无法工作,只能拖动最大输入值。你修复好了吗? - Toni Michel Caubet
6
哦,我来这里是想添加几乎完全相同的内容!http://www.simple.gy/blog/range-slider-two-handles/我的版本将两个输入框放在一起,并且使用了双输入。 - SimplGy
我现在已经为Firefox添加了一个概念验证。 - user1532132
3
你好!我发现这篇文章很有用,希望能贡献出来,方便将来的读者。截至 jQuery 3.0 版本,这个方法已经不再适用了。代码中的 origin = $(this).context.className; 需要修改为 origin = e.originalEvent.target.className; 或其它等价的方式。.context 功能在 jQuery 1.10 中被废弃,在 3.0 版本中被移除。 - Austen Holland
这正是我想处理双范围滑块的方式。谢谢,节省了我很多试错时间! - Bernesto

29

我实际上是直接在 HTML 中使用了我的脚本。但是在 JavaScript 中,当您为此事件添加 oninput 事件监听器时,它会自动提供数据。您只需要根据需要分配值即可。

[slider] {
  width: 300px;
  position: relative;
  height: 5px;
  margin: 45px 0 10px 0;
}

[slider] > div {
  position: absolute;
  left: 13px;
  right: 15px;
  height: 5px;
}
[slider] > div > [inverse-left] {
  position: absolute;
  left: 0;
  height: 5px;
  border-radius: 10px;
  background-color: #CCC;
  margin: 0 7px;
}

[slider] > div > [inverse-right] {
  position: absolute;
  right: 0;
  height: 5px;
  border-radius: 10px;
  background-color: #CCC;
  margin: 0 7px;
}


[slider] > div > [range] {
  position: absolute;
  left: 0;
  height: 5px;
  border-radius: 14px;
  background-color: #d02128;
}

[slider] > div > [thumb] {
  position: absolute;
  top: -7px;
  z-index: 2;
  height: 20px;
  width: 20px;
  text-align: left;
  margin-left: -11px;
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
  background-color: #FFF;
  border-radius: 50%;
  outline: none;
}

[slider] > input[type=range] {
  position: absolute;
  pointer-events: none;
  -webkit-appearance: none;
  z-index: 3;
  height: 14px;
  top: -2px;
  width: 100%;
  opacity: 0;
}

div[slider] > input[type=range]:focus::-webkit-slider-runnable-track {
  background: transparent;
  border: transparent;
}

div[slider] > input[type=range]:focus {
  outline: none;
}

div[slider] > input[type=range]::-webkit-slider-thumb {
  pointer-events: all;
  width: 28px;
  height: 28px;
  border-radius: 0px;
  border: 0 none;
  background: red;
  -webkit-appearance: none;
}

div[slider] > input[type=range]::-ms-fill-lower {
  background: transparent;
  border: 0 none;
}

div[slider] > input[type=range]::-ms-fill-upper {
  background: transparent;
  border: 0 none;
}

div[slider] > input[type=range]::-ms-tooltip {
  display: none;
}

[slider] > div > [sign] {
  opacity: 0;
  position: absolute;
  margin-left: -11px;
  top: -39px;
  z-index:3;
  background-color: #d02128;
  color: #fff;
  width: 28px;
  height: 28px;
  border-radius: 28px;
  -webkit-border-radius: 28px;
  align-items: center;
  -webkit-justify-content: center;
  justify-content: center;
  text-align: center;
}

[slider] > div > [sign]:after {
  position: absolute;
  content: '';
  left: 0;
  border-radius: 16px;
  top: 19px;
  border-left: 14px solid transparent;
  border-right: 14px solid transparent;
  border-top-width: 16px;
  border-top-style: solid;
  border-top-color: #d02128;
}

[slider] > div > [sign] > span {
  font-size: 12px;
  font-weight: 700;
  line-height: 28px;
}

[slider]:hover > div > [sign] {
  opacity: 1;
}
<div slider id="slider-distance">
  <div>
    <div inverse-left style="width:70%;"></div>
    <div inverse-right style="width:70%;"></div>
    <div range style="left:0%;right:0%;"></div>
    <span thumb style="left:0%;"></span>
    <span thumb style="left:100%;"></span>
    <div sign style="left:0%;">
      <span id="value">0</span>
    </div>
    <div sign style="left:100%;">
      <span id="value">100</span>
    </div>
  </div>
  <input type="range" value="0" max="100" min="0" step="1" oninput="
  this.value=Math.min(this.value,this.parentNode.childNodes[5].value-1);
  let value = (this.value/parseInt(this.max))*100
  var children = this.parentNode.childNodes[1].childNodes;
  children[1].style.width=value+'%';
  children[5].style.left=value+'%';
  children[7].style.left=value+'%';children[11].style.left=value+'%';
  children[11].childNodes[1].innerHTML=this.value;" />

  <input type="range" value="100" max="100" min="0" step="1" oninput="
  this.value=Math.max(this.value,this.parentNode.childNodes[3].value-(-1));
  let value = (this.value/parseInt(this.max))*100
  var children = this.parentNode.childNodes[1].childNodes;
  children[3].style.width=(100-value)+'%';
  children[5].style.right=(100-value)+'%';
  children[9].style.left=value+'%';children[13].style.left=value+'%';
  children[13].childNodes[1].innerHTML=this.value;" />
</div>


1
你真是个天才!这是一个很棒的想法,而且你的样式也很棒。 - Puspam
1
这是一个很棒的解决方案!我已经找了几天了,非常感谢你分享!! - dmboucher
我的10分就这样没了,但我不知道为什么在Mozilla中拖动时出现问题,如果我们添加input[type=range] ::-moz-range-thumb,那么在Edge中就会出现问题,有什么想法吗? - Braian Mellor

25

问题是:“是否可以制作一个带有两个输入值的HTML5滑块,例如选择价格范围?如果可以,如何实现?”

2020年,可以创建一个完全可访问的本地、非jQuery HTML5滑块,用于价格范围的双拇指。我在创建这个解决方案后发现了这篇文章,并认为在这里分享我的实现会很好。

此实现已在移动Chrome和Firefox(Android)以及Linux上进行了测试。我不确定其他平台,但它应该相当不错。我很想听听您的反馈并改进这个解决方案。

此解决方案允许在一个页面上使用多个实例,并且由两个输入组成(每个输入都有描述性标签供屏幕阅读器使用)。您可以根据网格标签数量设置拇指大小。此外,您可以使用触摸、键盘和鼠标与滑块交互。由于“on input”事件监听器,值会在调整过程中更新。

我的第一种方法是叠加滑块并对其进行裁剪。然而,这导致了复杂的代码和许多浏览器依赖项。然后我用两个“内联”的滑块重新创建了解决方案。这就是下面您将找到的解决方案。

var thumbsize = 14;

function draw(slider,splitvalue) {

    /* set function vars */
    var min = slider.querySelector('.min');
    var max = slider.querySelector('.max');
    var lower = slider.querySelector('.lower');
    var upper = slider.querySelector('.upper');
    var legend = slider.querySelector('.legend');
    var thumbsize = parseInt(slider.getAttribute('data-thumbsize'));
    var rangewidth = parseInt(slider.getAttribute('data-rangewidth'));
    var rangemin = parseInt(slider.getAttribute('data-rangemin'));
    var rangemax = parseInt(slider.getAttribute('data-rangemax'));

    /* set min and max attributes */
    min.setAttribute('max',splitvalue);
    max.setAttribute('min',splitvalue);

    /* set css */
    min.style.width = parseInt(thumbsize + ((splitvalue - rangemin)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
    max.style.width = parseInt(thumbsize + ((rangemax - splitvalue)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
    min.style.left = '0px';
    max.style.left = parseInt(min.style.width)+'px';
    min.style.top = lower.offsetHeight+'px';
    max.style.top = lower.offsetHeight+'px';
    legend.style.marginTop = min.offsetHeight+'px';
    slider.style.height = (lower.offsetHeight + min.offsetHeight + legend.offsetHeight)+'px';
    
    /* correct for 1 off at the end */
    if(max.value>(rangemax - 1)) max.setAttribute('data-value',rangemax);

    /* write value and labels */
    max.value = max.getAttribute('data-value'); 
    min.value = min.getAttribute('data-value');
    lower.innerHTML = min.getAttribute('data-value');
    upper.innerHTML = max.getAttribute('data-value');

}

function init(slider) {
    /* set function vars */
    var min = slider.querySelector('.min');
    var max = slider.querySelector('.max');
    var rangemin = parseInt(min.getAttribute('min'));
    var rangemax = parseInt(max.getAttribute('max'));
    var avgvalue = (rangemin + rangemax)/2;
    var legendnum = slider.getAttribute('data-legendnum');

    /* set data-values */
    min.setAttribute('data-value',rangemin);
    max.setAttribute('data-value',rangemax);
    
    /* set data vars */
    slider.setAttribute('data-rangemin',rangemin); 
    slider.setAttribute('data-rangemax',rangemax); 
    slider.setAttribute('data-thumbsize',thumbsize); 
    slider.setAttribute('data-rangewidth',slider.offsetWidth);

    /* write labels */
    var lower = document.createElement('span');
    var upper = document.createElement('span');
    lower.classList.add('lower','value');
    upper.classList.add('upper','value');
    lower.appendChild(document.createTextNode(rangemin));
    upper.appendChild(document.createTextNode(rangemax));
    slider.insertBefore(lower,min.previousElementSibling);
    slider.insertBefore(upper,min.previousElementSibling);
    
    /* write legend */
    var legend = document.createElement('div');
    legend.classList.add('legend');
    var legendvalues = [];
    for (var i = 0; i < legendnum; i++) {
        legendvalues[i] = document.createElement('div');
        var val = Math.round(rangemin+(i/(legendnum-1))*(rangemax - rangemin));
        legendvalues[i].appendChild(document.createTextNode(val));
        legend.appendChild(legendvalues[i]);

    } 
    slider.appendChild(legend);

    /* draw */
    draw(slider,avgvalue);

    /* events */
    min.addEventListener("input", function() {update(min);});
    max.addEventListener("input", function() {update(max);});
}

function update(el){
    /* set function vars */
    var slider = el.parentElement;
    var min = slider.querySelector('#min');
    var max = slider.querySelector('#max');
    var minvalue = Math.floor(min.value);
    var maxvalue = Math.floor(max.value);
    
    /* set inactive values before draw */
    min.setAttribute('data-value',minvalue);
    max.setAttribute('data-value',maxvalue);

    var avgvalue = (minvalue + maxvalue)/2;

    /* draw */
    draw(slider,avgvalue);
}

var sliders = document.querySelectorAll('.min-max-slider');
sliders.forEach( function(slider) {
    init(slider);
});
* {padding: 0; margin: 0;}
body {padding: 40px;}

.min-max-slider {position: relative; width: 200px; text-align: center; margin-bottom: 50px;}
.min-max-slider > label {display: none;}
span.value {height: 1.7em; font-weight: bold; display: inline-block;}
span.value.lower::before {content: "€"; display: inline-block;}
span.value.upper::before {content: "- €"; display: inline-block; margin-left: 0.4em;}
.min-max-slider > .legend {display: flex; justify-content: space-between;}
.min-max-slider > .legend > * {font-size: small; opacity: 0.25;}
.min-max-slider > input {cursor: pointer; position: absolute;}

/* webkit specific styling */
.min-max-slider > input {
  -webkit-appearance: none;
  outline: none!important;
  background: transparent;
  background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}
.min-max-slider > input::-webkit-slider-thumb {
  -webkit-appearance: none; /* Override default look */
  appearance: none;
  width: 14px; /* Set a specific slider handle width */
  height: 14px; /* Slider handle height */
  background: #eee; /* Green background */
  cursor: pointer; /* Cursor on hover */
  border: 1px solid gray;
  border-radius: 100%;
}
.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<div class="min-max-slider" data-legendnum="2">
    <label for="min">Minimum price</label>
    <input id="min" class="min" name="min" type="range" step="1" min="0" max="3000" />
    <label for="max">Maximum price</label>
    <input id="max" class="max" name="max" type="range" step="1" min="0" max="3000" />
</div>

请注意,为了防止重绘/重绘错误导致值的更改,应将步长保持为1。

在线查看:https://codepen.io/joosts/pen/rNLdxvK


1
谢谢,我真的很喜欢这个解决方案! - Svatopluk Ledl
11
这是一个巧妙的代码,但我不会说“时代已经改变”,因为它仍然使用两个滑块,并且仍需要添加JS和CSS来克服标准缺陷。在我看来,“时代已经改变”应该是:使用<input type="range" min="0" max="10" range>支持多值输入元素,或者其他类似的方式。 - JAR.JAR.beans
我同意这个解决方案,但是当我试图使用javascript更新值时遇到了问题。有人可以帮助我动态更新滑块吗? - Marco Teixeira
非常喜欢这个程序 大部分 是可以访问的,我认为应该做的唯一事情是添加一个轮廓、颜色变化或其他指示器,以便让用户在视觉上知道他们正在调整滑块的哪一侧。这应该发生在焦点上,以便在使用 Tab 和箭头键时也可以正常工作。 - maxshuty
我基于这个答案构建了一个小型库,你可以在这里看到我的答案和实现,修复了我在上面评论中提到的一些问题:https://dev59.com/CG445IYBdhLWcg3wpcAP#70561454 - maxshuty
显示剩余2条评论

20

2023 - 可访问解决方案 - 实施的30秒解决方案

我为此创建了一个简单库,或者你可以从这个答案中自行应用代码

这个解决方案是基于@Mr. Hugo的答案构建的。可访问性是所有答案都没有关注到的问题,所以我在上述答案的基础上进行了改进,使其更具可访问性和可扩展性,因为它存在一些缺陷。

使用非常简单:

  1. 使用CDN或本地托管脚本:https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components/dist/simpleRange.min.js
  2. 将此元素添加到您的模板或HTML中:<range-selector min-range="0" max-range="1000" />
  3. 通过监听range-changed事件(或任何您传递的event-name-to-emit-on-change)来连接它

就是这样。在这里查看完整演示。您可以通过简单应用属性来轻松自定义,例如inputs-for-labels以使用输入而不是标签,slider-color以调整颜色等等!

这是一个fiddle:

window.addEventListener('range-changed', (e) => {console.log(`Range changed for: ${e.detail.sliderId}. Min/Max range values are available in this object too`)})
<script src="https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components@latest/dist/simpleRange.min.js"></script>

<div>
  <range-selector
    id="rangeSelector1"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    number-of-legend-items-to-show="6"
  />
</div>

<div>
  <range-selector
    id="rangeSelector1"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1"
    max-range="500"
    number-of-legend-items-to-show="3"
    inputs-for-labels
  />
</div>

<div>
  <range-selector
    id="rangeSelector2"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    number-of-legend-items-to-show="3"
    slider-color="#6b5b95"
  />
</div>

<div>
  <range-selector
    id="rangeSelector3"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    hide-label
    hide-legend
  />
</div>

我决定解决链接答案中的问题,比如使用display: none来隐藏标签(对无障碍性不友好),滑块没有视觉焦点等,并通过清理事件监听器、使代码更加动态和可扩展来改进代码。
我创建了这个小巧的库,提供了许多选项来自定义颜色、事件名称,轻松地与之连接,使可访问的标签具备国际化能力等等。如果你想玩一下,这里有一个示例
你可以轻松地自定义图例项的数量,隐藏或显示标签和图例,并自定义所有元素的颜色,包括焦点颜色,就像这样。

使用几个属性的示例:

<range-selector
  min-label="i18n Minimum Range"
  max-label="i18n Maximum Range"
  min-range="5"
  max-range="555"
  number-of-legend-items-to-show="6"
  event-name-to-emit-on-change="my-custom-range-changed-event"
  slider-color="orange"
  circle-color="#f7cac9"
  circle-border-color="#083535"
  circle-focus-border-color="#3ec400"
/>

然后在你的脚本中:
window.addEventListener('my-custom-range-changed-event', (e) => { const data = e.detail; });

最后,如果你发现这个库缺少你需要的东西,我已经非常容易地让它可以自定义。
只需复制此文件,在顶部你可以看到包含大部分你可能想要进一步自定义的变量的cssHelpersconstants对象。
由于我是使用原生 Web 组件构建的,我利用了disconnectedCallback和其他钩子来清理事件监听器并进行设置。

2

这是一个可重复使用的双范围滑块实现,基于教程 Coding Artist的双范围滑块

  • 接近本地化用户界面,兼容Chrome / Firefox / Safari
  • 基于API EventTarget,具有change / input事件、minGap / maxGap属性

let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));

class DoubleRangeSlider extends EventTarget {
  #minGap = 0;
  #maxGap = Number.MAX_SAFE_INTEGER;
  #inputs;
  style = {
    trackColor: '#dadae5',
    rangeColor: '#3264fe',
  };
  constructor(container){
    super();
    let inputs = $$('input[type="range"]', container);
    if(inputs.length !== 2){
      throw new RangeError('2 range inputs expected');
    }
    let [input1, input2] = inputs;
    if(input1.min >= input1.max || input2.min >= input2.max){
      throw new RangeError('range min should be less than max');
    }
    if(input1.max > input2.max || input1.min > input2.min){
      throw new RangeError('input1\'s max/min should not be greater than input2\'s max/min');
    }
    this.#inputs = inputs;
    let sliderTrack = $('.slider-track', container);
    let lastValue1 = input1.value;
    input1.addEventListener('input', (e) => {
      let value1 = +input1.value;
      let value2 = +input2.value;
      let minGap = this.#minGap;
      let maxGap = this.#maxGap;
      let gap = value2 - value1;
      let newValue1 = value1;
      if(gap < minGap){
        newValue1 = value2 - minGap;
      }else if(gap > maxGap){
        newValue1 = value2 - maxGap;
      }
      input1.value = newValue1;
      if(input1.value !== lastValue1){
        lastValue1 = input1.value;
        passEvent(e);
        fillColor();
      }
    });
    let lastValue2 = input2.value;
    input2.addEventListener('input', (e) => {
      let value1 = +input1.value;
      let value2 = +input2.value;
      let minGap = this.#minGap;
      let maxGap = this.#maxGap;
      let gap = value2 - value1;
      let newValue2 = value2;
      if(gap < minGap){
        newValue2 = value1 + minGap;
      }else if(gap > maxGap){
        newValue2 = value1 + maxGap;
      }
      input2.value = newValue2;
      if(input2.value !== lastValue2){
        lastValue2 = input2.value;
        passEvent(e);
        fillColor();
      }
    });
    let passEvent = (e) => {
      this.dispatchEvent(new e.constructor(e.type, e));
    };
    input1.addEventListener('change', passEvent);
    input2.addEventListener('change', passEvent);
    let fillColor = () => {
      let overallMax = +input2.max;
      let overallMin = +input1.min;
      let overallRange = overallMax - overallMin;
      let left1 = ((input1.value - overallMin) / overallRange * 100) + '%';
      let left2 = ((input2.value - overallMin) / overallRange * 100) + '%';
      let {trackColor, rangeColor} = this.style;
      sliderTrack.style.background = `linear-gradient(to right, ${trackColor} ${left1}, ${rangeColor} ${left1}, ${rangeColor} ${left2}, ${trackColor} ${left2})`;
    };
    let init = () => {
      let overallMax = +input2.max;
      let overallMin = +input1.min;
      let overallRange = overallMax - overallMin;
      let range1 = input1.max - overallMin;
      let range2 = overallMax - input2.min;
      input1.style.left = '0px';
      input1.style.width = (range1 / overallRange * 100) + '%';
      input2.style.right = '0px';
      input2.style.width = (range2 / overallRange * 100) + '%';
      fillColor();
    };
    init();
  }
  get minGap(){
    return this.#minGap;
  }
  set minGap(v){
    this.#minGap = v;
  }
  get maxGap(){
    return this.#maxGap;
  }
  set maxGap(v){
    this.#maxGap = v;
  }
  get values(){
    return this.#inputs.map((el) => el.value);
  }
  set values(values){
    if(values.length !== 2 || !values.every(isFinite))
      throw new RangeError();
    let [input1, input2] = this.#inputs;
    let [value1, value2] = values;
    if(value1 > input1.max || value1 < input1.min)
      throw new RangeError('invalid value for input1');
    if(value2 > input2.max || value2 < input2.min)
      throw new RangeError('invalid value for input2');
    input1.value = value1;
    input2.value = value2;
  }
  get inputs(){
    return this.#inputs;
  }
  get overallMin(){
    return this.#inputs[0].min;
  }
  get overallMax(){
    return this.#inputs[1].max;
  }
}


function main(){
  let container = $('.slider-container');
  let slider = new DoubleRangeSlider(container);
  slider.minGap = 30;
  slider.maxGap = 70;

  let inputs = $$('input[name="a"]');
  let outputs = $$('output[name="a"]');
  outputs[0].value = inputs[0].value;
  outputs[1].value = inputs[1].value;

  slider.addEventListener('input', (e) => {
    let values = slider.values;
    outputs[0].value = values[0];
    outputs[1].value = values[1];
  });
  slider.addEventListener('change', (e) => {
    let values = slider.values;
    console.log('change', values);
    outputs[0].value = values[0];
    outputs[1].value = values[1];
  });
}
document.addEventListener('DOMContentLoaded', main);
.slider-container {
  display: inline-block;
  position: relative;
  width: 360px;
  height: 28px;
}
.slider-track {
  width: 100%;
  height: 5px;
  position: absolute;
  margin: auto;
  top: 0;
  bottom: 0;
  border-radius: 5px;
}
.slider-container>input[type="range"] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  position: absolute;
  margin: auto;
  top: 0;
  bottom: 0;
  width: 100%;
  outline: none;
  background-color: transparent;
  pointer-events: none;
}
.slider-container>input[type="range"]::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  height: 5px;
}
.slider-container>input[type="range"]::-moz-range-track {
  -moz-appearance: none;
  height: 5px;
}
.slider-container>input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -9px;
  height: 1.7em;
  width: 1.7em;
  background-color: #3264fe;
  cursor: pointer;
  pointer-events: auto;
  border-radius: 50%;
}
.slider-container>input[type="range"]::-moz-range-thumb {
  -moz-appearance: none;
  height: 1.7em;
  width: 1.7em;
  cursor: pointer;
  border: none;
  border-radius: 50%;
  background-color: #3264fe;
  pointer-events: auto;
}
.slider-container>input[type="range"]:active::-webkit-slider-thumb {
  background-color: #ffffff;
  border: 3px solid #3264fe;
}
<h3>Double Range Slider, Reusable Edition</h3>

<div class="slider-container">
  <div class="slider-track"></div>
  <input type="range" name="a" min="-130" max="-30" step="1" value="-100" autocomplete="off" />
  <input type="range" name="a" min="-60" max="0" step="2" value="-30" autocomplete="off" />
</div>

<div>
  <output name="a"></output> ~ <output name="a"></output>
</div>

<pre>
Changes:
1. allow different min/max/step for two inputs
2. new property 'maxGap'
3. added events 'input'/'change'
4. dropped IE/OldEdge support
</pre>


输入在Safari上没有居中。在Chrome和Firefox上没问题。top: -2px;可以解决,但这种情况下它们在Chrome/Firefox上不居中。 - wp-ap
我将高度从em改为px,现在在Safari上看起来也很好。这是最简单但也是最好的解决方案。谢谢! - wp-ap

1

对于那些使用Vue的人来说,现在有一个基于noUiSlider的Veeno可用。但是它似乎不再得到维护了。 :-(


1

纯CSS解决方案

    <html>    
        <head>        
            <meta http-equiv="content-type" content="text/html; charset=utf-8">        
                <style>            
                    .multi-ranges {
                        position: relative;
                        width: 800px;
                        height: 20px;
                        background-color: rgb(220,220,220);
                        border: 1px solid rgb(0,0,0);
                    }
                    input[type=range] {
                        -webkit-appearance: none;
                        position: absolute;
                        top: 8px;
                        left: -2px;
                        width: 100%;
                        height: 0;
                        outline: none;
                    }
                    .multi-range::-webkit-slider-thumb {
                        -webkit-appearance: none;
                        position: relative;
                        width: 20px;
                        height: 20px;
                    }
                    #range1::-webkit-slider-thumb {
                        background-color: rgb(255,0,0);
                        z-index: 2;
                    }
                    #range2::-webkit-slider-thumb {
                        background-color: rgb(0,255,0);
                        z-index: 3;
                    }
                    #range3::-webkit-slider-thumb {
                        background-color: rgb(0,0,255);
                        z-index: 4;
                    }
                    #range1:hover::-webkit-slider-thumb,
                    #range2:hover::-webkit-slider-thumb,
                    #range3:hover::-webkit-slider-thumb {
                        z-index: 5;
                        background-color: rgb(255,215,0);
                    }
                    .multi-range::-moz-range-thumb {
                        position: relative;
                        width: 20px;
                        height: 20px;
                        border: 0 none;
                        border-radius: 0;
                    }
                    #range1::-moz-range-thumb {
                        background-color: rgb(255,0,0);
                    }
                    #range2::-moz-range-thumb {
                        background-color: rgb(0,255,0);
                    }
                    #range3::-moz-range-thumb {
                        background-color: rgb(0,0,255);
                    }
                    #range1:hover::-moz-range-thumb,
                    #range2:hover::-moz-range-thumb,
                    #range3:hover::-moz-range-thumb {
                        background-color: rgb(255,215,0);
                    }
                </style>        
            <title>Multi-range         
            </title>
    <script type="text/javascript">
    function range_value(fn1,fn2) {document.getElementById('range-value').value = 'RANGE#' + fn1 + ' [' + fn2.value + ']';}
    </script>    
        </head>    
        <body>        
            <div>            
                <output id="range-value">              
                </output>        
            </div>        
            <div class="multi-ranges">            
                <input type="range" id="range1" class="multi-range" min="0" max="100" value=0 step="1" oninput="range_value(1,this);">            
                <input type="range" id="range2" class="multi-range" min="0" max="100" value=50" step="1" oninput="range_value(2,this);">            
                <input type="range" id="range3" class="multi-range" min="0" max="100" value=100 step="1" oninput="range_value(3,this);">        
            </div>    
        </body>
    </html>

1
您的回答如果能加上更多的支持信息将会更好。请[编辑]以添加进一步的细节,例如引用或文献资料,以便其他人可以确认您的回答是否正确。您可以在帮助中心中找到有关如何编写良好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接