jquery 見出しタグ(h1~h6)へのリンクリストを動的に生成する

2013年5月2日

ページの内容が多くなった場合などに、各見出しタグ(h1~h6)へのリンクのリストを jQuery を使って動的に生成する方法。

概要

  • リストとそれを表示する領域を ul 要素と div 要素で生成し、表示する位置へ配置(prependTo)。
  • 見出しタグ(h1~h6)要素のラップ集合「$(‘:header’)」のそれぞれの要素を調べる(each)。
  • 以下の例の場合は「h3~h5」の要素を対象にしている「for(var i = 3; i <= 5; i++)」。
  • 変数 elem に要素名を代入(例 h3)。
  • 現在調べている要素がその要素名と一致すれば、変数 idValue に「要素名 + 出現順(elem + n)」を代入する。
  • 現在調べている要素の id 属性を idValue として、リンクからのジャンプ先とする。
  • 現在調べている要素のテキスト(見出しの内容)「$(this).text() 」を使い、リスト(li)のテキストとする
  • リストのテキストを a 要素で囲みリンクとして、 href 属性の値を「”#’ + idValue」としてジャンプできるようにする。
  • それを li 要素として、最初に生成した ul 要素の末尾に追加(append)する
$('<div><ul id="index_list"></ul></div>').prependTo('div.single_content');
      $(':header').each(function(n) {
        for(var i = 3; i <= 5; i++) {
          var elem = 'h' + i;
          if($(this).is(elem)){
            var idValue = elem + n;
            $(this).attr({id: idValue});
            var li = '<li><a href="#' + idValue + '">' + $(this).text() + '</a></li>';
            $('ul#index_list').append(li);
          }   
        }
      });

上記のままだと、全てのページの全ての見出し要素に対してリンクリストを生成してしまうので、リンクリストを生成したい見出し要素にクラス「index」を指定している場合だけリンクリストを生成するようにする。(下の例の1行目)

また、下記の例では

  • h3 要素を表示した横に(スタイルで float:rightを指定)、先頭へ戻るリンクを追加
  • スタイルを指定しやすいように div 要素や li 要素に id や class 属性を指定
  • また、サイドバーなどの見出し要素はリンクリストに表示したくないので「$(‘div.content :header’).each」としてコンテクストを指定して、見出し要素を抽出する範囲を限定
if($(':header').is('.index')) {
      $('<div id="index_area"><ul id="index_list"></ul></div>').prependTo('div.content');
      $('div.content :header').each(function(n) {
        for(var i = 3; i <= 5; i++) {
          var elem = 'h' + i;
          if($(this).is(elem)){
            var idValue = elem + n;
            $(this).attr({id: idValue});
            var li = '<li class="' + elem + '"><a href="#' + idValue + '">' + $(this).text() + '</a></li>';
            $('ul#index_list').append(li);
          }   
        }
      }); 
      $('h3').after('<p style="float:right;"><a href="#">back to top</a></p>');
}

オプションを追加して、関数にする

以下はさらにオプションを追加して、関数にした例で、パラメータに何も指定しなければ、デフォルトの値で表示される。

  • 16行目:h1~h6 のいずれかの要素にクラス「h_index」が付いている場合にのみ
  • 18行目:パラメータ「area」で指定した領域のh1~h6 の全ての要素に対して実行
  • 19行目~27行目:for文で現在対象となっている要素がh1~h6 のどの要素かを判定(21行目)して、id を付加してそれへのリンク要素を作成し、ul 要素に追加。
  • 30行目:先頭に戻るリンクを表示する場合、「先頭に戻るリンク」を配置
