これでできる! クロスブラウザJavaScript入門

第23回JavaScriptによるUIの実装:スライダー

こんにちは、前回前々回に引き続き、JavaScriptでUIを実装する方法を紹介していきます。今回は前回のドラッグと関連の深いスライダーを実装してみます。

基本のスライダー

スライダーは簡単にいえば動きの制限されたドラッグです。

今回も、最初はなるべくシンプルに実装してみます。まず、基本となるHTMLは次の通りです。

スライダーの基本HTML
<form class="js-slider-form" onsubmit="return false;">
  <input type="text" name="slider" id="slider1o" value="0">
  <div id="slider1" class="js-slider">
    <div></div>
    <input type="button" value="">
  </div>
</form>

今回は少々手抜きで、input type="button"な要素をスライダーのつまみ替わりにします。また、つまみの兄弟要素の空divは背景の線を描くために使用します。

そして、CSSは次のとおりです。やはり、ドラッグする要素のpositionをabsoluteにして、その親要素をposition:relativeにしています。背景の線用のdivにfont-size:0px;という指定があります。IE 6はフォントサイズより要素のサイズを小さくできない(テキストを含まない空の要素であっても)というバグがあるので、その対策です。

スライダーの基本CSS
.js-slider{
  position:relative;
  width:300px;
  height:20px;
}
.js-slider div{
  background:#ddd;
  height:3px;
  border:1px inset #aaa;
  position:relative;
  top:12px;
  font-size:0px;
}
.js-slider input{
  position:absolute;
  width:15px;
  height:20px;
  display:block;
}

さて、スライダーを実現するJavaScriptです。

スライダーの基本JavaScript
(function(){
var slider = document.getElementById('slider1');
var output = document.getElementById('slider1o');

var input = slider.getElementsByTagName('input')[0];
var root = document.documentElement;
var dragging = false;
var value = output.value;// 初期位置
var width = input.clientWidth / 2;

var set_value = function (){
  // つまみのサイズ(input.clientWidth)だけ位置を調整
  input.style.left = (value - input.clientWidth/2) + 'px';
  output.value = value;
};
set_value();

// 目盛り部分をクリックしたとき
slider.onclick = function(evt){
  dragging = true;
  document.onmousemove(evt);
  document.onmouseup();
};
// ドラッグ開始
input.onmousedown = function(evt){
  dragging = true;
  return false;
};
// ドラッグ終了
document.onmouseup = function(evt){
  if (dragging) {
    dragging = false;
    output.value = value;
  }
};
document.onmousemove = function(evt){
  if(dragging){
    // ドラッグ途中
    if(!evt){
      evt = window.event;
    }
    var left = evt.clientX;
    var rect = slider.getBoundingClientRect();
    // マウス座標とスライダーの位置関係で値を決める
    value = Math.round(left - rect.left - width);
    // スライダーからはみ出したとき
    if (value < 0) {
      value = 0;
    } else if (value > slider.clientWidth) {
      value = slider.clientWidth;
    }
    set_value();
    return false;
  }
};
})();

主な処理はドラッグのときとほとんど変わりません。マウスダウンでドラッグを開始し、アップでドラッグ停止、移動中はスライダーとマウスの位置関係からスライダーの値を決めています。

HTML5のスライダーとの統合

さて、スライダーを汎用的にするに当たって、HTML5で新たにフォームのinput要素のタイプとして追加されたrangeについて触れておきましょう。

HTML5の<input type="range">は前述のようなスライダーをネイティブに提供してくれます。type=rangeはOperaやChromeやSafariなどで既に実装されています。

(range対応ブラウザではスライダーが表示されます)

