【全見出しタグ対応】高速&シンプルな自動生成目次

2021/12/30
4
thumbnail

記事の項目を一目で確認できる目次。手動で作るのは大変ですが、ありがたいことに様々な自動生成目次のスクリプトが公開されており、目次生成の手間を省くことができます。

私もしばらく他の方が作った自動生成目次を使っていたのですが、表示タイミングの若干の時差が気になっていたのをきっかけに、新たに独自の自動生成目次を作成することに。いろいろな自動生成目次のソースコードを読んで比較検討を重ねた結果、「これだ!」と思う理想の目次を作ることができました。

はじめに:目次作成にあたって

今回の自動生成目次の作成にあたり、リモスキ!さんとHoodさんの以下の記事を特に参考にさせて頂きました。すばらしい記事をありがとうございます。

【Blogger】折りたたみ式の目次を自動生成できた

目次のアイデアやスクリプトをいろいろ探しつつ自分のブログにあわせて試行錯誤してるうちに、折りたたみ式の自動目次ができました。素人なので参考サイトのつぎはぎコードなんですが、一応動くのでご紹介します。

【jQuery不要】JS で章番号 & リンク付きの目次を自動生成する

内容をひと目で確認できる目次を、JavaScript で自動生成してデザインを整える方法を紹介します。

自動生成目次のコードと使い方

目次の特徴は以下の通りです。

  • 最小限のJavaScriptで実装しており、軽量・高速
  • 指定した任意の見出しタグをピックアップし、階層表示
  • detailsタグとsummaryタグを使った折りたたみ式

detailsタグとsummaryタグを使った折りたたみ式目次というアイデアはリモスキ!さんのものをベースに、任意の見出しタグに対応&階層構造化させるためにHoodさんのスクリプトを参考にさせて頂き実装しました。

目次のイメージは以下のような感じです。

目次画像1
開いた状態
目次画像2
閉じた状態

タイプI:目次を表示したい場所に生成先を設置

生成先HTMLを設置した場所に目次が表示されます。設置しなかった場合は表示されないので、ページ単位で表示・非表示を分けることができます。

生成先HTML

<details id="toc" open>
  <summary class="toc-title">目次</summary>
</details>

detailsタグにopenがある場合は目次が開いた状態で表示され、ない場合は閉じた状態で表示されます。

JavaScript

</body>の前に以下のスクリプトを設置してください。

<script>
(function(window, document) {
  const toc = document.getElementById('toc');
  const selector = document.querySelector('.post-body'); // 処理対象セレクタ
  if (!toc || !selector) {
    return
  }
  const list = document.createElement('ul');
  list.className = 'toc-container';
  toc.appendChild(list);

  const headings = selector.querySelectorAll('h2, h3, h4'); // 対象見出しタグ
  const order = [];
  const stack = [{level: 1, element: list}];

  // 事前処理
  headings.forEach((heading) => {
    const level = parseInt(heading.tagName.substring(1))
    order.push(level);
  });

  headings.forEach((heading, i) => {
    const level = parseInt(heading.tagName.substring(1));
    const next = order[i + 1];
    const li = document.createElement('li');
    const a = document.createElement('a');
    const id = 'toc-' + (i + 1);
    const ul = document.createElement('ul');

    // 目次要素の生成
    a.textContent = heading.textContent;
    a.href = `#${id}`;
    li.appendChild(a);
    if (level < next) {
      li.appendChild(ul);
    }

    // リンク先の生成
    heading.id = id;

    // 階層構造の生成
    let parent;
    do {
      parent = stack.pop();
    } while (parent.level >= level);
    parent.element.appendChild(li);
    stack.push(parent);
    stack.push({level: level, element: ul});
  });
}(window, document));
</script>

// 処理対象セレクタ部分は目次処理の対象となる要素のセレクタなので、記事部分の要素のクラス名を指定してください(Bloggerはデフォルトで.post-body)。// 対象見出しタグ部分は、ピックアップの対象となる見出しタグの指定です。上記コードの場合は、h2, h3, h4タグが対象となります。

CSS

CSSの例です。好みに合わせてカスタマイズしてください。

#toc {
  max-width: 500px;
  border: 2px solid rgba(0,0,0,.1);
  margin: 2em auto;
}
.toc-title {
  color: #444;
  padding: .5em 1em;
}
.toc-container {
  padding: 1em !important;
  margin: 0 !important;
}
.toc-container, .toc-container ul {
  counter-reset: ul-counter;
  list-style: none;
}
.toc-container li a {
  display: flex;
  color: #444 !important;
}
.toc-container li a:before {
  counter-increment: ul-counter;
  content: counters(ul-counter,".");
  color: dodgerblue; /* お好みの色 */
  padding-right: .5em;
  white-space: nowrap;
}
#toc .toc-title:after {
  content: '[ひらく]';
  margin-left: .5em;
}
#toc[open] .toc-title:after {
  content: '[とじる]';
}

タイプII:最初の見出し前に自動生成

「生成先HTML設置するの忘れちゃいそうだし、基本的に全ページ自動で目次付けてほしい」という私のような方もいらっしゃると思います。

そうした場合に対応すべく、生成先HTML部分もすべて含めて自動生成するタイプの目次を作成しました。見出しタグが1個以上存在する場合は、最初の見出しタグの前に自動的に目次を挿入します。

