Contents
diff --git a/src/app/shared/table-of-contents/table-of-contents.module.ts b/src/app/shared/table-of-contents/table-of-contents.module.ts
index b28ec7804..6e596ac31 100644
--- a/src/app/shared/table-of-contents/table-of-contents.module.ts
+++ b/src/app/shared/table-of-contents/table-of-contents.module.ts
@@ -1,10 +1,11 @@
import {CommonModule} from '@angular/common';
+import {ScrollDispatchModule} from '@angular/cdk/scrolling';
import {NgModule} from '@angular/core';
import {TableOfContents} from './table-of-contents';
import {RouterModule} from '@angular/router';
@NgModule({
- imports: [CommonModule, RouterModule],
+ imports: [CommonModule, RouterModule, ScrollDispatchModule],
declarations: [TableOfContents],
exports: [TableOfContents],
entryComponents: [TableOfContents],
diff --git a/src/app/shared/table-of-contents/table-of-contents.spec.ts b/src/app/shared/table-of-contents/table-of-contents.spec.ts
index 40b4de43e..53e05495d 100644
--- a/src/app/shared/table-of-contents/table-of-contents.spec.ts
+++ b/src/app/shared/table-of-contents/table-of-contents.spec.ts
@@ -39,13 +39,12 @@ describe('TableOfContents', () => {
});
it('should have header and links', () => {
- component.links = [
+ component._links = [
{
type: 'h2',
id: 'test',
name: 'test',
top: 0,
- active: false
}
];
diff --git a/src/app/shared/table-of-contents/table-of-contents.ts b/src/app/shared/table-of-contents/table-of-contents.ts
index 9735aeed2..4df35b888 100644
--- a/src/app/shared/table-of-contents/table-of-contents.ts
+++ b/src/app/shared/table-of-contents/table-of-contents.ts
@@ -1,11 +1,10 @@
-import {Component, ElementRef, Inject, Input, OnInit} from '@angular/core';
+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 'rxjs/add/observable/fromEvent';
-import 'rxjs/add/operator/debounceTime';
-import 'rxjs/add/operator/takeUntil';
-import {Observable} from 'rxjs/Observable';
+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*/
@@ -14,9 +13,6 @@ interface Link {
/* header type h3/h4 */
type: string;
- /* If the anchor is in view of the page */
- active: boolean;
-
/* name of the anchor */
name: string;
@@ -27,14 +23,14 @@ interface Link {
@Component({
selector: 'table-of-contents',
styleUrls: ['./table-of-contents.scss'],
- templateUrl: './table-of-contents.html'
+ templateUrl: './table-of-contents.html',
})
-export class TableOfContents implements OnInit {
+export class TableOfContents implements OnDestroy, OnInit {
- @Input() links: Link[] = [];
- @Input() container: string;
@Input() headerSelectors = '.docs-markdown-h3,.docs-markdown-h4';
+ _links: Link[] = [];
+ _activeLinkIndex: number;
_rootUrl: string;
private _scrollContainer: any;
private _destroyed = new Subject();
@@ -43,92 +39,82 @@ export class TableOfContents implements OnInit {
constructor(private _router: Router,
private _route: ActivatedRoute,
private _element: ElementRef,
+ private _scrollDispatcher: ScrollDispatcher,
+ private _ngZone: NgZone,
@Inject(DOCUMENT) private _document: Document) {
- this._router.events.takeUntil(this._destroyed).subscribe((event) => {
- if (event instanceof NavigationEnd) {
+ // 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._links = this.createLinks();
this._rootUrl = rootUrl;
}
- }
- });
-
- this._route.fragment.takeUntil(this._destroyed).subscribe(fragment => {
- this._urlFragment = fragment;
-
- const target = document.getElementById(this._urlFragment);
- if (target) {
- target.scrollIntoView();
- }
- });
+ });
+
+ // Scroll to section when the fragment changes
+ this._route.fragment
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(fragment => {
+ this._urlFragment = fragment;
+ this.scrollFragmentIntoView();
+ });
}
- ngOnInit(): void {
- // On init, the sidenav content element doesn't yet exist, so it's not possible
- // to subscribe to its scroll event until next tick (when it does exist).
- Promise.resolve().then(() => {
- this._scrollContainer = this.container ?
- this._document.querySelectorAll(this.container)[0] : window;
-
- Observable.fromEvent(this._scrollContainer, 'scroll')
- .takeUntil(this._destroyed)
- .debounceTime(10)
- .subscribe(() => this.onScroll());
- });
+ 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();
}
- updateScrollPosition(): void {
- this.links = this.createLinks();
+ 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 the scroll offset of the scroll container */
- private getScrollOffset(): number {
- const {top} = this._element.nativeElement.getBoundingClientRect();
- if (typeof this._scrollContainer.scrollTop !== 'undefined') {
- return this._scrollContainer.scrollTop + top;
- } else if (typeof this._scrollContainer.pageYOffset !== 'undefined') {
- return this._scrollContainer.pageYOffset + top;
- }
- }
-
+ /** Gets links generated from header selectors. */
private createLinks(): Link[] {
- const links = [];
const headers =
Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[];
- if (headers.length) {
- for (const header of headers) {
- // remove the 'link' icon name from the inner text
- const name = header.innerText.trim().replace(/^link/, '');
- const {top} = header.getBoundingClientRect();
- links.push({
- name,
- type: header.tagName.toLowerCase(),
- top: top,
- id: header.id,
- active: false
- });
- }
- }
-
- return links;
+ 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 onScroll(): void {
- for (let i = 0; i < this.links.length; i++) {
- this.links[i].active = this.isLinkActive(this.links[i], this.links[i + 1]);
- }
+ private setActiveLink(): void {
+ this._activeLinkIndex = this._links
+ .findIndex((link, i) => this.isLinkActive(link, this._links[i + 1]));
}
private isLinkActive(currentLink: any, nextLink: any): boolean {
@@ -138,4 +124,24 @@ export class TableOfContents implements OnInit {
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;
+ }
+
}