作成日: 2023-4-16 更新日: 2023-4-25
カテゴリ- : ITブログ
目次にハイライト

目次にハイライト

ステップ1: Vue.jsプロジェクトのセットアップ

まず、Vue.jsプロジェクトを作成してください。Vue CLIを使用している場合は、以下のコマンドで新しいプロジェクトを作成できます。

vue create my-toc-project
cd my-toc-project
 

ステップ2: TableOfContentsコンポーネントの作成

src/componentsディレクトリ内にTableOfContents.vueコンポーネントを作成し、以下のような基本的な構造を追加してください。

<template>
  <div>
    <!-- テーブルオブコンテンツがここに表示されます -->
  </div>
</template>

<script>
export default {
  // コンポーネントの機能をここに実装します
}
</script>
 

ステップ3: data関数で初期状態を設定する

data関数を追加し、見出しのリスト、現在ハイライトされている見出し、およびIntersectionObserverのインスタンスを初期化します。

data() {
  return {
    headings: [],
    highlighted: '',
    observer: null,
  }
},

 

ステップ4: mountedライフサイクルフックで見出しを取得し、Observerを初期化する

mountedライフサイクルフックを追加し、getHeadings()関数を使ってページ内の見出しを取得し、initObserver()関数を呼び出してIntersectionObserverを初期化します。

mounted() {
  this.headings = this.getHeadings()
  this.initObserver()
},

 

ステップ5: beforeDestroyライフサイクルフックでObserverの監視を解除する

beforeDestroyライフサイクルフックを追加し、コンポーネントが破棄される前にIntersectionObserverの監視を解除します。

beforeDestroy() {
  this.observer.disconnect()
},

ステップ6: getHeadings関数でページ内の見出しを取得する

getHeadings関数を追加し、ページ内のh1, h2, h3, h4要素をすべて選択し、そのテキストと一意のIDを持つオブジェクトの配列に変換しています。

methods: {
  getHeadings() {
    const headingElements = document.querySelectorAll('h1, h2, h3, h4');
    return Array.from(headingElements).map((element, index) => {
      const id = `heading${index + 1}`;
      element.id = id;
      return {
        id,
        text: element.textContent,
      };
    });
  },
},

ステップ7: initObserver関数でIntersectionObserverを初期化する

initObserver関数を追加し、IntersectionObserverを初期化して各見出し要素の表示状態を監視します。また、スクロールの方向と現在のスクロール位置を考慮して、ハイライトすべき見出しを決定します。

methods: {
  // ...前述のgetHeadings関数がここにある...

  initObserver() {
    const options = {
      root: document.body,
      rootMargin: '0px 0px -30% 0px',
      threshold: Array.from({ length: 100 }, (_, i) => i / 100),
    };

    let lastY = 0;
    let lastHighlightedId = '';

    this.observer = new IntersectionObserver((entries) => {
      const currentY = window.scrollY;
      const scrollingDown = currentY > lastY;

      const relevantEntry = entries.reduce((acc, entry) => {
        if (!entry.isIntersecting) return acc;
        if (!acc) return entry;

        const isMoreRelevant = scrollingDown
          ? entry.boundingClientRect.top > acc.boundingClientRect.top
          : entry.boundingClientRect.top < acc.boundingClientRect.top;

        return isMoreRelevant ? entry : acc;
      }, null);

      if (relevantEntry && relevantEntry.target.id !== lastHighlightedId) {
        lastHighlightedId = relevantEntry.target.id;
        this.highlighted = lastHighlightedId;
      }

      lastY = currentY;
    }, options);

    this.headings.forEach((heading) => {
      const element = document.getElementById(heading.id);
      if (element) {
        this.observer.observe(element);
      }
    });
  },
},

 

この章では、initObserver関数を追加し、IntersectionObserverを初期化して各見出し要素の表示状態を監視する方法について説明します。また、スクロールの方向と現在のスクロール位置を考慮して、ハイライトすべき見出しを決定します。

7.1 initObserver関数を追加する

まず、methodsオブジェクト内にinitObserver関数を作成します。この関数では、IntersectionObserverのインスタンスを作成し、各見出し要素の表示状態を監視します。

methods: {
  // ...前述のgetHeadings関数がここにある...

  initObserver() {
    // この部分を実装します
  },
},

7.2 IntersectionObserverのオプションを設定する

