jsで見出しから目次を自動で作成する方法

JavaScriptで見出しから目次を自動で作成するプログラムの作り方をご紹介します。

設置も簡単ですよ。

やることを確認しよう

では、プログラムを作成する手順を確認しましょう。

  1. 見出しを出力する要素を検索する機能
  2. 対象となる見出し要素を検索する機能
  3. 検索した見出しのランクに応じで階層分け(ol)する機能
  4. 目次を作成する機能
  5. 目次を出力する機能

①見出しを出力する要素を検索する機能

これは非常にシンプルです。

document.querySelectorを使うことで、出力先の要素を検索することができます。

document.querySelectorにはCSSのセレクタを指定できますので、ID名やクラス名で対象要素を1つだけ検索できます。

下記の例では、TOC_INSERT_SELECTORに出力する要素のID名を入れ、document.querySelectorで検索を行います。

const TOC_INSERT_SELECTOR = '#toc';              // [セレクター指定] 目次を挿入する要素 querySelector用
const tocInsertElement    = document.querySelector(TOC_INSERT_SELECTOR);

②対象となる見出し要素を検索する機能

これも非常にシンプルです。

document.querySelectorAllを使うことで、見出し要素を検索することができます。

document.querySelectorAllにはCSSのセレクタを指定できますので、タグ名やID名やクラス名で対象要素を複数検索できます。

HEADING_SELECTORに検索する見出しのセレクターを入れ、document.querySelectorAllで検索を行います。

以下のように、h1,h2のようにも記述できますし、収集対象のh要素に「tocheading」と言うクラス名を付けた場合は、 .tocheadingとHEADING_SELECTORに指定することもできます。

const HEADING_SELECTOR    = 'h1,h2,h3,h4,h5,h6'; // [セレクター指定] 収集する見出し要素 querySelectorAll用
const headingElements     = document.querySelectorAll(HEADING_SELECTOR);

③ランクよって階層分けする機能

これが非常に重要な機能です。

見出しの階層構造を定義するためには、現在のランクと直前のランクを比較する必要があります。

Number(el.tagName.substring(1))で、h要素の1〜6の数字を抜き出しています。(つまりh要素以外では動作しません。)

ランクは、数字が小さいほど高く、数字が大きいほど低くなります。(h1〜h6でh1が最もランクが高いためです。)

それをrank、oldRankに保持し、layer変数で管理を行います。

layerの階層は、そのままh1~h6の階層と一致させています。layer[1]にはh1,layer[6]にはh6が対応します。

findParentElementは、現在のランクから見た上のランクの探索を行います(上のランクを探すためdiffは-1)。

仮に直前のランクより今のランクが低い場合には、findParentElementで見つけた要素の子要素として目次を追加します。

ランクが低い要素が初めて見つかった場合には、ol要素の作成も行います。

直前のランクより今のランクが高い場合には、layerから今よりも低いランクを削除(layer.length = rank + 1で行っています)し、layerに新たな目次(li)を追加します。

const layer = [];
let oldRank = -1;
const findParentElement = (layer, rank, diff) => {
    do {
        rank += diff;
        if (layer[rank]) return layer[rank];
    } while (0 < rank && rank < 7);
    return false;
};
headingElements.forEach( (el) => {
    let rank   = Number(el.tagName.substring(1));
    let parent = findParentElement(layer, rank, -1);
    if (oldRank > rank) layer.length = rank + 1;
    if (!layer[rank]) {
        layer[rank] = document.createElement('ol');
        if (parent.lastChild) parent.lastChild.appendChild(layer[rank]);
    }
    layer[rank].appendChild(createLink(el));
    oldRank = rank;
});

④目次を作成する機能

これも非常にシンプルです。

目次は全体をol要素で作成するため、li要素、a要素で作成します。

見つけた見出しにuid関数でユニークなID名を割り振り(元のIDがある場合にはそのまま利用)、そのIDを使って目次のa要素にページ内リンク(フラグメント識別子)を設定します。

const createLink = (el) => {
    let li = document.createElement('li');
    let a  = document.createElement('a');
    el.id  = el.id || uid();
    a.href = `#${el.id}`;
    a.innerText = el.innerText;
    a.className = LINK_CLASS_NAME;
    li.appendChild(a);
    return li;
};

⑤目次を出力する機能

目次を出力する要素に対して、ランクの管理を行うlayer変数の最も高いランクを出力するだけです。

findParentElementは、ランク1(h1)から探索を行い最もランクが高い目次を見つけ出します(diffは下のランクを探すため+1)。

const appendToc = (el, toc) => {
    el.appendChild(toc.cloneNode(true));
};
if (layer.length) appendToc(tocInsertElement, findParentElement(layer, 0, 1));

htmlでは下記のように記述することで反映します。

<div id="toc"></div>

今回の完成コードです

  // 設定
  const TOC_INSERT_SELECTOR = '#toc';              // [セレクター指定] 目次を挿入する要素 querySelector用
  const HEADING_SELECTOR    = 'h1,h2,h3,h4,h5,h6'; // [セレクター指定] 収集する見出し要素 querySelectorAll用
  const LINK_CLASS_NAME     = 'tocLink';           // [クラス名] 目次用aタグに追加するクラス名     .無し
  const ID_NAME             = 'heading';           // [ID名]    目次に追加するID名のプレフィックス #無し
  const tocInsertElement    = document.querySelector(TOC_INSERT_SELECTOR);
  const headingElements     = document.querySelectorAll(HEADING_SELECTOR);
  const layer = [];
  let id = 0;
  const uid   = () =>`${ID_NAME}${id++}`;
  let oldRank = -1;
  try {
    const createLink = (el) => {
      let li = document.createElement('li');
      let a  = document.createElement('a');
      el.id  = el.id || uid();
      a.href = `#${el.id}`;
      a.innerText = el.innerText;
      a.className = LINK_CLASS_NAME;
      li.appendChild(a);
      return li;
    };
    const findParentElement = (layer, rank, diff) => {
      do {
          rank += diff;
          if (layer[rank]) return layer[rank];
      } while (0 < rank && rank < 7);
      return false;
    };
    const appendToc = (el, toc) => {
      el.appendChild(toc.cloneNode(true));
    };
    headingElements.forEach( (el) => {
      let rank   = Number(el.tagName.substring(1));
      let parent = findParentElement(layer, rank, -1);
      if (oldRank > rank) layer.length = rank + 1;
      if (!layer[rank]) {
          layer[rank] = document.createElement('ol');
          if (parent.lastChild) parent.lastChild.appendChild(layer[rank]);
      }
      layer[rank].appendChild(createLink(el));
      oldRank = rank;
  });
  if (layer.length) appendToc(tocInsertElement, findParentElement(layer, 0, 1));
  } catch (e) {
    //error
  }

他にもこんな記事があります!