Imaginaryにブログの目次を表示するカスタマイズを紹介したけど、使用したJavaScriptのコンテンツの目次の自動生成のプログラムはBloggerの他のテーマを含めて他の全てのサイトで使うことができるので、もはや著者名を付けるだけで誰でも無料で使えるように提供しようと思った。
目次の自動生成のプログラムの特徴
コンテンツのHTMLの見出しタグの良く使われるh2からh4までを取り込んで順序付きリストタグのolとliに見出しへのジャンプリンクを入れて階層的に仕立てた目次を最初の見出しタグの直前に挿入する。
目次のジャンプリンクを得るために見出しのidを新しく付けているので、もしも既存のidを、そのまま、使いたい場合はソースコードのカスタマイズを行うと上書きを避けることができる。
目次の見出しのリストは開閉メニューのdetailsで、表題と開閉ボタンを付けてさらにサイトのナビゲーションを示すnavに入れて配置する。
見出しタグはサイトの構成として大きいもの、番号の小さなものから一つずつ下げるか先頭と同じか越えないものまで上げるのが基本的なマークアップだと思う。
とはいえ、そうならない場合もあるので、今回のプログラムでは最初の見出しタグを種類に関わりなく目次の最上位の階層として捉えて下げるときに例えばh2の次にh4へ行くなど、二つ以上、飛んだら目次では一つだけ下げたものとして扱い、上げるときに例えばh2が先頭だったら後からh1が来たり、前にh2からh4で一つ繰り上がった階層からh2が来て二つ上がるから越えるような場合でも目次では先頭と同じ階層に留まるものとして扱って表示するようにしている。
目次の自動生成のプログラムのソースコード
サイトのテンプレートかコンテンツにscriptで、プログラムのソースコードを記載して使う。
/* Copyright: Nagahito Yuki 2023 | https://www.nagahitoyuki.com/2023/03/a-program-that-automatically-generates-and-displays-a-table-of-contents-with-javascript.html | License: The MIT License */
const pb = document.querySelector("div.post-body"), hdgs = pb.querySelectorAll("h1, h2, h3, h4");
if (hdgs[0]) {
const toc = document.createElement("nav"), dts = document.createElement("details"), smr = document.createElement("summary"), fid = document.createElement("ol");
toc.id = "toc"; dts.open = true; smr.insertAdjacentHTML("afterbegin", "目次<span/>"); fid.className = "mdr"; dts.appendChild(smr); dts.appendChild(fid); toc.appendChild(dts);
let n;
hdgs.forEach((hdg, i) => {
const tci = document.createElement("li"), tcl = document.createElement("a"), tn = Number(hdg.tagName.substring(1));
hdg.id = `content_${i + 1}`; tcl.href = `#${hdg.id}`; tcl.textContent = hdg.textContent; tci.appendChild(tcl);
if (i !== 0 && tn > 1) {
const lid = [...fid.querySelectorAll("li")].pop(), dr = tn - n;
if (dr > 0) {
const idx = document.createElement("ol");
idx.appendChild(tci); lid.appendChild(idx); } else if (dr === 0) {
lid.after(tci); } else {
let pol = lid;
for (c = 0; c + dr < 0; c++) {
if (!pol.closest("ol").classList.contains("mdr")) pol = pol.closest("ol").parentElement; else c = -dr; }
pol.after(tci); }} else {
fid.appendChild(tci); }
n = tn; });
hdgs[0].before(toc); }
先頭のコメント(/*〜*/)が著作権を示しているので、無料で使用するかぎり、必ず残して置かなくてはならないし、ソースコードを編集するときなども誤って消さないようにして欲しい。
僕が提供する他の著作権付きプログラムやBlogger用のImaginaryテーマを使っている人はもう既にサイトのソースコードに著者名が記載されているからさらに追加する必要はなくて消しても構わない。
目次の自動生成のプログラムは色んなものがあると思うけれども今回のclosest()メソッドを使ったものは他にないようだ。調べると目次の見出しの種類に基づいて階層化をどうやるかというところにプログラマーの違いが出易くて面白い。
僕は前と今の見出しの大きさの差だけを記録して小さければ新しい階層を作り、大きければclosest()メソッドで親要素の古い階層へ遡って直ちに目次の項目を置くようにした。全体の行数を数十行まで切り詰めて軽量化と動作上の高速化も十分に図れたと思う。
目次のHTMLの構成について
初期設定では見出しのあるコンテンツの親ボックスの「post-body」のclassの付いたdivタグに目次が自動生成されて最初の見出しタグの直前に表示される。
プログラムを使用するサイトによって親ボックスのHTMLが違う場合は("div.post-body")
のところを相応しいものに変える必要がある。
- div.post-body
-
nav#toc
- details
- summary
- ol.mdr
- li
- a
- li
- details
-
nav#toc
初期設定では("h1, h2, h3, h4")
にあるように四種類の見出しタグを目次に取り込むようになっている。
取り込む見出しのカスタマイズで、不要な種類を減らしたり、必要な種類を増やしたりすることもできる。
目次のHTMLはサイトのナビゲーションを示すnavをtocというidを持つ親要素として先ずは開閉メニューのdetailsを入れて「目次」という表題summaryで付けている。
ソースコードの"目次<span/>"
のところがsummaryの部分で、目次
が表題で、<span/>
は空要素だけれども目次のCSSと合わせて開閉ボタンの「開く」と「閉じる」を表示するためにある。
そして目次は順序付きリストのolで、見出しの大きさによって階層化される。
最初のolは一つで、mdrというclassを付けて次に小さな見出しが来ると新しいolを入れ子にして記載する。同じか大きな見出しが来れば遡って同じか前の相応しい階層に新しいliを置く。
ソースコードの実装方法
目次の自動生成のプログラムのソースコードをscriptタグに入れてサイトのテンプレートかコンテンツのHTMLに記載する。
<script>
目次の自動生成のプログラムのソースコード
</script>
他にscriptタグがあればソースコードだけ組み込んでも干渉せずに大丈夫な場合もある。
サイトのどこに記載するのが最適か
目次をコンテンツに後からscriptタグで挿入するとそのときにページが広がって動いてしまう。初回画面だとコンテンツが押し下げられるのが、一瞬、見えたりもするけど、これがサイトのCLS(Cumulative Layout Shift/累積的な配置の変動)を上げて使い難くする要因になる。
やってみるとコンテンツの全ての見出しが出揃った直後にソースコードを記載すると表示の遅れを分からないくらい減らせる。
なので目次を表示したいコンテンツのなるべく近いところにソースコードを記載すれば支障はないかも知れない。
サイトによって違うと思うし、どうしても目次の挿入時のレイアウトの動きが辛い場合はCLSを完全に免れる方法もあるので、使うと良いと思う。
CLSの調整用のソースコード
目次の次の要素に上の外側の余白を付けるか目次の親ボックスの高さを付けるかの二つのCSSの方法が使い易いので、目次の自動生成のプログラムのカスタマイズについて紹介する。
目次の次の要素に上の外側の余白を付ける場合
目次の自動生成のプログラムで最初の見出しタグの直前に目次が挿入されるからその次の要素というと最初の見出しタグになる。
CSS
.post-body h2:first-of-type {margin-top:100vh}
コンテンツの親ボックスのclassが「post-body」で、最初のh2の見出しに画面の高さの上の外側の余白を付ける。
最初の見出しタグに何が来るかが分からないとそれを指定するCSSもその後に撤回するJavaScriptも煩雑にならざるを得ないので、最初に来る見出しの種類は一つに決めて使った方が良いと思う。
大抵、コンテンツのタイトルの見出しタグがh1で、コンテンツの見出しタグはh2から使うので、そうした定石通りにやって行けば問題ないだろう。
間違ってh3などの他の種類の見出しタグが先頭に来たら調整できなくなることだけ用心しなくてはならない。
JavaScript
hdgs[0].before(toc); hdgs[0].style.marginTop = "";
※太字が追加する部分。
こちらは最初の見出しタグがhdgs[0]
に取得できているので、どんな種類の見出しタグでも一つに絞られればソースコードは一つで済む。
CSSで付けられた最初の見出しタグの上の余白を最終的に消すので、目次の自動生成のプログラムの目次の挿入のhdgs[0].before(toc);
に続けて記載する。
目次の次の要素に上の外側の余白を付ける場合
目次の自動生成のプログラムで目次の親ボックスはtocのidを持つnavなので、それを予めコンテンツの最初の見出しタグの直前に記載しておく。
HTML
<nav id="toc"/>
最初の見出しタグ
目次が挿入されるから空要素で構わないけど、とにかく最初の見出しのタグとの間に他のタグが何も入らないようにする。
目次の自動生成のプログラムのtoc = document.createElement("nav"),
とtoc.id = "toc";
が重複して不具合の素になるから必ず削除する。
CSS
nav#toc {height:100vh}
目次の親ボックスに画面の高さと同じ高さを指定する。
JavaScript
hdgs[0].before(toc); toc.style.height = "";
※太字が追加する部分。
親ボックスのnav#tocに目次が挿入された高さを元に戻すので、目次の自動生成のプログラムの目次の挿入のhdgs[0].before(toc);
に続けて記載する。
目次のデザインのソースコード
サイトのCSSに次のソースコードを追加すると初期のデザインが付けられる。
#toc>details {background-color:rgba(176, 196, 222, 0.02);border:1px #b0c4de outset;padding:1em}
#toc>details>summary {list-style-type:none;display:flex;justify-content:space-between}
#toc>details>summary>span {text-decoration:dotted underline .125em}
#toc>details>summary>span::before {content:'開く'}
#toc>details[open]>summary>span::before {content:'閉じる'}
.mdr {margin-bottom:0}
.mdr ol {padding-left:1.25em}
初期のデザインのCSSの指定先と内容
- #toc>details
- background-color:rgba(176, 196, 222, 0.02)
本文の目次の背景色を付ける。 - border:1px #b0c4de outset
本文の目次の枠線を付ける。 - padding:1em
本文の目次の内側の余白を付ける。
- background-color:rgba(176, 196, 222, 0.02)
- #toc>details>summary
- list-style-type:none
目次の左横のマーカー(▼)を消す。 - display:flex
本文の目次の見出しの置き方を整える。 - justify-content:space-between}
本文の目次のタイトルと開閉ボタンを左右に分ける。
- list-style-type:none
- #toc>details>summary>span
- text-decoration:dotted underline .125em
本文の目次の開閉ボタンに点線の下線を引く。
- text-decoration:dotted underline .125em
- #toc>details>summary>span::before
- content:'開く'
本文の目次が閉じたときに「開く」と表示する。
- content:'開く'
- #toc>details[open]>summary>span::before
- content:'閉じる'
本文の目次が開いたときに「閉じる」と表示する。
- content:'閉じる'
- .mdr
- margin-bottom:0
下の外側の余白を消す。
- margin-bottom:0
- .mdr ol
- padding-left:1.25em
項目の左の内側の余白を調整する。
- padding-left:1.25em
- .mdr li
- margin:4px auto
個々の項目の外側の余白を調節する。
- margin:4px auto
目次に関係するデザインのCSSの指定先
- #toc
- 目次の親ボックスのnavのid
- #toc>details
- 目次の開閉メニューのdetails
- #toc>details>summary
- 目次の開閉メニューの表題とボタンのsummary
- #toc>details>summary:not(span)
- 目次の開閉メニューの表題のtext
- #toc>details>summary>span
- 目次の開閉メニューのボタンのspan
- .mdr
- 目次の最初のリストタグ/全ての項目が含まれることになる親要素のolのclass
- .mdr ol
- 目次の二番目以降の全ての階層のリストタグのol
- #toc>details ol
- 目次の全てのリストタグのol
- .mdr li
- 目次の全てのリストタグの項目のli
- .mdr li>a
- 目次の全ての項目のジャンプリンクのa
好みで、ジャンプリンクにスムーズスクロールなどのデザインを追加しても目次の自動生成のプログラムに支障はない。
目次の開閉メニューのボタンに関しては使用しない場合は目次の自動生成のプログラムのsmr.insertAdjacentHTML("afterbegin", "目次<span/>");
の<span/>
が不要になるので、削除すると無駄を減らすことができる。
目次の自動生成のプログラムのカスタマイズ
目次の表示や機能などの使い方を変える幾つかの方法を紹介する。
ジャンプリンクの見出しのidを操作する方法
目次の自動生成のプログラムは項目のジャンプリンクを作るために見出しにidを付けている。
hdg.id = `content_${i + 1}`;
見出しのidは「content_見出しの順番」という仕方で、付けられるけれども「content_」の部分は任意で変えることができる。
変更する場合はidに使える半角英数字とハイフン(-)やアンダーバー(_)などの記号で記載する。
見出しタグにidが既に付けられていても上書きされるので、注意しなくては行けない。既存のidに対してリンクがあれば飛べなくなってしまう、またはマークアップやプログラムに支障を来すかも知れない。
既存のidを上書きせず、そのまま、使用したい場合はそれに合わせてソースコードを編集する。
元のソースコード
hdg.id = `content_${i + 1}`;
新しいソースコード
if (!hdg.id) hdg.id = `content_${i + 1}`;
元のソースコードの先頭にif (!hdg.id)
を追加して改行する。すると見出しタグにidがないときだけidを付けて既存のidを上書きすることはなくなる。
取り込む見出しの最低数を設定する方法
見出しの数が少なくて目次を作る必要がない場合があるかも知れないので、目次に取り込む見出しの最低数を設定してそれに達したときだけ目次を作る方法を紹介する。
目次の自動生成のプログラムのソースコードの一部を目次の最低数に合わせて編集する。
元のソースコード
if (hdgs[0]) {
(中略)
}
新しいソースコード
if (hdgs.length >= 最低数) {
(中略)
}
元のソースコードを新しいソースコードで置き換えて最低数を半角数字で記載するとページの見出しの数が最低数と同じか越えたときだけ目次を作るようになる。
取り込む見出しの種類を増減する方法
目次に項目として取り込む見出しタグの種類は目次の自動生成のプログラムの次のソースコードで決まっている。
pb.querySelectorAll("h1, h2, h3, h4");
初期状態ではh1とh2とh3とh4の四種類が目次に取り込む見出しになっているけれども取り込まなくて良いものがあれば直後の区切りの半角コンマとスペースを含めて削除する。
h1を削除したらif (i !== 0 && tn > 1)
の一部が不要になり、if (i !== 0)
だけで、構わない。
h4を削除したらソースコードの一部をもっと短縮できる。
元のソースコード
let pol = lid;
for (c = 0; c + dr < 0; c++) {
if (!pol.closest("ol").classList.contains("mdr")) pol = pol.closest("ol").parentElement; else c = -dr; }
pol.after(tci);
新しいソースコード
lid.closest("ol").parentElement.after(tci);
見出しをh1からh3まで、あるいはh2とh3しか使わなければ元のソースコードは一行で纏められる。
h4に続いてh5、さらにh6を取り込む場合は取り込む見出しの種類のところに半角コンマ(,)で区切って追加する。
pb.querySelectorAll("h1, h2, h3, h4, h5, h6");
一般的にコンテンツのタイトルがh1で、コンテンツの中ではh2とh3だけで、十分で、多くてもh4まであれば足りるかも知れない。
特定のページを除外する方法
目次の自動生成のプログラムをテンプレートに組み込むと全てのページで、反映する。その中には目次は必要ないという場合もあるかも知れない。テンプレートに目次の自動生成のプログラムのソースコードはあっても特定のページにかぎって動作しないように除外する方法を紹介する。
手順①HTMLを書き換える
目次の自動生成のプログラムを止めたいページの最初の見出しタグに除外用のclassを付ける。
<h2 class="no-toc">見出し</h2>
classの内容は任意で構わない。セレクターに使える半角英数字とハイフン(-)やアンダーバー(_)などの記号で何か付ける。
手順②JavaScriptを書き換える
目次の自動生成のプログラムのソースコードの一部をページの除外に合わせて編集する。
if (hdgs[0] && hdgs[0].classList.contains("no-toc"))
※太字が追加する部分。
ソースコードのif (hdgs[0])
を書き換える。
追加するソースコードには除外用のclassを指定する部分があるので、目次の自動生成のプログラムを止めたいページの最初の見出しタグに付けたのと一致するものを入れておく。
コメント