This repository was archived by the owner on Dec 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 397
/
Copy pathtable-of-contents.ts
147 lines (124 loc) · 4.28 KB
/
table-of-contents.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import {Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {ScrollDispatcher, CdkScrollable} from '@angular/cdk/scrolling';
import {Subject} from 'rxjs/Subject';
import {filter} from 'rxjs/operators/filter';
import {takeUntil} from 'rxjs/operators/takeUntil';
interface Link {
/* id of the section*/
id: string;
/* header type h3/h4 */
type: string;
/* name of the anchor */
name: string;
/* top offset px of the anchor */
top: number;
}
@Component({
selector: 'table-of-contents',
styleUrls: ['./table-of-contents.scss'],
templateUrl: './table-of-contents.html',
})
export class TableOfContents implements OnDestroy, OnInit {
@Input() headerSelectors = '.docs-markdown-h3,.docs-markdown-h4';
_links: Link[] = [];
_activeLinkIndex: number;
_rootUrl: string;
private _scrollContainer: any;
private _destroyed = new Subject();
private _urlFragment = '';
constructor(private _router: Router,
private _route: ActivatedRoute,
private _element: ElementRef,
private _scrollDispatcher: ScrollDispatcher,
private _ngZone: NgZone,
@Inject(DOCUMENT) private _document: Document) {
// Create new links and save root url at the end of navigation
this._router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._destroyed),
)
.subscribe(event => {
const rootUrl = _router.url.split('#')[0];
if (rootUrl !== this._rootUrl) {
this._links = this.createLinks();
this._rootUrl = rootUrl;
}
});
// Scroll to section when the fragment changes
this._route.fragment
.pipe(takeUntil(this._destroyed))
.subscribe(fragment => {
this._urlFragment = fragment;
this.scrollFragmentIntoView();
});
}
ngOnInit() {
// Update active link after scroll events
this._scrollDispatcher.scrolled()
.pipe(takeUntil(this._destroyed))
.subscribe(scrollable =>
this._ngZone.run(() => {
this.updateScrollContainer(scrollable);
this.setActiveLink();
}));
}
ngOnDestroy(): void {
this._destroyed.next();
this._destroyed.complete();
}
createLinksAndScroll(): void {
this._links = this.createLinks();
this.scrollFragmentIntoView();
}
/** Find the target from the url fragment and scroll it into view. */
private scrollFragmentIntoView(): void {
const target = document.getElementById(this._urlFragment);
if (target) {
target.scrollIntoView();
}
}
/** Gets links generated from header selectors. */
private createLinks(): Link[] {
const headers =
Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[];
return headers.map(header => {
// remove the 'link' icon name from the inner text
const name = header.innerText.trim().replace(/^link/, '');
const {top} = header.getBoundingClientRect();
return {
name,
top,
type: header.tagName.toLowerCase(),
id: header.id
};
});
}
private setActiveLink(): void {
this._activeLinkIndex = this._links
.findIndex((link, i) => this.isLinkActive(link, this._links[i + 1]));
}
private isLinkActive(currentLink: any, nextLink: any): boolean {
// A link is considered active if the page is scrolled passed the anchor without also
// being scrolled passed the next link
const scrollOffset = this.getScrollOffset();
return scrollOffset >= currentLink.top && !(nextLink && nextLink.top < scrollOffset);
}
/** Gets the scroll offset of the scroll container */
private getScrollOffset(): number {
const {top} = this._element.nativeElement.getBoundingClientRect();
if (this._scrollContainer.scrollTop != null) {
return this._scrollContainer.scrollTop + top;
}
if (this._scrollContainer.pageYOffset != null) {
return this._scrollContainer.pageYOffset + top;
}
return 0;
}
private updateScrollContainer(scrollable: CdkScrollable | void): void {
this._scrollContainer = scrollable ?
scrollable.getElementRef().nativeElement :
window;
}
}