目次にハイライト
ステップ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;
}
ここで、lastElementIndex
とlastElement
を使って最後の見出し要素を取得し、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;
}
ここで、lastElementIndex
とlastElement
を使って最後の見出し要素を取得し、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コンポーネントを追加し、動的に目次を表示できます。ユーザーがスクロールすると、目次の対応する項目がハイライトされ、その見出しまでジャンプできるリンクが表示されます。