diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index 43df0c3794..d64217206c 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -1,11 +1,21 @@ - - - - - - + + + + + + - + + + + + + +
+ +
+ +
diff --git a/aio/src/app/app.component.scss b/aio/src/app/app.component.scss index 24f4a11d9d..7babcfc95e 100644 --- a/aio/src/app/app.component.scss +++ b/aio/src/app/app.component.scss @@ -20,3 +20,31 @@ md-input-container { display: none; } } + +.sidenav-container { + width: 100%; + height: 100vh; +} + +.sidenav-content { + height: 100%; + width: 100%; + margin: auto; + padding: 1rem; +} + +.sidenav-content button { + min-width: 50px; +} + +.sidenav { + padding: 0; +} + +// md-toolbar { +// display: none; +// padding-left: 10px !important; +// } +// md-toolbar.active { +// display: block; +// } diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index f3513f6f5c..f32093b400 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -2,8 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { AppModule } from './app.module'; -import { NavEngine } from './nav-engine/nav-engine.service'; - describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; @@ -12,7 +10,6 @@ describe('AppComponent', () => { TestBed.configureTestingModule({ imports: [ AppModule ], providers: [ - { provide: NavEngine, useValue: { currentDoc: undefined } } ] }); TestBed.compileComponents(); diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index a72528609b..1668793ecd 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -1,16 +1,107 @@ -import { Component, ViewChild } from '@angular/core'; - -import { SidenavComponent } from './sidenav/sidenav.component'; +import { Component, ViewChild, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { DocumentService, DocumentContents } from 'app/documents/document.service'; +import { NavigationService, NavigationViews } from 'app/navigation/navigation.service'; @Component({ selector: 'aio-shell', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + template: ` + + + + + + + + + + + + + + + +
+ + +
+ +
`, + styles: [ + `.fill-remaining-space { + flex: 1 1 auto; + } + + md-input-container { + margin-left: 10px; + input { + min-width:200px; + } + } + + + .md-input-element { + font-size: 70%; + font-style: italic; + } + + @media (max-width: 600px) { + aio-menu { + display: none; + } + } + + .sidenav-container { + width: 100%; + height: 100vh; + } + + .sidenav-content { + height: 100%; + width: 100%; + margin: auto; + padding: 1rem; + } + + .sidenav-content button { + min-width: 50px; + } + + .sidenav { + padding: 0; + } + + // md-toolbar { + // display: none; + // padding-left: 10px !important; + // } + // md-toolbar.active { + // display: block; + // }` + ] }) -export class AppComponent { +export class AppComponent implements OnInit { isHamburgerVisible = true; // always ... for now + isSideBySide = false; + sideBySideWidth = 600; - @ViewChild(SidenavComponent) sidenav: SidenavComponent; + currentDocument: Observable; + navigationViews: Observable; - toggleSideNav() { this.sidenav.toggle(); } + constructor(documentService: DocumentService, navigationService: NavigationService) { + this.currentDocument = documentService.currentDocument; + this.navigationViews = navigationService.navigationViews; + } + + ngOnInit() { + this.onResize(window.innerWidth); + } + + onResize(width) { + this.isSideBySide = width > this.sideBySideWidth; + } } diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 10af3bb29e..875a8f3708 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -2,6 +2,8 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpModule } from '@angular/http'; +import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; + import { MdToolbarModule } from '@angular/material/toolbar'; import { MdButtonModule} from '@angular/material/button'; import { MdIconModule} from '@angular/material/icon'; @@ -13,14 +15,17 @@ import { Platform } from '@angular/material/core'; // crashes with "missing first" operator when SideNav.mode is "over" import 'rxjs/add/operator/first'; -import { AppComponent } from './app.component'; -import { DocViewerComponent } from './doc-viewer/doc-viewer.component'; -import { embeddedComponents, EmbeddedComponents } from './embedded'; -import { Logger } from './logger.service'; -import { navDirectives, navProviders } from './nav-engine'; -import { SidenavComponent } from './sidenav/sidenav.component'; -import { NavItemComponent } from './sidenav/nav-item.component'; -import { MenuComponent } from './sidenav/menu.component'; +import { AppComponent } from 'app/app.component'; +import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; +import { embeddedComponents, EmbeddedComponents } from 'app/embedded'; +import { Logger } from 'app/shared/logger.service'; +import { LocationService } from 'app/shared/location.service'; +import { NavigationService } from 'app/navigation/navigation.service'; +import { DocumentService } from 'app/documents/document.service'; +import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component'; +import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component'; +import { NavItemComponent } from 'app/layout/nav-item/nav-item.component'; +import { LinkDirective } from 'app/shared/link.directive'; @NgModule({ imports: [ @@ -36,15 +41,19 @@ import { MenuComponent } from './sidenav/menu.component'; AppComponent, embeddedComponents, DocViewerComponent, - MenuComponent, - navDirectives, + TopMenuComponent, + NavMenuComponent, NavItemComponent, - SidenavComponent, + LinkDirective, ], providers: [ EmbeddedComponents, Logger, - navProviders, + Location, + { provide: LocationStrategy, useClass: PathLocationStrategy }, + LocationService, + NavigationService, + DocumentService, Platform ], entryComponents: [ embeddedComponents ], diff --git a/aio/src/app/documents/document.service.ts b/aio/src/app/documents/document.service.ts index 47a78eb7fb..1563571c05 100644 --- a/aio/src/app/documents/document.service.ts +++ b/aio/src/app/documents/document.service.ts @@ -4,12 +4,16 @@ import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/switchMap'; -import { DocumentContents } from './document'; -import { LocationService } from '../shared/location.service'; -import { Logger } from '../shared/logger.service'; +import { LocationService } from 'app/shared/location.service'; +import { Logger } from 'app/shared/logger.service'; const FILE_NOT_FOUND_DOC = 'file-not-found'; +export interface DocumentContents { + title: string; + contents: string; +} + @Injectable() export class DocumentService { diff --git a/aio/src/app/embedded/doc-title.component.ts b/aio/src/app/embedded/doc-title.component.ts index da94762de5..2417f995b8 100644 --- a/aio/src/app/embedded/doc-title.component.ts +++ b/aio/src/app/embedded/doc-title.component.ts @@ -1,14 +1,15 @@ /* tslint:disable component-selector */ import { Component } from '@angular/core'; -import { DocMetadataService } from '../nav-engine'; +import { Observable } from 'rxjs/Observable'; +import { DocumentService } from 'app/documents/document.service'; @Component({ selector: 'doc-title', - template: '

