[html+css+js]「ここからここまで」を指定するinputを作る

input:rangeは1個しか指定できないんですよ。

やりたいこと

イメージは、minとmaxを指定できるinput:range。

input:range自体にそんな機能はないらしいので、作るしかない。

input:rangeは数値用だけど、いっそのこと文字列も扱えたらいいよね?
「small、medium、large」みたいな。

文字列だとバーの中に「ここはこれ」ってラベルがついてると親切だよね、というか訳解んないからつけないとダメだよね。

1個だけ作る前提でもいいんだけど、カスタムなしに複数設置しても動くようにしたいよね。

結論

<div class="range">
    <div class="input" data-values="a,b,c,d,e,f,g">
        <input class="range01" type="range" min="0" max="" value="0">
        <input class="range02" type="range" min="0" max="" value="0">
        <input class="rangeVal" type="hidden" name="" value="">
    </div>
    <div class="bar"></div>
</div>
.range{
    width: calc(100% - 18px);
    margin: 0 auto;
    position: relative;
    .input{
        width: calc(100% + 18px);
        position: absolute;
        top: 0;
        left: -9px;
        pointer-events: none;
        input[type="range"]{
            width: 100%;
            height: 16px;
            position: absolute;
            top: 0;
            left: 0;
            appearance: none;
            -webkit-appearance: none;
            background: none;
            border: none;
            &::-webkit-slider-thumb{
                pointer-events: auto;
                appearance: none;
                -webkit-appearance: none;
                width: 24px;
                height: 24px;
                border-radius: 50%;
                border: 2px solid #fff;
                background: blue;
            }
            &::-moz-range-thumb{
                //Firefox対策
                pointer-events: auto;
                appearance: none;
                -webkit-appearance: none;
                width: 24px;
                height: 24px;
                border-radius: 50%;
                border: 2px solid #fff;
                background: blue;
            }
        }
    }
    .bar{
        width: 100%;
        display: flex;
        flex-direction: row;
        gap: 2px;
        span{
            flex: 1;
            height: 16px;
            line-height: 16px;
            text-align: center;
            font-size: .75rem;
            font-weight: 600;
            color: #999;
            background: #eee;
            display: inline-block;
            &.active{
                color: #fff;
                background: blue;
            }
        }
    }
}
$(function(){
    $('body').find('.range').each(function(){
        let rangeValues = $(this).find('.input').data('values').split(',');
        let rangeLength = rangeValues.length;
        $(this).find('input[type="range"]').attr('max',rangeLength);
        $(this).find('input[type="range"].range01').attr('value',rangeLength);
        $(this).find('input[type="range"].range02').attr('value',0);
        let rangeBar = '';
        for(let i = 0; i<rangeLength; i++){
            rangeBar += '<span class="active">'+rangeValues[i]+'</span>';
        }
        $(this).find('.bar').append(rangeBar);
        $(this).find('input[type="hidden"].rangeVal').attr('value',$(this).find('.input').data('values'));
    })
    $('.range').find('input[type="range"]').on('change',function(){
        let range01 = '';
        let range02 = '';
        if($(this).hasClass('range01')){
            range01 = $(this);
            range02 = $(this).siblings('input[type="range"]');
        }
        if($(this).hasClass('range02')){
            range01 = $(this).siblings('input[type="range"]');
            range02 = $(this);
        }
        let rangeMax = '';
        let rangeMin = '';
        if(Number(range01.val()) <= Number(range02.val())){
            rangeMax = range02.val();
            rangeMin = range01.val();
        }else{
            rangeMax = range01.val();
            rangeMin = range02.val();
        }
        let rangeValues = $(this).closest('.input').data('values').split(',');
        let rangeValAry = [];
        $(this).closest('.range').find('.bar span').removeClass('active');
        for(let i=Number(rangeMin); i<Number(rangeMax); i++){
            rangeValAry.push(rangeValues[i]);
            $(this).closest('.input').next('.bar').children('span').eq(i).addClass('active');
        }
        $(this).siblings('input[type="hidden"].rangeVal').val(rangeValAry.join(','));
    })
})

解説

HTML

まずjsが走ったらこんな感じの出力になる。

<div class="range">
    <div class="input" data-values="a,b,c,d,e,f,g">
        <input class="range01" type="range" min="0" max="7" value="0">
        <input class="range02" type="range" min="0" max="7" value="7">
        <input class="rangeVal" type="hidden" name="" value="a,b,c,d,e,f,g">
    </div>
    <div class="bar">
        <span class="active">a</span>
        <span class="active">b</span>
        <span class="active">c</span>
        <span class="active">d</span>
        <span class="active">e</span>
        <span class="active">f</span>
        <span class="active">g</span>
    </div>
</div>

inputの内訳は.inputのdata-valuesが該当する。
カンマ区切りのこの文字列をjsで配列にして、カウントしたりバー部分を作ったりしてる。
数値にも対応してるけど、例えば0~5で指定したければ「0,1,2,3,4,5」となる。
並び順は記入した順番に準拠してるので、降順・昇順などのソート機能はついていない。

input:rangeからはバー部分を削除してthumbのところだけを活かすようにする。
.barがバー部分を補完して、子要素のspanが「こことここにthumbがあったらこれが選択されてますよ」と分かるようにclassの付与で視覚的に分かるようにする。

input:rangeはユーザー操作とjs側のトリガーが用途になるわけで、選択したものをどこに格納しているかというと、input:hiddenのvalueがそれになる。

