2つのthumbを持つレンジスライダーを作る

# 概要
```<input>```要素の```range```型を利用して「◯◯〜◯◯」のような範囲の選択ができるレンジスライダーを作った。

ポイントは、
- ```range```型をz軸に2つ重ねる
- 手前のスライダーで奥になるスライダーの選択色を隠す

# 背景と課題
スライダーで値を選択する```<input>```要素として```range```型がある。これはつまみ(thumb)が1つのスライダーで、thumbを動かすことで値を選択できるが、「◯◯〜◯◯」といった選択ができない。

そこで、直感的にもわかりやすく、thumbが1つだけのスライダーと同じ大きさでthumbを2つ持つスライダーを実現する。

# 実装
## 完成形
作ったレンジスライダーです。
![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3978587/474ef275-1f2c-517e-533d-1195e801f560.png)


前提となるフレームワーク
- jQuery
- Bootstrap

実装は以下のとおりです。
```html:HTML
<div class="multi-slider">
    <input type="range" class="slider-lower" value="30" min="0" max="100" data-pb-color="var(--bs-secondary-bg)">
	<input type="range" class="slider-upper" value="70" min="0" max="100" data-pb-color="var(--bs-primary)">
</div>
```
```css:CSS
.slider-lower, .slider-upper {
    -webkit-appearance: none;
    appearance: none;
    cusor: pointer;
    outline: none;
    height: 8px;
    border-radius: 8px;
    background: var(--bs-secondary-bg);
}
.multi-slider ::-webkit-slider-thumb {
    background-color: var(--bs-primary);
    pointer-events: auto;
}
.multi-slider {
    display: grid;
    padding: 8px;
    width: 100%;
    /* thumb以外の操作を無効にする */
    pointer-events: none;
}
.slider-lower, .slider-upper {
    grid-column: 1;
    grid-row: 2;
}
.slider-lower {
    z-index: 1;
}
```
```js:JavaScript
$(function(){
    $('.slider-lower, .slider-upper').on('load input', function(e){
        if ( Number($('.slider-lower').val()) >= Number($('.slider-upper').val()) ) {
            if ( $(this).hasClass('slider-lower') ){
                $(this).val(Number($('.slider-upper').val())-1);
            } else {
                $(this).val(Number($('.slider-lower').val())+1);
            }
        }
        ratio = ($(this).val()/$(this).prop("max"))*100;
        left = $(this).data('pb-color');
        if ( $(this).hasClass('slider-lower') ){
            right = 'rgba(255,255,255,0.0)';
        } else {
            right = 'var(--bs-secondary-bg)';
        }
        $(this).css('background','linear-gradient(90deg, '+left+' '+ratio+'%, '+right+' '+ratio+'%)');
    });
    $('.slider-lower, .slider-upper').trigger('load');
});
```

## 実現方法の概略
```<input>```要素の```range```型は、要素と```-webkit-slider-runnable-track```、```-webkit-slider-thumb```という2つの疑似要素から構成される。
![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3978587/a552da77-5004-a68f-506e-3f530292ae67.png)

基本的なアイディアは、まず、図のような2つのスライダーを用意する。
![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3978587/7dafd20e-bbb9-5dd8-f993-70cfceaaa379.png)


```slider-lower```は最小値を選択するスライダーで、thumbの左を背景色に右を透明にする。```slider-upper```は最大値を選択するスライダーで、左を選択色に右を背景色にする。このようにthumbを境界にtrackを塗り分けたスライダーを```slider-lower```が手前になるように配置することで、最小値から最大値の間だけが選択色が見えるようにする。

## 具体的な実装の説明
### trackの塗り分け
trackの塗り分けはCSSで設定する方法とJavaScriptで設定する方法がある。今回は、調整が用意なためJavaScriptを使う。まず、ベースとなるスタイルを設定する。
```css:CSS
.slider-lower, .slider-upper {
    -webkit-appearance: none;
    appearance: none;
    cusor: pointer;
    outline: none;
    height: 8px;
    border-radius: 8px;
    background: var(--bs-secondary-bg);
}
.multi-slider ::-webkit-slider-thumb {
    background-color: var(--bs-primary);
}
```
要素自体、ここでは```slider-lower```と```slider-upper```にスタイルを設定する。```-webkit-appearance: none;```と```appearance: none;```でウィジェットの機能を非表示にし、以下基本的なスタイルを設定する。
```cursor: pointer;```でカーソルがあたったときの形を設定する。
```outline: none;```でフォーカスがあたったときに枠を表示させない。
```height```, ```border-radius```でtrackの高さと端のスタイルを設定する。
```background```でtrackの背景色を設定する。```--bs-secodary-bg```としたのはBootstrapに合わせるため。