{{title}}

' + template: '

{{title | async}}

' }) export class DocTitleComponent { - title: string; - constructor(metadataService: DocMetadataService) { - this.title = metadataService.metadata.title; + title: Observable; + constructor(docs: DocumentService) { + this.title = docs.currentDocument.map(doc => doc.title); } } diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts new file mode 100644 index 0000000000..1001545dca --- /dev/null +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts @@ -0,0 +1,305 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Component, DebugElement } from '@angular/core'; + +import { ComponentFactoryResolver, ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core'; + +import { DocViewerComponent } from './doc-viewer.component'; + +import { embeddedComponents, EmbeddedComponents } from 'app/embedded'; + + +// /// Embedded Test Components /// + +// ///// FooComponent ///// + +// @Component({ +// selector: 'aio-foo', +// template: `Foo Component` +// }) +// class FooComponent { } + +// ///// BarComponent ///// + +// @Component({ +// selector: 'aio-bar', +// template: ` +//
+//

Bar Component

+//

+//
+// ` +// }) +// class BarComponent implements OnInit { + +// @ViewChild('barContent') barContentRef: ElementRef; + +// constructor(public elementRef: ElementRef) { } + +// // Project content in ngOnInit just like CodeExampleComponent +// ngOnInit() { +// // Security: this is a test component; never deployed +// this.barContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBarContent; +// } +// } + +// ///// BazComponent ///// + +// @Component({ +// selector: 'aio-baz', +// template: ` +//
++++++++++++++
+//

Baz Component

+//