function show_index_link(my_options) {  
    var settings = $.extend({
      check_attr : '.h_index',  //h1~h6 のいずれかの要素にこの属性(クラス名)が付いている場合にのみリストを表示
      div_id : 'h_index_area',  //リストを表示するエリア(div 要素)の id 
      ul_id: 'index_list',  //リスト(ul 要素)の id 
      prependToMe : 'div#indexlistarea',  //リストを表示する場所を一意に示す要素名
      area : 'div.content',  //h1~h6 要素を抽出する対象エリア
      begin : 3,  //h1~h6 のどの要素を対象とするか(小さい方の数値を指定)
      end : 5,  //h1~h6 のどの要素を対象とするか(大きい方の数値を指定)
      use_bakcto : false,  //先頭に戻るリンクを表示するかどうか
      backtoElem: 'h3',  //「先頭に戻るリンク」を配置(append)する要素名
      backtoTitle: 'back to top',  //「先頭に戻るリンク」のテキスト
      backtoStyle: ''    //「先頭に戻るリンク」のスタイル
    }, my_options || {});
   
    if($(':header').is(settings.check_attr)) {
      $('<div id="' + settings.div_id + '"><ul id="' + settings.ul_id + '"></ul></div>').prependTo(settings.prependToMe);
      $(settings.area + ' :header').each(function(n) {
        for(var i = settings.begin; i <= settings.end ; i++) {
          var elem = 'h' + i;
          if($(this).is(elem)){
            var idValue = elem + '_index_' + n;
            $(this).attr({id: idValue});
            var li = '<li class="' + elem + '_class"><a href="#' + idValue + '">' + $(this).text() + '</a></li>';
            $('ul#' + settings.ul_id).append(li);
          }   
        }
      }); 
    }
    if(settings.use_backto) {
      $(settings.backtoElem).before('<p style="' + settings.backtoStyle + '"><a href="#">' + settings.backtoTitle + '</a></p>');
    }
    show_index_link({use_backto: true, backtoStyle:'float:right; font-size: 12px; font-size: 1.2rem; font-weight: normal', backtoTitle: 'トップへ'});                
}

特定の見出しを除外する

特定の見出しを除外したい場合は、その見出しに適当なクラスを付与して、それを除外するようにする。

「noindex」というクラスを持つ見出しを除外する場合、.not() を使って18行目を以下のようにする。

