サイドバーに現在位置を表示して追尾する目次を設置する

※追記(2018/01/15)
新しいバージョンを作ったので今後はこちらを参考にしてください。

※追記(2016/12/29)
サイドバーに現在位置を表示して追尾する目次を設置する【目次記法対応版】 - Twilyze blog

※追記(2016/12/22)
この記事から色々変更してるので終わったら新しく記事を書きます。(作業中)




前に記事上に目次を設置したけど
はてなブログを便利にするカスタマイズ - Twilyze blog

サイドバーにも欲しい。ついでに追尾してほしい。

やること

  • サイドバーに目次を設置
    • 現在位置の背景色変更
    • スクロールすると位置が固定(追従)される

やり方

HTMLとjavascript

デザイン設定の[カスタマイズ]-[サイドバー]-[モジュールを追加]-[HTML]に貼り付ける。
(タイトルは空白)

<div id='sectionListSide'></div>

<script src='//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'></script>
<script>
$(window).load(function(){
  // 記事ページの時だけ
  if ( !$('body').hasClass('page-entry') )
    return;

  var win = $(window);
  var lastModule = $('#sectionListSide');

  //-----------------------------------
  // 目次作成
  var list = [];
  var currentLevel = 0;
  var entryContentLen = 0;
  var sectionQuery = '.entry-content h2,.entry-content h3,.entry-content h4';
  var sectionTopArr = [];

  // 見出しを検索
  $(sectionQuery).each(function(i){
    var self = this;
    var idName = 'section' + i;
    var level = 0;

    $(self).attr('id', idName);
    list.push('<li><a href="#' + idName + '">' + $(self).text() + '</a>');
    if (self.nodeName.toLowerCase() == 'h3') {
      level = 1;
    } else if (self.nodeName.toLowerCase() == 'h4') {
      level = 2;
    }
    while (currentLevel < level) {
      list[i] = '<ol class="chapter">' + list[i];
      currentLevel++;
    }
    while (currentLevel > level) {
      list[i] = '</ol></li>' + list[i];
      currentLevel--;
    }
    entryContentLen++;

    // 各sectionの位置を保存
    sectionTopArr[i] = $(self).offset().top;
  });
  // 見出しが2つ以上あったら目次を表示する
  if (entryContentLen >= 2) {
    // サイドバーに追加する
    lastModule.append('<div class="sectionList"><h5>目次</h5><ol>' + list.join('') + '</ol></div>');
  }

  // スクロールを滑らかにする
  var lastModule_a = $('a', lastModule);
  lastModule_a.on('click', function() {
    $('html,body').animate({scrollTop: $(this.hash).offset().top}, 300);
    return false;
  });


  //-----------------------------------
  // 現在位置の設定
  var current = -1;
  function setCurrent( i ) {
    if (i != current) {
      current = i;
      lastModule_a.removeClass('current');
      lastModule_a.eq(i).addClass('current');
    }
  }

  setCurrent(0);  // 初期位置登録


  //-----------------------------------
  // 最後のモジュールを追従させる
  var MARGIN = 15;  // モジュールを固定した時の余白
  var scrollHeight = $(document).height();

  // fixedの時見た目が変わらないように横幅を指定
  lastModule.css('width', $('#box2').width());

  // モジュールの親要素の高さをコンテンツ部分の高さに合わせる
  $('#box2-inner').css({'height': $('#content').height(), 'position': 'relative'});

  // モジュールを固定したい位置 と 下までスクロールした時にモジュールの固定を解除したい位置を取得
  var arr = (function () {
    var container = $('#container');

    // サイドバーより上の部分の高さ
    // #header-body.height : 37
    var headerHeight = 37 + parseInt(container.css('margin-top'), 10);
    var blogTitleHeight = $('#blog-title').outerHeight(true);

    // 最後のモジュール以外のサイドバー高さ合計
    var sidebarHeight = 0;
    var module = $('.hatena-module');
    var moduleIndexLast = module.length - 1;
    var moduleMarginBottom = parseInt(module.css('margin-bottom'), 10);
    module.each(function(i) {
      if ( i >= moduleIndexLast )
        return false;
      sidebarHeight += $(this).outerHeight(true);
    });

    var stop = headerHeight + blogTitleHeight + sidebarHeight - MARGIN;

    var props = container.css(['margin-bottom', 'padding-bottom']);
    var containerBottom = parseInt(props['margin-bottom'], 10) + parseInt(props['padding-bottom'], 10);
    var start = scrollHeight - $('#footer').outerHeight() - containerBottom
      - moduleMarginBottom - lastModule.outerHeight() - (MARGIN * 2);

    return [stop, start];
  }());
  var scrollStop = arr[0];
  var scrollStart = arr[1];


  //-----------------------------------
  // スクロールイベント
  var SCROLL_MARGIN = 50;
  var sectionIndexLast = sectionTopArr.length - 1;
  win.scroll(function () {
    var winHeight = win.height();
    var scrollRange = scrollHeight - winHeight;
    var scrollTop = win.scrollTop();

    // 現在位置
    if (scrollTop <= SCROLL_MARGIN) {
      setCurrent(0);
    }
    else if ( (scrollRange - scrollTop) <= SCROLL_MARGIN) {
      setCurrent(sectionIndexLast);
    }
    else {
      for (var i = sectionIndexLast; i >= 0; i--) {
        if (scrollTop > sectionTopArr[i] - SCROLL_MARGIN) {
          setCurrent(i);
          break;
        }
      }
    }

    // 追従させる
    if (scrollStart < scrollTop ) {
      // モジュールの固定を解除する処理
      lastModule.css({'position': 'absolute', 'bottom': MARGIN + 'px', 'top': ''});
    } else if (scrollStop < scrollTop) {
      // モジュールを固定する処理
      lastModule.css({'position': 'fixed', 'top': MARGIN + 'px', 'bottom': ''});
    } else {
      // モジュールを通常の位置に戻す
      lastModule.css('position', 'static');
    }
  });
});
</script>

css

デザイン設定の[カスタマイズ]-[デザインCSS]に貼り付ける。

/* 目次(サイドバー) */
#sectionListSide h5 {
  font-size: 16px;
  font-weight: normal;
  margin: 0px;
}
#sectionListSide > ol {
  padding: 0px;
  margin-top: 8px;
}
#sectionListSide ol {
  padding: 0px;
  margin: 0px;
  list-style-type: none;
}
#sectionListSide ol .chapter {
  padding: 0px 0px 0px 10px;
}
#sectionListSide li > a {
  color: #7d9ab7;
  padding-left: 2px;
  display: block;
}
#sectionListSide li > a.current {
  background-color: #efefef;
}
#sectionListSide li > a:hover {
  background-color: #efefef;
  text-decoration: none;
}

少しだけ解説

このブログに合わせて作ったのでそのままでは追従する時の位置がずれるかもしれません。

javascript90行目付近の「サイドバーより上の部分の高さ」の辺が追従開始位置を決めるところなのでここを調整すれば合わせられるかも。 ヘッダー部分の高さはなぜか取得できなくてブラウザで調べてそのまま打ち込んでます。
Chromeの場合F12を押すと開く画面の虫眼鏡アイコンを押すと調べられる)


※追記(2016/01/20)
ウィンドウの横幅が小さい時に隠れてしまうのは仕様です。


他にもおかしなところがあったら各自頑張る感じで……

参考