サイトのページのHTMLの見出しタグを大きさで階層化してジャンプリンク付きの目次として表示するJavaScriptプログラムの内容を説明する。
JavaScript
// 目次を自動生成するコンテンツ(pb)とその中の見出しタグのh1からh4(hdgs)を取得する
const pb = document.querySelector("div.post-body"), hdgs = pb.querySelectorAll("h1, h2, h3, h4");
// 見出しタグがある場合
if (hdgs[0]) {
// 目次のHTMLのnav(toc)とdetails(dts)とsummary(smr)とol(fid)を作成する
const toc = document.createElement("nav"), dts = document.createElement("details"), smr = document.createElement("summary"), fid = document.createElement("ol");
// 目次のnavに「toc」のidを付けて最初から開くためにopen属性を「true」にしてsummaryに「目次」という文字列と開閉ボタン用の空のspanを入れてolに「mdr」のclassを付けてdetailsにsummaryとolを入れてnavにdetailsを入れる
toc.id = "toc"; dts.open = true; smr.insertAdjacentHTML("afterbegin", "目次<span/>"); fid.className = "mdr"; dts.appendChild(smr); dts.appendChild(fid); toc.appendChild(dts);
// 前の見出しの大きさを確保する変数(n)を空で宣言する
let n;
// 見出しタグをforEach()で反復処理する
hdgs.forEach((hdg, i) => {
// 目次のHTMLの項目のli(tci)とジャンプリンクのa(tcl)を作成して見出しの大きさの番号(tn)を取得する
const tci = document.createElement("li"), tcl = document.createElement("a"), tn = Number(hdg.tagName.substring(1));
// 見出しタグに固有のid(ジャンプリンクのリンク先になる)を付けて固有のidをaのhref属性に#付き(ジャンプリンクのリンク元になる)で入れて見出しの文字列(リンク名になる)をaに入れてaをliに入れる。
hdg.id = `content_${i + 1}`; tcl.href = `#${hdg.id}`; tcl.textContent = hdg.textContent; tci.appendChild(tcl);
// 見出しの反復処理の二回目以降で見出しがh1以外の場合
if (i !== 0 && tn > 1) {
// 目次の階層のolの最後のli(lid)と前の見出しとの大きさの差(dr)を取得する
const lid = [...fid.querySelectorAll("li")].pop(), dr = tn - n;
// 今の見出しが前のものよりも小さい(見出しタグの番号が大きい)場合
if (dr > 0) {
// 新しい階層のHTMLのol(idx)を作成して新しい項目のliを入れて目次の階層のolの最後のliに入れる
const idx = document.createElement("ol");
idx.appendChild(tci); lid.appendChild(idx); }
// 今の見出しが前のものと同じ大きさ(見出しタグの番号が同じ)の場合
else if (dr === 0) {
// 目次の階層のolの最後のliの次に新しい項目のliを追加する(同じ階層で後ろに並べる)
lid.after(tci); }
// その他、今の見出しが前のものよりも大きい(見出しタグの番号が小さい)場合
else {
// 目次の階層のolの最後のli(pol)を取得する
let pol = lid;
// 目次の階層の深さを調べるためにfor()で見出しの大きさの差があるだけ反復処理する
for (c = 0; c + dr < 0; c++) {
// 目次の階層のolの最後のliの直近の祖先のolが「mdr」のclassを持たない(目次の最初の階層に来てない)場合
if (!pol.closest("ol").classList.contains("mdr"))
// もう一つの目次の階層のolの最後のliの変数(pol)を直近の祖先のolの親要素(li)で書き換える
pol = pol.closest("ol").parentElement;
// その他、目次の階層のolの最後のliの直近の祖先のolが「mdr」のclassを持つ(目次の最初の階層に来ている)場合
else
// 反復処理の回数の変数(c)をマイナスの見出しの大きさの差に書き換えて反復処理を抜ける
c = -dr; }
// もう一つの目次の階層のolの最後のliの変数(pol)の次に新しい項目のliを追加する(同じ階層で後ろに並べる)
pol.after(tci); }}
// その他、見出しの反復処理の一回目か見出しがh1の場合
else {
// 目次の最初の階層のolに新しい項目のliを入れる
fid.appendChild(tci); }
// 前の見出しの大きさを確保する変数(n)を今の見出しの番号で書き換える
n = tn; });
// 最初の見出しの直前に目次を追加する
hdgs[0].before(toc); }
目次の自動生成は見出しの大きさに応じた階層化に様々な方法が考えられる。
今回のプログラムは前の見出しの大きさを確保して今の見出しのものと比較することによって階層化を実現している。
階層を下げる(見出しが小さくなる)ときは新しい階層を作って最後の階層の項目に追加する。
見出しの大きさが、二つ以上、下がった場合、階層は一つしか増やさないようにしている。例えばh1からh3へ飛んでも新しい項目は次の階層に入り、一度に二つ以上、階層を深くしない。
階層が同じ(見出しが等しい)ときは、そのまま、新しい項目を追加する。
階層を上げる(見出しが大きくなる)ときはclosest()メソッドで見出しの差があるだけ階層を遡って適切なものに新しい項目を追加する。
見出しの大きさが、二つ以上、上がった場合、最初の階層を越えない範囲で、階層を遡るようにしている。例えばh3から始まって後からh1が来たらその項目はh3の項目と同じ階層に入り、最初の階層の位置を後から下げない。