$(settings.area + ' :header').not('.noindex').each(function(n) {

使い方

  • リンクリストを表示したい位置に<div id=”indexlistarea”></div>を記述
  • 最初の見出しタグにはクラス「h_index」を指定
  • 後は必要なだけ見出しタグを作成(2つ目以降はクラスの指定は不要)

HTML

<div id="indexlistarea"></div>
<h3 class="h_index">概要</h3>
・・・省略・・・
<h3>オプションを追加して、関数にする</h3>
・・・省略・・・
<h3>アニメーションで表示</h3>

アニメーションで表示

リンク先へのジャンプをアニメーションで表示。これらは show_index_link() 関数を実行した後に記述しないと機能しない(その前ではまだ要素が生成されていないため)。

  • リンクのリストはデフォルトの「ul_id: ‘index_list’」と仮定。
  • リンクのリストからリンク要素を抽出 var h_index_link$ = $(‘#index_list li a’);
  • それぞれに.click() を設定。
  • リンク先の要素の id はその要素の href 属性の値になっている。
  • その id を使って相対位置を取得。
  • animate() でアニメーション。
  • scrolltopvalue – 100 の「100」は調整値(必要がなければ不要)
  • 位置によりスピードを調整(300 + scrolltopvalue / 25)
var h_index_link$ = $('#index_list li a');    
h_index_link$.click(function () {
    var target_element_id = $(this).attr('href');
    var scrolltopvalue = $(target_element_id).offset().top;
    $('body,html').animate({
      scrollTop: scrolltopvalue - 100
    }, 300 + scrolltopvalue / 25);
    return false;
});

‘back to top’(先頭に戻るリンク)をクリックした場合のアニメーション。

  • a 要素のうち、href 属性が「#」のものを抽出してアニメーションさせる。
  • 但し、階層メニューなどで「#」が意味を持つものは除外する。.not()
var back_to_tops$ = $('a').filter(function() {
    return  $(this).attr("href") == "#";
}).not('a.dropdown-toggle');
back_to_tops$.click(function () {
    $('body,html').animate({
      scrollTop: 0
    }, 500);
    return false;
});

(追記)
.filter() を使わなくても「$(“[属性名=’値’]”)」というセレクタを使えば簡単に記述できる。

$('a[href=#]').not('a.dropdown-toggle').click(function () {
    $('body,html').animate({
      scrollTop: 0
    }, 500);
    return false;
  });

また、リンク先とページ先頭へのアニメーションの両方に対応するには以下のように記述できる。
(詳細は「ページ内リンクへアニメーションで移動」)

jQuery(document).ready(function($) {
    $('a[href^=#]').not('a.dropdown-toggle').click(function(){
        var hrefval= $(this).attr('href');
        var positiontop;
        var speed;
        if(hrefval == "#") {
            positiontop = 0;
            speed = 200 + $(this).offset().top /30;
        }else{
            var targetelement = $(hrefval);
            positiontop = targetelement.offset().top -120;
            speed = 200 + (positiontop + 120) / 30;
        }
        $('body,html').animate({
            scrollTop: positiontop
        },  speed);
        return false;
    });
});

リンクのリストをサイドバーに表示

記事が長くなると、先頭のリンクのリスト(インデックス)に戻るのは面倒なので、ある程度以上スクロールするとサイドバーに固定(fixed)して表示するようにする。

リンクのリストが存在すればそのコピーを作成

  • 1行目と2行目:サイドバーに表示する際にその高さや幅を元に表示するのでそれらの情報を取得して変数に格納)
  • 3行目:表示されているかどうかのフラグを設定
  • 7行目:インデックスリンクが存在すれば実行
  • 8行目:コピーを作成(clone())してサイドバーに追加し、CSS を設定
  • 9行目:最初は非表示にしておく
  • 11行目と12行目:コピーした要素(div とその内側の ul 要素)は同じ ID になるため ID を変更
  • 15行目~22行目:作成した要素に CSS を設定(スタイルシートで指定することも可能)
  • 24行目~27行目:後ほど使用するセレクタを変数に代入
var sidebar_height = $('#sidebar').height();
var sidebar_width = $('#sidebar').width();
var is_h_index_visible = false;
var h_index$ = $('#h_index_area');  //リンクのリストのセレクタ

//インデックスリンクが存在すればそのコピーを作成してサイドバーに追加
if(h_index$.length > 0) {
  var h_index_clone$ = $('#h_index_area').clone().appendTo('#sidebar .container').css({
    display: 'none',
    width: sidebar_width,
    padding: '0 10px'      
  }).attr('id', '#h_index_area_side');   //idが重複するので変更
  h_index_clone$.find('ul').attr('id', 'index_list_side');   //idが重複するので変更
    
  $('#index_list_side').css({
    listStyle: 'none',
    lineHeight: '1.5em'
  });
  
  $('#index_list_side  li.h4_class').css('margin-left', '1em');
  $('#index_list_side  li.h5_class').css('margin-left', '2em');
  $('#index_list_side  li.h6_class').css('margin-left', '3em');

  var h_index_height = h_index$.height();
  var h_index_offsetTop = h_index$.offset().top;
  var h_index_li$ = h_index_clone$.find('li');
  var h_index_a$ = h_index_li$.find('a');      
}

リンクのリストをサイドバーに表示

スクロールしてメインの「リンクのリスト」が見えなくなったらサイドバーに表示するようにする。

  • 1行目:header フィルター(:header)と属性セレクタ(id が h から始まり’_index_’を含む )を使って対象とする要素を抽出
  • 2行目:それらの総数を変数に代入
  • 6行目:「リンクのリスト」が存在し、スクロールしてメインの「リンクのリスト」が見えなくなり、かつ元のサイドバーの高さより高い場合に表示
  • 7行目:表示されているかどうかのフラグ(とブラウザの幅:レスポンシブの場合)により、表示するか非表示にする
  • 12行目と13行目:固定して表示するため「position」を「fixed」にして、位置「top」を調整
var index_headers$ = $("[id^='h'][id*='_index_']:header");
var index_headers_length = index_headers$.length;
var window$ = $(window);
window$.scroll(function () {
  var this$ = $(this);  
  if(h_index$.length > 0 && this$.scrollTop() > h_index_height + h_index_offsetTop && this$.scrollTop() > sidebar_height) {
    if(!is_h_index_visible && window$.width() > 768) {
      h_index_clone$.fadeIn(1000);
      is_h_index_visible = true;
    }
    h_index_clone$.css({
      top: 100,
      position: 'fixed'        
    });
  }else{
    if(is_h_index_visible) {
      h_index_clone$.fadeOut(200);
      is_h_index_visible = false;
    }      
  }  
});

6行目では、単純にサイドバーの高さを使用しているが、高さが可変の要素(スライドダウンする等)がある場合は、それらの要素の高さを加えるか、可能であればサイドバー内の最後の要素のポジションとその要素の高さを加えた値を使用するといいかもしれない。

//最後の要素のポジション
var last_elem_pos;
if($('.logo_icon').length > 0) {
  last_elem_pos = $('.logo_icon').position().top + $('.logo_icon').outerHeight(true);
}
//サイドバーの高さではなく最後の要素のポジションを使用  
if(h_index$.length > 0 && this$.scrollTop() > h_index_height + h_index_offsetTop && this$.scrollTop() > last_elem_pos) {
  if(!is_h_index_visible && window$.width() > 768) {
    h_index_clone$.fadeIn(1000);
    is_h_index_visible = true;
  }
  h_index_clone$.css({
    top: 100,
    position: 'fixed'        
  });
}else{
  if(is_h_index_visible) {
    h_index_clone$.fadeOut(200);
    is_h_index_visible = false;
  }      
}

スクロール量により、リンクのリストの表示を変える

スクロール量により、現在どの部分が表示されているかをわかるように、リンクのリストの表示を変える。(21行目から48行目)

  • 対象とする h1~h6 要素(index_headers$)に「.each()」を使って設定
  • id:操作対象の id
  • offset_top:操作対象の位置(オフセットのトップの値)
  • content_height:操作対象のh1~h6 要素に続くコンテンツの高さ(次の操作対象の位置との差分)。初期値は「0」にしておく(最後の操作対象用)
  • next_id:次の操作対象の id
  • next_offset_top:次の操作対象の位置
  • 24行目:最後の要素の場合(次の操作対象がないため別に処理)
  • 31行目:スクロール量が現在の操作対象の位置より大きくかつ次の操作対象の位置より小さい場合(-50 は調整の適当な値)
  • 32行目:クラスを付加(CSS により表示を変更)
var window$ = $(window);
window$.scroll(function () {
  var this$ = $(this);
  //サイドバーにインデックスを表示(前述)  
  if(h_index$.length > 0 && this$.scrollTop() > h_index_height + h_index_offsetTop && this$.scrollTop() > sidebar_height) {
  //省略      
  }
  //現在表示されているコンテンツのサイドバーのインデックスの表示を変更
  index_headers$.each(function(index) {
    var this$ = $(this);
    var id = this$.attr('id');
    var offset_top = document.getElementById(id).offsetTop;
    var content_height = 0;
    //next_id,next_offset_topには最後の場合の値を入れておく(その他の場合は次の if 文で)
    var next_id = index_headers$.eq(index_headers_length - 1).attr('id');
    var next_offset_top = index_headers$.eq(index_headers_length - 1).offset().top;  
    if(index < index_headers_length - 1) {
      content_height = index_headers$.eq(index + 1).offset().top - offset_top;    
      next_id =  index_headers$.eq(index + 1).attr('id');
      next_offset_top = document.getElementById(next_id).offsetTop;
    }
    var window_st = window$.scrollTop();
    
    if(index == index_headers_length - 1) {
      if(window_st > offset_top ) {
        h_index_a$.eq(index).addClass('selected');
      }else{
        h_index_a$.eq(index).removeClass('selected');
      }
    }else{
      if(window_st > offset_top -50 && window_st < next_offset_top -50) {
        h_index_a$.eq(index).addClass('selected');        
      }else{
        h_index_a$.eq(index).removeClass('selected');
      }        
    }      
  });  
});
<!-- comment only -->

間違った方法

最初は以下のように記述したが、リンクのリストの数が増えるにつれ、位置の誤差が大きくなっていって、思ったとおりにならなかった失敗例。(また、下記では get() を使用しているが、eq() の方が良い)

//間違った方法(失敗例)
index_headers$.each(function(index) {
  var this$ = $(this);
  var id = this$.attr('id');
  var offset_top = document.getElementById(id).offsetTop;
  //var offset_top_jq = this$.offset().top;
  var content_height = 0;
  var next_id = $(index_headers$.get(index_headers_length - 1)).attr('id');
  var next_offset_top = $(index_headers$.get(index_headers_length - 1)).offset().top;  
  if(index < index_headers_length - 1) {
    content_height = $(index_headers$.get(index + 1)).offset().top - offset_top;    
    next_id =  $(index_headers$.get(index + 1)).attr('id');
    next_offset_top = document.getElementById(next_id).offsetTop;
  }
  //console.log(this$.text() + Math.floor(content_height) +' top: ' + Math.floor(offset_top) + ' jq: ' + Math.floor(offset_top_jq) + ' diff: ' + Math.floor(offset_top_jq - offset_top));
  //console.log(this$.text() + ' 高さ: ' + Math.floor(content_height) +' top: ' + Math.floor(offset_top) + ' next top: ' + Math.floor(content_height + offset_top )  + ' next_offset_top: ' +Math.floor(next_offset_top) );
      
  window$.scroll(function() { 
    var window_st = window$.scrollTop();
    //var window_height =window$.height();
    if(index == index_headers_length - 1) {
      if(window_st > offset_top ) {
        $(h_index_a$.get(index)).css('color', '#333280');   //default color: #2288cc
      }else{
        $(h_index_a$.get(index)).css('color', '#2288cc');
      }
    }else{
      if(window_st > offset_top && window_st < next_offset_top) {
        $(h_index_a$.get(index)).css('color', '#333280');
      }else{
        $(h_index_a$.get(index)).css('color', '#2288cc');
      }        
    }  
  });
});
<!-- comment only -->

リサイズされた場合の調整

以下はレスポンシブにしていて幅が「768px」より小さい場合は、サイドバーが一番下に来る場合。そのためその場合は非表示にする。また、逆に小さい幅から「768px」以上になった場合は表示するようにする例。

リサイズイベントは多数発生するので「setTimeout」を利用(詳細は「リサイズ $(window).resize が終了した時点で実行」)

  • 3行目:「リンクのリスト」が存在する場合のみ
  • 8行目:ブラウザの幅が「768px」以上でかつスクロール量がメインの「リンクのリスト」の位置より大きく、かつ元のサイドバーの高さより高い場合
  • 9行目:現在のサイドバーの幅を取得
  • 10行目~13行目:CSS で調整して表示
  • 15行目:フラグを「true」に
var timer = false;
$(window).resize(function(){ 
  if(h_index$.length > 0) {
    if (timer !== false) {
      clearTimeout(timer);
    }
    timer = setTimeout(function() {
      if(window$.width() >= 768 && window$.scrollTop() > h_index_height + h_index_offsetTop && window$.scrollTop() > sidebar_height)  {
        sidebar_width = $('#sidebar').width();
        h_index_clone$.css({
        display: 'block',
        width: sidebar_width,
        padding: '0 10px'      
      });
        is_h_index_visible = true;
      }else{  
        h_index_clone$.css('display', 'none');
        is_h_index_visible = false;  
      }
    }, 200);
  }  
});

以下はこのサイトの場合の例(横幅は960pxに変更)

var sidebar_height = $('#sidebar').outerHeight(true);
var sidebar_width = $('#sidebar').width();
var is_h_index_visible = false;
var h_index$ = $('#h_index_area');

//インデックスリンクが存在すればそのコピーを作成してサイドバーに追加
if(h_index$.length > 0) {
  var h_index_clone$ = $('#h_index_area').clone().appendTo('#sidebar .container').css({
    display: 'none',
    width: sidebar_width,
    padding: '0 10px'      
  }).attr('id', 'h_index_area_side');   //idが重複するので変更
  h_index_clone$.find('ul').attr('id', 'index_list_side');   //idが重複するので変更
    
  $('#index_list_side').css({
    listStyle: 'none',
    lineHeight: '1.5em'
  });
  
  if(h_index_clone$.height() > $(window).height() -70) {
    $('#index_list_side  li.h4_class').css('display', 'none');
    $('#index_list_side  li.h5_class').css('display', 'none');
    $('#index_list_side  li.h6_class').css('display', 'none');
  }
  
  $('#index_list_side  li.h4_class').css('margin-left', '1em');
  $('#index_list_side  li.h5_class').css('margin-left', '2em');
  $('#index_list_side  li.h6_class').css('margin-left', '3em');

  var h_index_height = h_index$.height();
  var h_index_offsetTop = h_index$.offset().top;
  var h_index_li$ = h_index_clone$.find('li');
  var h_index_a$ = h_index_li$.find('a');      
}
  
var index_headers$ = $("[id^='h'][id*='_index_']:header");
var index_headers_length = index_headers$.length;

window$.scroll(function () {
  var this$ = $(this);
  var last_elem_pos;
  if($('.logo_icon').length > 0) {
    last_elem_pos = $('.logo_icon').position().top + $('.logo_icon').outerHeight(true);
  }
  //サイドバーにインデックスを表示  
  if(h_index$.length > 0 && this$.scrollTop() > h_index_height + h_index_offsetTop && this$.scrollTop() > last_elem_pos) {
    if(!is_h_index_visible && window$.width() > 960) {
      h_index_clone$.fadeIn(1000);
      is_h_index_visible = true;
    }
    h_index_clone$.css({
      top: 50,
      position: 'fixed'        
    });
  }else{
    if(is_h_index_visible) {
      h_index_clone$.fadeOut(200);
      is_h_index_visible = false;
    }      
  }
  //現在表示されているコンテンツのサイドバーのインデックスの表示を変更
  index_headers$.each(function(index) {
    var this$ = $(this);
    var id = this$.attr('id');
    var offset_top = document.getElementById(id).offsetTop;
    var content_height = 0;
    //next_id,next_offset_topには最後の場合の値を入れておく/その他の場合は次のif文で
    var next_id = index_headers$.eq(index_headers_length - 1).attr('id');
    var next_offset_top = index_headers$.eq(index_headers_length - 1).offset().top;  
    if(index < index_headers_length - 1) {
      content_height = index_headers$.eq(index + 1).offset().top - offset_top;    
      next_id =  index_headers$.eq(index + 1).attr('id');
      next_offset_top = document.getElementById(next_id).offsetTop;
    }
    var window_st = window$.scrollTop();
    
    if(index == index_headers_length - 1) {
      if(window_st > offset_top ) {
        h_index_a$.eq(index).addClass('selected');
      }else{
        h_index_a$.eq(index).removeClass('selected');
      }
    }else{
      if(window_st > offset_top -50 && window_st < next_offset_top -50) {
        h_index_a$.eq(index).addClass('selected');        
      }else{
        h_index_a$.eq(index).removeClass('selected');
      }        
    }      
  });
});

var timer = false;
$(window).resize(function(){     
  if (timer !== false) {
    clearTimeout(timer);
  }
  timer = setTimeout(function() {
    if(h_index$.length > 0) {
      if(window$.width() >= 960 && window$.scrollTop() > h_index_height + h_index_offsetTop && window$.scrollTop() > sidebar_height)  {
        sidebar_width = $('#sidebar').width();
        h_index_clone$.css({
        display: 'block',
        width: sidebar_width,
        padding: '0 10px'      
      });
        is_h_index_visible = true;
      }else{  
        h_index_clone$.css('display', 'none');
        is_h_index_visible = false;  
      }
    }  
  }, 200);    
});