JavaScriptを使って目次を自動生成する version2

以前にもJavaScriptを使って目次を自動生成するコードを試作していましたが、今回しばらくぶりに作り直したのでメモとして。

構築環境
JavaScriptPure Javascript
対応ブラウザIE非対応(書き換えで対応可)

前提

  • jQueryや他のライブラリを使わずにJavascriptのコードを書いて作成する
  • 複数のH要素を対象に目次を作成する
  • 対象H要素にIDが存在している場合はそのまま使う
  • リストを入れ子にしない
  • IE非対応(テンプレートリテラルとconstを使っているため)
  • DOMContentLoadedで動作させる(起動用の記述を別で書かない)

リストの入れ子は参考にしたライブラリレベルの良い案が思い付かず、実装するなら参考にしたライブラリを少し改造するほうが良いと思えたので、作成を諦めました。

IE対応に関しては、テンプレートリテラルをやめればIE11対応可で、constletvarに書き換えればIE9まで対応可能なはずです(未チェック)。

なお、今回はある程度の部分を以下のライブラリを参考にして作成しています。
多少気になる点(日本のサイトだとH要素のIDが日本語になってしまう)はあるものの、それ以外の点では当記事よりもずっとちゃんとしているので、利用するなら以下を使った方が良いです。

サンプルコード


const myTOC = function() {
  //目次作成の範囲を指定。IDでもクラスでも良いが、クラスの場合は最初の一つ目が対象。
  let range = '.entry-content';
  range = document.querySelector(range);
  //目次作成の対象となるH要素の種類を列記
  let heading = 'h2,h3,h4';
  heading = range.querySelectorAll(heading);
  //目次作成の対象となるH要素の中で最も上位の要素を指定
  //自動で取得もできるが、指定したほうが無駄な動作を減らせるので作成
  const startHeading = 'h2';
  const startLevel = range.querySelector(startHeading).tagName.slice(1);

  //目次作成用の配列を作成し、H要素の階層構造の最上レベルを取得するための配列を作成する
  const elemArray = [];
  const createElemArray = function(id, level, text) {
    let array = [];
    array['id'] = id;
    array['level'] = level;
    array['text'] = text;
    elemArray.push(array);
  }

  //取得したH要素にIDを設定し、目次作成用の配列を作成する
  heading.forEach((value, index) => {
    if (!value.id) {
      value.id = 'toc_' + value.tagName + '-' + index;
    }
    createElemArray(value.id, value.tagName.slice(1), value.innerText);
  })

  //目次のHTMLの作成と出力
  const createHTML = function() {
    let levelClass;
    let html;
    const wrap = document.createElement("div");
    wrap.id = "toc_wrap";

    //目次のHTML作成
    html = '<ol id="toc_body">';
    //目次のliをforEachで作成
    elemArray.forEach(elem => {
      //H要素の階層構造をクラスに反映する
      Math.abs(elem.level - startLevel);
      levelClass = `toc_level_${Math.abs(elem.level - startLevel)}`;

      html += `<li class="toc_item ${levelClass}">`;
      html += `<a href="#${elem.id}">${elem.text}</a>`;
      html += `</li>`;
    });
    html += '</ol>';

    wrap.innerHTML = html;
    range.insertBefore(wrap, heading[0]);
  }
  //目次のHTMLを出力
  createHTML();

  //処理完了後にイベント削除
  document.removeEventListener('DOMContentLoaded', myTOC, false);

  //Google検索などで目次を用いたリンクが表示され、それをクリックされた場合に、該当の目次まで移動させる
  const move = function(){
    let urlHash = location.hash;
    if(urlHash){
      urlHash = urlHash.replace("#","");
      const scrollT = document.getElementById(urlHash);
      scrollT.scrollIntoView();
    }
    //処理完了後にイベント削除
    document.removeEventListener('load', move, false);
  }
  if (document.readyState !== 'loading') {
    move();
  } else {
    document.addEventListener('DOMContentLoaded', move,{ passive: true }, false);
  }
}
if (document.readyState !== 'loading') {
  myTOC();
} else {
  document.addEventListener('DOMContentLoaded', myTOC,{ passive: true }, false);
}

querySelectorAllを使ってH要素を取得する

以前作成した際は範囲内のHTMLを全て取得し、さらに正規表現でH要素を取得していましたが、querySelectorAllで簡単に対応できました。

もはやIE7など気にする必要もないですし、querySelectorAllでいいかなと。

H要素の階層構造

今回は目次作成用の多次元配列を作成し、そこに目次の飛び先用IDとラベルを入れています。

加えて前述のライブラリを参考にH要素からtagName.slice(1)で数値だけを取り出し配列に追加。これを階層構造把握のために利用しています。

リストの入れ子はできていませんが、クラス名でH要素の階層が判定できるので、それを目印にCSSでインデントさせることができます。

テンプレートリテラル

今回はIEを無視して作っていたので使いましたが、IE非対応になるかならないかの境目でもあったので、今回程度の利用方法なら無理に使わなくても良いかなという印象です。

なお個人的には`が一瞬全角記号に見えてしまうことがあり慣れません。

asyncによるDOMContentLoadedの未発火

asyncで読ませる場合は留意する必要のある事柄です。

検索経由での目次利用対応

  • Google検索などで見出しがリンクとして表示される場合がある
  • JSで動的にH要素にIDを付与している場合、その動作が間に合わず該当の場所まで移動しない

上記の問題が発生するため、const moveを作成して以下の処理を施しています。

  • loadイベントを監視して、URLにハッシュがあればそれを取得してスクロールさせる

scrollIntoView()のブラウザ対応

当記事公開時点(2020.9)ではMDNおよびCan I useによるとSafariはscrollIntoView()自体には対応していますが、まだsmoothオプションには対応していません。

そのため今回のサンプルコードではsmoothオプションは使わず素の状態で用いています。IE切り捨てになりますがCSSのscroll-behaviorを使えばスムーススクロールの動作自体はある程度実現できますし問題はないように思います。

結び

冒頭に書いていますが参考にしたライブラリのほうが当然ながら完成度が高く、自作する必要性はそれほどありませんでした。

しかしながら復習や確認の意味では収穫があり、ライブラリそのままでは都合が悪い部分もあったため、結果的にはよかったなと思います。

6人がこの記事を評価

役に立ったよという方は上の「記事を評価する」ボタンをクリックしてもらえると嬉しいです。

連投防止のためにCookie使用。SNSへの投稿など他サービスとの連動は一切ありません。

コメント欄