JavaScriptで目次のカレント表示をさせる方法(Intersection Observer API)

schedule Published
Category folderJumble
format_list_bulleted Contents

このブログの記事にある目次は、要素位置と現在のスクロール量を、スクロールの度に計算してカレント表示(現在地ハイライト)していましたが、Intersection Observer API(交差オブザーバー API) という便利なものがあるのを知ったので、ブログの目次機能を作り直してみました。

Intersection Observer APIとは

Intersection Observer API(交差オブザーバー API )は、監視対象の要素(ターゲット要素)ビューポートまたは指定された要素(root要素)交差した際に、それを検知してコールバックを呼び出せます。

交差の検知は、交差率というターゲット要素がどのくらい表示されたかを閾値(0.0~1.0)で指定できます。まだ交差していない状態で検知したい場合は、root要素を拡げるrootマージン(px値 or パーセント値)を利用することで検知できます。

Intersection Observer APIを使えば、スクロールやリサイズの度にイベントを発生させて、現在のスクロール位置とターゲット要素との差分を再計算する必要がなくなるようです!

Intersection Observer APIの使い方

① Intersection Observer(交差オブザーバー)の作成

コンストラクター関数で、オプションで指定した条件でターゲットが交差する度に、コールバック関数を渡すことでインスタンスを生成します。

const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

const observer = new IntersectionObserver(callback, options);

settingsオプションについて

  • root

    ターゲットが見えるかどうかを確認するためのビューポートを指定。指定されなかった場合、または null の場合は既定でブラウザーのビューポートが使用されます。

  • rootMargin

    root要素の範囲を拡大・縮小するマージンを設けることができます。マージンはpx値かパーセント値が使用できます。'10px 20px 30px 40px' の場合、上, 右, 下 ,左 のマージンで、CSSと同じような形で指定できます。

  • threshold

    ターゲット要素がどのくらい見えた場合にコールバックを実行するかを指定します。既定値は0で1pxでも見えるとコールバックを実行します。1つ以上の数値を指定でき、 0.5の場合は50%見えたとき、[0.1, 0.25, 0.5, 1]と配列で複数を指定した場合だと、10%, 25%, 50%, 100%見えたときにコールバックを実行します。

② 監視対象を指定して、作成したオブザーバーに監視させる

const target = document.querySelector('#hogehoge');
observer.observe(target);

③ コールバック関数を作成する

コールバックは、IntersectionObserverEntry オブジェクトの配列(entries)オブザーバー(observer)を受け取ります。

callback = (entries, observer) => {
  entries.forEach((entry) => {
    ︙
  });
};

tips_and_updatesIntersectionObserverEntryについて

IntersectionObserverEntryはIntersection Observer APIに用意されているインターフェイスで、各ターゲット要素と各ターゲットの交差状態を表します。このIntersectionObserverEntryのインスタンスが、コールバックにentriesの引数で渡されます。
お決まりごとなので、entriesで渡されると覚えておけば良いです。

tips_and_updatesIntersectionObserverEntryのプロパティ

  • boundingClientRect

    ターゲットのバウンディングボックス

  • intersectionRatio

    ターゲットがどのくらい見えているか、0.0 から 1.0 の数値。
    ターゲットの面積が0の場合でも、0か1を返します。

  • intersectionRect

    ターゲットの交差している部分のバウンディングボックス

  • isIntersecting

    target 要素が交差状態に遷移したか (true) または交差状態から脱したか (false) のブール値。

  • rootBounds

    root要素のバウンディングボックス

  • target

    そのままターゲット要素(element)。

  • time

    文書の作成時刻を基準とする交差状態が発生した時刻

これで事前の下調べは完了です。実際に目次のカレント表示機能を書き直していきます。

Intersection Observer API を使った目次のカレント表示

▼ HTML

HTMLはこのブログ記事の構成を前提としています。
コンテンツのボディ部分はsectionで分けて、見出しにh2を設けており、これを目次に使用しています。h3など下のレベルの見出しは、目次には不要と思っていますので、単純なルールとなっています。
なお目次のlistはJavaScriptで生成しています。

▼目次部分(目次のlistはJavaScriptで生成)
<nav class="toc-nav">
<ul id="toc-nav-list">
</ul>
</nav>

▼コンテンツ部分
<section>
<h2>見出し①が入ります</h2>
︙
</section>
<section>
<h2>見出し②が入ります</h2>
︙
</section>
<section>
<h2>見出し③が入ります</h2>
︙
</section>
<section>
<h2>見出し④が入ります</h2>
︙
</section>

▼ JavaScript

▼目次のlistを生成
const contentsList = document.getElementById("toc-nav-list");
const headings = document.querySelectorAll("h2");
const tocFragment = document.createDocumentFragment();

for(let i = 0; i < headings.length; i++) {
  const li = document.createElement("li");
  const a = document.createElement("a");
  const id = 'anchor' + (i+1);
  sections[i].id = id;
  li.classList.add('toc-nav-item');
  a.classList.add('anchor');
  a.textContent = headings[i].textContent;
  a.href = '#'+id;
  li.appendChild(a);
  tocFragment.appendChild(li);
}
contentsList.appendChild(tocFragment);

▼目次のカレント表示
(コンテンツが見えているときに目次にクラスを付け、見えてないときはクラスを取る)
const sections = document.querySelectorAll("section");
const sectionsArray = Array.from(sections);

/* -- ① Intersection Observer(交差オブザーバー)の作成 -- */
const options = {
  root: null, //ブラウザ画面をroot要素に
  rootMargin:  '-35% 0px -65%', //画面の垂直方向に交差監視する線を設けるイメージ(位置は適宜調整)
  threshold: 0 //ターゲット要素が1pxでも交差したとき
}
const observer = new IntersectionObserver(tocAddCurrent, options);

/* -- ② 監視対象を指定して、作成したオブサーバーに監視させる -- */
sections.forEach(section => {
  sectionObserver.observe(section);
});

/* -- ③ コールバック関数を作成する -- */
function tocAddCurrent(entries) {
  entries.forEach(entry => {
    const indexList = sectionsArray.indexOf(entry.target);
    const currentList = document.querySelector(".toc-nav-item.is-active");
    if (entry.isIntersecting) {
      if (currentList !== null) {
        currentList.classList.remove("is-active");
      }
      contentsList.children[indexList].classList.add("is-active");
    }
  });
}

ノンプログラマーながら何とか実装することができました。
カレント表示のスタイル(CSS)は割愛しています。

おわりに

目次のカレント表示は Intersection Observer API を使うことによって、従来のスクロールの度にイベント発火する必要はなくなりました!
ブログ記事は、上にスクロールした時、下にスクロールした時、それぞれに余計なスクロールエフェクトをつけているために、その部分はスクロールの度にイベント発火してます(泣)。

かなりブログも放置してしまったので、今年は一年で50記事ぐらいは書きたい。
この記事は昨年の下書き途中で放置したものを仕上げたもので、2023年一発目の記事とは言えない気分です。