初期値として全部指定されてるようにするので、input:rangeの片方を0、もう片方に最大値を指定。
当然input:hiddenのvalueにも全部入れておく。
.barのspanにはclassを付与しておく。

css

くっそ長いんだけどさ。
input自体の装飾を切っちゃってるから、自作しなきゃだから仕方ない。

.range{
    width: calc(100% - 24px);
    margin: 0 auto;
    position: relative;
    .input{
        width: calc(100% + 18px);
        position: absolute;
        top: 0;
        left: 0;
        pointer-events: none;
        input[type="range"]{
            width: calc(100% + 24px);
            height: 16px;
            position: absolute;
            top: 0;
            left: -12px;
            appearance: none;
            -webkit-appearance: none;
            background: none;
            border: none;
            &::-webkit-slider-thumb{
                pointer-events: auto;
                appearance: none;
                -webkit-appearance: none;
                width: 24px;
                height: 24px;
                border-radius: 50%;
                border: 2px solid #fff;
                background: blue;
            }
            &::-moz-range-thumb{
                //Firefox対策
                pointer-events: auto;
                appearance: none;
                -webkit-appearance: none;
                width: 24px;
                height: 24px;
                border-radius: 50%;
                border: 2px solid #fff;
                background: blue;
            }
        }
    }
    .bar{
        width: 100%;
        display: flex;
        flex-direction: row;
        gap: 2px;
        span{
            flex: 1;
            height: 16px;
            line-height: 16px;
            text-align: center;
            font-size: .75rem;
            font-weight: 600;
            color: #999;
            background: #eee;
            display: inline-block;
            &.active{
                color: #fff;
                background: blue;
            }
        }
    }
}

読めばそのとおりなので、頑張れば意味がわかる。
いじるなりそのまま使うなり。

一応気を使ってる点として、input:rangeのthumbは領域の端にあるとき、thumbの中心が領域の端になる。なので、画面いっぱいに設置するとthumbが画面外にはみ出してしまう場合がある。
この部分は実数で指定してしまったので、変えるときには調整する箇所がいくつかあるので注意。

thumbが基準となるので、今回は24pxにしてあるので、これに合わせて24pxを引いたり足したり、12pxをズラしたりしてる。

js(jQuery)

こっちも長い。

$(function(){
    //初期設定
    $('body').find('.range').each(function(){
        let rangeValues = $(this).find('.input').data('values').split(',');
        let rangeLength = rangeValues.length;
        //input:rangeの値設定
        $(this).find('input[type="range"]').attr('max',rangeLength);
        $(this).find('input[type="range"].range01').attr('value',rangeLength);
        $(this).find('input[type="range"].range02').attr('value',0);
        let rangeBar = '';
        for(let i = 0; i<rangeLength; i++){
            rangeBar += '<span class="active">'+rangeValues[i]+'</span>';
        }
        //バー部分の設置・配色
        $(this).find('.bar').append(rangeBar);
        //input:hiddenに値挿入
        $(this).find('input[type="hidden"].rangeVal').attr('value',$(this).find('.input').data('values'));
    })
    //選択時ギミック
    $('.range').find('input[type="range"]').on('change',function(){
        let range01 = '';
        let range02 = '';
        if($(this).hasClass('range01')){
            range01 = $(this);
            range02 = $(this).siblings('input[type="range"]');
        }
        if($(this).hasClass('range02')){
            range01 = $(this).siblings('input[type="range"]');
            range02 = $(this);
        }
        let rangeMax = '';
        let rangeMin = '';
        if(Number(range01.val()) <= Number(range02.val())){
            rangeMax = range02.val();
            rangeMin = range01.val();
        }else{
            rangeMax = range01.val();
            rangeMin = range02.val();
        }
        let rangeValues = $(this).closest('.input').data('values').split(',');
        let rangeValAry = [];
        $(this).closest('.range').find('.bar span').removeClass('active');
        for(let i=Number(rangeMin); i<Number(rangeMax); i++){
            rangeValAry.push(rangeValues[i]);
            //バー部分の配色
            $(this).closest('.input').next('.bar').children('span').eq(i).addClass('active');
        }
        //input:hiddenに値挿入
        $(this).siblings('input[type="hidden"].rangeVal').val(rangeValAry.join(','));
    })
})

一番ネックになる部分。

通常はinput:rangeのrange01がmax、range02がminとして扱うんだけど、実際の動作させてると位置が入れ替わる場合がある。
入れ替わらないように設定した場合に使いづらさがすごかった(最小値が最大値を超えないようにする→thumbが重なったら片方しか掴めないわけで、その仕様を素人が初見でわかるはずがない)から、値の大小が反転しても大丈夫なように調整してある。

どうせjsで処理するんだし、input:rangeの値にこだわるよりは大小それぞれの値が何になるかを設定しちゃえばいいじゃんって。

input:rangeで取得した数値は.ep()と親和性があるから、そのまま流用すればバーの装飾ができる。
input:rangeで取得した数値は変数の位置指定と親和性があるから、そのまま流用すれば該当の値が取得できる。

作ってるときに詰まったのは発想よりも数値の扱いで、if文に書いてるNumber()は必須。
これを入れなくても動いたっちゃ動いたんだけど、2桁だか何だかの数値の扱いがおかしくなった(不等号の判定が狂った)。

まとめ

参考にしたものもあるんだけどほぼほぼ自分のイメージに合わせて何したらいいかを探す、jsの素地に当たる部分の検索作業だった。
久しぶりにパクリ成分薄めのものを作った。

案の定というか、だいぶ時間がかかった。

コメント

タイトルとURLをコピーしました