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

第21回JavaScriptによるUIの実装:タブメニュー編

こんにちは。前回から引き続き、JavaScriptによるUIを実装する方法を紹介していきます。

基本のタブメニュー

ウェブアプリでよく使われるインタフェースのひとつ、タブメニューを実装してみましょう。まず、骨組みとなる基本のHTMLは以下のとおりです。

タブメニューの基本HTML
<div class="js-tabs">
  <ul id="tab_menu1" class="tab_menu">
    <li><a href="#page1-1">Page 1</a></li>
    <li><a href="#page1-2">Page 2</a></li>
    <li><a href="#page1-3">Page 3</a></li>
  </ul>
  <div id="tab_content1" class="tab_content">
    <div id="page1-1" class="page">
      Page 1
    </div>
    <div id="page1-2" class="page">
      Page 2
    </div>
    <div id="page1-3" class="page">
      Page 3
    </div>
  </div>
</div>

そして、CSSは次のとおりです。ひとまず今回はサイズを固定にして、特に凝ったスタイルは使っていません。あえて言えば、li要素をinlineにして横に並べている点と、リンクに対してアクティブなとき・マウスを乗せたときで、スタイルを変えている点がポイントです。

タブメニューの基本CSS
.js-tabs ul.tab_menu{
  list-style-type:none;
  margin:0px;
  padding:0px;
}
.js-tabs ul.tab_menu li{
  display:inline;
  background:#666;
  margin:0px;
  padding:2px;
}
.js-tabs .tab_menu li a{
  padding:3px;
}
.js-tabs .tab_menu li a:link,
.js-tabs .tab_menu li a:visited{
  background-color:#666;
  color:#fff;
}
.js-tabs .tab_menu li a.active:link,
.js-tabs .tab_menu li a.active:visited{
  background-color:#444;
  color:#fff;
}
.js-tabs .tab_menu li a:hover{
  background-color:#333;
  color:#f0f;
}
.tab_content{
  position:relative;
  height:200px;
}
.tab_content div.page{
  width:450px;
  height:200px;
  position:absolute;
  color:#222;
}
#page1-1{
  background-color:#ffa;
}
#page1-2{
  background-color:#faf;
}
#page1-3{
  background-color:#aff;
}

さて、このHTMLを操作するJavaScriptです。まずはなるべくシンプルに実装してみます。

タブメニューの基本JavaScript
(function(){
var menu = document.getElementById('tab_menu1');
var content = document.getElementById('tab_content1');
var menus = menu.getElementsByTagName('a');
var current; // 現在の状態を保持する変数
for (var i = 0, l = menus.length;i < l; i++){
  tab_init(menus[i], i);
}
function tab_init(link, index){
  var id = link.hash.slice(1);
  var page = document.getElementById(id);
  if (!current){ // 状態の初期化
    current = {page:page, menu:link};
    page.style.display = 'block';
    link.className = 'active';
  } else {
    page.style.display = 'none';
  }
  link.onclick = function(){
    current.page.style.display = 'none';
    current.menu.className = '';
    page.style.display = 'block';
    link.className = 'active';
    current.page = page;
    current.menu = link;
    return false;
  };
}
})();

