Angularで動的に生成した記事の目次(TableOfContents)とスムーズスクロールを実装する
作成 更新はじめに
Qiita などの記事投稿サービスやライブラリのドキュメントには TableOfContents と言われる記事の目次があります。その Angular での実装方法をまとめました。
1.サイドナビに見出しリスト作成
今回は、Async pipeでサブスクライブされ、動的に生成された要素に対して見出しリストを作成します。 まず、要素を取得する関数を作ります。
getHeading() {
setTimeout(() => {
//HTMLで生成されている箇所のクラスを取得
const headingTagElements = document.querySelectorAll(
'.main__content h2, .main__content h3, .main__content h4'
);
//forループして、生成されている箇所のidにindex番号を含めて代入
headingTagElements.forEach((headingTagElement, index) => {
headingTagElement.id = 'heading' + index;
this.headingElements.push(headingTagElement);
this.headingPositions.push(
headingTagElement.getBoundingClientRect().top
);
});
}, 100);
}
querySelectorAll は複数取得した場合、要素が HTMLElement ではなく、NodeElement と型が変わってしまうため offsetTop が使えないですが、getBoundingClientRect().topで現在のスクロール位置から要素の上までのスクロール量を取得できます。
headingElements、headingPositions は後述します。ここではエレメント自体と高さの値を配列に追加しています。
setTimeout を記述している理由は、FireStore などから流れてきた Observable を読み込む前に関数が動くと取得できないためです。そのため、関数の実行を記述する位置は、以下のように Observable が流れてきた後の Operator 内が適切だと思います。
article$.pipe(tap(() => this.getHeading()));
次に以下のように見出しリストを生成します。
<div class="table">
<div class="table__title">目次</div>
<ul class="table__content">
<li
class="table-list"
*ngFor="let headingElement of headingElements; index as i"
>
<a
class="table-list__item"
[href]="'#' + headingElement.id"
[class.active]="i === activeHeadingIndex"
[class.h3]="headingElement.tagName === 'H3'"
[class.h4]="headingElement.tagName === 'H4'"
(click)="scrollToHeading($event)"
>{{ headingElement.textContent }}</a
>
</li>
</ul>
</div>
ngFor で取得した要素をループさせ、見出しリストを生成しています。
後にスムーズスクロールを行うため、先ほど代入した要素の id に遷移するためのページ内リンクを href 属性にバインドしています。
activeHeadingIndex は条件が true の時にクラスを付与したいため、記述しています。
2.スクロール位置にハイライト
まず、HostListenerという Angular のデコレーターを使い、Angular から見たホストつまりブラウザの挙動を感知します。ここではスクロールイベントを検知します。この利用は Javascript の addEventListener に似ていると思います。
加えて、スクロール位置にハイライトするための関数を作成します。
@HostListener('window:scroll', ['$event'])
getTableOfContents() {
if (this.headingPositions.length) {
const headerHeight = 0;
const buffer = 20;
const position = window.pageYOffset + headerHeight + buffer;
this.headingPositions.forEach((headingPosition, index) => {
if (headingPosition < position) {
this.activeHeadingIndex = index;
}
});
}
}
現在のページ位置を position で定義しています。buffer は、スクロールした時に要素がページ上部にあるのにハイライトが前の要素を指すのを避けるため、余分に数値を追加するため記述しています。1 で取得した headingPositions をループし、見出しより下にスクロールした場合にその見出しに active というスタイルがつくようにここで activeHeadingIndex にどの見出しかの番号を代入しています。
3.クリックでページ内リンクスムーズスクロール
このままでもリンクの場所に遷移することはできますが、アニメーションもなく、header が fixed されている場合、位置がずれてしまいます。そのため、調整し、スムーズスクロールする関数を記述します。
scrollToHeading(event) {
const id = event.target.hash.replace('#', '');
if (id !== '') {
const rectTop = document.getElementById(id).getBoundingClientRect().top;
const position = window.pageYOffset;
const headerHeight = 0;
const top = rectTop + position - this.headerHeight;
window.scrollTo({
top,
behavior: 'smooth',
});
}
return false;
}
click イベントからクリックしたリンク先を受け取り、その現在のスクロール位置から要素の上までのスクロール量から現在のスクロール量を足し、scrollToでスムーズスクロールを実装できます。
実装コード
angular-toc-sample - StackBlitz
css は angular ドキュメントの toc を参考にしています。
> 一覧に戻る