次に、initObserver関数内で、IntersectionObserverのオプションを設定します。root, rootMargin, thresholdという3つのオプションを設定します。

initObserver() {
  const options = {
    root: document.body,
    rootMargin: '0px 0px -30% 0px',
    threshold: Array.from({ length: 100 }, (_, i) => i / 100),
  };

  // この部分でIntersectionObserverを初期化します
},

  • root: 監視対象の要素が表示される領域を指定します。ここではdocument.bodyを設定して、ページ全体を監視対象とします。
  • rootMargin: root要素の余白を設定します。見出し要素が表示領域の30%より上にある場合に交差判定が発生するように、30%を設定しています。
  • threshold: 交差判定が発生する閾値を設定します。0から1までの100個の値を配列で設定し、要素が表示領域に入る度合いに応じて判定が発生するようにします。

7.3 IntersectionObserverを初期化し、監視対象を設定する

initObserver関数内で、IntersectionObserverを初期化し、見出し要素を監視対象に設定します。

initObserver() {
  // ...前述のoptions変数がここにある...

  this.observer = new IntersectionObserver((entries) => {
    // この部分で見出し要素の表示状態に応じて処理を行います
  }, options);

  this.headings.forEach((heading) => {
    const element = document.getElementById(heading.id);
    if (element) {
      this.observer.observe(element);
    }
	});
},

上記のコードでは、`IntersectionObserver`インスタンスを作成し、コールバック関数と`options`を渡しています。`entries`は、交差判定が発生した要素の配列です。また、`headings`配列をループして、各見出し要素を監視対象に設定しています。

7.4 スクロール方向と現在のスクロール位置を検出する

`IntersectionObserver`のコールバック関数内で、スクロール方向と現在のスクロール位置を検出し、ハイライトすべき見出しを決定します。

this.observer = new IntersectionObserver((entries) => {
  const currentY = window.scrollY;
  const scrollingDown = currentY > lastY;

  // ...この部分でrelevantEntryを求める...

  lastY = currentY;
}, options);

currentYは、現在のスクロール位置を表します。scrollingDownは、前回のスクロール位置lastYと比較して、現在のスクロール位置が下にある場合にtrueとなります。最後に、現在のスクロール位置をlastYに格納して、次回の判定に使用します。

7.5 相対的に重要な見出し要素を選択する

entries配列を繰り返し処理し、相対的に重要な見出し要素を選択します。スクロール方向に応じて、交差判定が発生した要素のうち、最も上または下にあるものを選択します。

const relevantEntry = entries.reduce((acc, entry) => {
  if (!entry.isIntersecting) return acc;
  if (!acc) return entry;

  const isMoreRelevant = scrollingDown
    ? entry.boundingClientRect.top > acc.boundingClientRect.top
    : entry.boundingClientRect.top < acc.boundingClientRect.top;

  return isMoreRelevant ? entry : acc;
}, null);

reduce関数を使用して、entries配列の要素を比較し、最も関連性の高い要素(relevantEntry)を求めます。交差していない要素は無視され、交差している要素のうち最も上または下にあるものが選択されます。

7.6 ハイライトすべき見出しを更新する

最後に、relevantEntryがある場合、その見出し要素のIDをhighlightedデータプロパティに格納します。これにより、対応する目次項目がハイライトされます。

if (relevantEntry && relevantEntry.target.id !== lastHighlightedId) {
  lastHighlightedId = relevantEntry.target.id;
  this.highlighted = lastHighlightedId;
}

 

7.7 ページの最後の要素をハイライトする

ページの一番下までスクロールした場合、最後の見出し要素がハイライトされるように処理を追加します。この処理をIntersectionObserverのコールバック関数内に追加します。

javascriptCopy code
// ...relevantEntryを求める処理の後...

// 追加: ページの一番下で

7.5 相対的に重要な見出し要素を選択する

entries配列を繰り返し処理し、相対的に重要な見出し要素を選択します。スクロール方向に応じて、交差判定が発生した要素のうち、最も上または下にあるものを選択します。

javascriptCopy code
const relevantEntry = entries.reduce((acc, entry) => {
  if (!entry.isIntersecting) return acc;
  if (!acc) return entry;

  const isMoreRelevant = scrollingDown
    ? entry.boundingClientRect.top > acc.boundingClientRect.top
    : entry.boundingClientRect.top < acc.boundingClientRect.top;

  return isMoreRelevant ? entry : acc;
}, null);