```.multi-slider ::webkit-slider-thumbthumb{}```では、thumbのスタイルを設定する。ウィジェットの標準スタイルで十分だが、ここでは選択色と合わせるために```background-color```に```--bs-primary```を設定する。

次にJavaScriptで塗り分ける。
スクリプトは前掲のものを再掲する。
```javascript:JavaScript
$(function(){
    $('.slider-lower, .slider-upper').on('load input', function(e){
        if ( Number($('.slider-lower').val()) >= Number($('.slider-upper').val()) ) {
            if ( $(this).hasClass('slider-lower') ){
                $(this).val(Number($('.slider-upper').val())-1);
            } else {
                $(this).val(Number($('.slider-lower').val())+1);
            }
        }
        ratio = ($(this).val()/$(this).prop("max"))*100;
        left = $(this).data('pb-color');
        if ( $(this).hasClass('slider-lower') ){
            right = 'rgba(255,255,255,0.0)';
        } else {
            right = 'var(--bs-secondary-bg)';
        }
        $(this).css('background','linear-gradient(90deg, '+left+' '+ratio+'%, '+right+' '+ratio+'%)');
    });
    $('.slider-lower, .slider-upper').trigger('load');
});
```
スクリプトは```slider-lower```または```slider-upper```が```load```またはthumbを動かされたときに実行される。
最初の```if```文は、左右のthumbが逆転しないように制御している。
次は、thumbの位置を求め```ratio```に持つ。
次に、thumbの左側の色を```left```に、右側の色を```right```に持つ。左側の色は、HTMLで```<input>```要素の```data-pb-color```属性に持たせている。
最後に、要素の背景色を```linear-gradient```で2色に塗り分ける。
なお、本質からは外れるが、読み込み時に```load```が発火されなかったので```trigger('load')```で強制的に発火させている。

ここまでで、図のようなスライダーができているはず。
![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3978587/7dafd20e-bbb9-5dd8-f993-70cfceaaa379.png)

### 2つのスライダーを重ねる
```css:CSS
.multi-slider {
    display: grid;
    padding: 8px;
    width: 100%;
}
.slider-lower, .slider-upper {
    grid-column: 1;
    grid-row: 2;
}
.slider-lower {
    z-index: 1;
}
```
2つのスライダーをラップする```div```要素(```multi-slider```)を```display: grid;```でグリッドコンテナ化し、```slider-lower```と```slider-upper```を同じセルに配置する。また、```slider-lower```を```z-index:1;```で手前に配置する。

これで見た目は完成形と同じになるが、右側のthumbを操作できないので、```pointer-events```を使って以下のように設定する。
```
.multi-slider ::-webkit-slider-thumb {
    pointer-events: auto;
}
.multi-slider {
    pointer-events: none;
}
```
```multi-slider```全体を```pointer-events: none;```でポインターを無効にする。これはtrackをクリックしたときにthumbが移動するのを防ぐ。
次に、```multi-slider```要素の中の```-webkit-slider-thumb```疑似要素だけに```pointer-events: auto;```で操作権を与える。

# まとめ
```range```型の```<input>```属性を2つ重ねることで、「◯◯〜◯◯」といった範囲選択ができるレンジスライダーを作った。選択範囲と背景の塗り分けをCSSとJavaScriptを使って実装した。なお、今回の実装では-webkit付きの疑似要素を使っておりFirefox向けの実装は入っていない。


# 参考
- https://blog.logrocket.com/creating-custom-css-range-slider-javascript-upgrades/
- https://www.webdesignleaves.com/pr/css/input-range-style.html#google_vignette
- https://css-tricks.com/multi-thumb-sliders-particular-two-thumb-case/