JavaScript

</body>の前に以下のスクリプトを設置してください。

<script>
(function(window, document) {
  const selector = document.querySelector('.post-body');
  if (!selector) {
    return
  }

  const toc = document.createElement('details');
  const sum = document.createElement('summary');
  const list = document.createElement('ul');
  toc.id = 'toc';
  toc.open = true;
  sum.className = 'toc-title';
  sum.textContent = '目次';
  list.className = 'toc-container';
  toc.appendChild(sum);
  toc.appendChild(list);

  const headings = selector.querySelectorAll('h2, h3, h4');
  if (headings.length == 0) {
    return
  }
  headings[0].parentNode.insertBefore(toc, headings[0]);
  const order = [];
  const stack = [{level: 1, element: list}];

  // 事前処理
  headings.forEach((heading) => {
    const level = parseInt(heading.tagName.substring(1))
    order.push(level);
  });

  headings.forEach((heading, i) => {
    const level = parseInt(heading.tagName.substring(1));
    const next = order[i + 1];
    const li = document.createElement('li');
    const a = document.createElement('a');
    const id = 'toc-' + (i + 1);
    const ul = document.createElement('ul');

    // 目次要素の生成
    a.textContent = heading.textContent;
    a.href = `#${id}`;
    li.appendChild(a);
    if (level < next) {
      li.appendChild(ul);
    }

    // リンク先の生成
    heading.id = id;

    // 階層構造の生成
    let parent;
    do {
      parent = stack.pop();
    } while (parent.level >= level);
    parent.element.appendChild(li);
    stack.push(parent);
    stack.push({level: level, element: ul});
  });
}(window, document));
</script>

CSS

CSSの例です。好みに合わせてカスタマイズしてください。

#toc {
  max-width: 500px;
  border: 2px solid rgba(0,0,0,.1);
  margin: 2em auto;
}
.toc-title {
  color: #444;
  padding: .5em 1em;
}
.toc-container {
  padding: 1em !important;
  margin: 0 !important;
}
.toc-container, .toc-container ul {
  counter-reset: ul-counter;
  list-style: none;
}
.toc-container li a {
  display: flex;
  color: #444 !important;
}
.toc-container li a:before {
  counter-increment: ul-counter;
  content: counters(ul-counter,".");
  color: dodgerblue; /* お好みの色 */
  padding-right: .5em;
  white-space: nowra;
}
#toc .toc-title:after {
  content: '[ひらく]';
  margin-left: .5em;
}
#toc[open] .toc-title:after {
  content: '[とじる]';
}

自動生成目次についての補足

  • 記事本体部分(デフォルトは.post-body)の任意の見出しタグ(h2, h3, h4, ...)をピックアップします。.post-body部分はお使いのサイトやテンプレートに応じて修正する必要があります。
  • 指定した見出しタグ(h2, h3, h4, ...)が階層構造になります。
  • 見出しのIDを上から順にtoc-1, toc-2, ...と指定します。目次のリンクをクリックすると対応する見出しに飛びます。
  • 修正(2022/1/9):目次のカウンタをaタグ内の疑似要素に変更し、aタグを全体リンク化しました。

従来型からの改良点

以前導入していたスクリプトはDOMContentLoadedの時点で発火していたため、目次が表示されるまでに若干の時差が発生していました。これが結構気になっていたので、スクリプト本体を</body>直前に設置してDOMContentLoadedを外すことにより、表示の時差をなくすことに成功。

スクリプト本体も、スタックを利用するというHoodさんのアイデアを参考にさせて頂くことで大幅にコードサイズを減らすことができました。スタックを利用した階層構造化処理は実にスマートで、初めてコードを見たときは感動しました。すばらしいコードに感謝ですm(_ _)m

スムーススクロール機能

jQueryを使用しないスムーススクロールのスクリプトを作成しました。こちらもおすすめです↓

スムーススクロールをJavaScriptのみで実装してみる | IB-Note

スムーススクロールをJavaScriptだけで実装してみました。シンプルかつ軽量なおすすめスクリプトです。

4件のコメント
わー、全対応されていてすごい! さすがです!!
あけましておめでとうございます。
>りもさん
明けましておめでとうございます。新年初コメントありがとうございます(^^)
りもさんの記事がきっかけで、オリジナル目次を作ることができました。目次と記事の作成両方で参考にさせて頂き、感謝感謝です。いつも丁寧で参考になる記事をありがとうございます。
こんばんは。
うちもこちらの自動生成目次を導入させて頂きました。
スケ郎さんのものよりコードもシンプルで読み込みも早く快適になりました!ありがとうございますm(_ _)m
周りの Blogger ユーザーさんたちがみんなこちらに乗り換えてて私も気になってはいたものの、デザイン調整(前のと同じデザインを再現)が面倒そうで躊躇してたんですが、意外とあっさりできました(笑)
もっと早く導入しとけばよかったです(^^;
>ふじやんさん
こんにちは。
自動生成目次がお役に立っているようで何よりです。
既にカスタマイズして利用しているものがあると、新しいものへの移行は億劫に感じてしまいますよね。お気持ちよくわかります(笑)
デザイン調整、それほど手間がかからなかったとのことで安心しました。
ご報告ありがとうございます(^^)