reduce関数を使用して、entries配列の要素を比較し、最も関連性の高い要素(relevantEntry)を求めます。交差していない要素は無視され、交差している要素のうち最も上または下にあるものが選択されます。

7.6 ハイライトすべき見出しを更新する

最後に、relevantEntryがある場合、その見出し要素のIDをhighlightedデータプロパティに格納します。これにより、対応する目次項目がハイライトされます。

javascriptCopy code
if (relevantEntry && relevantEntry.target.id !== lastHighlightedId) {
  lastHighlightedId = relevantEntry.target.id;
  this.highlighted = lastHighlightedId;
}

// 追加: ページの一番下で最後の要素をハイライトする
const lastElementIndex = this.headings.length - 1;
const lastElement = document.getElementById(this.headings[lastElementIndex].id);
const lastElementReached = lastElement.getBoundingClientRect().bottom <= window.innerHeight;

if (lastElementReached) {
  this.highlighted = lastElement.id;
}

ここで、lastElementIndexlastElementを使って最後の見出し要素を取得し、lastElementReachedでその要素が表示領域の最後まで達しているかどうかを判断します。もし最後まで達していたら、this.highlightedにその要素のIDを設定してハイライトします。

7.8 コンポーネントのクリーンアップ

コンポーネントが破棄される前に、IntersectionObserverのインスタンスを切断し、メモリリークを防ぐようにします。beforeDestroyライフサイクルフックを使用して、observer.disconnect()を呼び出します。

beforeDestroy() {
  this.observer.disconnect();
},

これで、コンポーネントが破棄されるときに、IntersectionObserverのインスタンス

 

7.7 ページの最後の要素をハイライトする

ページの一番下までスクロールした場合、最後の見出し要素がハイライトされるように処理を追加します。この処理をIntersectionObserverのコールバック関数内に追加します。

// ...relevantEntryを求める処理の後...

// 追加: ページの一番下で最後の要素をハイライトする
const lastElementIndex = this.headings.length - 1;
const lastElement = document.getElementById(this.headings[lastElementIndex].id);
const lastElementReached = lastElement.getBoundingClientRect().bottom <= window.innerHeight;

if (lastElementReached) {
  this.highlighted = lastElement.id;
}

ここで、lastElementIndexlastElementを使って最後の見出し要素を取得し、lastElementReachedでその要素が表示領域の最後まで達しているかどうかを判断します。もし最後まで達していたら、this.highlightedにその要素のIDを設定してハイライトします。

 

ステップ8: 目次を表示する

<template>内に以下のコードを追加して、目次を表示します。v-forディレクティブを使って、headings配列の各項目をループし、highlightedデータプロパティに基づいてハイライトを表示します。

<template>
  <div>
    <ul>
      <li v-for="heading in headings" :key="heading.id">
        <a
          :href="'#' + heading.id"
          :class="{ highlighted: highlighted === heading.id }"
        >
          {{ heading.text }}
        </a>
      </li>
    </ul>
  </div>
</template>

 

ステップ9: スタイルを適用する

最後に、ハイライトされた見出しにスタイルを適用します。<style>タグを追加し、以下のCSSを追加してください。

<style>
.highlighted {
  color: #3498db;
  font-weight: bold;
}
</style>

これで、動的なテーブルオブコンテンツの実装が完了しました。スクロールすると、対応する見出しがハイライトされ、その見出しまでジャンプできるリンクが表示されます。

ステップ10: コンポーネントを使用する

TableOfContentsコンポーネントを使用するには、`src/App.vue`または適切なページコンポーネントにインポートし、`<table-of-contents>`タグを追加してください。

<template>
  <div id="app">
    <table-of-contents />
    <!-- 他のコンテンツがここに続く -->
  </div>
</template>

<script>
import TableOfContents from './components/TableOfContents.vue';

export default {
  components: {
    TableOfContents,
  },
};
</script>

これで、プロジェクト内の任意のページにTableOfContentsコンポーネントを追加し、動的に目次を表示できます。ユーザーがスクロールすると、目次の対応する項目がハイライトされ、その見出しまでジャンプできるリンクが表示されます。

 

お気軽にお問い合わせください

必須項目を全て入力してください (入力後、送信ボタンが表示されます) 24時間以内にご連絡を差し上げます。

※税理士に対する営業は業務の妨げになり迷惑です。やめてください。