Skip to content

Commit d301182

Browse files
committed
feat(toc): add table of contents to overview and guides
1 parent 60b84cc commit d301182

19 files changed

+388
-16
lines changed

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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
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 component-overview">
4+
</doc-viewer>
5+
<table-of-contents
6+
container=".mat-sidenav-content">
7+
</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
@@ -14,7 +14,32 @@ app-component-viewer {
1414
}
1515

1616
.docs-component-viewer-content {
17+
position: relative;
1718
min-height: 500px;
19+
20+
table-of-contents {
21+
display: inline-flex;
22+
top: 35px;
23+
24+
@media (max-width: $small-breakpoint-width) {
25+
display: none;
26+
}
27+
}
28+
}
29+
30+
.docs-component-view-text-content {
31+
display: inline-flex;
32+
flex-grow: 1;
33+
}
34+
35+
.component-overview {
36+
width: 80%;
37+
margin-right: 20px;
38+
39+
@media (max-width: $small-breakpoint-width) {
40+
width: 100%;
41+
margin-right: 0;
42+
}
1843
}
1944

2045
.docs-example-source {

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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ 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 {HeaderLinkModule} from '../../shared/header-link/header-link.module';
9+
import {TableOfContentsModule} from '../../shared/table-of-contents/table-of-contents.module';
910

1011
@Component({
1112
selector: 'app-component-viewer',
@@ -49,10 +50,15 @@ export class ComponentApi extends ComponentOverview {}
4950
})
5051
export class ComponentExamples extends ComponentOverview {}
5152

52-
53-
5453
@NgModule({
55-
imports: [MdTabsModule, RouterModule, DocViewerModule, CommonModule],
54+
imports: [
55+
MdTabsModule,
56+
RouterModule,
57+
DocViewerModule,
58+
CommonModule,
59+
HeaderLinkModule,
60+
TableOfContentsModule
61+
],
5662
exports: [ComponentViewer],
5763
declarations: [ComponentViewer, ComponentOverview, ComponentApi, ComponentExamples],
5864
providers: [DocumentationItems, ComponentPageTitle],

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

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

5-
<doc-viewer class="docs-guide-content" [documentUrl]="guide.document"></doc-viewer>
5+
<div class="docs-guide-content">
6+
<doc-viewer class="docs-guide-viewer" [documentUrl]="guide.document"></doc-viewer>
7+
<table-of-contents></table-of-contents>
8+
</div>
69

710
<app-footer></app-footer>

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import "../../../../node_modules/@angular/material/theming";
2+
@import '../../../styles/constants';
23

34
/* For desktop, the content should be aligned with the page title. */
45
$guide-content-margin-side: 70px;
@@ -20,3 +21,22 @@ $guide-content-margin-side-xs: 15px;
2021
margin-right: $guide-content-margin-side-xs;
2122
}
2223
}
24+
25+
.docs-guide-viewer {
26+
width: 80%;
27+
display: block;
28+
float: left;
29+
30+
@media (max-width: $small-breakpoint-width) {
31+
width: 100%;
32+
}
33+
}
34+
35+
table-of-contents {
36+
float: right;
37+
top: 35px;
38+
39+
@media (max-width: $small-breakpoint-width) {
40+
display: none;
41+
}
42+
}

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {ActivatedRoute, RouterModule} 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-
6+
import {HeaderLinkModule} from '../../shared/header-link/header-link.module';
7+
import {TableOfContentsModule} from '../../shared/table-of-contents/table-of-contents.module';
78

89
@Component({
910
selector: 'guide-viewer',
@@ -21,7 +22,7 @@ export class GuideViewer {
2122
}
2223

2324
@NgModule({
24-
imports: [DocViewerModule, FooterModule, RouterModule],
25+
imports: [DocViewerModule, FooterModule, RouterModule, HeaderLinkModule, TableOfContentsModule],
2526
exports: [GuideViewer],
2627
declarations: [GuideViewer],
2728
providers: [GuideItems],

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import {Http} from '@angular/http';
1212
import {ComponentPortal, DomPortalHost} from '@angular/material';
1313
import {ExampleViewer} from '../example-viewer/example-viewer';
14-
14+
import {HeaderLink} from '../header-link/header-link';
1515

1616
@Component({
1717
selector: 'doc-viewer',
@@ -41,7 +41,8 @@ export class DocViewer implements OnDestroy {
4141
if (response.ok) {
4242
let docHtml = response.text();
4343
this._elementRef.nativeElement.innerHTML = docHtml;
44-
this._loadLiveExamples();
44+
this._loadComponents('material-docs-example', ExampleViewer);
45+
this._loadComponents('header-link', HeaderLink);
4546
} else {
4647
this._elementRef.nativeElement.innerText =
4748
`Failed to load document: ${url}. Error: ${response.status}`;
@@ -58,22 +59,24 @@ export class DocViewer implements OnDestroy {
5859
// the wrong place in the DOM after switching tabs. This function is a workaround to
5960
// put the live examples back in the right place.
6061
this._clearLiveExamples();
61-
this._loadLiveExamples();
62+
this._loadComponents('material-docs-example', ExampleViewer);
63+
this._loadComponents('header-link', HeaderLink);
6264
}
6365

6466
/** Instantiate a ExampleViewer for each example. */
65-
private _loadLiveExamples() {
67+
private _loadComponents(componentName: string, componentClass: any) {
6668
let exampleElements =
67-
this._elementRef.nativeElement.querySelectorAll('[material-docs-example]');
69+
this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);
70+
6871
Array.prototype.slice.call(exampleElements).forEach((element: Element) => {
69-
let example = element.getAttribute('material-docs-example');
72+
let example = element.getAttribute(componentName);
7073

7174
let exampleContainer = document.createElement('div');
7275
element.appendChild(exampleContainer);
7376

7477
let portalHost = new DomPortalHost(
7578
exampleContainer, this._componentFactoryResolver, this._appRef, this._injector);
76-
let examplePortal = new ComponentPortal(ExampleViewer, this._viewContainerRef);
79+
let examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
7780
let exampleViewer = portalHost.attach(examplePortal);
7881
exampleViewer.instance.example = example;
7982

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 {HeaderLink} from './header-link';
4+
import {RouterModule} from '@angular/router';
5+
6+
@NgModule({
7+
imports: [CommonModule, RouterModule],
8+
declarations: [HeaderLink],
9+
exports: [HeaderLink],
10+
entryComponents: [HeaderLink],
11+
})
12+
export class HeaderLinkModule { }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Component, Input} from '@angular/core';
2+
import {Router} from '@angular/router';
3+
4+
@Component({
5+
selector: 'header-link',
6+
template: `
7+
<a
8+
title="Link to this heading"
9+
class="header-link docs-markdown-a"
10+
aria-hidden="true"
11+
[href]="url">
12+
<i class="material-icons">link</i>
13+
</a>
14+
`
15+
})
16+
export class HeaderLink {
17+
18+
@Input() example: string;
19+
20+
get url(): string {
21+
return `${this._rootUrl}#${this.example}`;
22+
}
23+
24+
private _rootUrl: string;
25+
26+
constructor(router: Router) {
27+
this._rootUrl = router.url.split('#')[0];
28+
}
29+
30+
}

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+
.toc-container {
9+
border-left: solid 4px mat-color($primary);
10+
}
11+
12+
a {
13+
color: mat-color($foreground, secondary-text);
14+
15+
&:hover,
16+
&.active {
17+
color: mat-color($primary);
18+
}
19+
}
20+
}
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 { }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
:host {
2+
font-size: 13px;
3+
width: 15%;
4+
position: sticky;
5+
top: 0;
6+
}
7+
8+
.toc-container {
9+
padding: 5px 0 10px 12px;
10+
}
11+
12+
h2 {
13+
margin: 0;
14+
padding: 0;
15+
font-size: 13px;
16+
font-weight: bold;
17+
}
18+
19+
ul, li {
20+
padding: 0;
21+
margin: 0;
22+
list-style: none;
23+
}
24+
25+
li {
26+
line-height: 16px;
27+
margin: 8px 0 0;
28+
position: relative;
29+
}
30+
31+
a {
32+
text-decoration: none;
33+
}
34+
35+
.li-h4 {
36+
margin-left: 12px;
37+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {Observable} from 'rxjs/Observable';
3+
import {ActivatedRoute} from '@angular/router';
4+
import {TableOfContents} from './table-of-contents';
5+
import {TableOfContentsModule} from './table-of-contents.module';
6+
import {DocsAppTestingModule} from '../../testing/testing-module';
7+
8+
const mockActivatedRoute = {
9+
fragment: Observable.create(observer => {
10+
observer.complete();
11+
})
12+
};
13+
14+
describe('TableOfContents', () => {
15+
16+
beforeEach(async(() => {
17+
TestBed.configureTestingModule({
18+
imports: [TableOfContentsModule, DocsAppTestingModule],
19+
providers: [
20+
{provide: ActivatedRoute, useValue: mockActivatedRoute},
21+
]
22+
}).compileComponents();
23+
}));
24+
25+
let fixture: ComponentFixture<TableOfContents>;
26+
let component: TableOfContents;
27+
28+
beforeEach(() => {
29+
fixture = TestBed.createComponent(TableOfContents);
30+
component = fixture.componentInstance;
31+
fixture.detectChanges();
32+
});
33+
34+
it('should have no header', () => {
35+
const header = fixture
36+
.nativeElement
37+
.querySelector('h2');
38+
expect(header).toBeNull();
39+
});
40+
41+
it('should have header and links', () => {
42+
component.links = [{ type: 'h2', id: 'test', name: 'test'}];
43+
44+
const header = fixture.nativeElement.querySelector('h2');
45+
expect(header).toBeDefined();
46+
47+
const links = fixture.nativeElement.querySelector('li');
48+
expect(links).toBeDefined();
49+
});
50+
});

0 commit comments

Comments
 (0)