feat(aio): implement doc and navigation UI
This commit is contained in:
parent
371dc4744c
commit
71e22b8d11
|
@ -1,11 +1,21 @@
|
|||
<md-toolbar color="primary" class="app-toolbar">
|
||||
<button *ngIf="isHamburgerVisible" class="hamburger" md-button (click)="toggleSideNav()"><md-icon>menu</md-icon></button>
|
||||
<aio-menu></aio-menu>
|
||||
<button *ngIf="isHamburgerVisible" class="hamburger" md-button (click)="sidenav.toggle()"><md-icon>menu</md-icon></button>
|
||||
<aio-top-menu [nodes]="(navigationViews | async)?.TopBar"></aio-top-menu>
|
||||
<md-input-container >
|
||||
<input #search mdInput placeholder="Search">
|
||||
</md-input-container>
|
||||
<span class="fill-remaining-space"></span>
|
||||
</md-toolbar>
|
||||
|
||||
<aio-sidenav></aio-sidenav>
|
||||
<md-sidenav-container class="sidenav-container" (window:resize)="onResize($event.target.innerWidth)">
|
||||
|
||||
<md-sidenav #sidenav class="sidenav" [opened]="isSideBySide" [mode] = "this.isSideBySide ? 'side' : 'over'">
|
||||
|
||||
<aio-nav-menu [nodes]="(navigationViews | async)?.SideNav"></aio-nav-menu>
|
||||
</md-sidenav>
|
||||
|
||||
<section class="sidenav-content">
|
||||
<aio-doc-viewer [doc]="currentDocument | async"></aio-doc-viewer>
|
||||
</section>
|
||||
|
||||
</md-sidenav-container>
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
|
|
|
@ -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<AppComponent>;
|
||||
|
@ -12,7 +10,6 @@ describe('AppComponent', () => {
|
|||
TestBed.configureTestingModule({
|
||||
imports: [ AppModule ],
|
||||
providers: [
|
||||
{ provide: NavEngine, useValue: { currentDoc: undefined } }
|
||||
]
|
||||
});
|
||||
TestBed.compileComponents();
|
||||
|
|
|
@ -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']
|
||||
})
|
||||
export class AppComponent {
|
||||
isHamburgerVisible = true; // always ... for now
|
||||
template: `
|
||||
<md-toolbar color="primary" class="app-toolbar">
|
||||
<button *ngIf="isHamburgerVisible" class="hamburger" md-button (click)="sidenav.toggle()"><md-icon>menu</md-icon></button>
|
||||
<aio-top-menu [nodes]="(navigationViews | async)?.TopBar"></aio-top-menu>
|
||||
<md-input-container >
|
||||
<input mdInput placeholder="Search" (keyup)="onSearch($event)">
|
||||
</md-input-container>
|
||||
<span class="fill-remaining-space"></span>
|
||||
</md-toolbar>
|
||||
|
||||
@ViewChild(SidenavComponent) sidenav: SidenavComponent;
|
||||
<md-sidenav-container class="sidenav-container" (window:resize)="onResize($event.target.innerWidth)">
|
||||
|
||||
toggleSideNav() { this.sidenav.toggle(); }
|
||||
<md-sidenav #sidenav class="sidenav" [opened]="isSideBySide" [mode] = "this.isSideBySide ? 'side' : 'over'">
|
||||
<aio-nav-menu [nodes]="(navigationViews | async)?.SideNav"></aio-nav-menu>
|
||||
</md-sidenav>
|
||||
|
||||
<section class="sidenav-content">
|
||||
<div class="search-results">
|
||||
<div *ngFor="let result of (searchResults | async)?.results">
|
||||
<a href="{{ result.path }}">{{ result.title }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<aio-doc-viewer [doc]="currentDocument | async"></aio-doc-viewer>
|
||||
</section>
|
||||
|
||||
</md-sidenav-container>`,
|
||||
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 implements OnInit {
|
||||
isHamburgerVisible = true; // always ... for now
|
||||
isSideBySide = false;
|
||||
sideBySideWidth = 600;
|
||||
|
||||
currentDocument: Observable<DocumentContents>;
|
||||
navigationViews: Observable<NavigationViews>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ],
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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: '<h1 class="docs-primary-header">{{title}}</h1>'
|
||||
template: '<h1 class="docs-primary-header">{{title | async}}</h1>'
|
||||
})
|
||||
export class DocTitleComponent {
|
||||
title: string;
|
||||
constructor(metadataService: DocMetadataService) {
|
||||
this.title = metadataService.metadata.title;
|
||||
title: Observable<string>;
|
||||
constructor(docs: DocumentService) {
|
||||
this.title = docs.currentDocument.map(doc => doc.title);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: `
|
||||
// <hr>
|
||||
// <h2>Bar Component</h2>
|
||||
// <p #barContent></p>
|
||||
// <hr>
|
||||
// `
|
||||
// })
|
||||
// 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: `
|
||||
// <div>++++++++++++++</div>
|
||||
// <h2>Baz Component</h2>
|
||||
// <p #bazContent></p>
|
||||
// <div>++++++++++++++</div>
|
||||
// `
|
||||
// })
|
||||
// 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: `
|
||||
// <aio-doc-viewer>Test Component</aio-doc-viewer>
|
||||
// `
|
||||
// })
|
||||
// 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<TestComponent>;
|
||||
|
||||
// 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 = '<p>Howdy, doc viewer</p>';
|
||||
// component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
// expect(docViewerEl.innerHTML).toEqual(content);
|
||||
// });
|
||||
|
||||
// it(('should display nothing after reset static content doc'), () => {
|
||||
// const content = '<p>Howdy, doc viewer</p>';
|
||||
// component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
// fixture.detectChanges();
|
||||
// component.docViewer.doc = { metadata: fakeDocMetadata, content: '' };
|
||||
// expect(docViewerEl.innerHTML).toEqual('');
|
||||
// });
|
||||
|
||||
// it(('should apply FooComponent'), () => {
|
||||
// const content = `
|
||||
// <p>Above Foo</p>
|
||||
// <p><aio-foo></aio-foo></p>
|
||||
// <p>Below Foo</p>
|
||||
// `;
|
||||
// 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 = `
|
||||
// <p>Above Foo</p>
|
||||
// <p><aio-foo></aio-foo></p>
|
||||
// <div style="margin-left: 2em;">
|
||||
// Holds a
|
||||
// <aio-foo>Ignored text</aio-foo>
|
||||
// </div>
|
||||
// <p>Below Foo</p>
|
||||
// `;
|
||||
// component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
// const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
// expect(foos.length).toBe(2);
|
||||
// });
|
||||
|
||||
// it(('should apply BarComponent'), () => {
|
||||
// const content = `
|
||||
// <p>Above Bar</p>
|
||||
// <aio-bar></aio-bar>
|
||||
// <p>Below Bar</p>
|
||||
// `;
|
||||
// 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 = `
|
||||
// <p>Above Bar</p>
|
||||
// <aio-bar>###bar content###</aio-bar>
|
||||
// <p>Below Bar</p>
|
||||
// `;
|
||||
// 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 = `
|
||||
// <p>Top</p>
|
||||
// <p><aio-foo>ignored</aio-foo></p>
|
||||
// <aio-bar>###bar content###</aio-bar>
|
||||
// <p><aio-foo></aio-foo></p>
|
||||
// <p>Bottom</p>
|
||||
// `;
|
||||
// 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 = `
|
||||
// <p>Top</p>
|
||||
// <div>
|
||||
// <aio-foo>
|
||||
// <aio-bar>###bar content###</aio-bar>
|
||||
// </aio-foo>
|
||||
// </div>
|
||||
// <p><aio-foo></aio-foo><p>
|
||||
// <p>Bottom</p>
|
||||
// `;
|
||||
// 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 = `
|
||||
// <p>Top</p>
|
||||
// <aio-bar>
|
||||
// <div style="margin-left: 2em">
|
||||
// Inner <aio-foo></aio-foo>
|
||||
// </div>
|
||||
// </aio-bar>
|
||||
// <p><aio-foo></aio-foo></p>
|
||||
// <p>Bottom</p>
|
||||
// `;
|
||||
// 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 <aio-baz> 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 = `
|
||||
// <p>Top</p>
|
||||
// <aio-bar>
|
||||
// <div style="margin-left: 2em">
|
||||
// Inner <aio-baz>---baz stuff---</aio-baz>
|
||||
// </div>
|
||||
// </aio-bar>
|
||||
// <p><aio-baz>---More baz--</aio-baz></p>
|
||||
// <p>Bottom</p>
|
||||
// `;
|
||||
// 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');
|
||||
|
||||
// });
|
||||
// });
|
|
@ -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<any>;
|
||||
}
|
||||
|
||||
// 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<string, EmbeddedComponentFactory> = 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<any>[] = [];
|
||||
|
||||
constructor(private doc: DocumentContents) {}
|
||||
|
||||
addEmbeddedComponent(component: ComponentRef<any>) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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: `
|
||||
<div>
|
||||
<a *ngIf="node.path || node.url"
|
||||
[href]="node.path || node.url"
|
||||
[ngClass]="classes"
|
||||
target={{node.target}}
|
||||
title={{node.title}}
|
||||
class="vertical-menu">
|
||||
{{node.title}}
|
||||
<template [ngIf]="node.children">
|
||||
<md-icon [class.active]="!isActive">keyboard_arrow_right</md-icon>
|
||||
<md-icon [class.active]="isActive">keyboard_arrow_down</md-icon>
|
||||
</template>
|
||||
</a>
|
||||
<div *ngIf="!(node.path || node.url)" [ngClass]="classes">{{node.title}}</div>
|
||||
<div class="TODO:heading-children" [ngClass]="classes" *ngIf="node.children">
|
||||
<aio-nav-item *ngFor="let node of node.children" [level]="level + 1" [node]="node"></aio-nav-item>
|
||||
</div>
|
||||
</div>`,
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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<NavMenuComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NavMenuComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NavMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-nav-menu',
|
||||
template: `<aio-nav-item *ngFor="let node of nodes" [node]="node"></aio-nav-item>`,
|
||||
// we don't expect the inputs to change
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class NavMenuComponent {
|
||||
|
||||
@Input()
|
||||
nodes: NavigationNode[];
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-top-menu',
|
||||
template: `<span *ngFor="let node of nodes"><a class="nav-link" [href]="node.path || node.url">{{ node.title }}</a></span>`,
|
||||
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[];
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { LinkDirective } from './link.directive';
|
||||
|
||||
describe('LinkDirective', () => {
|
||||
});
|
|
@ -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) { }
|
||||
}
|
Loading…
Reference in New Issue