前回の記事の派生というか。
どういうアレか
前回の記事がある。
これは1つのバーの上でinput:rangeが2つ動いて範囲指定するもの。
これに追加機能として、各input:rangeに対応したselectを作ろうという話。
イメージは簡単だったけど実現に地味に時間食った。
やりたいこと
input:rangeを操作したら、対応するselectのselectedも変える。
selectを操作したら対応するinput:rangeも変わる。
selectは最大・最小の用途を固定してるので、input:rangeを操作して最小・最大が入れ替わった場合、selectの中身を入れ替える。
selectは最大・最小の用途を固定してるので、最小用で選べるのは最大の値以下にする。
同様に、最大用で選べるのは最小の値以上にする。
前の記事同様に配列前提の範囲選択が目的なので、最小・最大という表現は怪しい言い方なんだけどね。「始点/終点」「ここから/ここまで」の方が正しいんだけどね。そうしたほうがわかりやすいかなと思った。どうなんですかね。
今回は最小・最大の値だけあればいいんで、前回の記事みたく内訳を拾ってくる作りにしはしていない。
こんなかんじ
<div class="wrap">
<div class="select">
<div class="input price" data-values="10,20,30,40,50,60,70,80,90,100">
<input class="select01" type="range" min="0" max="" value="0">
<input class="select02" type="range" min="0" max="" value="0">
</div>
<div class="bar"></div>
</div>
<ul class="select_list">
<li><select name="" class="selectValMin"></select></li>
<li>~</li>
<li><select name="" class="selectValMax"></select></li>
</ul>
</div>
.select{
width: calc(100% - 24px);
height: 24px;
margin: 0 auto;
position: relative;
.input{
width: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
input[type="range"]{
width: calc(100% + 24px);
height: 24px;
margin: 0;
padding: 0;
position: absolute;
top: 0;
left: -12px;
z-index: 1;
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;
cursor: grab;
transition: background .2s;
}
&::-moz-range-thumb{
pointer-events: auto;
appearance: none;
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #fff;
background: blue;
cursor: grab;
transition: background .2s;
}
&:hover{
&::-webkit-slider-thumb{
background: red;
}
&::-moz-range-thumb{
background: red;
}
}
}
}
.bar{
width: 100%;
display: flex;
flex-direction: row;
position: absolute;
top: calc(50% - 1px);
span{
flex: 1;
height: 2px;
background: #eee;
display: inline-block;
&.active{
color: #fff;
background: blue;
}
}
}
+ ul.select_list{
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: row;
gap: 10px;
}
}
$(function(){
$('.wrap').find('.select').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"].select01').attr('value',rangeLength);
$(this).find('input[type="range"].select02').attr('value',0);
let rangeBar = '';
let selectOption01 = '';
let selectOption02 = '';
for(let i = 0; i<rangeLength; i++){
rangeBar += '<span class="active"></span>';
if($(this).find('.input').hasClass('price')){
(i==0) ? selectOption01 += '<option value="'+rangeValues[i]+'" selected>'+Number(rangeValues[i]).toLocaleString()+'円</option>':selectOption01 += '<option value="'+rangeValues[i]+'">'+Number(rangeValues[i]).toLocaleString()+'円</option>';
(i==0) ? selectOption02 += '<option value="'+rangeValues[i]+'">'+Number(rangeValues[i]).toLocaleString()+'円</option>':selectOption02 += '<option value="'+rangeValues[i]+'" selected>'+Number(rangeValues[i]).toLocaleString()+'円</option>';
}
}
$(this).find('.bar').append(rangeBar);
$(this).next('ul.select_list').find('select.selectValMin').append(selectOption01);
$(this).next('ul.select_list').find('select.selectValMax').append(selectOption02);
})
$('.wrap .select').find('input[type="range"]').on('change',function(){
let select01 = '';
let select02 = '';
if($(this).hasClass('select01')){
select01 = $(this);
select02 = $(this).siblings('input[type="range"]');
}
if($(this).hasClass('select02')){
select01 = $(this).siblings('input[type="range"]');
select02 = $(this);
}
let selectMin = '';
let selectMax = '';
if(Number(select01.val()) >= Number(select02.val())){
selectMin = select02.val();
selectMax = select01.val();
}else{
selectMin = select01.val();
selectMax = select02.val();
}
let rangeValues = $(this).closest('.input').data('values').split(',');
let rangeValAry = [];
$(this).closest('.select').find('.bar span').removeClass('active');
for(let i=Number(selectMin); i<Number(selectMax); i++){
rangeValAry.push(rangeValues[i]);
$(this).closest('.input').next('.bar').children('span').eq(i).addClass('active');
}
$(this).closest('.select').next('ul.select_list').find('select.selectValMin option[value="'+rangeValAry[0]+'"]').prop('selected',true);
$(this).closest('.select').next('ul.select_list').find('select.selectValMax option[value="'+rangeValAry.slice(-1)[0]+'"]').prop('selected',true);
})
$('.wrap ul.select_list').find('select').on('change',function(){
let selectValMin = '';
let selectValMax = '';
if($(this).hasClass('selectValMin')){
selectValMin = $(this);
selectValMax = $(this).closest('ul.select_list').find('select.selectValMax');
}else if($(this).hasClass('selectValMax')){
selectValMin = $(this).closest('ul.select_list').find('select.selectValMin');
selectValMax = $(this);
}
let rangeValues = $(this).closest('.select_list').siblings('.select').find('.input').data('values').split(',');
let rangeValuesMin = '';
let rangeValuesMax = '';
if(Number(selectValMin.val())>Number(selectValMax.val())){
rangeValuesMin = rangeValues.indexOf(selectValMax.val());
rangeValuesMax = rangeValues.indexOf(selectValMin.val());
}else{
rangeValuesMin = rangeValues.indexOf(selectValMin.val());
rangeValuesMax = rangeValues.indexOf(selectValMax.val());
}
$(this).closest('ul.select_list').siblings('.select').find('.bar span').removeClass('active');
for(let i=Number(rangeValuesMin); i<=Number(rangeValuesMax); i++){
$(this).closest('ul.select_list').siblings('.select').find('.bar').children('span').eq(i).addClass('active');
}
$(this).closest('ul.select_list').prev('.select').find('input[type="range"].select01').val(Number(rangeValuesMin));
$(this).closest('ul.select_list').prev('.select').find('input[type="range"].select02').val(Number(rangeValuesMax)+1);
selectValMin.find('option').each(function(){
if(Number($(this).val()) >= selectValMax.val()){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
selectValMax.find('option').each(function(){
if(Number($(this).val()) <= selectValMin.val()){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
})
})
解説
ベースは前の記事なので、それ以外の部分を。
HTML
jsが走ったらこんな感じになる。
<div class="wrap">
<div class="select">
<div class="input price" data-values="10,20,30,40,50,60,70,80,90,100">
<input class="select01" type="range" min="0" max="0" value="0">
<input class="select02" type="range" min="0" max="10" value="10">
</div>
<div class="bar"></div>
</div>
<ul class="select_list">
<li>
<select name="" class="selectValMin">
<option value="10" selected>10円</option>
<option value="20">20円</option>
<option value="30">30円</option>
<option value="40">40円</option>
<option value="50">50円</option>
<option value="60">60円</option>
<option value="70">70円</option>
<option value="80">80円</option>
<option value="90">90円</option>
<option value="100">100円</option>
</select>
</li>
<li>~</li>
<li>
<select name="" class="selectValMax">
<option value="10">10円</option>
<option value="20">20円</option>
<option value="30">30円</option>
<option value="40">40円</option>
<option value="50">50円</option>
<option value="60">60円</option>
<option value="70">70円</option>
<option value="80">80円</option>
<option value="90">90円</option>
<option value="100" selected>100円</option>
</select>
</li>
</ul>
</div>
<option>は表示内容とvalueをそれぞれ生成できるんで、表示用の部分は単位を付けたり任意のものしたりができる。初期値用にselectedを仕込むこともできる。
css
前の記事と違ってラベルを使わないパターンにした。
それ以外は特段なにもやってない。
js(jQuery)
$(function(){
//初期設定
$('.wrap').find('.select').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"].select01').attr('value',rangeLength);
$(this).find('input[type="range"].select02').attr('value',0);
let rangeBar = '';
let selectOption01 = '';
let selectOption02 = '';
//<option>生成
for(let i = 0; i<rangeLength; i++){
rangeBar += '<span class="active"></span>';
//<option>内訳。単位付けたりするのはここ
if($(this).find('.input').hasClass('price')){
(i==0) ? selectOption01 += '<option value="'+rangeValues[i]+'" selected>'+Number(rangeValues[i]).toLocaleString()+'円</option>':selectOption01 += '<option value="'+rangeValues[i]+'">'+Number(rangeValues[i]).toLocaleString()+'円</option>';
(i==0) ? selectOption02 += '<option value="'+rangeValues[i]+'">'+Number(rangeValues[i]).toLocaleString()+'円</option>':selectOption02 += '<option value="'+rangeValues[i]+'" selected>'+Number(rangeValues[i]).toLocaleString()+'円</option>';
}
}
$(this).find('.bar').append(rangeBar);
$(this).next('ul.select_list').find('select.selectValMin').append(selectOption01);
$(this).next('ul.select_list').find('select.selectValMax').append(selectOption02);
})
//input:rangeを操作したときの内容
$('.wrap .select').find('input[type="range"]').on('change',function(){
//ここから・ここまでそれぞれのセレクタを変数に格納
let select01 = '';
let select02 = '';
if($(this).hasClass('select01')){
select01 = $(this);
select02 = $(this).siblings('input[type="range"]');
}
if($(this).hasClass('select02')){
select01 = $(this).siblings('input[type="range"]');
select02 = $(this);
}
//ここから・ここまでそれぞれの値を変数化
let selectMin = '';
let selectMax = '';
if(Number(select01.val()) >= Number(select02.val())){
selectMin = select02.val();
selectMax = select01.val();
}else{
selectMin = select01.val();
selectMax = select02.val();
}
//input:rangeの選択に対応したバーの装飾
let rangeValues = $(this).closest('.input').data('values').split(',');
let rangeValAry = [];
$(this).closest('.select').find('.bar span').removeClass('active');
for(let i=Number(selectMin); i<Number(selectMax); i++){
rangeValAry.push(rangeValues[i]);
$(this).closest('.input').next('.bar').children('span').eq(i).addClass('active');
}
//input:rangeの選択に対応したselectの操作
$(this).closest('.select').next('ul.select_list').find('select.selectValMin option[value="'+rangeValAry[0]+'"]').prop('selected',true);
$(this).closest('.select').next('ul.select_list').find('select.selectValMax option[value="'+rangeValAry.slice(-1)[0]+'"]').prop('selected',true);
//selectの選択不可設定
let selectValMin = $(this).closest('.select').next('ul.select_list').find('select.selectValMin');
let selectValMax = $(this).closest('.select').next('ul.select_list').find('select.selectValMax');
selectValMin.find('option').each(function(){
if(Number($(this).val()) >= Number(selectValMax.val())){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
selectValMax.find('option').each(function(){
if(Number($(this).val()) <= Number(selectValMin.val())){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
})
//selectを操作したときの内容
$('.wrap ul.select_list').find('select').on('change',function(){
//ここから・ここまでそれぞれのselectを変数に格納
let selectValMin = '';
let selectValMax = '';
if($(this).hasClass('selectValMin')){
selectValMin = $(this);
selectValMax = $(this).closest('ul.select_list').find('select.selectValMax');
}else if($(this).hasClass('selectValMax')){
selectValMin = $(this).closest('ul.select_list').find('select.selectValMin');
selectValMax = $(this);
}
//ここから・ここまでそれぞれの値を取得
let rangeValues = $(this).closest('.select_list').siblings('.select').find('.input').data('values').split(',');
let rangeValuesMin = '';
let rangeValuesMax = '';
if(Number(selectValMin.val())>Number(selectValMax.val())){
rangeValuesMin = rangeValues.indexOf(selectValMax.val());
rangeValuesMax = rangeValues.indexOf(selectValMin.val());
}else{
rangeValuesMin = rangeValues.indexOf(selectValMin.val());
rangeValuesMax = rangeValues.indexOf(selectValMax.val());
}
//selectの選択に対応したバーの装飾
$(this).closest('ul.select_list').siblings('.select').find('.bar span').removeClass('active');
for(let i=Number(rangeValuesMin); i<=Number(rangeValuesMax); i++){
$(this).closest('ul.select_list').siblings('.select').find('.bar').children('span').eq(i).addClass('active');
}
//selectの選択に対応したinput:rangeの操作
$(this).closest('ul.select_list').prev('.select').find('input[type="range"].select01').val(Number(rangeValuesMin));
$(this).closest('ul.select_list').prev('.select').find('input[type="range"].select02').val(Number(rangeValuesMax)+1);
//選択不可項目の指定
selectValMin.find('option').each(function(){
if(Number($(this).val()) >= selectValMax.val()){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
selectValMax.find('option').each(function(){
if(Number($(this).val()) <= selectValMin.val()){
$(this).prop('disabled',true);
}else{
$(this).prop('disabled',false);
}
})
})
})
コメントアウトを入れたからそれで分かる感じだと思う。
めっちゃ長いけど頑張れば読める。
input:rangeとselectを同期させるには「配列のn番目」が関わるので、なかなかめんどくさかった。
前述とかにもあったselectの選択不可部分だけど、最小・最大どちらも最初は振り切ってるから選択不可の設定が不要で、input:rangeなりselectなり、数値を動かしたタイミングでdisabledを振ればいいという考え。
「//selectの選択に対応したバーの装飾」のforが「<=」になってる点、「//selectの選択に対応したinput:rangeの操作」の.select02に指定してる数値を「+1」になってる点は、他の記述からすれば両方とも+1しているということで、なんでかっていうと、こうしないとinput:rangeの最大値の方の位置がずれるからです。
まとめ
かなり長くなったところに、地味に使いまわしポイントもあるから関数化したら短くできそうな気もしてる。
前回の件というベースがあるのが逆に作用して冗長になってる気もしなくもない。
前回のものに比べてHTMLの階層がだるくなってる分、実装するとclassが干渉するかもしれない。
調整してください。
コメント