feat(aio): add Table of Contents (toc) component. (#16078)
This commit is contained in:
parent
2a7f63650c
commit
3f46645f5f
@ -1,17 +1,12 @@
|
|||||||
@title
|
# Animations
|
||||||
Animations
|
|
||||||
|
|
||||||
@intro
|
|
||||||
A guide to Angular's animation system.
|
|
||||||
|
|
||||||
@description
|
|
||||||
|
|
||||||
|
|
||||||
Motion is an important aspect in the design of modern web applications. Good
|
Motion is an important aspect in the design of modern web applications. Good
|
||||||
user interfaces transition smoothly between states with engaging animations
|
user interfaces transition smoothly between states with engaging animations
|
||||||
that call attention where it's needed. Well-designed animations can make a UI not only
|
that call attention where it's needed. Well-designed animations can make a UI not only
|
||||||
more fun but also easier to use.
|
more fun but also easier to use.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
Angular's animation system lets you build animations that run with the same kind of native
|
Angular's animation system lets you build animations that run with the same kind of native
|
||||||
performance found in pure CSS animations. You can also tightly integrate your
|
performance found in pure CSS animations. You can also tightly integrate your
|
||||||
animation logic with the rest of your application code, for ease of control.
|
animation logic with the rest of your application code, for ease of control.
|
||||||
@ -33,7 +28,7 @@ add it to your page.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
# Contents
|
# Contents
|
||||||
|
|
||||||
* [Example: Transitioning between two states](guide/animations#example-transitioning-between-states).
|
* [Example: Transitioning between two states](guide/animations#example-transitioning-between-states).
|
||||||
@ -46,7 +41,7 @@ add it to your page.
|
|||||||
* [Multi-step animations with keyframes](guide/animations#multi-step-animations-with-keyframes).
|
* [Multi-step animations with keyframes](guide/animations#multi-step-animations-with-keyframes).
|
||||||
* [Parallel animation groups](guide/animations#parallel-animation-groups).
|
* [Parallel animation groups](guide/animations#parallel-animation-groups).
|
||||||
* [Animation callbacks](guide/animations#animation-callbacks).
|
* [Animation callbacks](guide/animations#animation-callbacks).
|
||||||
|
-->
|
||||||
|
|
||||||
<div class="l-sub-section">
|
<div class="l-sub-section">
|
||||||
|
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
@title
|
<h1 class="no-toc">NgModules</h1>
|
||||||
NgModules
|
|
||||||
|
|
||||||
@intro
|
|
||||||
Define application modules with @NgModule.
|
|
||||||
|
|
||||||
@description
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**NgModules** help organize an application into cohesive blocks of functionality.
|
**NgModules** help organize an application into cohesive blocks of functionality.
|
||||||
@ -25,32 +18,23 @@ of creating and maintaining a single root `AppModule` for the entire application
|
|||||||
|
|
||||||
This page covers NgModules in greater depth.
|
This page covers NgModules in greater depth.
|
||||||
|
|
||||||
## Table of Contents
|
<!-- CF: See my comment in the "Resolve directive conflicts" section below proposing renaming or reorganizing that section. -->
|
||||||
|
|
||||||
<!-- CF: The titling for tables of contents in the advanced chapters is inconsistent:
|
|
||||||
|
|
||||||
* some are titled "Contents" while others are titled "Table of Contents" (should probably be sentence case as it's an H2
|
|
||||||
* some headings are H2, some are H3
|
|
||||||
* some pages don't have tables of contents
|
|
||||||
|
|
||||||
I didn't make changes here as I'm not sure what the correct style is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
* [Angular modularity](guide/ngmodule#angular-modularity "Add structure to the app with NgModule")
|
* [Angular modularity](guide/ngmodule#angular-modularity "Add structure to the app with NgModule")
|
||||||
* [The application root module](guide/ngmodule#root-module "The startup module that every app requires")
|
* [The application root module](guide/ngmodule#root-module "The startup module that every app requires")
|
||||||
* [Bootstrap](guide/ngmodule#bootstrap "Launch the app in a browser with the root module as the entry point") the root module
|
* [Bootstrap the root module](guide/ngmodule#bootstrap "Launch the app in a browser with the root module as the entry point")
|
||||||
* [Declarations](guide/ngmodule#declarations "Declare the components, directives, and pipes that belong to a module")
|
* [Declarations](guide/ngmodule#declarations "Declare the components, directives, and pipes that belong to a module")
|
||||||
* [Providers](guide/ngmodule#providers "Extend the app with additional services")
|
* [Providers](guide/ngmodule#providers "Extend the app with additional services")
|
||||||
* [Imports](guide/ngmodule#imports "Import components, directives, and pipes for use in component templates")
|
* [Imports](guide/ngmodule#imports "Import components, directives, and pipes for use in component templates")
|
||||||
* [Resolve conflicts](guide/ngmodule#resolve-conflicts "When two directives have the same selector")
|
* [Resolve conflicts](guide/ngmodule#resolve-conflicts "When two directives have the same selector")
|
||||||
<!-- CF: See my comment in the "Resolve diretive conflicts" section below proposing renaming or reorganizing that section. -->
|
|
||||||
* [Feature modules](guide/ngmodule#feature-modules "Partition the app into feature modules")
|
* [Feature modules](guide/ngmodule#feature-modules "Partition the app into feature modules")
|
||||||
* [Lazy loaded modules](guide/ngmodule#lazy-load "Load modules asynchronously") with the router
|
* [Lazy loaded modules with the router](guide/ngmodule#lazy-load "Load modules asynchronously")
|
||||||
* [Shared modules](guide/ngmodule#shared-module "Create modules for commonly used components, directives, and pipes")
|
* [Shared modules](guide/ngmodule#shared-module "Create modules for commonly used components, directives, and pipes")
|
||||||
* [The Core module](guide/ngmodule#core-module "Create a core module with app-wide singleton services and single-use components")
|
* [The Core module](guide/ngmodule#core-module "Create a core module with app-wide singleton services and single-use components")
|
||||||
* [Configure core services with _forRoot_](guide/ngmodule#core-for-root "Configure providers during module import")
|
* [Configure core services with _forRoot_](guide/ngmodule#core-for-root "Configure providers during module import")
|
||||||
* [Prevent reimport of the _CoreModule_](guide/ngmodule#prevent-reimport "because bad things happen if a lazy loaded module imports Core")
|
* [Prevent re-import of the _CoreModule_](guide/ngmodule#prevent-reimport "because bad things happen if a lazy loaded module imports Core")
|
||||||
* [NgModule metadata properties](guide/ngmodule#ngmodule-properties "A technical summary of the @NgModule metadata properties")
|
* [NgModule metadata properties](guide/ngmodule#ngmodule-properties "A technical summary of the @NgModule metadata properties")
|
||||||
|
|
||||||
<!-- CF: This link goes to the top of this page. I would expect it to go to an "NgModule metadata properties"
|
<!-- CF: This link goes to the top of this page. I would expect it to go to an "NgModule metadata properties"
|
||||||
section at the end of this page, but that section doesn't exist. -->
|
section at the end of this page, but that section doesn't exist. -->
|
||||||
|
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
@title
|
<h1 class="no-toc">QuickStart</h1>
|
||||||
QuickStart
|
|
||||||
|
|
||||||
@description
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Angular applications are made up of _components_.
|
Angular applications are made up of _components_.
|
||||||
A _component_ is the combination of an HTML template and a component class that controls a portion of the screen. Here is an example of a component that displays a simple string:
|
A _component_ is the combination of an HTML template and a component class that controls a portion of the screen. Here is an example of a component that displays a simple string:
|
||||||
|
|
||||||
|
|
||||||
<code-example path="quickstart/src/app/app.component.ts" title="src/app/app.component.ts" linenums="false">
|
<code-example path="quickstart/src/app/app.component.ts" title="src/app/app.component.ts" linenums="false">
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<h1 class="title center">Angular Contributors</h1>
|
<h1 class="title center no-toc">Angular Contributors</h1>
|
||||||
<h2>Building For the Future</h2>
|
<h2>Building For the Future</h2>
|
||||||
<p>Angular is built by a team of engineers who share a passion for
|
<p>Angular is built by a team of engineers who share a passion for
|
||||||
making web development feel effortless. We believe that writing
|
making web development feel effortless. We believe that writing
|
||||||
beautiful apps should be joyful and fun. We're building a
|
beautiful apps should be joyful and fun. We're building a
|
||||||
platform for the future.</p>
|
platform for the future.</p>
|
||||||
|
|
||||||
<aio-contributor-list></aio-contributor-list>
|
<aio-contributor-list></aio-contributor-list>
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
<h1>API List</h1>
|
<h1 class="no-toc">API List</h1>
|
||||||
<aio-api-list></aio-api-list>
|
<aio-api-list></aio-api-list>
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
@title
|
# Contribute to Angular
|
||||||
Contribute
|
|
||||||
|
|
||||||
@intro
|
|
||||||
Contribute to Angular
|
|
||||||
|
|
||||||
@description
|
|
||||||
Help us build the framework of the future!
|
Help us build the framework of the future!
|
||||||
|
|
||||||
## Angular Projects
|
## Angular Projects
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<h1>Events</h1>
|
<h1 class="no-toc">Events</h1>
|
||||||
|
|
||||||
<h3>Where we'll be presenting:</h3>
|
<h3>Where we'll be presenting:</h3>
|
||||||
<article class="l-content ">
|
<article class="l-content ">
|
||||||
<table class="is-full-width">
|
<table class="is-full-width">
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<h1>Features & Benefits</h1>
|
<h1 class="no-toc">Features & Benefit</h1>
|
||||||
|
|
||||||
<article class="l-content ">
|
<article class="l-content ">
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<div><h2 class="text-headline">Cross Platform</h2>
|
<div><h2 class="text-headline">Cross Platform</h2>
|
||||||
@ -20,7 +21,7 @@
|
|||||||
<p class="text-body">Create desktop-installed apps across Mac, Windows, and Linux using the same Angular methods you've learned for the web plus the ability to access native OS APIs.</p>
|
<p class="text-body">Create desktop-installed apps across Mac, Windows, and Linux using the same Angular methods you've learned for the web plus the ability to access native OS APIs.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-headline">Speed and Performance</h2>
|
<h2 class="text-headline">Speed and Performance</h2>
|
||||||
<div class="feature-row">
|
<div class="feature-row">
|
||||||
|
|
||||||
@ -43,7 +44,7 @@
|
|||||||
|
|
||||||
<h2 class="text-headline">Productivity</h2>
|
<h2 class="text-headline">Productivity</h2>
|
||||||
<div class="feature-row">
|
<div class="feature-row">
|
||||||
|
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<h5>Templates</h5>
|
<h5>Templates</h5>
|
||||||
<p class="text-body">Quickly create UI views with simple and powerful template syntax.</p>
|
<p class="text-body">Quickly create UI views with simple and powerful template syntax.</p>
|
||||||
@ -53,7 +54,7 @@
|
|||||||
<h5>Angular CLI</h5>
|
<h5>Angular CLI</h5>
|
||||||
<p class="text-body">Command line tools: start building fast, add components and tests, then instantly deploy.</p>
|
<p class="text-body">Command line tools: start building fast, add components and tests, then instantly deploy.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<h5>IDEs</h5>
|
<h5>IDEs</h5>
|
||||||
<p class="text-body">Get intelligent code completion, instant errors, and other feedback in popular editors and IDEs.</p>
|
<p class="text-body">Get intelligent code completion, instant errors, and other feedback in popular editors and IDEs.</p>
|
||||||
@ -81,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cta-bar"><a href="guide/quickstart" md-button="md-button" class="button button-large button-shield mat-raised mat-primary">Get Started</a></div>
|
<div class="cta-bar"><a href="guide/quickstart" md-button="md-button" class="button button-large button-shield mat-raised mat-primary">Get Started</a></div>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="hero background-superhero-paper is-large">
|
<div class="hero background-superhero-paper is-large">
|
||||||
|
|
||||||
<img src="assets/images/logos/angular/angular.svg" class="hero-logo"/>
|
<img src="assets/images/logos/angular/angular.svg" class="hero-logo"/>
|
||||||
<h1 class="text-headline">One framework.<br>Mobile & desktop.</h1>
|
<h1 class="text-headline no-toc">One framework.<br>Mobile & desktop.</h1>
|
||||||
<a href="guide/quickstart" md-button="md-button" class="hero-cta mat-raised button button-large button-plain">Get Started</a>
|
<a href="guide/quickstart" md-button="md-button" class="hero-cta mat-raised button button-large button-plain">Get Started</a>
|
||||||
|
|
||||||
<announcement-bar class="announcement-bar">
|
<announcement-bar class="announcement-bar">
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<a href="http://angularjs.blogspot.com/2017/03/angular-400-now-available.html" target="_blank" class="button mat-button">Learn More</a>
|
<a href="http://angularjs.blogspot.com/2017/03/angular-400-now-available.html" target="_blank" class="button mat-button">Learn More</a>
|
||||||
</div>
|
</div>
|
||||||
</announcement-bar>
|
</announcement-bar>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<img src="assets/images/home/responsive-framework.svg" alt="responsive framework">
|
<img src="assets/images/home/responsive-framework.svg" alt="responsive framework">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<div class="text-block promo-1-desc l-pad-top-2">
|
<div class="text-block promo-1-desc l-pad-top-2">
|
||||||
<h2 class="text-headline">Develop Across All Platforms</h2>
|
<h2 class="text-headline">Develop Across All Platforms</h2>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<header class="hero background-sky"><h1 class="hero-title ">News</h1>
|
<header class="hero background-sky">
|
||||||
|
<h1 class="hero-title no-toc">News</h1>
|
||||||
<div class="clear"></div>
|
<div class="clear"></div>
|
||||||
</header>
|
</header>
|
||||||
<article class="l-content ">
|
<article class="l-content ">
|
||||||
|
@ -10,19 +10,19 @@ 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 { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { GaService } from 'app/shared/ga.service';
|
import { GaService } from 'app/shared/ga.service';
|
||||||
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
import { MockLogger } from 'testing/logger.service';
|
||||||
|
import { MockLocationService } from 'testing/location.service';
|
||||||
|
import { MockSearchService } from 'testing/search.service';
|
||||||
|
import { MockSwUpdateNotificationsService } from 'testing/sw-update-notifications.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 { SearchService } from 'app/search/search.service';
|
||||||
import { MockSearchService } from 'testing/search.service';
|
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
|
||||||
import { LocationService } from 'app/shared/location.service';
|
|
||||||
import { MockLocationService } from 'testing/location.service';
|
|
||||||
import { Logger } from 'app/shared/logger.service';
|
|
||||||
import { MockLogger } from 'testing/logger.service';
|
|
||||||
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
||||||
import { MockSwUpdateNotificationsService } from 'testing/sw-update-notifications.service';
|
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
let component: AppComponent;
|
let component: AppComponent;
|
||||||
@ -245,7 +245,7 @@ describe('AppComponent', () => {
|
|||||||
it('should display a marketing page', () => {
|
it('should display a marketing page', () => {
|
||||||
locationService.go('features');
|
locationService.go('features');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(docViewer.innerText).toMatch(/Features Doc/i);
|
expect(docViewer.innerText).toMatch(/Features/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the document title', () => {
|
it('should update the document title', () => {
|
||||||
@ -443,7 +443,11 @@ class TestHttp {
|
|||||||
{
|
{
|
||||||
"url": "features",
|
"url": "features",
|
||||||
"title": "Features"
|
"title": "Features"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"url": "no-title",
|
||||||
|
"title": "No Title"
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"SideNav": [
|
"SideNav": [
|
||||||
{
|
{
|
||||||
@ -459,7 +463,7 @@ class TestHttp {
|
|||||||
"url": "guide/bags",
|
"url": "guide/bags",
|
||||||
"title": "Bags",
|
"title": "Bags",
|
||||||
"tooltip": "Pack your bags for a code adventure."
|
"tooltip": "Pack your bags for a code adventure."
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -493,12 +497,11 @@ class TestHttp {
|
|||||||
} else {
|
} else {
|
||||||
const match = /content\/docs\/(.+)\.json/.exec(url);
|
const match = /content\/docs\/(.+)\.json/.exec(url);
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
|
// Make up a title for test purposes
|
||||||
const title = id.split('/').pop().replace(/^([a-z])/, (_, letter) => letter.toUpperCase());
|
const title = id.split('/').pop().replace(/^([a-z])/, (_, letter) => letter.toUpperCase());
|
||||||
const contents = `<h1>${title} Doc</h1><h2 id="#somewhere">Some heading</h2>`;
|
const h1 = (id === 'no-title') ? '' : `<h1>${title}</h1>`;
|
||||||
data = { id, title, contents };
|
const contents = `${h1}<h2 id="#somewhere">Some heading</h2>`;
|
||||||
if (id === 'no-title') {
|
data = { id, contents };
|
||||||
data.title = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return of({ json: () => data });
|
return of({ json: () => data });
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Component, ElementRef, HostListener, OnInit,
|
import { Component, ElementRef, HostListener, OnInit,
|
||||||
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||||
import { MdSidenav } from '@angular/material';
|
import { MdSidenav } from '@angular/material';
|
||||||
import { Title } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
import { CurrentNode, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||||
@ -62,8 +61,7 @@ export class AppComponent implements OnInit {
|
|||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private locationService: LocationService,
|
private locationService: LocationService,
|
||||||
private navigationService: NavigationService,
|
private navigationService: NavigationService,
|
||||||
private swUpdateNotifications: SwUpdateNotificationsService,
|
private swUpdateNotifications: SwUpdateNotificationsService
|
||||||
private titleService: Title
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -73,7 +71,6 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
this.documentService.currentDocument.subscribe(doc => {
|
this.documentService.currentDocument.subscribe(doc => {
|
||||||
this.currentDocument = doc;
|
this.currentDocument = doc;
|
||||||
this.setDocumentTitle(doc.title);
|
|
||||||
this.setPageId(doc.id);
|
this.setPageId(doc.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,14 +152,6 @@ export class AppComponent implements OnInit {
|
|||||||
this.sidenav.toggle(value);
|
this.sidenav.toggle(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDocumentTitle(title: string) {
|
|
||||||
if (title.trim()) {
|
|
||||||
this.titleService.setTitle(`Angular - ${title}`);
|
|
||||||
} else {
|
|
||||||
this.titleService.setTitle('Angular');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPageId(id: string) {
|
setPageId(id: string) {
|
||||||
// Special case the home page
|
// Special case the home page
|
||||||
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
|
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
|
||||||
|
@ -16,6 +16,8 @@ import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
|||||||
|
|
||||||
import { AppComponent } from 'app/app.component';
|
import { AppComponent } from 'app/app.component';
|
||||||
import { ApiService } from 'app/embedded/api/api.service';
|
import { ApiService } from 'app/embedded/api/api.service';
|
||||||
|
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
||||||
|
import { CustomMdIconRegistry, SVG_ICONS } from 'app/shared/custom-md-icon-registry';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
||||||
import { EmbeddedModule } from 'app/embedded/embedded.module';
|
import { EmbeddedModule } from 'app/embedded/embedded.module';
|
||||||
@ -31,8 +33,7 @@ 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';
|
||||||
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
||||||
import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
import { TocService } from 'app/shared/toc.service';
|
||||||
import { CustomMdIconRegistry, SVG_ICONS } from 'app/shared/custom-md-icon-registry';
|
|
||||||
|
|
||||||
// These are the hardcoded inline svg sources to be used by the `<md-icon>` component
|
// These are the hardcoded inline svg sources to be used by the `<md-icon>` component
|
||||||
export const svgIconProviders = [
|
export const svgIconProviders = [
|
||||||
@ -83,18 +84,19 @@ export const svgIconProviders = [
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ApiService,
|
ApiService,
|
||||||
|
AutoScrollService,
|
||||||
|
DocumentService,
|
||||||
GaService,
|
GaService,
|
||||||
Logger,
|
Logger,
|
||||||
Location,
|
Location,
|
||||||
{ provide: LocationStrategy, useClass: PathLocationStrategy },
|
{ provide: LocationStrategy, useClass: PathLocationStrategy },
|
||||||
LocationService,
|
LocationService,
|
||||||
|
{ provide: MdIconRegistry, useClass: CustomMdIconRegistry },
|
||||||
NavigationService,
|
NavigationService,
|
||||||
DocumentService,
|
|
||||||
SearchService,
|
SearchService,
|
||||||
Platform,
|
Platform,
|
||||||
AutoScrollService,
|
svgIconProviders,
|
||||||
{ provide: MdIconRegistry, useClass: CustomMdIconRegistry },
|
TocService
|
||||||
svgIconProviders
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
export interface DocumentContents {
|
export interface DocumentContents {
|
||||||
/** The unique identifier for this document */
|
/** The unique identifier for this document */
|
||||||
id: string;
|
id: string;
|
||||||
/** The string to display in the browser tab when this document is being viewed */
|
|
||||||
title: string;
|
|
||||||
/** The HTML to display in the doc viewer */
|
/** The HTML to display in the doc viewer */
|
||||||
contents: string;
|
contents: string;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ 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';
|
||||||
import { MockLogger } from 'testing/logger.service';
|
import { MockLogger } from 'testing/logger.service';
|
||||||
import { DocumentService, DocumentContents } from './document.service';
|
import { DocumentService, DocumentContents,
|
||||||
|
FETCHING_ERROR_ID, FILE_NOT_FOUND_ID } from './document.service';
|
||||||
|
|
||||||
|
|
||||||
const CONTENT_URL_PREFIX = 'content/docs/';
|
const CONTENT_URL_PREFIX = 'content/docs/';
|
||||||
@ -61,8 +62,8 @@ describe('DocumentService', () => {
|
|||||||
|
|
||||||
it('should emit a document each time the location changes', () => {
|
it('should emit a document each time the location changes', () => {
|
||||||
let latestDocument: DocumentContents;
|
let latestDocument: DocumentContents;
|
||||||
const doc0 = { title: 'doc 0', id: 'initial/doc' };
|
const doc0 = { contents: 'doc 0', id: 'initial/doc' };
|
||||||
const doc1 = { title: 'doc 1', id: 'new/doc' };
|
const doc1 = { contents: 'doc 1', id: 'new/doc' };
|
||||||
const { docService, backend, locationService } = getServices('initial/doc');
|
const { docService, backend, locationService } = getServices('initial/doc');
|
||||||
const connections = backend.connectionsArray;
|
const connections = backend.connectionsArray;
|
||||||
|
|
||||||
@ -86,16 +87,16 @@ describe('DocumentService', () => {
|
|||||||
connections[0].mockError(new Response(new ResponseOptions({ status: 404, statusText: 'NOT FOUND'})) as any);
|
connections[0].mockError(new Response(new ResponseOptions({ status: 404, statusText: 'NOT FOUND'})) as any);
|
||||||
expect(connections.length).toEqual(2);
|
expect(connections.length).toEqual(2);
|
||||||
expect(connections[1].request.url).toEqual(CONTENT_URL_PREFIX + 'file-not-found.json');
|
expect(connections[1].request.url).toEqual(CONTENT_URL_PREFIX + 'file-not-found.json');
|
||||||
const fileNotFoundDoc = { id: 'file-not-found', title: 'Page Not Found', contents: '<h1>Page Not Found</h1>' };
|
const fileNotFoundDoc = { id: FILE_NOT_FOUND_ID, contents: '<h1>Page Not Found</h1>' };
|
||||||
connections[1].mockRespond(createResponse(fileNotFoundDoc));
|
connections[1].mockRespond(createResponse(fileNotFoundDoc));
|
||||||
expect(currentDocument).toEqual(fileNotFoundDoc);
|
expect(currentDocument).toEqual(fileNotFoundDoc);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a hard-coded not-found document if the not-found document is not found on the server', () => {
|
it('should emit a hard-coded not-found document if the not-found document is not found on the server', () => {
|
||||||
let currentDocument: DocumentContents;
|
let currentDocument: DocumentContents;
|
||||||
const notFoundDoc: DocumentContents = { title: 'Not Found', contents: 'Document not found', id: 'file-not-found' };
|
const notFoundDoc: DocumentContents = { contents: 'Document not found', id: FILE_NOT_FOUND_ID };
|
||||||
const nextDoc = { title: 'Next Doc', id: 'new/doc' };
|
const nextDoc = { contents: 'Next Doc', id: 'new/doc' };
|
||||||
const { docService, backend, locationService } = getServices('file-not-found');
|
const { docService, backend, locationService } = getServices(FILE_NOT_FOUND_ID);
|
||||||
const connections = backend.connectionsArray;
|
const connections = backend.connectionsArray;
|
||||||
docService.currentDocument.subscribe(doc => currentDocument = doc);
|
docService.currentDocument.subscribe(doc => currentDocument = doc);
|
||||||
|
|
||||||
@ -117,9 +118,9 @@ describe('DocumentService', () => {
|
|||||||
docService.currentDocument.subscribe(doc => latestDocument = doc);
|
docService.currentDocument.subscribe(doc => latestDocument = doc);
|
||||||
|
|
||||||
connections[0].mockRespond(new Response(new ResponseOptions({ body: 'this is invalid JSON' })));
|
connections[0].mockRespond(new Response(new ResponseOptions({ body: 'this is invalid JSON' })));
|
||||||
expect(latestDocument.title).toMatch('Document retrieval error');
|
expect(latestDocument.id).toEqual(FETCHING_ERROR_ID);
|
||||||
|
|
||||||
const doc1 = { title: 'doc 1' };
|
const doc1 = { contents: 'doc 1' };
|
||||||
locationService.go('new/doc');
|
locationService.go('new/doc');
|
||||||
connections[1].mockRespond(createResponse(doc1));
|
connections[1].mockRespond(createResponse(doc1));
|
||||||
expect(latestDocument).toEqual(jasmine.objectContaining(doc1));
|
expect(latestDocument).toEqual(jasmine.objectContaining(doc1));
|
||||||
@ -129,8 +130,8 @@ describe('DocumentService', () => {
|
|||||||
let latestDocument: DocumentContents;
|
let latestDocument: DocumentContents;
|
||||||
let subscription: Subscription;
|
let subscription: Subscription;
|
||||||
|
|
||||||
const doc0 = { title: 'doc 0' };
|
const doc0 = { contents: 'doc 0' };
|
||||||
const doc1 = { title: 'doc 1' };
|
const doc1 = { contents: 'doc 1' };
|
||||||
const { docService, backend, locationService} = getServices('url/0');
|
const { docService, backend, locationService} = getServices('url/0');
|
||||||
const connections = backend.connectionsArray;
|
const connections = backend.connectionsArray;
|
||||||
|
|
||||||
@ -141,7 +142,7 @@ describe('DocumentService', () => {
|
|||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
||||||
// modify the response so we can check that future subscriptions do not trigger another request
|
// modify the response so we can check that future subscriptions do not trigger another request
|
||||||
connections[0].response.next(createResponse({ title: 'error 0' }));
|
connections[0].response.next(createResponse({ contents: 'error 0' }));
|
||||||
|
|
||||||
subscription = docService.currentDocument.subscribe(doc => latestDocument = doc);
|
subscription = docService.currentDocument.subscribe(doc => latestDocument = doc);
|
||||||
locationService.go('url/1');
|
locationService.go('url/1');
|
||||||
@ -151,7 +152,7 @@ describe('DocumentService', () => {
|
|||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
||||||
// modify the response so we can check that future subscriptions do not trigger another request
|
// modify the response so we can check that future subscriptions do not trigger another request
|
||||||
connections[1].response.next(createResponse({ title: 'error 1' }));
|
connections[1].response.next(createResponse({ contents: 'error 1' }));
|
||||||
|
|
||||||
subscription = docService.currentDocument.subscribe(doc => latestDocument = doc);
|
subscription = docService.currentDocument.subscribe(doc => latestDocument = doc);
|
||||||
locationService.go('url/0');
|
locationService.go('url/0');
|
||||||
|
@ -14,9 +14,10 @@ export { DocumentContents } from './document-contents';
|
|||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
|
||||||
|
export const FILE_NOT_FOUND_ID = 'file-not-found';
|
||||||
|
export const FETCHING_ERROR_ID = 'fetching-error';
|
||||||
|
|
||||||
const CONTENT_URL_PREFIX = 'content/docs/';
|
const CONTENT_URL_PREFIX = 'content/docs/';
|
||||||
const FILE_NOT_FOUND_ID = 'file-not-found';
|
|
||||||
const FETCHING_ERROR_ID = 'fetching-error';
|
|
||||||
const FETCHING_ERROR_CONTENTS = `
|
const FETCHING_ERROR_CONTENTS = `
|
||||||
<div class="nf-container l-flex-wrap flex-center">
|
<div class="nf-container l-flex-wrap flex-center">
|
||||||
<div class="nf-icon material-icons">error_outline</div>
|
<div class="nf-icon material-icons">error_outline</div>
|
||||||
@ -72,9 +73,8 @@ export class DocumentService {
|
|||||||
return this.getDocument(FILE_NOT_FOUND_ID);
|
return this.getDocument(FILE_NOT_FOUND_ID);
|
||||||
} else {
|
} else {
|
||||||
return of({
|
return of({
|
||||||
title: 'Not Found',
|
id: FILE_NOT_FOUND_ID,
|
||||||
contents: 'Document not found',
|
contents: 'Document not found'
|
||||||
id: FILE_NOT_FOUND_ID
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,9 +83,8 @@ export class DocumentService {
|
|||||||
this.logger.error('Error fetching document', error);
|
this.logger.error('Error fetching document', error);
|
||||||
this.cache.delete(id);
|
this.cache.delete(id);
|
||||||
return Observable.of({
|
return Observable.of({
|
||||||
title: 'Document retrieval error',
|
id: FETCHING_ERROR_ID,
|
||||||
contents: FETCHING_ERROR_CONTENTS,
|
contents: FETCHING_ERROR_CONTENTS
|
||||||
id: FETCHING_ERROR_ID
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { Component, ElementRef, OnInit } from '@angular/core';
|
|||||||
<aio-code [ngClass]="{'headed-code':title, 'simple-code':!title}" [code]="code" [language]="language" [linenums]="linenums"></aio-code>
|
<aio-code [ngClass]="{'headed-code':title, 'simple-code':!title}" [code]="code" [language]="language" [linenums]="linenums"></aio-code>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class CodeExampleComponent implements OnInit { // implements AfterViewInit {
|
export class CodeExampleComponent implements OnInit {
|
||||||
|
|
||||||
code: string;
|
code: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
/* tslint:disable component-selector */
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { DocumentService } from 'app/documents/document.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'doc-title',
|
|
||||||
template: '<h1 class="docs-primary-header">{{title | async}}</h1>'
|
|
||||||
})
|
|
||||||
export class DocTitleComponent {
|
|
||||||
title: Observable<string>;
|
|
||||||
constructor(docs: DocumentService) {
|
|
||||||
this.title = docs.currentDocument.map(doc => doc.title);
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,18 +18,18 @@ import { CodeExampleComponent } from './code/code-example.component';
|
|||||||
import { CodeTabsComponent } from './code/code-tabs.component';
|
import { CodeTabsComponent } from './code/code-tabs.component';
|
||||||
import { ContributorListComponent } from './contributor/contributor-list.component';
|
import { ContributorListComponent } from './contributor/contributor-list.component';
|
||||||
import { ContributorComponent } from './contributor/contributor.component';
|
import { ContributorComponent } from './contributor/contributor.component';
|
||||||
import { DocTitleComponent } from './doc-title.component';
|
|
||||||
import { CurrentLocationComponent } from './current-location.component';
|
import { CurrentLocationComponent } from './current-location.component';
|
||||||
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
||||||
import { ResourceListComponent } from './resource/resource-list.component';
|
import { ResourceListComponent } from './resource/resource-list.component';
|
||||||
import { ResourceService } from './resource/resource.service';
|
import { ResourceService } from './resource/resource.service';
|
||||||
|
import { TocComponent } from './toc/toc.component';
|
||||||
|
|
||||||
/** Components that can be embedded in docs
|
/** Components that can be embedded in docs
|
||||||
* such as CodeExampleComponent, LiveExampleComponent,...
|
* such as CodeExampleComponent, LiveExampleComponent,...
|
||||||
*/
|
*/
|
||||||
export const embeddedComponents: any[] = [
|
export const embeddedComponents: any[] = [
|
||||||
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
||||||
CurrentLocationComponent, DocTitleComponent, LiveExampleComponent, ResourceListComponent
|
CurrentLocationComponent, LiveExampleComponent, ResourceListComponent, TocComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||||
|
@ -26,17 +26,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--</div>-->
|
|
||||||
|
|
||||||
<!--<div class="c3">-->
|
|
||||||
<div class="c-resource-nav shadow-1 l-flex--column h-affix" [ngClass]="{ 'affix-top': scrollPos > 200 }">
|
|
||||||
<div class="category" *ngFor="let category of categories">
|
|
||||||
<a class="category-link h-capitalize" [href]="href(category)">{{category.title}}</a>
|
|
||||||
<div class="subcategory" *ngFor="let subCategory of category.subCategories">
|
|
||||||
<a class="subcategory-link" [href]="href(subCategory)">{{subCategory.title}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--</div>-->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
24
aio/src/app/embedded/toc/toc.component.html
Normal file
24
aio/src/app/embedded/toc/toc.component.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div *ngIf="hasToc" [class.closed]="isClosed">
|
||||||
|
<div *ngIf="!hasSecondary"class="toc-heading">Contents</div>
|
||||||
|
<div *ngIf="hasSecondary" class="toc-heading secondary"
|
||||||
|
(click)="toggle()"
|
||||||
|
title="Expand/collapse contents"
|
||||||
|
aria-label="Expand/collapse contents">
|
||||||
|
Contents
|
||||||
|
<button type="button"
|
||||||
|
class="toc-show-all material-icons" [class.closed]="isClosed">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="toc-list">
|
||||||
|
<li *ngFor="let toc of tocList" title="{{toc.title}}" class="{{toc.level}}" [class.secondary]="toc.isSecondary">
|
||||||
|
<a [href]="toc.href" [innerHTML]="toc.content"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button type="button" (click)="toggle()" *ngIf="hasSecondary"
|
||||||
|
class="toc-more-items material-icons" [class.closed]="isClosed"
|
||||||
|
title="Expand/collapse contents"
|
||||||
|
aria-label="Expand/collapse contents">
|
||||||
|
</button>
|
||||||
|
</div>
|
232
aio/src/app/embedded/toc/toc.component.spec.ts
Normal file
232
aio/src/app/embedded/toc/toc.component.spec.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import { Component, DebugElement } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By, DOCUMENT } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TocComponent } from './toc.component';
|
||||||
|
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
|
describe('TocComponent', () => {
|
||||||
|
let tocComponentDe: DebugElement;
|
||||||
|
let tocComponent: TocComponent;
|
||||||
|
let tocService: TestTocService;
|
||||||
|
|
||||||
|
let page: {
|
||||||
|
listItems: DebugElement[];
|
||||||
|
tocHeading: DebugElement;
|
||||||
|
tocHeadingButton: DebugElement;
|
||||||
|
tocMoreButton: DebugElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setPage(): typeof page {
|
||||||
|
return {
|
||||||
|
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
|
||||||
|
tocHeading: tocComponentDe.query(By.css('.toc-heading')),
|
||||||
|
tocHeadingButton: tocComponentDe.query(By.css('.toc-heading button')),
|
||||||
|
tocMoreButton: tocComponentDe.query(By.css('button.toc-more-items')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ HostEmbeddedTocComponent, HostNotEmbeddedTocComponent, TocComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: TocService, useClass: TestTocService }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when embedded in doc body', () => {
|
||||||
|
let fixture: ComponentFixture<HostEmbeddedTocComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HostEmbeddedTocComponent);
|
||||||
|
tocComponentDe = fixture.debugElement.children[0];
|
||||||
|
tocComponent = tocComponentDe.componentInstance;
|
||||||
|
tocService = TestBed.get(TocService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create tocComponent', () => {
|
||||||
|
expect(tocComponent).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be in embedded state', () => {
|
||||||
|
expect(tocComponent.isEmbedded).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display anything when no TocItems', () => {
|
||||||
|
tocService.tocList = [];
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(tocComponentDe.children.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when four TocItems', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tocService.tocList.length = 4;
|
||||||
|
fixture.detectChanges();
|
||||||
|
page = setPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have four displayed items', () => {
|
||||||
|
expect(page.listItems.length).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have secondary items', () => {
|
||||||
|
expect(tocComponent.hasSecondary).toEqual(false, 'hasSecondary flag');
|
||||||
|
const aSecond = page.listItems.find(item => item.classes.secondary);
|
||||||
|
expect(aSecond).toBeFalsy('should not find a secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display expando buttons', () => {
|
||||||
|
expect(page.tocHeadingButton).toBeFalsy('top expand/collapse button');
|
||||||
|
expect(page.tocMoreButton).toBeFalsy('bottom more button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when many TocItems', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
page = setPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have more than 4 displayed items', () => {
|
||||||
|
expect(page.listItems.length).toBeGreaterThan(4);
|
||||||
|
expect(page.listItems.length).toEqual(tocService.tocList.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be in "closed" (not expanded) state at the start', () => {
|
||||||
|
expect(tocComponent.isClosed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have "closed" class at the start', () => {
|
||||||
|
expect(tocComponentDe.children[0].classes.closed).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display expando buttons', () => {
|
||||||
|
expect(page.tocHeadingButton).toBeTruthy('top expand/collapse button');
|
||||||
|
expect(page.tocMoreButton).toBeTruthy('bottom more button');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have secondary items', () => {
|
||||||
|
expect(tocComponent.hasSecondary).toEqual(true, 'hasSecondary flag');
|
||||||
|
});
|
||||||
|
|
||||||
|
// CSS should hide items with the secondary class when closed
|
||||||
|
it('should have secondary item with a secondary class', () => {
|
||||||
|
const aSecondary = page.listItems.find(item => item.classes.secondary);
|
||||||
|
expect(aSecondary).toBeTruthy('should find a secondary');
|
||||||
|
expect(aSecondary.classes.secondary).toEqual(true, 'has secondary class');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after click expando button', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page.tocHeadingButton.nativeElement.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be "closed"', () => {
|
||||||
|
expect(tocComponent.isClosed).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have "closed" class', () => {
|
||||||
|
expect(tocComponentDe.children[0].classes.closed).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when in side panel (not embedded))', () => {
|
||||||
|
let fixture: ComponentFixture<HostNotEmbeddedTocComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HostNotEmbeddedTocComponent);
|
||||||
|
tocComponentDe = fixture.debugElement.children[0];
|
||||||
|
tocComponent = tocComponentDe.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
page = setPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be in embedded state', () => {
|
||||||
|
expect(tocComponent.isEmbedded).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display all items', () => {
|
||||||
|
expect(page.listItems.length).toEqual(tocService.tocList.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have secondary items', () => {
|
||||||
|
expect(tocComponent.hasSecondary).toEqual(false, 'hasSecondary flag');
|
||||||
|
const aSecond = page.listItems.find(item => item.classes.secondary);
|
||||||
|
expect(aSecond).toBeFalsy('should not find a secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display expando buttons', () => {
|
||||||
|
expect(page.tocHeadingButton).toBeFalsy('top expand/collapse button');
|
||||||
|
expect(page.tocMoreButton).toBeFalsy('bottom more button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//// helpers ////
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-embedded-host',
|
||||||
|
template: '<aio-toc class="embedded"></aio-toc>'
|
||||||
|
})
|
||||||
|
class HostEmbeddedTocComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-not-embedded-host',
|
||||||
|
template: '<aio-toc></aio-toc>'
|
||||||
|
})
|
||||||
|
class HostNotEmbeddedTocComponent {}
|
||||||
|
|
||||||
|
class TestTocService {
|
||||||
|
tocList: TocItem[] = getTestTocList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable:quotemark
|
||||||
|
|
||||||
|
function getTestTocList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"content": "Heading one",
|
||||||
|
"href": "fizz/buzz#heading-one-special-id",
|
||||||
|
"level": "h2",
|
||||||
|
"title": "Heading one"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "H2 Two",
|
||||||
|
"href": "fizz/buzz#h2-two",
|
||||||
|
"level": "h2",
|
||||||
|
"title": "H2 Two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "H2 <b>Three</b>",
|
||||||
|
"href": "fizz/buzz#h2-three",
|
||||||
|
"level": "h2",
|
||||||
|
"title": "H2 Three"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "H3 3a",
|
||||||
|
"href": "fizz/buzz#h3-3a",
|
||||||
|
"level": "h3",
|
||||||
|
"title": "H3 3a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "H3 3b",
|
||||||
|
"href": "fizz/buzz#h3-3b",
|
||||||
|
"level": "h3",
|
||||||
|
"title": "H3 3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "<i>H2 <b>four</b></i>",
|
||||||
|
"href": "fizz/buzz#h2-four",
|
||||||
|
"level": "h2",
|
||||||
|
"title": "H2 4"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
42
aio/src/app/embedded/toc/toc.component.ts
Normal file
42
aio/src/app/embedded/toc/toc.component.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Component, ElementRef, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-toc',
|
||||||
|
templateUrl: 'toc.component.html',
|
||||||
|
styles: []
|
||||||
|
})
|
||||||
|
export class TocComponent implements OnInit {
|
||||||
|
|
||||||
|
hasSecondary = false;
|
||||||
|
hasToc = true;
|
||||||
|
isClosed = true;
|
||||||
|
isEmbedded = false;
|
||||||
|
private primaryMax = 4;
|
||||||
|
tocList: TocItem[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
private tocService: TocService) {
|
||||||
|
const hostElement = this.elementRef.nativeElement;
|
||||||
|
this.isEmbedded = hostElement.className.indexOf('embedded') !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const tocList = this.tocList = this.tocService.tocList;
|
||||||
|
const count = tocList.length;
|
||||||
|
this.hasToc = count > 0;
|
||||||
|
if (this.isEmbedded && this.hasToc) {
|
||||||
|
// If TOC is embedded in doc, mark secondary (sometimes hidden) items
|
||||||
|
this.hasSecondary = tocList.length > this.primaryMax;
|
||||||
|
for (let i = this.primaryMax; i < count; i++) {
|
||||||
|
tocList[i].isSecondary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isClosed = !this.isClosed;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { ComponentFactoryResolver, ElementRef, Injector, NgModule, OnInit, ViewChild, Component, DebugElement } from '@angular/core';
|
import {
|
||||||
|
Component, ComponentFactoryResolver, DebugElement,
|
||||||
|
ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { DocViewerComponent } from './doc-viewer.component';
|
import { DocViewerComponent } from './doc-viewer.component';
|
||||||
import { DocumentContents } from 'app/documents/document.service';
|
import { DocumentContents } from 'app/documents/document.service';
|
||||||
import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module';
|
import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
/// Embedded Test Components ///
|
/// Embedded Test Components ///
|
||||||
|
|
||||||
@ -86,6 +89,17 @@ class TestComponent {
|
|||||||
@ViewChild(DocViewerComponent) docViewer: DocViewerComponent;
|
@ViewChild(DocViewerComponent) docViewer: DocViewerComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//// Test Services ////
|
||||||
|
|
||||||
|
class TestTitleService {
|
||||||
|
setTitle = jasmine.createSpy('reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestTocService {
|
||||||
|
reset = jasmine.createSpy('reset');
|
||||||
|
genToc = jasmine.createSpy('genToc');
|
||||||
|
}
|
||||||
|
|
||||||
//////// Tests //////////////
|
//////// Tests //////////////
|
||||||
|
|
||||||
describe('DocViewerComponent', () => {
|
describe('DocViewerComponent', () => {
|
||||||
@ -94,6 +108,10 @@ describe('DocViewerComponent', () => {
|
|||||||
let docViewerEl: HTMLElement;
|
let docViewerEl: HTMLElement;
|
||||||
let fixture: ComponentFixture<TestComponent>;
|
let fixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
function setCurrentDoc(contents = '', id = 'fizz/buzz') {
|
||||||
|
component.currentDoc = { contents, id };
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ TestModule ],
|
imports: [ TestModule ],
|
||||||
@ -103,7 +121,9 @@ describe('DocViewerComponent', () => {
|
|||||||
embeddedTestComponents
|
embeddedTestComponents
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: EmbeddedComponents, useValue: {components: embeddedTestComponents}}
|
{ provide: EmbeddedComponents, useValue: {components: embeddedTestComponents} },
|
||||||
|
{ provide: Title, useClass: TestTitleService },
|
||||||
|
{ provide: TocService, useClass: TestTocService }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
@ -122,23 +142,23 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(('should display nothing when set currentDoc has no content'), () => {
|
it(('should display nothing when set currentDoc has no content'), () => {
|
||||||
component.currentDoc = { title: 'fake title', contents: '', id: 'a/b' };
|
setCurrentDoc();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(docViewerEl.innerHTML).toBe('');
|
expect(docViewerEl.innerHTML).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(('should display simple static content doc'), () => {
|
it(('should display simple static content doc'), () => {
|
||||||
const contents = '<p>Howdy, doc viewer</p>';
|
const contents = '<p>Howdy, doc viewer</p>';
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(docViewerEl.innerHTML).toEqual(contents);
|
expect(docViewerEl.innerHTML).toEqual(contents);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(('should display nothing after reset static content doc'), () => {
|
it(('should display nothing after reset static content doc'), () => {
|
||||||
const contents = '<p>Howdy, doc viewer</p>';
|
const contents = '<p>Howdy, doc viewer</p>';
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.currentDoc = { title: 'fake title', contents: '', id: 'a/c' };
|
component.currentDoc = { contents: '', id: 'a/c' };
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(docViewerEl.innerHTML).toEqual('');
|
expect(docViewerEl.innerHTML).toEqual('');
|
||||||
});
|
});
|
||||||
@ -149,7 +169,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Below Foo</p>
|
<p>Below Foo</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
||||||
expect(fooHtml).toContain('Foo Component');
|
expect(fooHtml).toContain('Foo Component');
|
||||||
@ -165,7 +185,7 @@ describe('DocViewerComponent', () => {
|
|||||||
</div>
|
</div>
|
||||||
<p>Below Foo</p>
|
<p>Below Foo</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||||
expect(foos.length).toBe(2);
|
expect(foos.length).toBe(2);
|
||||||
@ -177,7 +197,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<aio-bar></aio-bar>
|
<aio-bar></aio-bar>
|
||||||
<p>Below Bar</p>
|
<p>Below Bar</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||||
expect(barHtml).toContain('Bar Component');
|
expect(barHtml).toContain('Bar Component');
|
||||||
@ -189,7 +209,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<aio-bar>###bar content###</aio-bar>
|
<aio-bar>###bar content###</aio-bar>
|
||||||
<p>Below Bar</p>
|
<p>Below Bar</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
|
|
||||||
// necessary to trigger projection within ngOnInit
|
// necessary to trigger projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -207,7 +227,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -230,7 +250,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<p><aio-foo></aio-foo><p>
|
<p><aio-foo></aio-foo><p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -254,7 +274,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -282,7 +302,7 @@ describe('DocViewerComponent', () => {
|
|||||||
<p><aio-baz>---More baz--</aio-baz></p>
|
<p><aio-baz>---More baz--</aio-baz></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.currentDoc = { title: 'fake title', contents, id: 'a/b' };
|
setCurrentDoc(contents);
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -298,4 +318,86 @@ describe('DocViewerComponent', () => {
|
|||||||
'expected 2nd Baz template content');
|
'expected 2nd Baz template content');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Title', () => {
|
||||||
|
let titleService: TestTitleService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
titleService = TestBed.get(Title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the default empty title when no <h1>', () => {
|
||||||
|
setCurrentDoc('Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the expected title when has <h1>', () => {
|
||||||
|
setCurrentDoc('<h1>Features</h1>Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Features');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the expected title with a no-toc <h1>', () => {
|
||||||
|
setCurrentDoc('<h1 class="no-toc">Features</h1>Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Features');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TOC', () => {
|
||||||
|
let tocService: TestTocService;
|
||||||
|
|
||||||
|
function getAioToc(): HTMLElement {
|
||||||
|
return fixture.debugElement.nativeElement.querySelector('aio-toc');
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tocService = TestBed.get(TocService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if no <h1> title', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setCurrentDoc('Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have an <aio-toc>', () => {
|
||||||
|
expect(getAioToc()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset Toc Service', () => {
|
||||||
|
expect(tocService.reset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call Toc Service genToc()', () => {
|
||||||
|
expect(tocService.genToc).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have an <aio-toc> with a no-toc <h1>', () => {
|
||||||
|
setCurrentDoc('<h1 class="no-toc">Features</h1>Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getAioToc()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when has an <h1> (title)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setCurrentDoc('<h1>Features</h1>Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add <aio-toc>', () => {
|
||||||
|
expect(getAioToc()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have <aio-toc> with "embedded" class', () => {
|
||||||
|
expect(getAioToc().classList.contains('embedded')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Toc Service genToc()', () => {
|
||||||
|
expect(tocService.genToc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
|
|
||||||
import { EmbeddedComponents } from 'app/embedded/embedded.module';
|
import { EmbeddedComponents } from 'app/embedded/embedded.module';
|
||||||
import { DocumentContents } from 'app/documents/document.service';
|
import { DocumentContents } from 'app/documents/document.service';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
interface EmbeddedComponentFactory {
|
interface EmbeddedComponentFactory {
|
||||||
contentPropertyName: string;
|
contentPropertyName: string;
|
||||||
@ -18,13 +20,7 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-doc-viewer',
|
selector: 'aio-doc-viewer',
|
||||||
template: '',
|
template: ''
|
||||||
styles: [ `
|
|
||||||
:host >>> doc-title.not-found h1 {
|
|
||||||
color: white;
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
`]
|
|
||||||
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
||||||
// encapsulation: ViewEncapsulation.Native
|
// encapsulation: ViewEncapsulation.Native
|
||||||
})
|
})
|
||||||
@ -41,7 +37,9 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
componentFactoryResolver: ComponentFactoryResolver,
|
componentFactoryResolver: ComponentFactoryResolver,
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
embeddedComponents: EmbeddedComponents,
|
embeddedComponents: EmbeddedComponents,
|
||||||
private injector: Injector
|
private injector: Injector,
|
||||||
|
private titleService: Title,
|
||||||
|
private tocService: TocService
|
||||||
) {
|
) {
|
||||||
this.hostElement = elementRef.nativeElement;
|
this.hostElement = elementRef.nativeElement;
|
||||||
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
||||||
@ -77,6 +75,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
|
|
||||||
if (!doc.contents) { return; }
|
if (!doc.contents) { return; }
|
||||||
|
|
||||||
|
this.addTitleAndToc(doc.id);
|
||||||
|
|
||||||
// TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators?
|
// TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators?
|
||||||
this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => {
|
this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => {
|
||||||
const embeddedComponentElements = this.hostElement.querySelectorAll(selector);
|
const embeddedComponentElements = this.hostElement.querySelectorAll(selector);
|
||||||
@ -92,8 +92,27 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addTitleAndToc(docId: string) {
|
||||||
|
this.tocService.reset();
|
||||||
|
let title = '';
|
||||||
|
const titleEl = this.hostElement.querySelector('h1');
|
||||||
|
// Only create TOC for docs with an <h1> title
|
||||||
|
// If you don't want a TOC, don't have an <h1>
|
||||||
|
if (titleEl) {
|
||||||
|
title = titleEl.innerText.trim();
|
||||||
|
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
|
||||||
|
this.tocService.genToc(this.hostElement, docId);
|
||||||
|
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
||||||
|
}
|
||||||
|
|
||||||
ngDoCheck() {
|
ngDoCheck() {
|
||||||
if (this.displayedDoc) { this.displayedDoc.detectChanges(); }
|
// TODO: make sure this isn't called too often on the same doc
|
||||||
|
if (this.displayedDoc) {
|
||||||
|
this.displayedDoc.detectChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
227
aio/src/app/shared/toc.service.spec.ts
Normal file
227
aio/src/app/shared/toc.service.spec.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { ReflectiveInjector, SecurityContext } from '@angular/core';
|
||||||
|
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TocItem, TocService } from './toc.service';
|
||||||
|
|
||||||
|
describe('TocService', () => {
|
||||||
|
let injector: ReflectiveInjector;
|
||||||
|
let tocService: TocService;
|
||||||
|
|
||||||
|
// call TocService.genToc
|
||||||
|
function callGenToc(html = '', docId = 'fizz/buzz'): HTMLDivElement {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = html;
|
||||||
|
tocService.genToc(el, docId);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
injector = ReflectiveInjector.resolveAndCreate([
|
||||||
|
{ provide: DomSanitizer, useClass: TestDomSanitizer },
|
||||||
|
{ provide: DOCUMENT, useValue: document },
|
||||||
|
TocService,
|
||||||
|
]);
|
||||||
|
tocService = injector.get(TocService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be creatable', () => {
|
||||||
|
expect(tocService).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should clear tocList', () => {
|
||||||
|
// Start w/ dummy data from previous usage
|
||||||
|
beforeEach(() => tocService.tocList = [{}, {}] as TocItem[]);
|
||||||
|
|
||||||
|
it('when reset()', () => {
|
||||||
|
tocService.reset();
|
||||||
|
expect(tocService.tocList.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when given undefined doc element', () => {
|
||||||
|
tocService.genToc(undefined);
|
||||||
|
expect(tocService.tocList.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when given doc element w/ no headings', () => {
|
||||||
|
callGenToc('<p>This</p><p>and</p><p>that</p>');
|
||||||
|
expect(tocService.tocList.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when given doc element w/ headings other than h2 & h3', () => {
|
||||||
|
callGenToc('<h1>This</h1><h4>and</h4><h5>that</h5>');
|
||||||
|
expect(tocService.tocList.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when given doc element w/ no-toc headings', () => {
|
||||||
|
// tolerates different spellings/casing of the no-toc class
|
||||||
|
callGenToc(`
|
||||||
|
<h2 class="no-toc">one</h2><p>some one</p>
|
||||||
|
<h2 class="notoc">two</h2><p>some two</p>
|
||||||
|
<h2 class="no-Toc">three</h2><p>some three</p>
|
||||||
|
<h2 class="noToc">four</h2><p>some four</p>
|
||||||
|
`);
|
||||||
|
expect(tocService.tocList.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given many headings', () => {
|
||||||
|
let docId: string;
|
||||||
|
let docEl: HTMLDivElement;
|
||||||
|
let tocList: TocItem[];
|
||||||
|
let headings: NodeListOf<HTMLHeadingElement>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
docId = 'fizz/buzz';
|
||||||
|
|
||||||
|
docEl = callGenToc(`
|
||||||
|
<h1>Fun with TOC</h1>
|
||||||
|
|
||||||
|
<h2 id="heading-one-special-id">Heading one</h2>
|
||||||
|
<p>h2 toc 0</p>
|
||||||
|
|
||||||
|
<h2>H2 Two</h2>
|
||||||
|
<p>h2 toc 1</p>
|
||||||
|
|
||||||
|
<h2>H2 <b>Three</b></h2>
|
||||||
|
<p>h2 toc 2</p>
|
||||||
|
<h3 id="h3-3a">H3 3a</h3> <p>h3 toc 3</p>
|
||||||
|
<h3 id="h3-3b">H3 3b</h3> <p>h3 toc 4</p>
|
||||||
|
|
||||||
|
<!-- h4 shouldn't be in TOC -->
|
||||||
|
<h4 id="h4-3b">H4 of h3-3b</h4> <p>an h4</p>
|
||||||
|
|
||||||
|
<h2><i>H2 4 <b>repeat</b></i></h2>
|
||||||
|
<p>h2 toc 5</p>
|
||||||
|
|
||||||
|
<h2><b>H2 4 <i>repeat</i></b></h2>
|
||||||
|
<p>h2 toc 6</p>
|
||||||
|
|
||||||
|
<h2 class="no-toc" id="skippy">Skippy</h2>
|
||||||
|
<p>Skip this header</p>
|
||||||
|
|
||||||
|
<h2 id="h2-6">H2 6</h2>
|
||||||
|
<p>h2 toc 7</p>
|
||||||
|
<h3 id="h3-6a">H3 6a</h3> <p>h3 toc 8</p>
|
||||||
|
`, docId);
|
||||||
|
|
||||||
|
tocList = tocService.tocList;
|
||||||
|
headings = docEl.querySelectorAll('h1,h2,h3,h4') as NodeListOf<HTMLHeadingElement>;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have tocList with expect number of TocItems', () => {
|
||||||
|
// should ignore h1, h4, and the no-toc h2
|
||||||
|
expect(tocList.length).toEqual(headings.length - 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have href with docId and heading\'s id', () => {
|
||||||
|
const tocItem = tocList[0];
|
||||||
|
expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have level "h2" for an <h2>', () => {
|
||||||
|
const tocItem = tocList[0];
|
||||||
|
expect(tocItem.level).toEqual('h2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have level "h3" for an <h3>', () => {
|
||||||
|
const tocItem = tocList[3];
|
||||||
|
expect(tocItem.level).toEqual('h3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have title which is heading\'s innerText ', () => {
|
||||||
|
const heading = headings[3];
|
||||||
|
const tocItem = tocList[2];
|
||||||
|
expect(heading.innerText).toEqual(tocItem.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
|
||||||
|
const heading = headings[3];
|
||||||
|
const content = tocList[2].content;
|
||||||
|
expect((<TestSafeHtml>content).changingThisBreaksApplicationSecurity)
|
||||||
|
.toEqual(heading.innerHTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate and set id of heading without an id', () => {
|
||||||
|
const id = headings[2].getAttribute('id');
|
||||||
|
expect(id).toEqual('h2-two');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have href with docId and calculated heading id', () => {
|
||||||
|
const tocItem = tocList[1];
|
||||||
|
expect(tocItem.href).toEqual(`${docId}#h2-two`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore HTML in heading when calculating id', () => {
|
||||||
|
const id = headings[3].getAttribute('id');
|
||||||
|
const tocItem = tocList[2];
|
||||||
|
expect(id).toEqual('h2-three', 'heading id');
|
||||||
|
expect(tocItem.href).toEqual(`${docId}#h2-three`, 'tocItem href');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should avoid repeating an id when calculating', () => {
|
||||||
|
const tocItem4a = tocList[5];
|
||||||
|
const tocItem4b = tocList[6];
|
||||||
|
expect(tocItem4a.href).toEqual(`${docId}#h2-4-repeat`, 'first');
|
||||||
|
expect(tocItem4b.href).toEqual(`${docId}#h2-4-repeat-2`, 'second');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TocItem for an h2 with anchor link and extra whitespace', () => {
|
||||||
|
let docId: string;
|
||||||
|
let docEl: HTMLDivElement;
|
||||||
|
let tocItem: TocItem;
|
||||||
|
let expectedTocContent: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
docId = 'fizz/buzz/';
|
||||||
|
expectedTocContent = 'Setup to develop <i>locally</i>.';
|
||||||
|
|
||||||
|
// An almost-actual <h2> ... with extra whitespace
|
||||||
|
docEl = callGenToc(`
|
||||||
|
<h2 id="setup-to-develop-locally">
|
||||||
|
<a href="tutorial/toh-pt1#setup-to-develop-locally" aria-hidden="true">
|
||||||
|
<span class="icon icon-link"></span>
|
||||||
|
</a>
|
||||||
|
${expectedTocContent}
|
||||||
|
</h2>
|
||||||
|
`, docId);
|
||||||
|
|
||||||
|
tocItem = tocService.tocList[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have expected href', () => {
|
||||||
|
expect(tocItem.href).toEqual(`${docId}#setup-to-develop-locally`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have expected title', () => {
|
||||||
|
expect(tocItem.title).toEqual('Setup to develop locally.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have removed anchor link from tocItem html content', () => {
|
||||||
|
expect((<TestSafeHtml>tocItem.content)
|
||||||
|
.changingThisBreaksApplicationSecurity)
|
||||||
|
.toEqual('Setup to develop <i>locally</i>.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
|
||||||
|
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
|
||||||
|
expect(domSanitizer.bypassSecurityTrustHtml)
|
||||||
|
.toHaveBeenCalledWith(expectedTocContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TestSafeHtml extends SafeHtml {
|
||||||
|
changingThisBreaksApplicationSecurity: string;
|
||||||
|
getTypeName: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDomSanitizer {
|
||||||
|
bypassSecurityTrustHtml = jasmine.createSpy('bypassSecurityTrustHtml')
|
||||||
|
.and.callFake(html => {
|
||||||
|
return {
|
||||||
|
changingThisBreaksApplicationSecurity: html,
|
||||||
|
getTypeName: () => 'HTML',
|
||||||
|
} as TestSafeHtml;
|
||||||
|
});
|
||||||
|
}
|
81
aio/src/app/shared/toc.service.ts
Normal file
81
aio/src/app/shared/toc.service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||||
|
|
||||||
|
import { DocumentContents } from 'app/documents/document.service';
|
||||||
|
|
||||||
|
export interface TocItem {
|
||||||
|
content: SafeHtml;
|
||||||
|
href: string;
|
||||||
|
isSecondary?: boolean;
|
||||||
|
level: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TocService {
|
||||||
|
tocList: TocItem[];
|
||||||
|
|
||||||
|
constructor(@Inject(DOCUMENT) private document: any, private domSanitizer: DomSanitizer) { }
|
||||||
|
|
||||||
|
genToc(docElement: Element, docId = '') {
|
||||||
|
const tocList = this.tocList = [];
|
||||||
|
if (!docElement) { return; }
|
||||||
|
|
||||||
|
const headings = docElement.querySelectorAll('h2,h3');
|
||||||
|
const idMap = new Map<string, number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < headings.length; i++) {
|
||||||
|
const heading = headings[i] as HTMLHeadingElement;
|
||||||
|
// skip if heading class is 'no-toc'
|
||||||
|
if (/(no-toc|notoc)/i.test(heading.className)) { continue; }
|
||||||
|
|
||||||
|
const id = this.getId(heading, idMap);
|
||||||
|
const toc: TocItem = {
|
||||||
|
content: this.extractHeadingSafeHtml(heading),
|
||||||
|
href: `${docId}#${id}`,
|
||||||
|
level: heading.tagName.toLowerCase(),
|
||||||
|
title: heading.innerText.trim(),
|
||||||
|
};
|
||||||
|
tocList.push(toc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.tocList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// This bad boy exists only to strip off the anchor link attached to a heading
|
||||||
|
private extractHeadingSafeHtml(heading: HTMLHeadingElement) {
|
||||||
|
const a = this.document.createElement('a') as HTMLAnchorElement;
|
||||||
|
a.innerHTML = heading.innerHTML;
|
||||||
|
const anchorLink = a.querySelector('a');
|
||||||
|
if (anchorLink) {
|
||||||
|
a.removeChild(anchorLink);
|
||||||
|
}
|
||||||
|
// security: the document element which provides this heading content
|
||||||
|
// is always authored by the documentation team and is considered to be safe
|
||||||
|
return this.domSanitizer.bypassSecurityTrustHtml(a.innerHTML.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the id from the heading; create one if necessary
|
||||||
|
// Is it possible for a heading to lack an id?
|
||||||
|
private getId(h: HTMLHeadingElement, idMap: Map<string, number>) {
|
||||||
|
let id = h.id;
|
||||||
|
if (id) {
|
||||||
|
addToMap(id);
|
||||||
|
} else {
|
||||||
|
id = h.innerText.toLowerCase().replace(/\W+/g, '-');
|
||||||
|
id = addToMap(id);
|
||||||
|
h.id = id;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
|
||||||
|
// Map guards against duplicate id creation.
|
||||||
|
function addToMap(key: string) {
|
||||||
|
const count = idMap[key] = idMap[key] ? idMap[key] + 1 : 1;
|
||||||
|
return count === 1 ? key : `${key}-${count}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,4 +25,5 @@
|
|||||||
@import 'resources';
|
@import 'resources';
|
||||||
@import 'edit-page-cta';
|
@import 'edit-page-cta';
|
||||||
@import 'heading-anchors';
|
@import 'heading-anchors';
|
||||||
@import 'api-info-bar';
|
@import 'api-info-bar';
|
||||||
|
@import 'toc';
|
||||||
|
103
aio/src/styles/2-modules/_toc.scss
Normal file
103
aio/src/styles/2-modules/_toc.scss
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
aio-toc > div {
|
||||||
|
font-size: 13px;
|
||||||
|
border-left: 10px solid #4285f4;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding: 4px 0 0 10px;
|
||||||
|
|
||||||
|
.toc-heading {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-heading.secondary {
|
||||||
|
padding-bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
top: -8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $accentblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-show-all,
|
||||||
|
button.toc-more-items {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: $mediumgray;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $accentblue;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-show-all {
|
||||||
|
min-width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-show-all::after {
|
||||||
|
content: 'expand_less';
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-show-all.closed::after {
|
||||||
|
content: 'expand_more';
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-more-items {
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-more-items::after {
|
||||||
|
content: 'expand_less';
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toc-more-items.closed::after {
|
||||||
|
content: 'more_horiz';
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.toc-list {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.toc-list li {
|
||||||
|
line-height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $mediumgray;
|
||||||
|
display:inline-block;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $accentblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.toc-list li.h3 {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aio-toc > div.closed li.secondary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
aio-toc.embedded:not(:empty) {
|
||||||
|
display: block;
|
||||||
|
margin: 20px 0 24px;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user