タブを切り替えるには、クリックしたリンクに対応するコンテンツを表示するだけでなく、それまで表示されていたコンテンツを非表示にする必要があります(ついでにリンクのスタイルも変更⁠⁠。今回は現在表示されているコンテンツ・リンク自体を変数(current)に入れておき、現在のコンテンツを非表示に変更してから、次のコンテンツを表示する、という方法で実装しました。

なお、for文でのループの中でtab_init関数を呼び出していますが、この関数の中身をfor文の中に直接書いても問題ないように見えると思います。しかし、実際には直接書いてしまっては意図通りに動作しなくなります。その理由はもちろんクロージャーです。

上記コードにおいて、tab_init内のlink・pageなどの変数はtab_init内のローカル変数であり、同時にtab_init内部のonclickに設定された関数からもlinkとpageは参照可能です。

つまり、変数のスコープは次のようになっています(こちらは実際に動くコードではありません⁠⁠。

クロージャーと関数
function (){
  var current;
  function (){ // #1
    var page, link;
    function(){
      // onclick
    }
  }
  function (){ // #2
    var page, link;
    function(){
      // onclick
    }
  }
  function (){ // #3
    var page, link;
    function(){
      // onclick
    }
  }
}

変数pageとlinkがそれぞれ3つ存在する点がポイントです。

クロージャーの復習はこれくらいにして、実際の動作サンプルは次のとおりです。

Page 1
Page 2
Page 3

機能的には問題ありませんが、リンクの間の隙間が気になると思います。これは、⁠li要素をインラインにしているので)li要素の間にある改行が空白文字列として認識されてしまうためです。

これを回避する方法はいくつかあり、単純にliとliの間の改行を取り除くというのが手軽な方法です。

または、HTMLではli要素は終了タグを省略できること、タグを省略した場合に改行がその前のタグの中身になる点を利用する方法もあります(加えて、li要素をinlineではなくinline-blockにする必要があります⁠⁠。ただ、今回のケースではIEでリンクの後ろに空白が残ってしまいます。

というわけで、今回は改行の位置を工夫してリンクの中に空白を入れるようにしてみます。

タブメニューの基本HTML(改行位置の変更)
<div class="js-tabs">
  <ul id="tab_menu1" class="tab_menu">
    <li><a href="#page1-1"> Page 1 
    </a></li><li><a href="#page1-2"> Page 2
    </a></li><li><a href="#page1-3"> Page 3
    </a></li>
  </ul>
  <div id="tab_content1" class="tab_content">
    <div id="page1-1" class="page">
      Page 1
    </div>
    <div id="page1-2" class="page">
      Page 2
    </div>
    <div id="page1-3" class="page">
      Page 3
    </div>
  </div>
</div>
Page 1
Page 2
Page 3

タブメニューのアニメーション

さて、タブメニューの切り替えを簡単なアニメーションにしてみます。

まず、HTMLはほぼ同じです。変更点はひとつだけで、コンテンツ部分のdivを増やしました。

タブメニューのHTML
<div class="js-tabs3">
  <ul id="tab_menu3" class="tab_menu">
    <li><a href="#page3-1"> Page 1 
    </a></li><li><a href="#page3-2"> Page 2
    </a></li><li><a href="#page3-3"> Page 3
    </a></li>
  </ul>
  <div id="tab_content3" class="tab_content">
    <div id="tab_content_inner3" class="tab_content_inner">
      <div id="page3-1" class="page">
        Page 1
      </div>
      <div id="page3-2" class="page">
        Page 2
      </div>
      <div id="page3-3" class="page">
        Page 3
      </div>
    </div>
  </div>
</div>

CSSも概ね同じですが、tab_contentはposition:relativeに、追加したinnerとpageはposition:absoluteにしています。

さらに、innerはwidthを1350pxと大きく取りました。このinnerの中に各pageを横に並べて配置した状態でinnerを左右に動かせば、表示されるpageが切り替わるという仕組みです。

タブメニューのCSS
.tab_content{
  position:relative;
  height:200px;
  width:450px;
  overflow:hidden;
}
.tab_content_inner{
  position:absolute;
  height:200px;
  width:1350px;
  overflow:auto;
}
.tab_content div.page{
  width:450px;
  height:200px;
  position:absolute;
  top:0px;
  color:#222;
}
#page3-1{
  background-color:#ffa;
  left:0px;
}
#page3-2{
  background-color:#faf;
  left:450px;
}
#page3-3{
  background-color:#aff;
  left:900px;
}

最後にJavaScriptで450px動かすようにするだけです。とてもシンプルですね。

タブメニューのJavaScript
(function(){
var menu = document.getElementById('tab_menu3');
var inner = document.getElementById('tab_content_inner3');
var menus = menu.getElementsByTagName('a');
var current;
for (var i = 0, l = menus.length;i < l; i++){
  tab_init(menus[i], i);
}
function tab_init(link, index){
  if (!current){
    current = link;
    link.className = 'active';
  }
  link.onclick = function(){
    current.className = '';
    link.className = 'active';
    current = link;
    inner.style.left = -450 * index + 'px';
    return false;
  };
}
})();
Page 1
Page 2
Page 3

あとは、このleftの値をアニメーションさせるだけです。第17回で作ったMiniTweenerを使ってアニメーションさせましょう。

