thumbnail

追従&ハイライト、自動スクロールする目次の作り方

記事のサイドバーにある「追従する目次」の作り方を紹介します。また、単に追従させるだけではなく、更に「現在位置のハイライト機能」と「現在位置に応じた目次自体のスクロール機能」を搭載します。

作成する目次の仕様

今回作成する目次の仕様は以下のとおりです。本ブログで搭載している目次になります。

  • サイドバーに常に表示されており、自動的に追従する
  • 現在の見出しをハイライトする
  • 目次の高さを制限して、自動的にスクロールするようにする

それでは目次の作り方を、順を追って説明します。

目次のHTMLコードを出力する

まずは目次のHTMLコードを作成します。
『①自力で0から作成する方法』と『②WordPressを用いてる場合の簡単な方法』の2ケース紹介します。

ケース1: 自力で0から作成する方法

次の手順で作成していきます。

  • ① 目次にしたい要素(h2やh3)にID属性を指定する
  • ② 目次にしたい要素の『テキスト』を取得する
  • ③ 目次のHTMLを組み立てる

順に説明していきます。

①目次にしたい要素にID属性を指定する

話は簡単で、id=~ でIDを割り振っていきます。次のように手動で振っても良いですが、プログラムを書いたほうがミスは少なくなります。

<h2 id="titleA">タイトルA</h2>

プログラムで振る場合は、document.querySelector等でh2やh3の要素を取得し、setAttributeメソッドを用いて記事全体で名前が一意になるようなID要素をつけます。

② 目次にしたい要素の『テキスト』を取得する

document.querySelector等でh2, h3の要素を指定し、innerTextメソッドでタイトルの文字列を取得します。なお、この処理は①と同時に行っても構いません。

③目次のHTMLを組み立てる

①,②で取得した要素を用いて、スクリプトで次のような構造で目次を組み立てます。名前は何でも構いませんが、この記事では次の名前で統一します。#はidで.(ドット)はclassです。

つまり次のようなHTMLになります。

<div id="toc-widget-3">
  <h2 class="widgettitle">目次タイトル</h2>
  <ul class="toc_widget_list">
    <li>
      <a href="#">
        <span>見出し1</span>
      </a>
    </li>
    <li>
      <a href="#">
        <span>見出し1</span>
      </a>
    </li>
  </ul>
</div>

ネーミングルールが変ですが、これはWordPressのプラグインの名前に合わせた結果です。
以上で下準備は終わりです。次のケース2の章はWordPress専用なので飛ばしてください。

ケース2:WordPressを使っている場合

WordPressを使っているならば、Table of Contents Plusというプラグインで、簡単かつ自動的に目次を出力することが出来ます。ケース1の方法で実装した場合はこの章は飛ばしてください。

①functions.phpでウジェットエリアの登録

ウィジェットを利用していない場合は、funtions.phpでサイドバーのウィジェットエリアの登録を行います。

//functions.php
add_action('widgets_init', function() {
  register_sidebar([
    'id' => 'sidebar-1',
    'name' => 'sidebar',
  ]);
});

ウィジェットはデフォルトで<li id="toc-widget-3">タグに囲まれて出力されますが、'before_widget'キーと'after_widget'キーを記述すると出力するタグを他のタグにすることも出来ます。次のコードは<aside>タグにする例です。

//functions.php(出力タグを指定)
add_action('widgets_init', function() {
  register_sidebar([
    'id' => 'sidebar-1',
    'name' => 'sidebar',
    'before_widget' => '<aside id="%1$s" class="widget %2$s %1$s"><div class="%2$s-inner">',
    'after_widget' => '</div></aside>'
  ]);
});

%1$sはtoc-widget-3に、%2$sはtoc_widgetに置換されます。

②Table of Contents Plusプラグインの追加

Table of Contents Plus(通称TOC+)を追加して有効化します。

③TOC+ウィジェットの登録

WordPressの管理画面→「外観」→「ウィジェット」からTOC+のウィジェットを登録します。先程functions.phpでregister_sidebar()を記述しましたが、その際に引数で渡した連想配列のname属性(今回はsidebar)がウィジェットに表示されています。そこに登録して下さい。

④テンプレートで出力

サイドバーのテンプレートファイルにおいて、目次を出力したい場所に以下の記述を追加します。

dynamic_sidebar('sidebar-1');

以下サイドバー直下に記述したものとして説明を進めます。

結果として出力されるHTML構造は次のようになります。
比較のため、本サイトで利用している実際の目次の画像も右に掲載します。

#toc-widget-3全体が目次を表しています。

自分で目次のコードを書いた場合も、概ね上のような構造にすると今回紹介する方法が適用できます。

CSSでスティッキーにする

目次の1番外側の要素のCSSをposition: stickyに指定します。
ウィジェットを他の要素でラップしていない限りは、#toc-widget-3が目次の一番外側の要素になります。
ここで、サイドエリアの高さは記事の高さいっぱいにしないと上手く動作しません。

topの指定も忘れずにしましょう。

#toc-widget-3 {
  position: sticky;
  top: 30px;
}

ここまでのコードで追従する目次の最低要件は満たせます。

ハイライト機能の実装

続いてハイライト機能を実装します。

各見出しの絶対位置を取得する

まずは目次にする要素の絶対位置を取得します。絶対位置とは、ブラウザの最上部からの距離、長さのことです。
絶対位置の詳しい取得方法は以下の記事で説明しています。

