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

Commit 7d0297b

Browse files
amcdnljelbourn
authored andcommitted
feat(toc): add table of contents to overview and guides (#230)
1 parent 1b29bb0 commit 7d0297b

22 files changed

+4259
-551
lines changed

package-lock.json

Lines changed: 3816 additions & 522 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/_app-theme.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@import './app/shared/example-viewer/example-viewer-theme';
1010
@import './app/shared/footer/footer-theme';
1111
@import './app/shared/navbar/navbar-theme';
12+
@import './app/shared/table-of-contents/table-of-contents-theme';
1213
@import './styles/api-theme';
1314
@import './styles/markdown-theme';
1415
@import './styles/svg-theme';
@@ -53,4 +54,5 @@
5354
@include guide-list-theme($theme);
5455
@include home-page-theme($theme);
5556
@include nav-bar-theme($theme);
57+
@include table-of-contents-theme($theme);
5658
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
<doc-viewer
22
documentUrl="/assets/documents/overview/{{componentViewer.componentDocItem.id}}.html"
3-
class="docs-component-view-text-content"></doc-viewer>
3+
class="docs-component-view-text-content docs-component-overview"
4+
(contentLoaded)="toc.updateScrollPosition()">
5+
</doc-viewer>
6+
<table-of-contents #toc container=".mat-sidenav-content"></table-of-contents>

src/app/pages/component-viewer/component-viewer.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,30 @@ app-component-viewer {
2424
}
2525

2626
.docs-component-viewer-content {
27+
position: relative;
2728
min-height: 500px;
29+
30+
table-of-contents {
31+
display: inline-flex;
32+
top: 35px;
33+
34+
@media (max-width: $small-breakpoint-width) {
35+
display: none;
36+
}
37+
}
38+
}
39+
40+
.docs-component-view-text-content {
41+
display: inline-flex;
42+
flex-grow: 1;
43+
}
44+
45+
.docs-component-overview {
46+
width: 80%;
47+
margin-right: 20px;
48+
49+
@media (max-width: $small-breakpoint-width) {
50+
width: 100%;
51+
margin-right: 0;
52+
}
2853
}

src/app/pages/component-viewer/component-viewer.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const exampleKey = 'button-types';
1414

1515
const mockActivatedRoute = {
1616
snapshot: {},
17+
fragment: Observable.create(observer => {
18+
observer.complete();
19+
}),
1720
params: Observable.create(observer => {
1821
observer.next({id: docItemsId});
1922
observer.complete();

src/app/pages/component-viewer/component-viewer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {ComponentPageTitle} from '../page-title/page-title';
55
import {MdTabsModule} from '@angular/material';
66
import {DocViewerModule} from '../../shared/doc-viewer/doc-viewer-module';
77
import {CommonModule} from '@angular/common';
8-
8+
import {TableOfContentsModule} from '../../shared/table-of-contents/table-of-contents.module';
99

1010
@Component({
1111
selector: 'app-component-viewer',
@@ -55,10 +55,14 @@ export class ComponentApi extends ComponentOverview {}
5555
})
5656
export class ComponentExamples extends ComponentOverview {}
5757

58-
59-
6058
@NgModule({
61-
imports: [MdTabsModule, RouterModule, DocViewerModule, CommonModule],
59+
imports: [
60+
MdTabsModule,
61+
RouterModule,
62+
DocViewerModule,
63+
CommonModule,
64+
TableOfContentsModule
65+
],
6266
exports: [ComponentViewer],
6367
declarations: [ComponentViewer, ComponentOverview, ComponentApi, ComponentExamples],
6468
providers: [DocumentationItems, ComponentPageTitle],

src/app/pages/guide-viewer/guide-viewer.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ <h1>{{guide.name}}</h1>
33
</div>
44

55
<div class="docs-guide-wrapper">
6-
<doc-viewer class="docs-guide-content" [documentUrl]="guide.document"></doc-viewer>
6+
<div class="docs-guide-toc-and-content">
7+
<doc-viewer class="docs-guide-content"
8+
(contentLoaded)="toc.updateScrollPosition()"
9+
[documentUrl]="guide.document"></doc-viewer>
10+
<table-of-contents #toc></table-of-contents>
11+
</div>
712
</div>
813

914
<app-footer></app-footer>
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
@import "../../../../node_modules/@angular/material/theming";
12
@import '../../../styles/constants';
23

4+
/* For desktop, the content should be aligned with the page title. */
5+
$guide-content-margin-side: 70px;
6+
$guide-content-margin-side-xs: 15px;
7+
38
:host {
49
display: flex;
510
flex-direction: column;
@@ -8,15 +13,37 @@
813

914
.docs-guide-wrapper {
1015
padding: 20px $content-padding-side 0;
16+
display: block;
17+
text-align: center;
1118

1219
@media ($mat-xsmall) {
1320
padding-left: $content-padding-side-xs;
1421
padding-right: $content-padding-side-xs;
1522
}
1623
}
1724

25+
.docs-guide-toc-and-content {
26+
display: inline-block;
27+
text-align: left;
28+
max-width: 940px;
29+
}
30+
1831
.docs-guide-content {
1932
display: block;
20-
margin: 0 auto;
21-
max-width: 940px;
33+
float: left;
34+
width: 80%;
35+
36+
@media (max-width: $small-breakpoint-width) {
37+
width: 100%;
38+
}
39+
}
40+
41+
table-of-contents {
42+
top: 35px;
43+
float: right;
44+
width: 15%;
45+
46+
@media (max-width: $small-breakpoint-width) {
47+
display: none;
48+
}
2249
}

src/app/pages/guide-viewer/guide-viewer.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {DocsAppTestingModule} from '../../testing/testing-module';
77
const guideItemsId = 'getting-started';
88

99
const mockActivatedRoute = {
10+
fragment: Observable.create(observer => {
11+
observer.complete();
12+
}),
1013
params: Observable.create(observer => {
1114
observer.next({id: guideItemsId});
1215
observer.complete();

src/app/pages/guide-viewer/guide-viewer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {ActivatedRoute, RouterModule, Router} from '@angular/router';
33
import {GuideItem, GuideItems} from '../../shared/guide-items/guide-items';
44
import {FooterModule} from '../../shared/footer/footer';
55
import {DocViewerModule} from '../../shared/doc-viewer/doc-viewer-module';
6+
import {TableOfContentsModule} from '../../shared/table-of-contents/table-of-contents.module';
67
import {ComponentPageTitle} from '../page-title/page-title';
78

89
@Component({
@@ -32,7 +33,7 @@ export class GuideViewer implements OnInit {
3233
}
3334

3435
@NgModule({
35-
imports: [DocViewerModule, FooterModule, RouterModule],
36+
imports: [DocViewerModule, FooterModule, RouterModule, TableOfContentsModule],
3637
exports: [GuideViewer],
3738
declarations: [GuideViewer],
3839
providers: [GuideItems, ComponentPageTitle],

src/app/shared/doc-viewer/doc-viewer-module.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@angular/material';
1111
import {CommonModule} from '@angular/common';
1212
import {NgModule} from '@angular/core';
13+
import {HeaderLink} from './header-link';
1314

1415

1516
// ExampleViewer is included in the DocViewerModule because they have a circular dependency.
@@ -23,8 +24,8 @@ import {NgModule} from '@angular/core';
2324
PortalModule,
2425
PlunkerButtonModule
2526
],
26-
declarations: [DocViewer, ExampleViewer],
27-
entryComponents: [ExampleViewer],
28-
exports: [DocViewer, ExampleViewer],
27+
declarations: [DocViewer, ExampleViewer, HeaderLink],
28+
entryComponents: [ExampleViewer, HeaderLink],
29+
exports: [DocViewer, ExampleViewer, HeaderLink],
2930
})
3031
export class DocViewerModule { }

src/app/shared/doc-viewer/doc-viewer.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import {
33
Component,
44
ComponentFactoryResolver,
55
ElementRef,
6+
EventEmitter,
67
Injector,
78
Input,
89
OnDestroy,
9-
ViewContainerRef
10+
ViewContainerRef,
11+
Output,
1012
} from '@angular/core';
1113
import {Http} from '@angular/http';
1214
import {ComponentPortal, DomPortalHost} from '@angular/material';
1315
import {ExampleViewer} from '../example-viewer/example-viewer';
14-
16+
import {HeaderLink} from './header-link';
1517

1618
@Component({
1719
selector: 'doc-viewer',
@@ -26,6 +28,8 @@ export class DocViewer implements OnDestroy {
2628
this._fetchDocument(url);
2729
}
2830

31+
@Output() contentLoaded = new EventEmitter<void>();
32+
2933
constructor(private _appRef: ApplicationRef,
3034
private _componentFactoryResolver: ComponentFactoryResolver,
3135
private _elementRef: ElementRef,
@@ -39,9 +43,10 @@ export class DocViewer implements OnDestroy {
3943
response => {
4044
// TODO(mmalerba): Trust HTML.
4145
if (response.ok) {
42-
let docHtml = response.text();
43-
this._elementRef.nativeElement.innerHTML = docHtml;
44-
this._loadLiveExamples();
46+
this._elementRef.nativeElement.innerHTML = response.text();
47+
this._loadComponents('material-docs-example', ExampleViewer);
48+
this._loadComponents('header-link', HeaderLink);
49+
this.contentLoaded.next();
4550
} else {
4651
this._elementRef.nativeElement.innerText =
4752
`Failed to load document: ${url}. Error: ${response.status}`;
@@ -58,22 +63,24 @@ export class DocViewer implements OnDestroy {
5863
// the wrong place in the DOM after switching tabs. This function is a workaround to
5964
// put the live examples back in the right place.
6065
this._clearLiveExamples();
61-
this._loadLiveExamples();
66+
this._loadComponents('material-docs-example', ExampleViewer);
67+
this._loadComponents('header-link', HeaderLink);
6268
}
6369

6470
/** Instantiate a ExampleViewer for each example. */
65-
private _loadLiveExamples() {
71+
private _loadComponents(componentName: string, componentClass: any) {
6672
let exampleElements =
67-
this._elementRef.nativeElement.querySelectorAll('[material-docs-example]');
73+
this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);
74+
6875
Array.prototype.slice.call(exampleElements).forEach((element: Element) => {
69-
let example = element.getAttribute('material-docs-example');
76+
let example = element.getAttribute(componentName);
7077

7178
let exampleContainer = document.createElement('div');
7279
element.appendChild(exampleContainer);
7380

7481
let portalHost = new DomPortalHost(
7582
exampleContainer, this._componentFactoryResolver, this._appRef, this._injector);
76-
let examplePortal = new ComponentPortal(ExampleViewer, this._viewContainerRef);
83+
let examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
7784
let exampleViewer = portalHost.attach(examplePortal);
7885
exampleViewer.instance.example = example;
7986

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {Component, Input, OnInit} from '@angular/core';
2+
import {Router} from '@angular/router';
3+
4+
/**
5+
* Header link is a component that handles normalizing
6+
* the anchor jump tags with the current route url.
7+
*
8+
* For example:
9+
*
10+
* <a href="#foo">Foo</a>
11+
*
12+
* would result in the wrong url, this component
13+
* combines the current route with that jump link:
14+
*
15+
* <a href="/guide#foo">Foo</a>
16+
*/
17+
@Component({
18+
selector: 'header-link',
19+
template: `
20+
<a
21+
title="Link to this heading"
22+
[attr.aria-describedby]="example"
23+
class="docs-markdown-a docs-header-link"
24+
aria-label="Link to this heading"
25+
[href]="url">
26+
<md-icon>link</md-icon>
27+
</a>
28+
`
29+
})
30+
export class HeaderLink implements OnInit {
31+
32+
@Input() example: string;
33+
34+
url: string;
35+
private _rootUrl: string;
36+
37+
constructor(router: Router) {
38+
this._rootUrl = router.url.split('#')[0];
39+
}
40+
41+
ngOnInit(): void {
42+
this.url = `${this._rootUrl}#${this.example}`;
43+
}
44+
45+
}

src/app/shared/plunker/plunker-button.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class PlunkerButton {
1818
* The form creation usually happens extremely quickly, but we handle the case of the
1919
* plunker not yet being ready for people will poor network connections or slow devices.
2020
*/
21-
isDisabled: boolean = false;
21+
isDisabled = false;
2222
plunkerForm: HTMLFormElement;
2323

2424
@Input()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@mixin table-of-contents-theme($theme) {
2+
$primary: map-get($theme, primary);
3+
$accent: map-get($theme, accent);
4+
$warn: map-get($theme, warn);
5+
$background: map-get($theme, background);
6+
$foreground: map-get($theme, foreground);
7+
8+
.docs-toc-container {
9+
border-left: solid 4px mat-color($primary);
10+
11+
.docs-link {
12+
color: mat-color($foreground, secondary-text);
13+
14+
&:hover,
15+
&.docs-active {
16+
color: mat-color($primary);
17+
}
18+
}
19+
}
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div *ngIf="links?.length" class="docs-toc-container">
2+
<div class="docs-toc-heading">Contents</div>
3+
<nav>
4+
<a [href]="_rootUrl + '#' + link.id"
5+
*ngFor="let link of links; let i = index"
6+
class="docs-level-{{link.type}} docs-link"
7+
[class.docs-active]="link.active">
8+
{{link.name}}
9+
</a>
10+
</nav>
11+
</div>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {CommonModule} from '@angular/common';
2+
import {NgModule} from '@angular/core';
3+
import {TableOfContents} from './table-of-contents';
4+
import {RouterModule} from '@angular/router';
5+
6+
@NgModule({
7+
imports: [CommonModule, RouterModule],
8+
declarations: [TableOfContents],
9+
exports: [TableOfContents],
10+
entryComponents: [TableOfContents],
11+
})
12+
export class TableOfContentsModule { }

0 commit comments

Comments
 (0)