uhyohyo.net

JavaScript初級者から中級者になろう

七章第五回 サンプル:見出しのリスト

このページの最終更新日:

仕様を決める

今回はちょっと息抜きに、今まで学習した内容を用いて何か実践的なサンプルを作ってみます。今回作るのは、ある文書のアウトライン(見出しやセクションの構造)からリストを作るということです。具体的には、


<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>

    <h1>見出し1</h1>

    <h2>見出し1-1</h1>

    <h2>見出し1-2</h2>

    <h3>見出し1-2-1</h3>

    <h1>見出し2</h1>

    <h2>見出し2-1</h2>

    <h2>見出し2-2</h2>
  </body>
</html>

という見出しの構造があったとき、


<ol>
  <li>見出し1
    <ol>
      <li>見出し1-1</li>
      <li>見出し1-2
        <ol>
          <li>見出し1-2-1</li>
        </ol>
      </li>
    </ol>
  </li>
  <li>見出し2
    <ol>
      <li>見出し2-1</li>
      <li>見出し2-2</li>
    </ol>
  </li>
</ol>

というリストを作りたいわけです。具体的なリストにすると、

  1. 見出し1
    1. 見出し1-1
    2. 見出し1-2
      1. 見出し1-2-1
  2. 見出し2
    1. 見出し2-1
    2. 見出し2-2

というようになります。

同じレベル(同じ数字)の見出しはあるol要素の兄弟として扱い、h2要素のol要素はその上にあるh1要素にあたるli要素の子としてあるようにします。h3要素も同様にh2要素にあたるli要素の子にします。以下、h6まで同様です。

ちなみに、h2のすぐ下にh4が来たりとかするように、数字をとばす場合は考えません。そういった方法はすすめられたものではないからです。ちゃんと数字が飛ばさず順番になっている構造を扱うという前提でいきます。

今回は少しずつ、これを実装する方法を解説していきます。自分でできそうだと思ったらぜひこのページを見ないで続きを実装してみてください。

方法を考える

さて、ではどうすればいいかを考えましょう。まず、見出し要素1つにつき1つのli要素がリストに追加されることは明らかです。そこで、h1〜h6要素それぞれについて上から順番に処理しながらli要素を追加していくことになります。

これを実現するにはTreeWalker七章第三回)を使えばいいですね。「h1〜h6要素」という条件で要素を見ていきます。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });

第三引数の中で、正規表現が使われています。node.tagNameが「Hのあとに数字の1〜6のどれかが続く文字列」という条件に当てはまればFILTER_ACCEPTを返すということになります。

そして、順番にそれぞれを処理していきます。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
while(node=walker.nextNode()){

}

それぞれの処理ですることは、li要素を作って追加するということです。だからそうすればいいのですが、追加先は何なのでしょう。当然、ol要素です。だから、ol要素を作っておき、それに追加しましょう。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
while(node=walker.nextNode()){
  var newli = document.createElement('li');
  ol.appendChild(newli);
}

さて、これだと、見出しの大きさ(数字)などは関係なしに全て1つのol要素に全て突っ込んでしまいます。そうではありませんね。

次に、見出しの数字に応じて処理を変える必要があります。小さい見出しが出てきたなら、新しいol要素を作ってli要素に追加し、それを追加先とします。逆に、大きい見出しが出てきたら、追加先をひとつ上のol要素とします。同じなら、今のol要素にそのまま追加すればいいので何もしません。

ここで、「小さい」とか「大きい」とかいうことを判別するには、「今の番号」が何か覚えておく必要があります。変数に入れておきましょう。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
var number = 1;
while(node=walker.nextNode()){
  var newli = document.createElement('li');
  ol.appendChild(newli);
}

最初から数字が1のときのolは用意してあるので、「現在の数字」は1とします。

この数字と処理する見出し要素(nodeに代入されてるやつですね)を処理するわけですが、ではnodeの番号はどうやって取得するのでしょう。ここで、また正規表現を使います。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
var number = 1;
while(node=walker.nextNode()){
  var result = node.tagName.match(/^H([1-6])$/);
  var itsnumber = parseInt(result[1]);
  var newli = document.createElement('li');
  ol.appendChild(newli);
}

この2つの数字を比べて処理します。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
var number = 1;
while(node=walker.nextNode()){
  var result = node.tagName.match(/^H([1-6])$/);
  var itsnumber = parseInt(result[1]);

  if(number < itsnumber){
    //数字が大きい
    var newol = document.createElement('ol');
    ol.lastChild.appendChild(newol);
    ol = newol;	//追加先を新しいolにする

  }else if(number > itsnumber){
    //数字が小さい
    for(var i=0;i<number-itsnumber;i++){
      ol = ol.parentNode.parentNode;
    }
  }
  number = itsnumber;	//「現在の番号」を更新

  var newli = document.createElement('li');
  ol.appendChild(newli);
}

