Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

refactor(toc): use cdkScrollable for scrolling, remove unnecessary inputs #321

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 2 additions & 4 deletions src/app/pages/component-viewer/component-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@
<doc-viewer
documentUrl="/assets/documents/api/{{componentViewer.componentDocItem.packageName}}-{{componentViewer.componentDocItem.id}}.html"
class="docs-component-view-text-content docs-component-api"
(contentLoaded)="toc.updateScrollPosition()"></doc-viewer>
<table-of-contents #toc
headerSelectors=".docs-api-h3,.docs-api-h4"
container=".mat-drawer-content"></table-of-contents>
(contentLoaded)="focusInitialTarget(); toc.createLinksAndScroll()"></doc-viewer>
<table-of-contents #toc headerSelectors=".docs-api-h3,.docs-api-h4"></table-of-contents>
4 changes: 2 additions & 2 deletions src/app/pages/component-viewer/component-overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
<doc-viewer
documentUrl="/assets/documents/overview/{{componentViewer.componentDocItem.packageName}}-{{componentViewer.componentDocItem.id}}.html"
class="docs-component-view-text-content docs-component-overview"
(contentLoaded)="toc.updateScrollPosition()">
(contentLoaded)="focusInitialTarget(); toc.createLinksAndScroll()">
</doc-viewer>
<table-of-contents #toc container=".mat-drawer-content"></table-of-contents>
<table-of-contents #toc></table-of-contents>
9 changes: 4 additions & 5 deletions src/app/pages/component-viewer/component-viewer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CommonModule} from '@angular/common';
import {Component, ElementRef, NgModule, OnInit, ViewChild, ViewEncapsulation} from '@angular/core';
import {Component, ElementRef, NgModule, ViewChild, ViewEncapsulation} from '@angular/core';
import {MatTabsModule} from '@angular/material';
import {ActivatedRoute, Params, Router, RouterModule} from '@angular/router';
import 'rxjs/add/operator/map';
Expand Down Expand Up @@ -50,14 +50,13 @@ export class ComponentViewer {
templateUrl: './component-overview.html',
encapsulation: ViewEncapsulation.None,
})
export class ComponentOverview implements OnInit {
export class ComponentOverview {
@ViewChild('intialFocusTarget') focusTarget: ElementRef;

constructor(public componentViewer: ComponentViewer) {}

ngOnInit() {
// 100ms timeout is used to allow the page to settle before moving focus for screen readers.
setTimeout(() => this.focusTarget.nativeElement.focus(), 100);
focusInitialTarget() {
this.focusTarget.nativeElement.focus();
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/guide-viewer/guide-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ <h1>{{guide.name}}</h1>

<div class="docs-guide-wrapper">
<div class="docs-guide-toc-and-content">
<doc-viewer class="docs-guide-content"
(contentLoaded)="toc.updateScrollPosition()"
<doc-viewer class="docs-guide-content"
(contentLoaded)="toc.createLinksAndScroll()"
[documentUrl]="guide.document"></doc-viewer>
<table-of-contents #toc></table-of-contents>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/shared/table-of-contents/table-of-contents.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div *ngIf="links?.length" class="docs-toc-container">
<div *ngIf="_links?.length" class="docs-toc-container">
<div class="docs-toc-heading">Contents</div>
<nav>
<a [href]="_rootUrl + '#' + link.id"
*ngFor="let link of links; let i = index"
*ngFor="let link of _links; let i = index"
class="docs-level-{{link.type}} docs-link"
[class.docs-active]="link.active">
[class.docs-active]="_activeLinkIndex === i">
{{link.name}}
</a>
</nav>
Expand Down
3 changes: 2 additions & 1 deletion src/app/shared/table-of-contents/table-of-contents.module.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down
3 changes: 1 addition & 2 deletions src/app/shared/table-of-contents/table-of-contents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
];

Expand Down
148 changes: 77 additions & 71 deletions src/app/shared/table-of-contents/table-of-contents.ts
Original file line number Diff line number Diff line change
@@ -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*/
Expand All @@ -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;

Expand All @@ -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();
Expand All @@ -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 {
Expand All @@ -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;
}

}