+//
++++++++++++++
+// ` +// }) +// class BazComponent implements OnInit { + +// @ViewChild('bazContent') bazContentRef: ElementRef; + +// constructor(public elementRef: ElementRef) { } + +// // Project content in ngOnInit just like CodeExampleComponent +// ngOnInit() { +// // Security: this is a test component; never deployed +// this.bazContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBazContent; +// } +// } +// ///// Test Module ////// + +// const embeddedTestComponents = [FooComponent, BarComponent, BazComponent, ...embeddedComponents]; + +// @NgModule({ +// entryComponents: embeddedTestComponents +// }) +// class TestModule { } + +// //// Test Component ////// + +// @Component({ +// selector: 'aio-test', +// template: ` +// Test Component +// ` +// }) +// class TestComponent { +// private currentDoc: Doc; + +// @ViewChild(DocViewerComponent) docViewer: DocViewerComponent; + +// setDoc(doc: Doc) { +// if (this.docViewer) { +// this.docViewer.doc = doc; +// } +// } +// } + +// //////// Tests ////////////// + +// describe('DocViewerComponent', () => { +// const fakeDocMetadata: DocMetadata = { docId: 'fake', title: 'fake Doc' }; +// let component: TestComponent; +// let docViewerDE: DebugElement; +// let docViewerEl: HTMLElement; +// let fixture: ComponentFixture; + +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// imports: [ TestModule ], +// declarations: [ +// TestComponent, +// DocViewerComponent, +// embeddedTestComponents +// ], +// providers: [ +// {provide: EmbeddedComponents, useValue: {components: embeddedTestComponents}} +// ] +// }) +// .compileComponents(); +// })); + +// beforeEach(() => { +// fixture = TestBed.createComponent(TestComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// docViewerDE = fixture.debugElement.children[0]; +// docViewerEl = docViewerDE.nativeElement; +// }); + +// it('should create a DocViewer', () => { +// expect(component.docViewer).toBeTruthy(); +// }); + +// it(('should display nothing when set DocViewer.doc to doc w/o content'), () => { +// component.docViewer.doc = { metadata: fakeDocMetadata, content: '' }; +// expect(docViewerEl.innerHTML).toBe(''); +// }); + +// it(('should display simple static content doc'), () => { +// const content = '

Howdy, doc viewer

'; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; +// expect(docViewerEl.innerHTML).toEqual(content); +// }); + +// it(('should display nothing after reset static content doc'), () => { +// const content = '

Howdy, doc viewer

'; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; +// fixture.detectChanges(); +// component.docViewer.doc = { metadata: fakeDocMetadata, content: '' }; +// expect(docViewerEl.innerHTML).toEqual(''); +// }); + +// it(('should apply FooComponent'), () => { +// const content = ` +//

Above Foo

+//

+//

Below Foo

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; +// const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML; +// expect(fooHtml).toContain('Foo Component'); +// }); + +// it(('should apply multiple FooComponents'), () => { +// const content = ` +//

Above Foo

+//

+//
+// Holds a +// Ignored text +//
+//

Below Foo

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; +// const foos = docViewerEl.querySelectorAll('aio-foo'); +// expect(foos.length).toBe(2); +// }); + +// it(('should apply BarComponent'), () => { +// const content = ` +//

Above Bar

+// +//

Below Bar

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; +// const barHtml = docViewerEl.querySelector('aio-bar').innerHTML; +// expect(barHtml).toContain('Bar Component'); +// }); + +// it(('should project bar content into BarComponent'), () => { +// const content = ` +//

Above Bar

+// ###bar content### +//

Below Bar

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; + +// // necessary to trigger projection within ngOnInit +// fixture.detectChanges(); + +// const barHtml = docViewerEl.querySelector('aio-bar').innerHTML; +// expect(barHtml).toContain('###bar content###'); +// }); + + +// it(('should include Foo and Bar'), () => { +// const content = ` +//

Top

+//

ignored

+// ###bar content### +//

+//

Bottom

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; + +// // necessary to trigger Bar's projection within ngOnInit +// fixture.detectChanges(); + +// const foos = docViewerEl.querySelectorAll('aio-foo'); +// expect(foos.length).toBe(2, 'should have 2 foos'); + +// const barHtml = docViewerEl.querySelector('aio-bar').innerHTML; +// expect(barHtml).toContain('###bar content###', 'should have bar with projected content'); +// }); + +// it(('should not include Bar within Foo'), () => { +// const content = ` +//

Top

+//
+// +// ###bar content### +// +//
+//

+//