ここでは、h2とh3の要素のみを考えます。これらの絶対位置を取得するコードは次のとおりです。

//single-contentを記事のクラスとし、記事内のh2、h3のみ取得
const headingContents = document.querySelectorAll('.single-content h2, .single-content h3')

//各見出しの絶対位置
let headingPos = headingPos = [...headingContents].map(element => Math.floor(element.getBoundingClientRect().top + window.scrollY)) 

スクロール位置と要素の位置を比較する

見出しN〜見出しN+1の間にいるときに見出しNをハイライトするようにします。
すなわち、見出しNの位置<スクロール量<見出しN+1の位置のときに見出しNをハイライトします

しかし、スクロール量=見出しNの絶対位置になった瞬間にハイライトするのではタイミングが遅く感じられます。
適当な大きさのオフセットを設け、ある程度上までスクロールされた時点でハイライトするとことで自然になります。

//サイド目次
const sideIndex = document.getElementById('toc-widget-3')
//サイド目次の全見出しタイトル
const sideIndexItem = sideIndex.querySelectorAll('a[href]')
//見出しの数
const headingNum = headingContents.length
//オフセット
const offset = 300

window.addEventListener('scroll', () => {
    const currentPos = window.scrollY // 現在値
    //現在値の判定
    //見出しiの位置<スクロール量<見出しi+1のとき、見出しiにcurrentクラスをつける
    for (let i = 0; i < headingNum; i++) {
        if (i < headingNum - 1 && currentPos + offset >= headingPos[i] && currentPos + offset < headingPos[i + 1]) {
          sideIndexItem[i].classList.add('current')
        } else if (i === headingNum - 1 && currentPos + offset >= headingPos[i]) {
          sideIndexItem[i].classList.add('current')
        }
      }
})

currentクラスはCSSで予めスタイリングしておきます。

目次の高さ制限、自動スクロール機能の実装

はみ出る目次への対処方法

記事が長くなれば目次も長くなります。

しかしあまりに目次が長いと、画面から目次がはみ出たまま追従してしまいます。
そのせいで、はみ出た部分の見出しはハイライトされても見えませんし、何よりクリックしてジャンプすることが出来ません。

そこで、目次の高さを制限します。
そしてページのスクロールに応じて目次も自動的にスクロールし、現在位置が常に表示される仕掛けを作ります。

スクロールする目次の実装

説明の前に、HTMLの構造を再掲します。

それでは実装の説明に入ります。

まずはCSSで目次のmax-heightを指定し、overflowをscrollにして高さを制限します。具体的には、<ul class="toc_widget_list">に指定します。

.toc_widget_list {
  max-height:460px;
  overflow: scroll;
    position: relative;
}

このposition指定は後にスクリプトで使用します。

次にJavaScriptで「目次内でハイライトされている見出しまでスクロール」する処理を書きます。
クラスNがハイライトされているとき、目次をスクロールするコードは次のようになります。

const offset = 60
//見出しのul
const widgetList = document.querySelector('.toc_widget_list')

//見出しのul li a
//すなわち、ハイライトする見出し一覧
const sideIndexItem = widgetList.querySelectorAll('a')

//sideIndexItem[N]:現在ハイライトされている見出し
widgetList.scroll({top: sideIndexItem[N].offsetTop - offset})

最後の行が重要で、主に2つの処理を行っています。

①現在ハイライトされている見出しの、目次内での相対位置を取得する。

要素A.offsetTopで、CSSでpositionプロパティを指定している「直近の祖先要素」からの「要素A」までの相対的な距離、高さを取得することが出来ます。

今回は要素Aを「ハイライトされている見出し」とし、直近の祖先要素は、先程position指定した「ul.toc_widget_list」になります、
よって、このコードではul.toc-widget_listの一番上を基準位置とし、そこから「ハイライトされている見出し」までの相対位置・距離が取得できます

高さ制限して非表示となった場所にある見出しも「本来はそこに存在している」と仮定したときの相対距離が取得できるので、その位置までスクロールするスクリプトを書くことが出来ます。

②目次内でスクロールイベントを発生させる

そして要素.scroll()で、CSSでscroll指定されている要素内でスクロールを発生させることが出来ます。

したがって上のコードでハイライトされている見出しまで自動スクロールされるわけです。
この処理は「ハイライトする見出し」の設定と同じタイミングで行うので、結局次のようなコードとなります。

window.addEventListener('scroll', () => {
    const currentPos = window.scrollY // 現在値
    //現在値の判定
    //見出しiの位置<スクロール量<見出しi+1のとき、見出しiにcurrentクラスをつける
    for (let i = 0; i < headingNum; i++) {
        if (i < headingNum - 1 && currentPos + offset >= headingPos[i] && currentPos + offset < headingPos[i + 1]) {
          sideIndexItem[i].classList.add('current')//ハイライト
          widgetList.scroll({top: sideIndexItem[i].offsetTop - offset2})//●追加:スクロール
        } else if (i === headingNum - 1 && currentPos + offset >= headingPos[i]) {
          sideIndexItem[i].classList.add('current')//ハイライト
          widgetList.scroll({top: sideIndexItem[i].offsetTop - offset2})//●追加:スクロール
        }
      }
})

まとめ

以上、追従する目次の作り方について説明しました。また、単に追従させるだけではなく、ハイライト機能と自動スクロール機能の実装方法についても説明しました。今回はTOC+プラグインを利用した方法を紹介しましたが、自分で目次を出力するコードを書いた場合にも同じ処理が適用できます。