タブメニューのJavaScript(アニメーション対応)
(function(){
var menu = document.getElementById('tab_menu4');
var inner = document.getElementById('tab_content_inner4');
var menus = menu.getElementsByTagName('a');
var current;
for (var i = 0, l = menus.length;i < l; i++){
  tab_init(menus[i], i);
}
function tab_init(link, index){
  if (!current){
    current = {menu:link, index:index};
    link.className = 'active';
  }
  link.onclick = function(){
    current.menu.className = '';
    link.className = 'active';
    current.menu = link;
    new MiniTweener(inner.style, {
      left:{
        from: current.index * -450,
        to: -450 * index,
        suffix:'px'
      }
    }, {duration:300});
    current.index = index;
    return false;
  };
}
})();
Page 1
Page 2
Page 3

タブメニューの汎用化

ここまで、幅や高さなどの値が決め打ちで、汎用的ではありませんでした。最後に少しだけ汎用化してみましょう。

まず、HTMLはこれまで通りです。CSSはtab_content、inner、pageに指定していたwidthとheightを取り除き、代わりに各ページごとにバラバラなサイズを指定します。

タブメニューのCSS(汎用化)
.js-tabs5 .tab_content{
  position:relative;
  overflow:hidden;
}
.js-tabs5 .tab_content_inner{
  position:absolute;
  overflow:auto;
}
.js-tabs5 .tab_content div.page{
  position:absolute;
  top:0px;
  color:#222;
}
#page5-1{
  background-color:#ffa;
  width:450px;
  height:150px;
}
#page5-2{
  background-color:#faf;
  width:400px;
  height:200px;
}
#page5-3{
  background-color:#aff;
  width:350px;
  height:250px;
}

あとはJavaScriptで各pageごとのサイズを取得し、それをtab_contentとinnerに反映・調整してあげるだけです。

要素のサイズを取得するにはclientHeight, clientWidthを用います。ほかにgetBoundingClientRectメソッドを利用するという方法もあります。getBoundingClientRectは要素のサイズや位置情報をまとめて取得できる便利なメソッドです。元々IEが独自に実装したメソッドでCSSOM View Moduleで標準化されており、ほとんどのブラウザで利用できます(万が一、Firefox 2をサポートしなければいけない場合、getBoundingClientRectをサポートしていないので、ほかの方法で代替する必要があります⁠⁠。ただし、IEのgetBoundingClientRectはtop, right, bottom, leftの4つのプロパティしかサポートしていません。今回のように幅と高さを取得したい場合はbottom-topのように計算が必要になります。

タブメニューのJavaScript(汎用化)
var menu = document.getElementById('tab_menu5');
var content = document.getElementById('tab_content5');
var inner = document.getElementById('tab_content_inner5');
var menus = menu.getElementsByTagName('a');
var current,
    width = 0,  // innerの幅
    height = 0; // innerの高さ
var lefts = []; // 各要素のleft値を記憶するための配列
for (var i = 0, l = menus.length;i < l; i++){
  tab_init(menus[i], i);
}
inner.style.width = width + 'px';

function tab_init(link, index){
  var id = link.hash.slice(1);
  var page = document.getElementById(id);
  page.style.left = width + 'px';
  page.style.top = '0px';
  lefts[index] = width;
  width += page.clientWidth;
  if (height < page.clientHeight) {
    height = page.clientHeight;
    inner.style.height = 20 + height + 'px';
  }
  if (!current) {
    current = {menu:link, index:index};
    link.className = 'active';
    content.style.width = width + 'px';
    content.style.height = height + 'px';
  }
  link.onclick = function(){
    current.menu.className = '';
    link.className = 'active';
    current.menu = link;
    // 中身にあわせてcontentのサイズを修正
    content.style.width = page.clientWidth + 'px';
    content.style.height = page.clientHeight + 'px';
    // 記憶しておいた位置を使って移動させる
    new MiniTweener(inner.style, {
      left:{
        from: -lefts[current.index],
        to: -lefts[index],
        suffix:'px'
      }
    }, {duration:300});
    current.index = index;
    return false;
  };
}
})();
Page 1
Page 2
Page 3

まとめ

今回はJavaScriptを使ったUIとしてタブメニューの作り方を取り上げました。こういったUIはまだまだつくり込む余地がたくさんあります。是非、自分なりの改良を加えてみてください。

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

おすすめ記事

記事・ニュース一覧