今回はこのtype="range"になるべく合わせるように実装してみます(ただし、この記事ではtype=rangeが持っているstepや、stepDown()、stepUp()などのメソッドはサポートしませんし、横へのスライド限定です。是非、それらに挑戦してみてください⁠⁠。

汎用版スライダーのHTML
<div id="js-slider2">
  <form class="js-slider-form" onsubmit="return false;">
    <input type="range">
  </form>
  <form class="js-slider-form" onsubmit="return false;">
    <input type="range" min="100" max="400" value="200"
       style="width:300px;" >
  </form>
</div>
汎用版スライダーのJavaScript
function html5_slider(node){

var addEvent = document.addEventListener ?
  function(node,type,listener){
    node.addEventListener(type,listener,false);
  } :
  function(node,type,listener){
    node.attachEvent('on'+type, listener);
  }
var removeEvent = document.removeEventListener ?
  function(node,type,listener){
    node.removeEventListener(type,listener,false);
  } :
  function(node,type,listener){
    node.detachEvent('on'+type, listener);
  }

var dragging = false;
var slider, button, input;
var min = 0, max = 100;
var value = 0;
function set_value(){
  button.style.left = (value - button.clientWidth/2) +'px';
  input.value = min + value;
}
if (!node){
  node = document;
}
var inputs = node.getElementsByTagName('input');
// getElementsByTagNameで取得した要素を走査中に置き換えると
// 繰り上がりされるので、繰り上がりが起きても良いように、
// 後ろから走査する
var n = inputs.length;
while (n){
  n--;
  var _input = inputs[n];
  var type = _input.getAttribute('type');
  if(type === 'range' && !('step' in _input)){
    init(_input);
  }
}
function init(_input){
  var parent = _input.parentNode;
  var _min = parseInt(_input.getAttribute('min'), 10)||0;
  var _max = parseInt(_input.getAttribute('max'), 10)||100;
  value = parseInt(_input.value, 10) - _min || 0;

  var outer = document.createElement('div');
  var inner = document.createElement('div');
  var _button = document.createElement('input');
  _button.type = 'button';
  outer.className = 'js-slider';
  outer.style.width = (_max - _min)+ 'px';
  parent.insertBefore(outer, _input);
  outer.appendChild(inner);
  outer.appendChild(_button);
  if(window.ActiveXObject){
    // IEはinputのtypeをhiddenに書き換えることができない
    // 少々強引だが、outerHTMLについて置換を行う
    parent.removeChild(_input);
    _input = document.createElement(
      _input.outerHTML.replace('type=range', 'type=hidden')
    );
    parent.insertBefore(_input, outer.nextSbling || null);
  } else {
    _input.type = 'hidden';
  }
  button = _button;
  input = _input;
  set_value();
  addEvent(outer,'click', function(evt){
    dragging = true;
    min = _min;
    max = _max;
    input = _input;
    button = _button;
    slider = outer;
    mousemove(evt);
  });
  addEvent(_button,'mousedown', function(evt){
    if(!evt){
      evt = window.event;
    }
    dragging = true;
    min = _min;
    max = _max;
    input = _input;
    button = _button;
    slider = outer;
    if (evt.preventDefault){
      evt.preventDefault();
    } else {
      evt.returnValue = false;
    }
    addEvent(document, 'mousemove', mousemove);
  });
}
addEvent(document, 'mouseup', function(evt){
  if(dragging){
    dragging = false;
    removeEvent(document, 'mousemove', mousemove);
    input = null;
    button = null;
    slider = null;
  }
});
function mousemove(evt){
  if(!evt){
    evt = window.event;
  }
  if(dragging){
    var left = evt.clientX;
    var rect = slider.getBoundingClientRect();
    var width = button.clientWidth / 2;
    // マウス座標とスライダーの位置関係で値を決める
    value = Math.round(left - rect.left - width);
    // スライダーからはみ出したとき
    if (value < 0) {
      value = 0;
    } else if (value > slider.clientWidth) {
      value = slider.clientWidth;
    }
    set_value();
    if (evt.preventDefault){
      evt.preventDefault();
    } else {
      evt.returnValue = false;
    }
  }
}
}

いくつかポイントがありますが、まず、!('step' in _input) でrangeをサポートしているか簡易チェックしています。rangeをサポートしているブラウザではそのままrangeを使うようにします。そのため、ChromeやSafariではネイティブのスライダーが表示されていると思います。

また、IEは一度作ったinput要素のtypeをあとからhiddenにすることができません。document.createElement('<input type="hidden">') のようにして最初からhiddenな要素として作る必要があります(ちなみに、createElementにタグ名でなくタグ全体を書くのもIEの独自拡張です⁠⁠。

まとめ

今回はJavaScriptを使ったUIとしてスライダーパーツの実装方法を紹介しました。こちらもまだまだ改良の余地があるので、是非自分なり(もしくはHTML5の仕様にそって)に改良してみてください。

次回も引き続きJavaScriptを使ったクロスブラウザなUIの実装を見ていきたいと思います。

おすすめ記事

記事・ニュース一覧