数字が大きいとき、ol要素を作っています。その追加先は、ol.lastChildとなっています。これはすなわち、直前に追加したli要素です。

また、数字が小さいときは、for文を使っています。これは(number-itsnumber)回繰り返すという書き方で、H2の階層に追加した後にH1がきた場合は1階層上に上がればいいですが、例えばH3の次にいきなりH1が出た場合などは、2階層上がる必要があります。そのため、このように処理しています。

さて、これでリストは完成しました。動作を確かめるために、表示してみましょう。表示するにはdocument.bodyにでもappendChildすればいいのですが、ここでこのままolをappendChildしてはいけません。

なぜなら、このolはリストの一番上(H1要素のリスト)ではないかもしれないからです。最初にolを作った段階で先に追加しておくという手もありますが、次のような方法もあります。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
var number = 1;
while(node=walker.nextNode()){
  var result = node.tagName.match(/^H([1-6])$/);
  var itsnumber = parseInt(result[1]);

  if(number < itsnumber){
    //数字が大きい
    var newol = document.createElement('ol');
    ol.lastChild.appendChild(newol);
    ol = newol;	//追加先を新しいolにする

  }else if(number > itsnumber){
    //数字が小さい
    for(var i=0;i<number-itsnumber;i++){
      ol = ol.parentNode.parentNode;
    }
  }
  number = itsnumber;	//「現在の番号」を更新

  var newli = document.createElement('li');
  ol.appendChild(newli);
}
while(ol.parentNode){
  ol=ol.parentNode;
}
document.body.appendChild(ol);

whileでは、ol.parentNodeがなくなるまで、ひたすら親を巡り続けます。最初のolはまだ追加されてないので、一番上のolには親がなく、そこで止まります。最後にこれを追加すれば、見事完成です。

……といいたいですが、実はまだ完成とはいえません。li要素を作ったとき、肝心なことを忘れています。li要素のテキストとなる、テキストノードを追加していません。

さて、テキストノードの中身ですが、見出しのテキストをそのまま持ってくるというのがいいですね。このノードの子ノードがテキストノードだから、それをコピーして持ってくればいいと思うかもしれませんが、単純にそうするわけにもいきません。なぜなら、

<h1>あああ<strong>いいい</strong>ううう</h1>

のような場合があるからです。この場合、"あああいいいううう"を見出しのテキストとして取得したいです。実は、そのように、中に含まれる要素などに関わらず中身を全てテキストで手に入れる方法があります。それはノードが持つtextContentというプロパティです。

これは初登場のプロパティです。このプロパティはノードの中身のテキストを取得するためのものであり、中にある要素の木構造等を無視してテキストでつなげて表示してくれます。

また、textContentプロパティに文字列を代入することができ、その場合その要素の子がテキストノード1つになります。これは、ある要素の内容を単なるテキストにしたい場合に便利です。特に、新しい要素を作ってその内容をテキストとしたい場合に、実はdocument.createTextNodeとappendChildを使う必要がなく、textContentにテキストを代入するだけでよいというのは便利です。

これを利用すると次のようになります。


var walker = document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,
  function(node){
    if(/^H[1-6]$/.test(node.tagName)){
      return NodeFilter.FILTER_ACCEPT;
    }else{
      return NodeFilter.FILTER_SKIP;
    }
  });
var node;
var ol = document.createElement('ol');
var number = 1;
while(node=walker.nextNode()){
  var result = node.tagName.match(/^H([1-6])$/);
  var itsnumber = parseInt(result[1]);

  if(number < itsnumber){
    //数字が大きい
    var newol = document.createElement('ol');
    ol.lastChild.appendChild(newol);
    ol = newol;	//追加先を新しいolにする

  }else if(number > itsnumber){
    //数字が小さい
    for(var i=0;i<number-itsnumber;i++){
      ol = ol.parentNode.parentNode;
    }
  }
  number = itsnumber;	//「現在の番号」を更新

  var newli = document.createElement('li');
  newli.textContent=node.textContent;
  ol.appendChild(newli);
}
while(ol.parentNode){
  ol=ol.parentNode;
}
document.body.appendChild(ol);

サンプル

これで完成です。このページで試してみました。ちゃんと動作していることが分かりますね。