Bottom

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; + +// // necessary to trigger Bar's projection within ngOnInit +// fixture.detectChanges(); + +// const foos = docViewerEl.querySelectorAll('aio-foo'); +// expect(foos.length).toBe(2, 'should have 2 foos'); + +// const bars = docViewerEl.querySelectorAll('aio-bar'); +// expect(bars.length).toBe(0, 'did not expect Bar inside Foo'); +// }); + +// // because FooComponents are processed before BazComponents +// it(('should include Foo within Bar'), () => { +// const content = ` +//

Top

+// +//
+// Inner +//
+//
+//

+//

Bottom

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; + +// // necessary to trigger Bar's projection within ngOnInit +// fixture.detectChanges(); + +// const foos = docViewerEl.querySelectorAll('aio-foo'); +// expect(foos.length).toBe(2, 'should have 2 foos'); + +// const bars = docViewerEl.querySelectorAll('aio-bar'); +// expect(bars.length).toBe(1, 'should have a bar'); +// expect(bars[0].innerHTML).toContain('Bar Component', 'should have bar template content'); +// }); + +// // The tag and its inner content is copied +// // But the BazComponent is not created and therefore its template content is not displayed +// // because BarComponents are processed before BazComponents +// // and no chance for first Baz inside Bar to be processed by builder. +// it(('should NOT include Bar within Baz'), () => { +// const content = ` +//

Top

+// +//
+// Inner ---baz stuff--- +//
+//
+//

---More baz--

+//

Bottom

