feat(aio): refactors AppComponent and its kids + sidenav hiding (#15570)
-hides sidenav when current doc is not in sidenav menu -displays top menu on the side as nodes instead of mini top menu
This commit is contained in:
parent
9c77a7cdaf
commit
9f2acf54bc
|
@ -1,28 +1,86 @@
|
||||||
{
|
{
|
||||||
"TopBar": [
|
"TopBar": [
|
||||||
{
|
{
|
||||||
"url": "api",
|
|
||||||
"title": "API"
|
|
||||||
}, {
|
|
||||||
"url": "news",
|
|
||||||
"title": "News"
|
|
||||||
}, {
|
|
||||||
"url": "features",
|
"url": "features",
|
||||||
"title": "Features"
|
"title": "Features"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "events",
|
||||||
|
"title": "Events"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"SideNav": [
|
"SideNav": [
|
||||||
{
|
{
|
||||||
"url": "quickstart",
|
"url": "overview",
|
||||||
"title": "Quickstart",
|
"title": "Docs",
|
||||||
"tooltip": "A quick look at an Angular app."
|
"hidden": true
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"url": "cli-quickstart",
|
"title": "Getting started",
|
||||||
"title": "CLI Quickstart",
|
"tooltip": "A gentle introduction to Angular.",
|
||||||
"tooltip": "A quick look at an Angular app built with the Angular CLI."
|
"children": [
|
||||||
},
|
{
|
||||||
|
"url": "guide/learning-angular",
|
||||||
|
"title": "Learning Angular",
|
||||||
|
"tooltip": "A suggested path through the documentation for Angular newcomers."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "quickstart",
|
||||||
|
"title": "Basic Quickstart",
|
||||||
|
"tooltip": "A quick look at an Angular app without tooling."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "cli-quickstart",
|
||||||
|
"title": "CLI Quickstart",
|
||||||
|
"tooltip": "A quick look at an Angular app built with the Angular CLI."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/setup",
|
||||||
|
"title": "Setup",
|
||||||
|
"tooltip": "Install the Angular QuickStart seed for faster, more efficient development on your machine."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/architecture",
|
||||||
|
"title": "Architecture",
|
||||||
|
"tooltip": "The basic building blocks of Angular applications."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/appmodule",
|
||||||
|
"title": "The root AppModule",
|
||||||
|
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/displaying-data",
|
||||||
|
"title": "Displaying data",
|
||||||
|
"tooltip": "Property binding helps show app data in the UI."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/user-input",
|
||||||
|
"title": "User Input",
|
||||||
|
"tooltip": "User input triggers DOM events. We listen to those events with event bindings that funnel updated values back into our components and models."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/forms",
|
||||||
|
"title": "Forms",
|
||||||
|
"tooltip": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/dependency-injection",
|
||||||
|
"title": "Dependency Injection",
|
||||||
|
"tooltip": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\"."
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
|
||||||
{
|
{
|
||||||
"title": "Tutorial",
|
"title": "Tutorial",
|
||||||
|
@ -66,94 +124,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"title": "Getting started",
|
|
||||||
"tooltip": "A gentle introduction to Angular.",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"url": "guide/docs-overview",
|
|
||||||
"title": "Overview",
|
|
||||||
"tooltip": "How to read and use this documentation."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "guide/setup",
|
|
||||||
"title": "Setup",
|
|
||||||
"tooltip": "Install the Angular QuickStart seed for faster, more efficient development on your machine."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/learning-angular",
|
|
||||||
"title": "Learning Angular",
|
|
||||||
"tooltip": "A suggested path through the documentation for Angular newcomers."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/architecture",
|
|
||||||
"title": "Architecture",
|
|
||||||
"tooltip": "The basic building blocks of Angular applications."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/appmodule",
|
|
||||||
"title": "The root AppModule",
|
|
||||||
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/displaying-data",
|
|
||||||
"title": "Displaying data",
|
|
||||||
"tooltip": "Property binding helps show app data in the UI."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/user-input",
|
|
||||||
"title": "User Input",
|
|
||||||
"tooltip": "User input triggers DOM events. We listen to those events with event bindings that funnel updated values back into our components and models."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/forms",
|
|
||||||
"title": "Forms",
|
|
||||||
"tooltip": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/dependency-injection",
|
|
||||||
"title": "Dependency Injection",
|
|
||||||
"tooltip": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\"."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/template-syntax",
|
|
||||||
"title": "Template Syntax",
|
|
||||||
"tooltip": "Learn how to write templates that display data and consume user events with the help of data binding."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/cheatsheet",
|
|
||||||
"title": "Cheat Sheet",
|
|
||||||
"tooltip": "A quick guide to common Angular coding techniques."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/style-guide",
|
|
||||||
"title": "Style guide",
|
|
||||||
"tooltip": "Write Angular with style."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/glossary",
|
|
||||||
"title": "Glossary",
|
|
||||||
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "guide/change-log",
|
|
||||||
"title": "Change Log",
|
|
||||||
"tooltip": "An annotated history of recent documentation improvements."
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"title": "Core",
|
"title": "Core",
|
||||||
"tooltip": "Learn the core capabilities of Angular",
|
"tooltip": "Learn the core capabilities of Angular",
|
||||||
|
@ -266,9 +236,21 @@
|
||||||
"url": "guide/router",
|
"url": "guide/router",
|
||||||
"title": "Routing & navigation",
|
"title": "Routing & navigation",
|
||||||
"tooltip": "Discover the basics of screen navigation with the Angular Router."
|
"tooltip": "Discover the basics of screen navigation with the Angular Router."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/template-syntax",
|
||||||
|
"title": "Template Syntax",
|
||||||
|
"tooltip": "Learn how to write templates that display data and consume user events with the help of data binding."
|
||||||
}
|
}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "API",
|
||||||
|
"tooltip": "Details of the Angular classes and values.",
|
||||||
|
"url": "api"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"title": "Additional Techniques",
|
"title": "Additional Techniques",
|
||||||
"tooltip": "Other",
|
"tooltip": "Other",
|
||||||
|
@ -381,9 +363,27 @@
|
||||||
"title": "Resources",
|
"title": "Resources",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"url": "about",
|
"url": "guide/change-log",
|
||||||
"title": "About",
|
"title": "Change Log",
|
||||||
"tooltip": "The people behind Angular."
|
"tooltip": "An annotated history of recent documentation improvements."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/cheatsheet",
|
||||||
|
"title": "Cheat Sheet",
|
||||||
|
"tooltip": "A quick guide to common Angular coding techniques."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/glossary",
|
||||||
|
"title": "Glossary",
|
||||||
|
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "guide/style-guide",
|
||||||
|
"title": "Style guide",
|
||||||
|
"tooltip": "Write Angular with style."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,9 +21,13 @@ describe('site App', function() {
|
||||||
|
|
||||||
// navigate to a different page
|
// navigate to a different page
|
||||||
page.getLink('features').click();
|
page.getLink('features').click();
|
||||||
|
expect(page.getDocViewerText()).toMatch(/Features/i);
|
||||||
|
|
||||||
// check that we can navigate to the tutorial page via a link in the navigation
|
// Show the menu; the tutorial section should be fully open from previous visit
|
||||||
page.getLink('tutorial').click();
|
page.docsMenuLink.click();
|
||||||
|
|
||||||
|
// Navigate to the tutorial introduction via a link in the sidenav
|
||||||
|
page.getNavItem(/introduction/i).click();
|
||||||
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);
|
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ const githubRegex = /https:\/\/github.com\/angular\/angular\//;
|
||||||
|
|
||||||
export class SitePage {
|
export class SitePage {
|
||||||
links = element.all(by.css('md-toolbar a'));
|
links = element.all(by.css('md-toolbar a'));
|
||||||
|
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
|
||||||
docViewer = element(by.css('aio-doc-viewer'));
|
docViewer = element(by.css('aio-doc-viewer'));
|
||||||
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
||||||
ghLink = this.docViewer
|
ghLink = this.docViewer
|
||||||
|
@ -11,7 +12,7 @@ export class SitePage {
|
||||||
.filter((a: ElementFinder) => a.getAttribute('href').then(href => githubRegex.test(href)))
|
.filter((a: ElementFinder) => a.getAttribute('href').then(href => githubRegex.test(href)))
|
||||||
.first();
|
.first();
|
||||||
gaReady: promise.Promise<any>;
|
gaReady: promise.Promise<any>;
|
||||||
getNavHeading(pattern: RegExp) {
|
getNavItem(pattern: RegExp) {
|
||||||
return element.all(by.css('aio-nav-item a'))
|
return element.all(by.css('aio-nav-item a'))
|
||||||
.filter(element => element.getText().then(text => pattern.test(text)))
|
.filter(element => element.getText().then(text => pattern.test(text)))
|
||||||
.first();
|
.first();
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
<md-toolbar color="primary" class="app-toolbar">
|
<md-toolbar color="primary" class="app-toolbar">
|
||||||
<button *ngIf="isHamburgerVisible" class="hamburger" md-button (click)="sidenav.toggle()"><md-icon>menu</md-icon></button>
|
<button class="hamburger" md-button
|
||||||
<a class="nav-link home" href="/"><img [src]="homeImageUrl" title="Home" alt="Home"></a>
|
(click)="sidenav.toggle()" title="Docs menu">
|
||||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="(navigationViews | async)?.TopBar"></aio-top-menu>
|
<md-icon>menu</md-icon>
|
||||||
|
</button>
|
||||||
|
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
||||||
|
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
||||||
<aio-search-box class="search-container" #searchBox></aio-search-box>
|
<aio-search-box class="search-container" #searchBox></aio-search-box>
|
||||||
<span class="fill-remaining-space"></span>
|
<span class="fill-remaining-space"></span>
|
||||||
</md-toolbar>
|
</md-toolbar>
|
||||||
|
|
||||||
<md-sidenav-container class="sidenav-container">
|
<md-sidenav-container class="sidenav-container">
|
||||||
|
|
||||||
<md-sidenav #sidenav class="sidenav" [opened]="isSideBySide" [mode] = "isSideBySide ? 'side' : 'over'">
|
<md-sidenav #sidenav class="sidenav" [opened]="isOpened" [mode]="mode">
|
||||||
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="(navigationViews | async)?.TopBar" [selectedNodes]="selectedNodes | async"></aio-nav-menu>
|
<aio-nav-menu *ngIf="!isSideBySide" class="top-menu" [nodes]="topMenuNodes" [currentNode]="currentNode"></aio-nav-menu>
|
||||||
<aio-nav-menu [nodes]="(navigationViews | async)?.SideNav" [selectedNodes]="selectedNodes | async"></aio-nav-menu>
|
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNode" ></aio-nav-menu>
|
||||||
</md-sidenav>
|
</md-sidenav>
|
||||||
|
|
||||||
<section class="sidenav-content">
|
<section class="sidenav-content">
|
||||||
<aio-doc-viewer [doc]="currentDocument | async" (docRendered)="onDocRendered($event)"></aio-doc-viewer>
|
<aio-doc-viewer [doc]="currentDocument" (docRendered)="onDocRendered($event)"></aio-doc-viewer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</md-sidenav-container>
|
</md-sidenav-container>
|
||||||
|
|
||||||
<aio-search-results #searchResults></aio-search-results>
|
<aio-search-results #searchResults></aio-search-results>
|
||||||
|
|
||||||
<footer>
|
<aio-footer [versionInfo]="versionInfo" ></aio-footer>
|
||||||
<div class="footer">
|
|
||||||
<p>Powered by Google ©2010-2017. Code licensed under an <a href="/license">MIT-style License</a>.</p>
|
|
||||||
<p>Documentation licensed under <a href="http://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
|
|
||||||
</p>
|
|
||||||
<p class="version-info">Version Info | {{ (versionInfo | async)?.full }}</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { async, inject, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, inject, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { Http } from '@angular/http';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { GaService } from 'app/shared/ga.service';
|
import { GaService } from 'app/shared/ga.service';
|
||||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||||
|
import { SearchService } from 'app/search/search.service';
|
||||||
|
import { MockSearchService } from 'testing/search.service';
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { MockLocationService } from 'testing/location.service';
|
import { MockLocationService } from 'testing/location.service';
|
||||||
|
@ -23,8 +29,10 @@ describe('AppComponent', () => {
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||||
{ provide: GaService, useClass: TestGaService },
|
{ provide: GaService, useClass: TestGaService },
|
||||||
|
{ provide: Http, useClass: TestHttp },
|
||||||
{ provide: LocationService, useFactory: () => new MockLocationService(initialUrl) },
|
{ provide: LocationService, useFactory: () => new MockLocationService(initialUrl) },
|
||||||
{ provide: Logger, useClass: MockLogger }
|
{ provide: Logger, useClass: MockLogger },
|
||||||
|
{ provide: SearchService, useClass: MockSearchService }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
TestBed.compileComponents();
|
TestBed.compileComponents();
|
||||||
|
@ -40,19 +48,8 @@ describe('AppComponent', () => {
|
||||||
expect(component).toBeDefined();
|
expect(component).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('google analytics', () => {
|
describe('is Hamburger Visible', () => {
|
||||||
it('should call gaService.locationChanged with initial URL', () => {
|
console.log('PENDING: AppComponent');
|
||||||
const { locationChanged } = TestBed.get(GaService) as TestGaService;
|
|
||||||
expect(locationChanged.calls.count()).toBe(1, 'gaService.locationChanged');
|
|
||||||
const args = locationChanged.calls.first().args;
|
|
||||||
expect(args[0]).toBe(initialUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Todo: add test to confirm tracking URL when navigate.
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isHamburgerVisible', () => {
|
|
||||||
console.log('PENDING: AppComponent isHamburgerVisible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onResize', () => {
|
describe('onResize', () => {
|
||||||
|
@ -64,6 +61,39 @@ describe('AppComponent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('shows/hide SideNav based on doc\'s navigation view', () => {
|
||||||
|
let locationService: MockLocationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
locationService = fixture.debugElement.injector.get(LocationService) as any;
|
||||||
|
component.onResize(1000); // side-by-side
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have sidenav open when doc in the sidenav (guide/pipes)', () => {
|
||||||
|
locationService.urlSubject.next('guide/pipes');
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
const sidenav = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
|
||||||
|
expect(sidenav.className).toMatch(/sidenav-open/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have sidenav open when doc is an api page', () => {
|
||||||
|
locationService.urlSubject.next('api/a/b/c/d');
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
const sidenav = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
|
||||||
|
expect(sidenav.className).toMatch(/sidenav-open/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have sidenav closed when doc not in the sidenav (features)', () => {
|
||||||
|
locationService.urlSubject.next('features');
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
const sidenav = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
|
||||||
|
expect(sidenav.className).toMatch(/sidenav-clos/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('currentDocument', () => {
|
describe('currentDocument', () => {
|
||||||
console.log('PENDING: AppComponent currentDocument');
|
console.log('PENDING: AppComponent currentDocument');
|
||||||
});
|
});
|
||||||
|
@ -89,6 +119,14 @@ describe('AppComponent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('should initialize the search worker', inject([SearchService], (searchService: SearchService) => {
|
||||||
|
fixture.detectChanges(); // triggers ngOnInit
|
||||||
|
expect(searchService.initWorker).toHaveBeenCalled();
|
||||||
|
expect(searchService.loadIndex).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('click intercepting', () => {
|
describe('click intercepting', () => {
|
||||||
it('should intercept clicks on anchors and call `location.handleAnchorClick()`',
|
it('should intercept clicks on anchors and call `location.handleAnchorClick()`',
|
||||||
inject([LocationService], (location: LocationService) => {
|
inject([LocationService], (location: LocationService) => {
|
||||||
|
@ -120,8 +158,95 @@ describe('AppComponent', () => {
|
||||||
expect(searchResultsComponent.hideResults).not.toHaveBeenCalled();
|
expect(searchResultsComponent.hideResults).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('should have version number', () => {
|
||||||
|
const versionEl: HTMLElement = fixture.debugElement.query(By.css('aio-footer p.version-info')).nativeElement;
|
||||||
|
expect(versionEl.innerText).toContain(TestHttp.versionFull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class TestGaService {
|
class TestGaService {
|
||||||
locationChanged = jasmine.createSpy('locationChanged');
|
locationChanged = jasmine.createSpy('locationChanged');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TestSearchService {
|
||||||
|
initWorker = jasmine.createSpy('initWorker');
|
||||||
|
loadIndex = jasmine.createSpy('loadIndex');
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestHttp {
|
||||||
|
static versionFull = '4.0.0-local+sha.73808dd';
|
||||||
|
|
||||||
|
// tslint:disable:quotemark
|
||||||
|
navJson = {
|
||||||
|
"TopBar": [
|
||||||
|
{
|
||||||
|
"url": "features",
|
||||||
|
"title": "Features"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SideNav": [
|
||||||
|
{
|
||||||
|
"title": "Core",
|
||||||
|
"tooltip": "Learn the core capabilities of Angular",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "guide/pipes",
|
||||||
|
"title": "Pipes",
|
||||||
|
"tooltip": "Pipes transform displayed values within a template."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "api",
|
||||||
|
"title": "API",
|
||||||
|
"tooltip": "Details of the Angular classes and values."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"__versionInfo": {
|
||||||
|
"raw": "4.0.0-rc.6",
|
||||||
|
"major": 4,
|
||||||
|
"minor": 0,
|
||||||
|
"patch": 0,
|
||||||
|
"prerelease": [
|
||||||
|
"local"
|
||||||
|
],
|
||||||
|
"build": "sha.73808dd",
|
||||||
|
"version": "4.0.0-local",
|
||||||
|
"codeName": "snapshot",
|
||||||
|
"isSnapshot": true,
|
||||||
|
"full": TestHttp.versionFull,
|
||||||
|
"branch": "master",
|
||||||
|
"commitSHA": "73808dd38b5ccd729404936834d1568bd066de81"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
apiDoc = {
|
||||||
|
"title": "API",
|
||||||
|
"contents": "<h1>API Doc</h1>"
|
||||||
|
};
|
||||||
|
|
||||||
|
pipesDoc = {
|
||||||
|
"title": "Pipes",
|
||||||
|
"contents": "<h1>Pipes Doc</h1>"
|
||||||
|
};
|
||||||
|
|
||||||
|
testDoc = {
|
||||||
|
"title": "Test",
|
||||||
|
"contents": "<h1>Test Doc</h1>"
|
||||||
|
};
|
||||||
|
|
||||||
|
// get = jasmine.createSpy('get').and.callFake((url: string) => { ... });
|
||||||
|
get(url: string) {
|
||||||
|
const json =
|
||||||
|
/navigation.json/.test(url) ? this.navJson :
|
||||||
|
/api/.test(url) ? this.apiDoc :
|
||||||
|
/pipes/.test(url) ? this.pipesDoc :
|
||||||
|
this.testDoc;
|
||||||
|
return of({ json: () => json });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,33 +1,45 @@
|
||||||
import { Component, ElementRef, HostListener, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
import { Component, ElementRef, HostListener, OnInit,
|
||||||
import { Observable } from 'rxjs/Observable';
|
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||||
|
import { MdSidenav } from '@angular/material/sidenav';
|
||||||
|
|
||||||
import { GaService } from 'app/shared/ga.service';
|
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||||
import { DocumentService, DocumentContents } from 'app/documents/document.service';
|
import { DocumentService, DocumentContents } from 'app/documents/document.service';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
|
||||||
|
const sideNavView = 'SideNav';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-shell',
|
selector: 'aio-shell',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
readonly sideBySideWidth = 600;
|
|
||||||
get homeImageUrl() {
|
|
||||||
return this.isSideBySide ?
|
|
||||||
'assets/images/logos/standard/logo-nav.png' :
|
|
||||||
'assets/images/logos/standard/logo-nav-small.png';
|
|
||||||
}
|
|
||||||
|
|
||||||
isHamburgerVisible = true; // always ... for now
|
currentNode: CurrentNode;
|
||||||
|
currentDocument: DocumentContents;
|
||||||
|
footerNodes: NavigationNode[];
|
||||||
isSideBySide = false;
|
isSideBySide = false;
|
||||||
|
private isSideNavDoc = false;
|
||||||
|
private previousNavView: string;
|
||||||
|
private readonly sideBySideWidth = 600;
|
||||||
|
sideNavNodes: NavigationNode[];
|
||||||
|
topMenuNodes: NavigationNode[];
|
||||||
|
versionInfo: VersionInfo;
|
||||||
|
|
||||||
currentDocument: Observable<DocumentContents>;
|
get homeImageUrl() {
|
||||||
navigationViews: Observable<NavigationViews>;
|
return this.isSideBySide ?
|
||||||
selectedNodes: Observable<NavigationNode[]>;
|
'assets/images/logos/standard/logo-nav.png' :
|
||||||
versionInfo: Observable<VersionInfo>;
|
'assets/images/logos/standard/logo-nav-small.png';
|
||||||
|
}
|
||||||
|
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
||||||
|
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
||||||
|
|
||||||
|
// Need the doc-viewer element for scrolling the contents
|
||||||
|
@ViewChild(DocViewerComponent, { read: ElementRef })
|
||||||
|
docViewer: ElementRef;
|
||||||
|
|
||||||
@ViewChildren('searchBox, searchResults', { read: ElementRef })
|
@ViewChildren('searchBox, searchResults', { read: ElementRef })
|
||||||
searchElements: QueryList<ElementRef>;
|
searchElements: QueryList<ElementRef>;
|
||||||
|
@ -35,36 +47,54 @@ export class AppComponent implements OnInit {
|
||||||
@ViewChild(SearchResultsComponent)
|
@ViewChild(SearchResultsComponent)
|
||||||
searchResults: SearchResultsComponent;
|
searchResults: SearchResultsComponent;
|
||||||
|
|
||||||
// We need the doc-viewer element for scrolling the contents
|
@ViewChild(MdSidenav)
|
||||||
@ViewChild(DocViewerComponent, { read: ElementRef })
|
sidenav: MdSidenav;
|
||||||
docViewer: ElementRef;
|
|
||||||
|
|
||||||
constructor(documentService: DocumentService,
|
constructor(
|
||||||
gaService: GaService,
|
private autoScrollService: AutoScrollService,
|
||||||
navigationService: NavigationService,
|
private documentService: DocumentService,
|
||||||
private autoScroll: AutoScrollService,
|
private locationService: LocationService,
|
||||||
private locationService: LocationService) {
|
private navigationService: NavigationService
|
||||||
this.currentDocument = documentService.currentDocument;
|
) { }
|
||||||
locationService.currentUrl.subscribe(url => gaService.locationChanged(url));
|
|
||||||
this.navigationViews = navigationService.navigationViews;
|
|
||||||
this.selectedNodes = navigationService.selectedNodes;
|
|
||||||
this.versionInfo = navigationService.versionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.onResize(window.innerWidth);
|
/* No need to unsubscribe because this root component never dies */
|
||||||
|
|
||||||
// The url changed, so scroll to the anchor in the hash fragment.
|
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
|
||||||
// This subscription is needed when navigating between anchors within a document
|
|
||||||
// and the document itself has not changed
|
// scroll even if only the hash fragment changed
|
||||||
this.locationService.currentUrl.subscribe(url => this.autoScroll.scroll(this.docViewer.nativeElement.offsetParent));
|
this.locationService.currentUrl.subscribe(url => this.autoScroll());
|
||||||
|
|
||||||
|
this.navigationService.currentNode.subscribe(currentNode => {
|
||||||
|
this.currentNode = currentNode;
|
||||||
|
|
||||||
|
// Toggle the sidenav if the kind of view changed
|
||||||
|
if (this.previousNavView === currentNode.view) { return; }
|
||||||
|
this.previousNavView = currentNode.view;
|
||||||
|
this.isSideNavDoc = currentNode.view === sideNavView;
|
||||||
|
this.sideNavToggle(this.isSideNavDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.navigationViews.subscribe(views => {
|
||||||
|
this.footerNodes = views.Footer || [];
|
||||||
|
this.sideNavNodes = views.SideNav || [];
|
||||||
|
this.topMenuNodes = views.TopBar || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.versionInfo.subscribe( vi => this.versionInfo = vi );
|
||||||
|
|
||||||
|
this.onResize(window.innerWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to the anchor in the hash fragment.
|
||||||
|
autoScroll() {
|
||||||
|
this.autoScrollService.scroll(this.docViewer.nativeElement.offsetParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDocRendered(doc: DocumentContents) {
|
onDocRendered(doc: DocumentContents) {
|
||||||
// A new document has been rendered, so scroll to the anchor in the hash fragment.
|
|
||||||
// This handler is needed because the subscription to the `currentUrl` in `ngOnInit`
|
// This handler is needed because the subscription to the `currentUrl` in `ngOnInit`
|
||||||
// gets triggered too early before the doc-viewer has finished rendering the doc
|
// gets triggered too early before the doc-viewer has finished rendering the doc
|
||||||
this.autoScroll.scroll(this.docViewer.nativeElement.offsetParent);
|
this.autoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||||
|
@ -84,9 +114,16 @@ export class AppComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deal with anchor clicks
|
// Deal with anchor clicks
|
||||||
|
if (eventTarget instanceof HTMLImageElement) {
|
||||||
|
eventTarget = eventTarget.parentElement; // assume image wrapped in Anchor
|
||||||
|
}
|
||||||
if (eventTarget instanceof HTMLAnchorElement) {
|
if (eventTarget instanceof HTMLAnchorElement) {
|
||||||
return this.locationService.handleAnchorClick(eventTarget, button, ctrlKey, metaKey);
|
return this.locationService.handleAnchorClick(eventTarget, button, ctrlKey, metaKey);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sideNavToggle(value?: boolean) {
|
||||||
|
this.sidenav.toggle(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { NavigationService } from 'app/navigation/navigation.service';
|
||||||
import { DocumentService } from 'app/documents/document.service';
|
import { DocumentService } from 'app/documents/document.service';
|
||||||
import { SearchService } from 'app/search/search.service';
|
import { SearchService } from 'app/search/search.service';
|
||||||
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
|
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
|
||||||
|
import { FooterComponent } from 'app/layout/footer/footer.component';
|
||||||
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||||
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
|
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
|
||||||
import { SearchResultsComponent } from './search/search-results/search-results.component';
|
import { SearchResultsComponent } from './search/search-results/search-results.component';
|
||||||
|
@ -50,6 +51,7 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
DocViewerComponent,
|
DocViewerComponent,
|
||||||
|
FooterComponent,
|
||||||
TopMenuComponent,
|
TopMenuComponent,
|
||||||
NavMenuComponent,
|
NavMenuComponent,
|
||||||
NavItemComponent,
|
NavItemComponent,
|
||||||
|
|
|
@ -3,6 +3,9 @@ import { Http, Response } from '@angular/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { AsyncSubject } from 'rxjs/AsyncSubject';
|
import { AsyncSubject } from 'rxjs/AsyncSubject';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import 'rxjs/add/operator/catch';
|
||||||
|
import 'rxjs/add/operator/map';
|
||||||
import 'rxjs/add/operator/switchMap';
|
import 'rxjs/add/operator/switchMap';
|
||||||
|
|
||||||
import { DocumentContents } from './document-contents';
|
import { DocumentContents } from './document-contents';
|
||||||
|
@ -51,7 +54,7 @@ export class DocumentService {
|
||||||
// using `getDocument` means that we can fetch the 404 doc contents from the server and cache it
|
// using `getDocument` means that we can fetch the 404 doc contents from the server and cache it
|
||||||
return this.getDocument(FILE_NOT_FOUND_URL);
|
return this.getDocument(FILE_NOT_FOUND_URL);
|
||||||
} else {
|
} else {
|
||||||
return Observable.of({ title: 'Not Found', contents: 'Document not found' });
|
return of({ title: 'Not Found', contents: 'Document not found' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.error('Error fetching document', error);
|
this.logger.error('Error fetching document', error);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { VersionInfo } from 'app/navigation/navigation.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-footer',
|
||||||
|
template: `
|
||||||
|
<footer>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Powered by Google ©2010-2017. Code licensed under an <a href="/license">MIT-style License</a>.</p>
|
||||||
|
<p>Documentation licensed under <a href="http://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
|
||||||
|
</p>
|
||||||
|
<p class="version-info">Version Info | {{ versionInfo?.full }}</p>
|
||||||
|
</div>
|
||||||
|
</footer>`
|
||||||
|
})
|
||||||
|
export class FooterComponent {
|
||||||
|
@Input() versionInfo: VersionInfo;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
|
||||||
import { NavMenuComponent } from './nav-menu.component';
|
import { NavMenuComponent } from './nav-menu.component';
|
||||||
|
import { CurrentNode, NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
|
||||||
|
|
||||||
|
|
||||||
describe('NavMenuComponent', () => {
|
describe('NavMenuComponent', () => {
|
||||||
let component: NavMenuComponent;
|
let component: NavMenuComponent;
|
||||||
|
@ -9,7 +14,10 @@ describe('NavMenuComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ NavMenuComponent ],
|
declarations: [ NavMenuComponent ],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
providers: [
|
||||||
|
{provide: NavigationService, useClass: TestNavigationService }
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@ -24,3 +32,22 @@ describe('NavMenuComponent', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//// Test Helpers ////
|
||||||
|
class TestNavigationService {
|
||||||
|
navJson = {
|
||||||
|
SideNav: [
|
||||||
|
{ title: 'a', children: [
|
||||||
|
{ url: 'b', title: 'b', children: [
|
||||||
|
{ url: 'c', title: 'c' },
|
||||||
|
{ url: 'd', title: 'd' }
|
||||||
|
] },
|
||||||
|
{ url: 'e', title: 'e' }
|
||||||
|
] },
|
||||||
|
{ url: 'f', title: 'f' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
navigationViews = new BehaviorSubject<NavigationViews>(this.navJson);
|
||||||
|
currentNode = new BehaviorSubject<CurrentNode>(undefined);
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
import { CurrentNode, NavigationNode } from 'app/navigation/navigation.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-nav-menu',
|
selector: 'aio-nav-menu',
|
||||||
template: `<aio-nav-item *ngFor="let node of nodes" [selectedNodes]="selectedNodes" [node]="node"></aio-nav-item>`
|
template: `
|
||||||
|
<aio-nav-item *ngFor="let node of filteredNodes" [node]="node" [selectedNodes]="currentNode.nodes">
|
||||||
|
</aio-nav-item>`
|
||||||
})
|
})
|
||||||
export class NavMenuComponent {
|
export class NavMenuComponent {
|
||||||
|
@Input() currentNode: CurrentNode;
|
||||||
@Input()
|
@Input() nodes: NavigationNode[] ;
|
||||||
selectedNodes: NavigationNode[];
|
get filteredNodes() { return this.nodes ? this.nodes.filter(n => !n.hidden) : []; }
|
||||||
|
|
||||||
@Input()
|
|
||||||
nodes: NavigationNode[];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
.fill-remaining-space {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.home img {
|
||||||
|
position: relative;
|
||||||
|
margin-top: -15px;
|
||||||
|
top: 12px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
|
||||||
|
import { TopMenuComponent } from './top-menu.component';
|
||||||
|
import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
|
||||||
|
|
||||||
|
describe('TopMenuComponent', () => {
|
||||||
|
let component: TopMenuComponent;
|
||||||
|
let fixture: ComponentFixture<TopMenuComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ TopMenuComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: NavigationService, useClass: TestNavigationService }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TopMenuComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//// Test Helpers ////
|
||||||
|
class TestNavigationService {
|
||||||
|
navJson = {
|
||||||
|
TopBar: [
|
||||||
|
{url: 'api', title: 'API' },
|
||||||
|
{url: 'features', title: 'Features' }
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
navigationViews = new BehaviorSubject<NavigationViews>(this.navJson);
|
||||||
|
}
|
|
@ -5,42 +5,12 @@ import { NavigationNode } from 'app/navigation/navigation.service';
|
||||||
selector: 'aio-top-menu',
|
selector: 'aio-top-menu',
|
||||||
template: `
|
template: `
|
||||||
<ul role="navigation">
|
<ul role="navigation">
|
||||||
<li *ngFor="let node of nodes"><a class="nav-link" [href]="node.path || node.url">{{ node.title }}</a></li>
|
<li><a class="nav-link" href="overview" title="Angular Documentation">Docs</a></li>
|
||||||
|
<li *ngFor="let node of nodes"><a class="nav-link" [href]="node.path || node.url" [title]="node.title">{{ node.title }}</a></li>
|
||||||
</ul>`,
|
</ul>`,
|
||||||
styles: [`
|
styleUrls: ['top-menu.component.scss']
|
||||||
.fill-remaining-space {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-left: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.home img {
|
|
||||||
position: relative;
|
|
||||||
margin-top: -15px;
|
|
||||||
top: 12px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
export class TopMenuComponent {
|
||||||
@Input()
|
@Input() nodes: NavigationNode[];
|
||||||
nodes: NavigationNode[];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
export interface NavigationNode {
|
|
||||||
url?: string;
|
|
||||||
title?: string;
|
|
||||||
tooltip?: string;
|
|
||||||
target?: string;
|
|
||||||
children?: NavigationNode[];
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Pulled all interfaces out of `navigation.service.ts` because of this:
|
||||||
|
// https://github.com/angular/angular-cli/issues/2034
|
||||||
|
// Then re-export them from `navigation.service.ts`
|
||||||
|
|
||||||
|
export interface NavigationNode {
|
||||||
|
url?: string;
|
||||||
|
title?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
hidden?: string;
|
||||||
|
children?: NavigationNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavigationResponse = {__versionInfo: VersionInfo } & { [name: string]: NavigationNode[]|VersionInfo };
|
||||||
|
|
||||||
|
export interface NavigationViews {
|
||||||
|
[name: string]: NavigationNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation information about a node at specific URL
|
||||||
|
* url: the current URL
|
||||||
|
* view: 'SideNav' | 'TopBar' | 'Footer'
|
||||||
|
* nodes: the current node and its ancestor nodes within that view
|
||||||
|
*/
|
||||||
|
export interface CurrentNode {
|
||||||
|
url: string;
|
||||||
|
view: 'SideNav' | 'TopBar' | 'Footer';
|
||||||
|
nodes: NavigationNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
raw: string;
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
prerelease: string[];
|
||||||
|
build: string;
|
||||||
|
version: string;
|
||||||
|
codeName: string;
|
||||||
|
isSnapshot: boolean;
|
||||||
|
full: string;
|
||||||
|
branch: string;
|
||||||
|
commitSHA: string;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { ReflectiveInjector } from '@angular/core';
|
import { ReflectiveInjector } from '@angular/core';
|
||||||
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
|
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
|
||||||
import { MockBackend } from '@angular/http/testing';
|
import { MockBackend } from '@angular/http/testing';
|
||||||
import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { MockLocationService } from 'testing/location.service';
|
import { MockLocationService } from 'testing/location.service';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
@ -76,68 +76,112 @@ describe('NavigationService', () => {
|
||||||
it('should do WHAT(?) if the request fails');
|
it('should do WHAT(?) if the request fails');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('selectedNodes', () => {
|
describe('currentNode', () => {
|
||||||
let service: NavigationService, location: MockLocationService;
|
let service: NavigationService, location: MockLocationService;
|
||||||
let currentNodes: NavigationNode[];
|
let currentNode: CurrentNode;
|
||||||
const nodeTree: NavigationNode[] = [
|
|
||||||
{ title: 'a', children: [
|
const topBarNodes: NavigationNode[] = [{ url: 'features', title: 'Features' }];
|
||||||
{ url: 'b', title: 'b', children: [
|
const sideNavNodes: NavigationNode[] = [
|
||||||
{ url: 'c/', title: 'c' },
|
{ title: 'a', children: [
|
||||||
{ url: 'd', title: 'd' }
|
{ url: 'b', title: 'b', children: [
|
||||||
|
{ url: 'c', title: 'c' },
|
||||||
|
{ url: 'd', title: 'd' }
|
||||||
|
] },
|
||||||
|
{ url: 'e', title: 'e' }
|
||||||
] },
|
] },
|
||||||
{ url: 'e', title: 'e' }
|
{ url: 'f', title: 'f' }
|
||||||
] },
|
];
|
||||||
{ url: 'f', title: 'f' }
|
|
||||||
];
|
const navJson = {
|
||||||
|
TopBar: topBarNodes,
|
||||||
|
SideNav: sideNavNodes,
|
||||||
|
__versionInfo: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
location = injector.get(LocationService);
|
location = injector.get(LocationService);
|
||||||
|
|
||||||
service = injector.get(NavigationService);
|
service = injector.get(NavigationService);
|
||||||
service.selectedNodes.subscribe(nodes => currentNodes = nodes);
|
service.currentNode.subscribe(selected => currentNode = selected);
|
||||||
|
|
||||||
const backend = injector.get(ConnectionBackend);
|
const backend = injector.get(ConnectionBackend);
|
||||||
backend.connectionsArray[0].mockRespond(createResponse({ nav: nodeTree }));
|
backend.connectionsArray[0].mockRespond(createResponse(navJson));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list the navigation node that matches the current location, and all its ancestors', () => {
|
it('should list the side navigation node that matches the current location, and all its ancestors', () => {
|
||||||
location.urlSubject.next('b');
|
location.urlSubject.next('b');
|
||||||
expect(currentNodes).toEqual([
|
expect(currentNode).toEqual({
|
||||||
nodeTree[0].children[0],
|
url: 'b',
|
||||||
nodeTree[0]
|
view: 'SideNav',
|
||||||
]);
|
nodes: [
|
||||||
|
sideNavNodes[0].children[0],
|
||||||
|
sideNavNodes[0]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
location.urlSubject.next('d');
|
location.urlSubject.next('d');
|
||||||
expect(currentNodes).toEqual([
|
expect(currentNode).toEqual({
|
||||||
nodeTree[0].children[0].children[1],
|
url: 'd',
|
||||||
nodeTree[0].children[0],
|
view: 'SideNav',
|
||||||
nodeTree[0]
|
nodes: [
|
||||||
]);
|
sideNavNodes[0].children[0].children[1],
|
||||||
|
sideNavNodes[0].children[0],
|
||||||
|
sideNavNodes[0]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
location.urlSubject.next('f');
|
location.urlSubject.next('f');
|
||||||
expect(currentNodes).toEqual([
|
expect(currentNode).toEqual({
|
||||||
nodeTree[1]
|
url: 'f',
|
||||||
]);
|
view: 'SideNav',
|
||||||
|
nodes: [ sideNavNodes[1] ]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be an empty array if no navigation node matches the current location', () => {
|
it('should be a TopBar selected node if the current location is a top menu node', () => {
|
||||||
|
location.urlSubject.next('features');
|
||||||
|
expect(currentNode).toEqual({
|
||||||
|
url: 'features',
|
||||||
|
view: 'TopBar',
|
||||||
|
nodes: [ topBarNodes[0] ]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be undefined if no side navigation node matches the current location', () => {
|
||||||
location.urlSubject.next('g');
|
location.urlSubject.next('g');
|
||||||
expect(currentNodes).toEqual([]);
|
expect(currentNode).toEqual({
|
||||||
|
url: 'g',
|
||||||
|
view: '',
|
||||||
|
nodes: []
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore trailing slashes on URLs in the navmap', () => {
|
it('should ignore trailing slashes, hashes, and search params on URLs in the navmap', () => {
|
||||||
|
const cnode = {
|
||||||
|
url: 'c',
|
||||||
|
view: 'SideNav',
|
||||||
|
nodes: [
|
||||||
|
sideNavNodes[0].children[0].children[0],
|
||||||
|
sideNavNodes[0].children[0],
|
||||||
|
sideNavNodes[0]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
location.urlSubject.next('c');
|
location.urlSubject.next('c');
|
||||||
expect(currentNodes).toEqual([
|
expect(currentNode).toEqual(cnode, 'location: c');
|
||||||
nodeTree[0].children[0].children[0],
|
|
||||||
nodeTree[0].children[0],
|
|
||||||
nodeTree[0]
|
|
||||||
]);
|
|
||||||
location.urlSubject.next('c/');
|
location.urlSubject.next('c/');
|
||||||
expect(currentNodes).toEqual([
|
expect(currentNode).toEqual(cnode, 'location: c/');
|
||||||
nodeTree[0].children[0].children[0],
|
|
||||||
nodeTree[0].children[0],
|
location.urlSubject.next('c#foo');
|
||||||
nodeTree[0]
|
expect(currentNode).toEqual(cnode, 'location: c#foo');
|
||||||
]);
|
|
||||||
|
location.urlSubject.next('c?foo=1');
|
||||||
|
expect(currentNode).toEqual(cnode, 'location: c?foo=1');
|
||||||
|
|
||||||
|
location.urlSubject.next('c#foo?bar=1&baz=2');
|
||||||
|
expect(currentNode).toEqual(cnode, 'location: c#foo?bar=1&baz=2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,49 +1,31 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Http } from '@angular/http';
|
import { Http } from '@angular/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { AsyncSubject } from 'rxjs/AsyncSubject';
|
import { AsyncSubject } from 'rxjs/AsyncSubject';
|
||||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||||
import 'rxjs/add/operator/publishReplay';
|
|
||||||
import 'rxjs/add/operator/publishLast';
|
import 'rxjs/add/operator/publishLast';
|
||||||
|
import 'rxjs/add/operator/publishReplay';
|
||||||
|
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
|
||||||
import { NavigationNode } from './navigation-node';
|
// Import and re-export the Navigation model types
|
||||||
export { NavigationNode } from './navigation-node';
|
import { CurrentNode, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model';
|
||||||
|
export { CurrentNode, NavigationNode, NavigationResponse, NavigationViews, VersionInfo } from './navigation.model';
|
||||||
|
|
||||||
export type NavigationResponse = {__versionInfo: VersionInfo } & { [name: string]: NavigationNode[]|VersionInfo };
|
|
||||||
|
|
||||||
export interface NavigationViews {
|
|
||||||
[name: string]: NavigationNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavigationMap {
|
|
||||||
[url: string]: NavigationNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VersionInfo {
|
|
||||||
raw: string;
|
|
||||||
major: number;
|
|
||||||
minor: number;
|
|
||||||
patch: number;
|
|
||||||
prerelease: string[];
|
|
||||||
build: string;
|
|
||||||
version: string;
|
|
||||||
codeName: string;
|
|
||||||
isSnapshot: boolean;
|
|
||||||
full: string;
|
|
||||||
branch: string;
|
|
||||||
commitSHA: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigationPath = 'content/navigation.json';
|
const navigationPath = 'content/navigation.json';
|
||||||
|
|
||||||
|
const urlParser = document.createElement('a');
|
||||||
|
function cleanUrl(url: string) {
|
||||||
|
// remove hash (#) and query params (?)
|
||||||
|
urlParser.href = '/' + url;
|
||||||
|
// strip leading and trailing slashes
|
||||||
|
return urlParser.pathname.replace(/^\/+|\/$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NavigationService {
|
export class NavigationService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable collection of NavigationNode trees, which can be used to render navigational menus
|
* An observable collection of NavigationNode trees, which can be used to render navigational menus
|
||||||
*/
|
*/
|
||||||
|
@ -55,16 +37,18 @@ export class NavigationService {
|
||||||
versionInfo: Observable<VersionInfo>;
|
versionInfo: Observable<VersionInfo>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable array of nodes that indicate which nodes in the `navigationViews` match the current URL location
|
* An observable of the current node with info about the
|
||||||
|
* node (if any) that matches the current URL location
|
||||||
|
* including its navigation view and its ancestor nodes in that view
|
||||||
*/
|
*/
|
||||||
selectedNodes: Observable<NavigationNode[]>;
|
currentNode: Observable<CurrentNode>;
|
||||||
|
|
||||||
constructor(private http: Http, private location: LocationService, private logger: Logger) {
|
constructor(private http: Http, private location: LocationService, private logger: Logger) {
|
||||||
const navigationInfo = this.fetchNavigationInfo();
|
const navigationInfo = this.fetchNavigationInfo();
|
||||||
// The version information is packaged inside the navigation response to save us an extra request.
|
// The version information is packaged inside the navigation response to save us an extra request.
|
||||||
this.versionInfo = this.getVersionInfo(navigationInfo);
|
this.versionInfo = this.getVersionInfo(navigationInfo);
|
||||||
this.navigationViews = this.getNavigationViews(navigationInfo);
|
this.navigationViews = this.getNavigationViews(navigationInfo);
|
||||||
this.selectedNodes = this.getSelectedNodes(this.navigationViews);
|
this.currentNode = this.getCurrentNode(this.navigationViews);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,45 +83,48 @@ export class NavigationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an observable that will list the nodes that are currently selected
|
* Get an observable of the current node (the one that matches the current URL)
|
||||||
* We use `publishReplay(1)` because otherwise subscribers will have to wait until the next
|
* We use `publishReplay(1)` because otherwise subscribers will have to wait until the next
|
||||||
* URL change before they receive an emission.
|
* URL change before they receive an emission.
|
||||||
* See above for discussion of using `connect`.
|
* See above for discussion of using `connect`.
|
||||||
*/
|
*/
|
||||||
private getSelectedNodes(navigationViews: Observable<NavigationViews>) {
|
private getCurrentNode(navigationViews: Observable<NavigationViews>): Observable<CurrentNode> {
|
||||||
const selectedNodes = combineLatest(
|
const currentNode = combineLatest(
|
||||||
navigationViews.map(this.computeUrlToNodesMap),
|
navigationViews.map(this.computeUrlToNavNodesMap),
|
||||||
this.location.currentUrl,
|
this.location.currentUrl,
|
||||||
(navMap, url) => {
|
(navMap, url) => {
|
||||||
// strip trailing slashes from the currentUrl - they are not relevant to matching against the navMap
|
let urlKey = cleanUrl(url);
|
||||||
url = url.replace(/\/$/, '');
|
urlKey = urlKey.startsWith('api/') ? 'api' : urlKey;
|
||||||
return navMap[url] || [];
|
return navMap[urlKey] || { view: '', url, nodes: [] };
|
||||||
})
|
})
|
||||||
.publishReplay(1);
|
.publishReplay(1);
|
||||||
selectedNodes.connect();
|
currentNode.connect();
|
||||||
return selectedNodes;
|
return currentNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute a mapping from URL to an array of nodes, where the first node in the array
|
* Compute a mapping from URL to an array of nodes, where the first node in the array
|
||||||
* is the one that matches the URL and the rest are the ancestors of that node.
|
* is the one that matches the URL and the rest are the ancestors of that node.
|
||||||
*
|
*
|
||||||
* @param navigation A collection of navigation nodes that are to be mapped
|
* @param navigation - A collection of navigation nodes that are to be mapped
|
||||||
*/
|
*/
|
||||||
private computeUrlToNodesMap(navigation: NavigationViews) {
|
private computeUrlToNavNodesMap(navigation: NavigationViews) {
|
||||||
const navMap: NavigationMap = {};
|
const navMap = new Map<string, CurrentNode>();
|
||||||
Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node)));
|
Object.keys(navigation)
|
||||||
|
.forEach(view => navigation[view].forEach(node => walkNodes(view, node)));
|
||||||
return navMap;
|
return navMap;
|
||||||
|
|
||||||
function walkNodes(node: NavigationNode, ancestors: NavigationNode[] = []) {
|
function walkNodes(view: string, node: NavigationNode, ancestors: NavigationNode[] = []) {
|
||||||
const nodes = [node, ...ancestors];
|
const nodes = [node, ...ancestors];
|
||||||
|
const url = node.url;
|
||||||
|
|
||||||
// only map to this node if it has a url associated with it
|
// only map to this node if it has a url associated with it
|
||||||
if (node.url) {
|
if (url) {
|
||||||
// Strip off trailing slashes from nodes in the navMap - they are not relevant to matching
|
// Strip off trailing slashes from nodes in the navMap - they are not relevant to matching
|
||||||
navMap[node.url.replace(/\/$/, '')] = nodes;
|
navMap[url.replace(/\/$/, '')] = { url, view, nodes };
|
||||||
}
|
}
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
node.children.forEach(child => walkNodes(child, nodes));
|
node.children.forEach(child => walkNodes(view, child, nodes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<input #searchBox
|
|
||||||
placeholder="Search"
|
|
||||||
(keyup)="onSearch($event.target.value, $event.which)"
|
|
||||||
(focus)="onSearch($event.target.value)"
|
|
||||||
(click)="onSearch($event.target.value)">
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { SearchBoxComponent } from './search-box.component';
|
import { SearchBoxComponent } from './search-box.component';
|
||||||
import { SearchService } from '../search.service';
|
import { SearchService } from '../search.service';
|
||||||
|
@ -17,8 +16,7 @@ describe('SearchBoxComponent', () => {
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useFactory: () => new MockSearchService() },
|
{ provide: SearchService, useFactory: () => new MockSearchService() },
|
||||||
{ provide: LocationService, useFactory: () => new MockLocationService('') }
|
{ provide: LocationService, useFactory: () => new MockLocationService('') }
|
||||||
],
|
]
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -16,7 +16,11 @@ import { LocationService } from 'app/shared/location.service';
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-search-box',
|
selector: 'aio-search-box',
|
||||||
templateUrl: './search-box.component.html',
|
template: `<input #searchBox
|
||||||
|
placeholder="Search"
|
||||||
|
(keyup)="onSearch($event.target.value, $event.which)"
|
||||||
|
(focus)="onSearch($event.target.value)"
|
||||||
|
(click)="onSearch($event.target.value)">`
|
||||||
})
|
})
|
||||||
export class SearchBoxComponent implements OnInit {
|
export class SearchBoxComponent implements OnInit {
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { ReflectiveInjector } from '@angular/core';
|
import { ReflectiveInjector } from '@angular/core';
|
||||||
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
|
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
|
||||||
import { MockLocationStrategy } from '@angular/common/testing';
|
import { MockLocationStrategy } from '@angular/common/testing';
|
||||||
import { LocationService } from './location.service';
|
|
||||||
|
|
||||||
class MockPlatformLocation {
|
import { GaService } from 'app/shared/ga.service';
|
||||||
pathname = 'a/b/c';
|
import { LocationService } from './location.service';
|
||||||
replaceState = jasmine.createSpy('PlatformLocation.replaceState');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('LocationService', () => {
|
describe('LocationService', () => {
|
||||||
|
|
||||||
|
@ -16,6 +13,7 @@ describe('LocationService', () => {
|
||||||
injector = ReflectiveInjector.resolveAndCreate([
|
injector = ReflectiveInjector.resolveAndCreate([
|
||||||
LocationService,
|
LocationService,
|
||||||
Location,
|
Location,
|
||||||
|
{ provide: GaService, useClass: TestGaService },
|
||||||
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
||||||
{ provide: PlatformLocation, useClass: MockPlatformLocation }
|
{ provide: PlatformLocation, useClass: MockPlatformLocation }
|
||||||
]);
|
]);
|
||||||
|
@ -341,4 +339,54 @@ describe('LocationService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('google analytics - GaService#locationChanged', () => {
|
||||||
|
|
||||||
|
let gaLocationChanged: jasmine.Spy;
|
||||||
|
let location: Location;
|
||||||
|
let service: LocationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const gaService = injector.get(GaService);
|
||||||
|
gaLocationChanged = gaService.locationChanged;
|
||||||
|
location = injector.get(Location);
|
||||||
|
service = injector.get(LocationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call locationChanged with initial URL', () => {
|
||||||
|
const initialUrl = location.path().replace(/^\/+/, '');
|
||||||
|
|
||||||
|
expect(gaLocationChanged.calls.count()).toBe(1, 'gaService.locationChanged');
|
||||||
|
const args = gaLocationChanged.calls.first().args;
|
||||||
|
expect(args[0]).toBe(initialUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call locationChanged when `go` to a page', () => {
|
||||||
|
service.go('some-new-url');
|
||||||
|
expect(gaLocationChanged.calls.count()).toBe(2, 'gaService.locationChanged');
|
||||||
|
const args = gaLocationChanged.calls.argsFor(1);
|
||||||
|
expect(args[0]).toBe('some-new-url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call locationChanged when window history changes', () => {
|
||||||
|
const locationStrategy: MockLocationStrategy = injector.get(LocationStrategy);
|
||||||
|
locationStrategy.simulatePopState('/next-url');
|
||||||
|
|
||||||
|
expect(gaLocationChanged.calls.count()).toBe(2, 'gaService.locationChanged');
|
||||||
|
const args = gaLocationChanged.calls.argsFor(1);
|
||||||
|
expect(args[0]).toBe('next-url');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Test Helpers ///
|
||||||
|
class MockPlatformLocation {
|
||||||
|
pathname = 'a/b/c';
|
||||||
|
replaceState = jasmine.createSpy('PlatformLocation.replaceState');
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestGaService {
|
||||||
|
locationChanged = jasmine.createSpy('locationChanged');
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Location, PlatformLocation } from '@angular/common';
|
import { Location, PlatformLocation } from '@angular/common';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import 'rxjs/add/operator/do';
|
||||||
|
import 'rxjs/add/operator/publishReplay';
|
||||||
|
|
||||||
|
import { GaService } from 'app/shared/ga.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocationService {
|
export class LocationService {
|
||||||
|
|
||||||
private readonly urlParser = document.createElement('a');
|
private readonly urlParser = document.createElement('a');
|
||||||
private urlSubject = new ReplaySubject<string>(1);
|
private urlSubject = new Subject<string>();
|
||||||
currentUrl = this.urlSubject.asObservable();
|
currentUrl = this.urlSubject
|
||||||
|
.do(url => this.gaService.locationChanged(url))
|
||||||
|
.publishReplay(1);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private gaService: GaService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
private platformLocation: PlatformLocation) {
|
private platformLocation: PlatformLocation) {
|
||||||
|
|
||||||
|
this.currentUrl.connect();
|
||||||
const initialUrl = this.stripLeadingSlashes(location.path(true));
|
const initialUrl = this.stripLeadingSlashes(location.path(true));
|
||||||
this.urlSubject.next(initialUrl);
|
this.urlSubject.next(initialUrl);
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
|
/** STEF: HELP WITH LOGO SPACING **/
|
||||||
|
.nav-link.home {
|
||||||
|
margin: 0 20px 0 10px;
|
||||||
|
}
|
||||||
|
/** STEF: MENU AT TOP OF SIDE-NAV HELP! **/
|
||||||
|
aio-nav-menu.top-menu .vertical-menu-item {
|
||||||
|
background-color: $lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-sidenav.sidenav {
|
.mat-sidenav.sidenav {
|
||||||
box-shadow: 6px 0 6px rgba(0,0,0,0.10);
|
box-shadow: 6px 0 6px rgba(0,0,0,0.10);
|
||||||
|
@ -109,7 +117,6 @@
|
||||||
.heading-children.expanded {
|
.heading-children.expanded {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
// height: auto;
|
|
||||||
max-height: 4000px; // Arbitrary max-height. Can increase if needed. Must have measurement to transition height.
|
max-height: 4000px; // Arbitrary max-height. Can increase if needed. Must have measurement to transition height.
|
||||||
transition: visibility 500ms, opacity 500ms, max-height 500ms;
|
transition: visibility 500ms, opacity 500ms, max-height 500ms;
|
||||||
-webkit-transition-timing-function: ease-in-out;
|
-webkit-transition-timing-function: ease-in-out;
|
||||||
|
@ -125,6 +132,7 @@
|
||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.selected.level-1,
|
||||||
.heading.selected.level-1,
|
.heading.selected.level-1,
|
||||||
.heading-children.selected.level-1 {
|
.heading-children.selected.level-1 {
|
||||||
border-left: 3px $blue solid;
|
border-left: 3px $blue solid;
|
||||||
|
@ -134,7 +142,6 @@
|
||||||
font-family: $main-font;
|
font-family: $main-font;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-transform: uppercase;
|
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue