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

JSで目次を作る
JSで目次を作る

以前から作ってみたかった目次作成用のJSができたので、メモ的に記事にしたいと思います。

気になる部分はあるので後ほど改修するかもしれませんが。

[追記 2020.9.28]
新しく作り直しました。

構築環境
JavaScriptpure JavaScript
対応ブラウザIE10+

実現したいこと

実現したいことは以下の通り。

  • JSのみでパンくずを自動生成
  • 指定h要素のうち、最初の1つの上に自動挿入
  • data属性の利用
  • 静的サイト対応

今回はWPではなく静的サイトで使うためにつくったので、PHPはなくJSのみです。

data属性の利用

今回はdata属性の利用を念頭において作成しています。

h要素を取得して自動で生成する場合、実際のh要素と目次項目を変えたい場合はよくあるかと思いますので、その要望に対応するためでした。

data属性がなければh要素のテキストを取得するので、生成漏れは発生せず、運用においては便利に使えるかなと。

コード

以下がコードですが、記録もかねていますのでコメント内にいろいろ説明を書いています。


(!function(){
//DOMの構築が完了したら動作開始
document.addEventListener('DOMContentLoaded', myTOC());
function myTOC(){
  //目次作成と表示の対象となる範囲を、ラップ要素のIDで指定。 ※必須
  var range   = "main";
  //目次のタイトルを設定。 ※必須
  var title   = "目次";
  //目次として取得するh要素を指定。 ※必須
  var h       = "h2";
  //目次を表示する目印となるタグ(指定範囲内に存在する最初のタグの上に目次が表示される)を、取得するh要素から変更したい場合はタグ名を入力。
  //※オプション
  var tag     = "";
  //IDで指定する場合はこちらを入力。
  //IDの指定がない、あるいは、IDで指定した要素がない場合は、タグ指定が優先。
  //※オプション
  var tagId   = "";
  //目次から移動した際の位置調整。
  //上からの余白を 数値 で指定。pxは不要。
  //spanにpadding-topを追加するので、不具合のでる可能性がある。
  //※オプション
  var space   = "";
  //スマホサイズでの閲覧時に設定。
  //※オプション
  var spaceS  = "";
  //スマホ用の余白を適用するためのトリガーとなる画面サイズを指定。入力しない場合はデフォルトの375pxが適用される。
  //※オプション
  var display = "";


  //目次のタイトルに使うpタグを生成。
  var p      = document.createElement("p");
  //目次のラップ要素を生成。
  var wrap   = document.createElement("div");
  //目次に使うolを生成。
  var ol     = document.createElement("ol");
  //目次に使うliを入れる変数。
  var li     = "";
  //目次に使うaを入れる変数。
  var a      = "";
  //目次に使うIDを入れる変数。
  var tocId  = "";
  //目次に使うアンカーテキストを入れる変数。
  var tocStr = "";
  //目次のループ内の連番用変数。
  var num    = "";
  //目次対象のh要素の中身を入れる変数。
  var hStr   = "";


  //目次を作るための範囲をIDで指定して取得。
  range = document.getElementById(range);
  //対象範囲内のh要素を取得。
  h     = range.getElementsByTagName(h);
  //目次を入れる場所の指定位置を確定。
  //優先順位は ID指定 > タグ指定。
  tagId = document.getElementById(tagId)
  if(tagId){
    //目次を入れる目印の要素としてIDが指定されている場合。
    tag = tagId;
  } else if(!tag || tag == h) {
    //タグ指定がないか、目印の要素と取得する要素が同じ場合。
    tag = h[0];
  } else {
    //目印の要素と、目次として取得するh要素が違う場合。
    tag = range.getElementsByTagName(tag);
    //目印の要素の中で最初の1つを取得。
    tag = tag[0];
  }


  //スマホ画面サイズの余白指定があれば、指定した数値を設定
  //指定がなければ375pxを設定
  if(display && isNaN(display)){
    //入力値が数値でなければアラート
    alert('数値を入力してください');
  } else if(!display) {
    display = 375;
  }

  //スマホの画面サイズで
  if(spaceS && !isNaN(spaceS) && window.matchMedia( "screen and (max-width:" + display + "px)" ).matches ) {
    space = spaceS;
  }

  //目次から移動後の位置調整に数値があればpadding-topで設定。数値以外であれば消す。
  if(space && !isNaN(space)){
    space = ' style="padding-top:' + space + 'px;"';
  } else {
    space = "";
  }

  //最初のh2の上に目次のラップ要素を追加。
  tag.parentNode.insertBefore(wrap , tag);
  //ラップ要素にIDを設定
  wrap.id = "my-toc";

  //ラップ要素の中にタイトルのPタグを入れる。
  wrap.appendChild(p);
  //目次のタイトルの文言を設定。
  p.innerHTML = title;
  //目次のタイトルにクラス名を設定。
  p.classList.add("my-toc-title");

  //olをラップ要素の中に入れる。
  wrap.appendChild(ol);

  for (var i = 0; i < h.length; i++) {
    //構造化マークアップやクラス名に使うために1からはじまる連番を設定。
    num   = i + 1;
    //目次に使うIDを設定。
    tocId = "my-toc-" + num;

    //hの中身を取得。
    hStr  = h[i].innerHTML;

    //hの中身を連番id付きspanで囲む。
    h[i].innerHTML = '<span id="' + tocId + '"' + space + '>' + hStr + '</span>';

    //hのdata属性(data-toc)を取得。
    tocStr = h[i].getAttribute("data-toc");
    if (!tocStr){
      //data属性(data-toc)がなければh要素のテキストを取得。
      tocStr = hStr;
    }

    //liを生成。
    li   = document.createElement("li");
    //aを生成。
    a    = document.createElement("a");

    //aタグにページ内リンクを設定。
    a.setAttribute("href", "#" + tocId);
    //aタグにクラスを設定 計測用。
    a.classList.add("my-toc-item", "my-toc-item-" + num);
    //aタグにh2のテキストかdata属性の値を設定。
    a.innerHTML = tocStr;

    //liにaを入れる。
    li.appendChild(a);
    //liをolの中に入れる。
    ol.appendChild(li);

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

変数が多すぎですが、一応以下の部分で基本設定が変更しやすいようにしています。


//目次作成と表示の対象となる範囲を、ラップ要素のIDで指定。 ※必須
var range   = "main";
//目次のタイトルを設定。 ※必須
var title   = "目次";
//目次として取得するh要素を指定。 ※必須
var h       = "h2";
//目次を表示する目印となるタグ(指定範囲内に存在する最初のタグの上に目次が表示される)を、取得するh要素から変更したい場合はタグ名を入力。
//※オプション
var tag     = "";
//IDで指定する場合はこちらを入力。
//IDの指定がない、あるいは、IDで指定した要素がない場合は、タグ指定が優先。
//※オプション
var tagId   = "";
//目次から移動した際の位置調整。
//上からの余白を 数値 で指定。pxは不要。
//spanにpadding-topを追加するので、不具合のでる可能性がある。
//※オプション
var space   = "";
//スマホサイズでの閲覧時に設定。
//※オプション
var spaceS  = "";
//スマホ用の余白を適用するためのトリガーとなる画面サイズを指定。入力しない場合はデフォルトの375pxが適用される。
//※オプション
var display = "";

ここまでやるなら設定だけを抜き出した方がよいとは思いますが、諸般の事情により(疲れたというのもありますが)、上記の状態でおいてます。

目次の表示位置

目次自体の表示位置に関しては、実験も兼ねて以下のような動作も可能なようにしました。

  • 出力箇所指定の優先順位は「ID > タグ > 未入力(目次として取得する要素の最初の1つ)」
  • 目次に使うh要素を変更可能
  • 指定したIDを持つ要素の上に表示可能(IDが存在しない場合は、指定のタグか目次に使うh要素の上部に表示)
  • 飛び先のh要素が既にIDを持っている場合は、既存のIDをそのまま使う

目次として取得できるh要素

残念ながらh2h3など複数のh要素を対象にした目次作成までは作れていません。

そのため目次に用いるh要素は1種類しか選べません。

[2017.12.24追記]

複数のh要素を対象とした目次自動生成のJSを試作しました。

移動後の表示位置

移動後の表示位置に関しては、以下のようになっています。

  • h要素内に追加したspanpaddingを設定する
  • 指定サイズと指定サイズ外で余白を変更できる

インライン要素であるspanpaddingを設定してもレイアウトには影響が及ばないはず、といのうが実行の理由です。

もっとも、これがだめなら他に手がなかったのも理由ではありますが(別案もありましたがバグみたいな方法のようでIEとEdgeでは動きませんでした)。

なお問題点としては以下が考えられます。

  • spanの領域自体は広がっているため、上記で指定した余白内にaタグなどがあると押せなくなる可能性がある
  • 意図しないレイアウト崩れの原因にならないともいいきれない

そのため、不具合がでるようならspacespaceSの項目は入力しないほうが良いでしょう。

なお、当記事には実験としてこの仕組みを適用していますから、開発者ツールなどでh2の状態をみれば確認可能です。

375pxを境目に、目次から移動した際に100pxか40pxの余白が確保できているはずです。

Googleの検索結果に表示されるのか?

現時点ではGoogleなどの検索結果に対して有効かは確認できていません。

目次はいまいち表示条件が分からない印象なので、当記事のコードで表示可能であるかは不明です。

表示されれば追記したいと思いますが、いつまでも追記がない場合はだめだったのだとご理解ください。

[2017.12.23]
記事公開時のindexされた時点では表示されず。

[2017.12.25]
検索ワードによりますが、検索結果で「〇〇に移動」のリンクが表示されたことを確認しました。

結び

動作保証はできませんが、とりあえず私の環境で動くのは確認しました。

無駄な設定項目を増やした気もしますし、使用時には適宜削除を行ってスタマイズする形がよさそうです。

一応の注記ですが、目次のデザイン用CSSはこの記事には書いていないので、適宜CSSを設定する必要があります。

補足1

入力値のチェックなども穴があり、意図しない入力をされると脆いと思います。

とはいえ、そもそもJSファイルに直接書き込む形なので、何かの拍子に編集をミスして壊れる可能性は高く、どこまで整えればよいのかの判断がよく分からなくなってきました。

functionのfを消すだけでアウトですし。

補足2

もともと想定していなかったものを後付けしたため、広めの画面サイズでは「構築環境」のパーツが意図した表示位置からずれています…。

このように、「最初のh2の前に自動で挿入する」という動作は他の仕組みで利用されている可能性もあるため、導入時には各所の調整が必要となるかもしれません。

0人がこの記事を評価

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

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

コメント欄