+// `; +// component.docViewer.doc = { metadata: fakeDocMetadata, content }; + +// // necessary to trigger Bar's projection within ngOnInit +// fixture.detectChanges(); +// const bazs = docViewerEl.querySelectorAll('aio-baz'); + +// // Both baz tags are there ... +// expect(bazs.length).toBe(2, 'should have 2 bazs'); + +// expect(bazs[0].innerHTML).not.toContain('Baz Component', +// 'did not expect 1st Baz template content'); + +// expect(bazs[1].innerHTML).toContain('Baz Component', +// 'expected 2nd Baz template content'); + +// }); +// }); diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts new file mode 100644 index 0000000000..11f6c6575f --- /dev/null +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -0,0 +1,132 @@ +import { + Component, ComponentFactory, ComponentFactoryResolver, ComponentRef, + DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation +} from '@angular/core'; + +import { EmbeddedComponents } from 'app/embedded'; +import { DocumentContents } from 'app/documents/document.service'; + +interface EmbeddedComponentFactory { + contentPropertyName: string; + factory: ComponentFactory; +} + +// Initialization prevents flicker once pre-rendering is on +const initialDocViewerElement = document.querySelector('aio-doc-viewer'); +const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : ''; + +@Component({ + selector: 'aio-doc-viewer', + template: '', + styles: [ ` + :host >>> doc-title.not-found h1 { + color: white; + background-color: red; + } + `] + // TODO(robwormald): shadow DOM and emulated don't work here (?!) + // encapsulation: ViewEncapsulation.Native +}) +export class DocViewerComponent implements DoCheck, OnDestroy { + + private displayedDoc: DisplayedDoc; + private embeddedComponentFactories: Map = new Map(); + private hostElement: HTMLElement; + + constructor( + componentFactoryResolver: ComponentFactoryResolver, + elementRef: ElementRef, + embeddedComponents: EmbeddedComponents, + private injector: Injector + ) { + this.hostElement = elementRef.nativeElement; + // Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure + this.hostElement.innerHTML = initialDocViewerContent; + + for (const component of embeddedComponents.components) { + const factory = componentFactoryResolver.resolveComponentFactory(component); + const selector = factory.selector; + const contentPropertyName = this.selectorToContentPropertyName(selector); + this.embeddedComponentFactories.set(selector, { contentPropertyName, factory }); + } + } + + @Input() + set doc(newDoc: DocumentContents) { + console.log(newDoc); + this.ngOnDestroy(); + if (newDoc) { + window.scrollTo(0, 0); + this.build(newDoc); + } + } + + /** + * Add doc content to host element and build it out with embedded components + */ + private build(doc: DocumentContents) { + + const displayedDoc = this.displayedDoc = new DisplayedDoc(doc); + + // security: the doc.content is always authored by the documentation team + // and is considered to be safe + this.hostElement.innerHTML = doc.contents || ''; + + if (!doc.contents) { return; } + + // TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators? + this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => { + const embeddedComponentElements = this.hostElement.querySelectorAll(selector); + + // cast due to https://github.com/Microsoft/TypeScript/issues/4947 + for (const element of embeddedComponentElements as any as HTMLElement[]){ + // hack: preserve the current element content because the factory will empty it out + // security: the source of this innerHTML is always authored by the documentation team + // and is considered to be safe + element[contentPropertyName] = element.innerHTML; + displayedDoc.addEmbeddedComponent(factory.create(this.injector, [], element)); + } + }); + } + + ngDoCheck() { + if (this.displayedDoc) { this.displayedDoc.detectChanges(); } + } + + ngOnDestroy() { + // destroy components otherwise there will be memory leaks + if (this.displayedDoc) { + this.displayedDoc.destroy(); + this.displayedDoc = undefined; + } + } + + /** + * Compute the component content property name by converting the selector to camelCase and appending + * 'Content', e.g. live-example => liveExampleContent + */ + private selectorToContentPropertyName(selector: string) { + return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content'; + } +} + +class DisplayedDoc { + + private embeddedComponents: ComponentRef[] = []; + + constructor(private doc: DocumentContents) {} + + addEmbeddedComponent(component: ComponentRef) { + this.embeddedComponents.push(component); + } + + detectChanges() { + this.embeddedComponents.forEach(comp => comp.changeDetectorRef.detectChanges()); + } + + destroy() { + // destroy components otherwise there will be memory leaks + this.embeddedComponents.forEach(comp => comp.destroy()); + this.embeddedComponents.length = 0; + } +} diff --git a/aio/src/app/layout/nav-item/nav-item.component.scss b/aio/src/app/layout/nav-item/nav-item.component.scss new file mode 100644 index 0000000000..19ba2a0b86 --- /dev/null +++ b/aio/src/app/layout/nav-item/nav-item.component.scss @@ -0,0 +1,114 @@ + +/************************************ + + Media queries + +To use these, put this snippet in the approriate selector: + + @include bp(tiny) { + background-color: purple; + } + + Replace "tiny" with "medium" or "big" as necessary. + +*************************************/ + +@mixin bp($point) { + + $bp-xsmall: "(min-width: 320px)"; + $bp-teeny: "(min-width: 480px)"; + $bp-tiny: "(min-width: 600px)"; + $bp-small: "(min-width: 650px)"; + $bp-medium: "(min-width: 800px)"; + $bp-big: "(min-width: 1000px)"; + + @if $point == big { + @media #{$bp-big} { @content; } + } + @else if $point == medium { + @media #{$bp-medium} { @content; } + } + @else if $point == small { + @media #{$bp-small} { @content; } + } + @else if $point == tiny { + @media #{$bp-tiny} { @content; } + } + @else if $point == teeny { + @media #{$bp-teeny} { @content; } + } + @else if $point == xsmall { + @media #{$bp-xsmall} { @content; } + } +} + +/************************************/ + +.vertical-menu { + padding-left: 0; +} + +a.vertical-menu { + color: #545454; + cursor: pointer; + display: block; + padding-bottom: 10px; + padding-top: 10px; + padding-right: 10px; + text-decoration: none; + text-align: left; + &:hover { + background-color: #ddd; + } +} + +.vertical-menu.selected { + color:#018494; +} + +.heading { + color: #444; + cursor: pointer; + font-size: .85rem; + min-width: 200px; + padding-left: 10px; + position: relative; + text-transform: uppercase; +} + +.material-icons { + display: none; +} + +.material-icons.active { + display: inline-block; + position: absolute; + top: 6px; + // left: 4px; +} + +.heading-children { + display: none; +} + +.heading-children.active { + display: block; +} + + +.heading.selected.level-1, +.heading-children.selected.level-1 { + border-left: 3px #00bcd4 solid; +} + +.level-1 { + padding-left: 10px; +} + +.level-2 { + padding-left: 20px; +} + +.level-3 { + padding-left: 30px; +} diff --git a/aio/src/app/layout/nav-item/nav-item.component.ts b/aio/src/app/layout/nav-item/nav-item.component.ts new file mode 100644 index 0000000000..3a4c8b001c --- /dev/null +++ b/aio/src/app/layout/nav-item/nav-item.component.ts @@ -0,0 +1,50 @@ +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { NavigationService, NavigationNode } from 'app/navigation/navigation.service'; + +@Component({ + selector: 'aio-nav-item', + template: ` +
+ + {{node.title}} + + +
{{node.title}}
+
+ +
+
`, + styles: ['nav-item.component.scss'], + // we don't expect the inputs to change + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NavItemComponent implements OnInit { + @Input() node: NavigationNode; + @Input() level = 1; + + isActive: boolean; + + classes: {[index: string]: boolean }; + + constructor(navigation: NavigationService) { + navigation.activeNodes.subscribe(nodes => { + this.classes['active'] = nodes.indexOf(this.node) !== -1; + }); + } + + ngOnInit() { + this.classes = { + ['level-' + this.level]: true, + active: false, + heading: !!this.node.children + }; + } +} diff --git a/aio/src/app/layout/nav-menu/nav-menu.component.spec.ts b/aio/src/app/layout/nav-menu/nav-menu.component.spec.ts new file mode 100644 index 0000000000..c6e15c57e5 --- /dev/null +++ b/aio/src/app/layout/nav-menu/nav-menu.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavMenuComponent } from './nav-menu.component'; + +describe('NavMenuComponent', () => { + let component: NavMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NavMenuComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NavMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/src/app/layout/nav-menu/nav-menu.component.ts b/aio/src/app/layout/nav-menu/nav-menu.component.ts new file mode 100644 index 0000000000..9847082236 --- /dev/null +++ b/aio/src/app/layout/nav-menu/nav-menu.component.ts @@ -0,0 +1,14 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { NavigationNode } from 'app/navigation/navigation.service'; + +@Component({ + selector: 'aio-nav-menu', + template: ``, + // we don't expect the inputs to change + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NavMenuComponent { + + @Input() + nodes: NavigationNode[]; +} diff --git a/aio/src/app/layout/top-menu/top-menu.component.ts b/aio/src/app/layout/top-menu/top-menu.component.ts new file mode 100644 index 0000000000..09fc2cf437 --- /dev/null +++ b/aio/src/app/layout/top-menu/top-menu.component.ts @@ -0,0 +1,36 @@ +import { Component, Input } from '@angular/core'; +import { NavigationNode } from 'app/navigation/navigation.service'; + +@Component({ + selector: 'aio-top-menu', + template: `{{ node.title }}`, + styles: [` + .fill-remaining-space { + flex: 1 1 auto; + } + + .nav-link { + margin-right: 10px; + margin-left: 20px; + cursor: pointer; + } + + @media (max-width: 700px) { + .nav-link { + margin-right: 8px; + margin-left: 0px; + } + } + @media (max-width: 600px) { + .nav-link { + font-size: 80%; + margin-right: 8px; + margin-left: 0px; + } + }` + ] +}) +export class TopMenuComponent { + @Input() + nodes: NavigationNode[]; +} diff --git a/aio/src/app/shared/link.directive.spec.ts b/aio/src/app/shared/link.directive.spec.ts new file mode 100644 index 0000000000..7669908aae --- /dev/null +++ b/aio/src/app/shared/link.directive.spec.ts @@ -0,0 +1,4 @@ +import { LinkDirective } from './link.directive'; + +describe('LinkDirective', () => { +}); diff --git a/aio/src/app/shared/link.directive.ts b/aio/src/app/shared/link.directive.ts new file mode 100644 index 0000000000..c58a8cfdf3 --- /dev/null +++ b/aio/src/app/shared/link.directive.ts @@ -0,0 +1,23 @@ +import { Directive, HostListener, HostBinding, Input } from '@angular/core'; +import { LocationService } from 'app/shared/location.service'; +@Directive({ + /* tslint:disable-next-line:directive-selector */ + selector: 'a[href]' +}) +export class LinkDirective { + + // We need both these decorators to ensure that we can access + // the href programmatically, and that it appears as a real + // attribute on the element. + @Input() + @HostBinding() + href: string; + + @HostListener('click', ['$event']) + onClick($event) { + this.location.go(this.href); + return false; + } + + constructor(private location: LocationService) { } +}