feat(aio): add API list page (#14899)
This commit is contained in:
parent
174d4c8ef7
commit
5ad5301a3e
|
@ -0,0 +1 @@
|
|||
<aio-api-list></aio-api-list>
|
|
@ -16,6 +16,7 @@ import { Platform } from '@angular/material/core';
|
|||
import 'rxjs/add/operator/first';
|
||||
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { ApiService } from 'app/embedded/api/api.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { embeddedComponents, EmbeddedComponents } from 'app/embedded';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
@ -48,6 +49,7 @@ import { LinkDirective } from 'app/shared/link.directive';
|
|||
LinkDirective,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
EmbeddedComponents,
|
||||
Logger,
|
||||
Location,
|
||||
|
|
|
@ -57,7 +57,8 @@ export class DocumentService {
|
|||
return subject.asObservable();
|
||||
}
|
||||
|
||||
private computePath(url) {
|
||||
private computePath(url: string) {
|
||||
url = url.match(/[^#?]*/)[0]; // strip off fragment and query
|
||||
url = '/' + url;
|
||||
url = url.endsWith('/') ? url + 'index' : url;
|
||||
return 'content/docs' + url + '.json';
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<div class="l-flex-wrap banner is-plain api-filter">
|
||||
|
||||
<div class="form-select-menu">
|
||||
<button class="form-select-button has-symbol" (click)="toggleTypeMenu()">
|
||||
<strong>Type:</strong><span class="symbol {{type.name}}"></span>{{type.title}}
|
||||
</button>
|
||||
<ul class="form-select-dropdown" *ngIf="showTypeMenu">
|
||||
<li *ngFor="let t of types" (click)="setType(t)" [class.selected]="t === type">
|
||||
<span class="symbol {{t.name}}"></span>{{t.title}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-select-menu">
|
||||
<button class="form-select-button" (click)="toggleStatusMenu()">
|
||||
<strong>Status:</strong>{{status.title}}
|
||||
</button>
|
||||
<ul class="form-select-dropdown" *ngIf="showStatusMenu">
|
||||
<li *ngFor="let s of statuses" (click)="setStatus(s)" [class.selected]="s === status">
|
||||
{{s.title}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-search">
|
||||
<i class="material-icons">search</i>
|
||||
<input #filter placeholder="Filter" (input)="setQuery($event.target.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="l-content-small docs-content">
|
||||
<div *ngFor="let section of filteredSections | async" >
|
||||
<h2>{{section.title}}</h2>
|
||||
<ul class="api-list">
|
||||
<ng-container *ngFor="let item of section.items">
|
||||
<li *ngIf="item.show" class="api-item">
|
||||
<a [href]="item.path"><span class="symbol {{item.docType}}"></span> {{item.title}}</a>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
|
@ -0,0 +1,512 @@
|
|||
/* HACKED FROM aio v.1 */
|
||||
|
||||
// TODO:refactor out those parts that should be shared SASS files and import them here
|
||||
|
||||
/*
|
||||
* Metrics
|
||||
*
|
||||
* Metrics based on material design 8pt unit
|
||||
*/
|
||||
|
||||
$unit: 8px;
|
||||
$phone-breakpoint: 480px;
|
||||
$tablet-breakpoint: 800px;
|
||||
|
||||
/*
|
||||
* Layer Stacking
|
||||
*
|
||||
* The approved range that can be used for layering (z-indexes)
|
||||
*/
|
||||
|
||||
$layer-1: 1;
|
||||
$layer-2: 2;
|
||||
$layer-3: 3;
|
||||
$layer-4: 4;
|
||||
$layer-5: 5;
|
||||
|
||||
/*
|
||||
* Colors from original _colors.scss
|
||||
*/
|
||||
$white: #FFFFFF;
|
||||
$black: #000000;
|
||||
|
||||
$amber-700: #FFA000;
|
||||
$blue-400: #42A5F5;
|
||||
$blue-500: #2196F3;
|
||||
$blue-600: #1E88E5;
|
||||
$blue-800: #1565C0;
|
||||
$blue-grey-50: #ECEFF1;
|
||||
$blue-grey-100: #CFD8DC;
|
||||
$blue-grey-500: #607D8B;
|
||||
$blue-grey-600: #546E7A;
|
||||
$green-500: #4CAF50;
|
||||
$green-800: #2E7D32;
|
||||
$light-green-600: #7CB342;
|
||||
$pink-600: #D81B60;
|
||||
$purple-600: #8E24AA;
|
||||
$teal-500: #009688;
|
||||
|
||||
$lightgrey: #F5F6F7;
|
||||
|
||||
|
||||
/*
|
||||
* Layout
|
||||
*/
|
||||
.docs-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.l-content-small {
|
||||
padding: $unit * 6;
|
||||
max-width: 1100px;
|
||||
margin: 0;
|
||||
|
||||
@media handheld and (max-width: $phone-breakpoint),
|
||||
screen and (max-device-width: $phone-breakpoint),
|
||||
screen and (max-width: $tablet-breakpoint) {
|
||||
padding: 0;
|
||||
padding-top: ($unit * 3);
|
||||
}
|
||||
}
|
||||
|
||||
.l-flex-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/*
|
||||
* Banner
|
||||
*/
|
||||
.banner {
|
||||
background: rgba($blue-grey-50, .24);
|
||||
border-bottom: 1px solid $blue-grey-50;
|
||||
box-sizing: border-box;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
padding: ($unit * 4) ($unit * 6);
|
||||
min-height: 97px;
|
||||
|
||||
include respond-to('mobile') {
|
||||
padding: ($unit * 2);
|
||||
}
|
||||
|
||||
&.is-plain {
|
||||
background: $white;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
p, .text-body {
|
||||
color: $blue-grey-500;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* SEARCH BAR
|
||||
*/
|
||||
|
||||
.form-search {
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $white;
|
||||
color: $blue-600;
|
||||
font-size: 16px;
|
||||
height: $unit * 4;
|
||||
line-height: $unit * 4;
|
||||
outline: none;
|
||||
padding: 0 ($unit *2) 0 ($unit * 4);
|
||||
transition: all .2s;
|
||||
|
||||
// PLACEHOLDER TEXT
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $blue-grey-100;
|
||||
font-size: 14px;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $blue-grey-100;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $blue-grey-100;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $blue-grey-100;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $blue-400;
|
||||
box-shadow: 0 2px 2px rgba($blue-400, 0.24), 0 0 2px rgba($blue-400, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
color: $blue-grey-100;
|
||||
font-size: 20px;
|
||||
left: $unit;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
z-index: $layer-1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Select Menu
|
||||
*/
|
||||
|
||||
$form-select-width: 200px;
|
||||
|
||||
.form-select-menu {
|
||||
position: relative;
|
||||
width: $form-select-width;
|
||||
}
|
||||
|
||||
.form-select-button {
|
||||
background: $white;
|
||||
box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $white;
|
||||
color: $blue-grey-600;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
height: $unit * 4;
|
||||
line-height: $unit * 4;
|
||||
outline: none;
|
||||
padding: 0 ($unit * 2);
|
||||
text-align: left;
|
||||
width: $form-select-width - ($unit * 2);
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
margin-right: $unit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.has-symbol {
|
||||
.symbol {
|
||||
margin-right: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-select-dropdown {
|
||||
background: $white;
|
||||
box-shadow: 0 16px 16px rgba($black, 0.24), 0 0 16px rgba($black, 0.12);
|
||||
border-radius: 4px;
|
||||
left: -$unit;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: $unit 0;
|
||||
position: absolute;
|
||||
top: -$unit;
|
||||
width: 200px;
|
||||
z-index: $layer-2;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: $unit * 4;
|
||||
margin: 0;
|
||||
padding: 0 ($unit * 2) 0 ($unit * 5);
|
||||
position: relative;
|
||||
transition: all .2s;
|
||||
|
||||
&:hover {
|
||||
background: $blue-grey-50;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $blue-grey-100;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
left: $unit * 2;
|
||||
position: absolute;
|
||||
top: $unit;
|
||||
z-index: $layer-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* API Symbols
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Variables
|
||||
*/
|
||||
|
||||
|
||||
$api-symbols: (
|
||||
all: (
|
||||
content: ' ',
|
||||
background: $white
|
||||
),
|
||||
decorator: (
|
||||
content: '@',
|
||||
background: $blue-800
|
||||
),
|
||||
directive: (
|
||||
content: 'D',
|
||||
background: $pink-600
|
||||
),
|
||||
pipe: (
|
||||
content: 'P',
|
||||
background: $blue-grey-600
|
||||
),
|
||||
class: (
|
||||
content: 'C',
|
||||
background: $blue-500
|
||||
),
|
||||
interface: (
|
||||
content: 'I',
|
||||
background: $teal-500
|
||||
),
|
||||
function: (
|
||||
content: 'F',
|
||||
background: $green-500
|
||||
),
|
||||
enum: (
|
||||
content: 'E',
|
||||
background: $amber-700
|
||||
),
|
||||
const: (
|
||||
content: 'K',
|
||||
background: $purple-600
|
||||
),
|
||||
type-alias: (
|
||||
content: 'T',
|
||||
background: $light-green-600
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Symbol Class
|
||||
*/
|
||||
|
||||
.symbol {
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 2px rgba($black, .24);
|
||||
color: $white;
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: $unit * 2;
|
||||
text-align: center;
|
||||
width: $unit * 2;
|
||||
|
||||
// SYMBOL TYPES
|
||||
@each $name, $symbol in $api-symbols {
|
||||
&.#{$name} {
|
||||
background: map-get($symbol, background);
|
||||
|
||||
&:before {
|
||||
content: map-get($symbol, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* API Home Page
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* API Filter Menu
|
||||
*
|
||||
*/
|
||||
|
||||
.api-filter {
|
||||
.form-select-menu {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.form-search {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* API Class List
|
||||
*
|
||||
*/
|
||||
|
||||
.docs-content .api-list {
|
||||
list-style: none;
|
||||
margin: 0 0 ($unit * 6) 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
margin: 0 0 ($unit * 2) 0;
|
||||
line-height: 14px;
|
||||
padding: 0;
|
||||
float: left;
|
||||
width: 33%;
|
||||
min-width: 220px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.symbol {
|
||||
margin-right: $unit;
|
||||
}
|
||||
a {
|
||||
color: $blue-grey-600;
|
||||
display: inline-block;
|
||||
line-height: $unit * 2;
|
||||
padding: 0 ($unit * 2) 0 0;
|
||||
text-decoration: none;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover {
|
||||
background: $blue-grey-50;
|
||||
color: $blue-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-content .h2-api-docs,
|
||||
.docs-content .h2-api-docs:first-of-type {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.code-links {
|
||||
a {
|
||||
code, .api-doc-code {
|
||||
color: #1E88E5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openParens {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.endParens {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
&.selector {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.location-badge {
|
||||
margin: 0 0 16px 16px !important;
|
||||
}
|
||||
|
||||
.api-doc-code {
|
||||
border-bottom: 0px;
|
||||
|
||||
:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-margin {
|
||||
margin-bottom: 36px;
|
||||
h2 {
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-margin {
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.hr-margin {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid $lightgrey;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-bg {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-bg-with-indent {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 16px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.code-background {
|
||||
padding: 0 5px 0;
|
||||
|
||||
span.pln {
|
||||
color: #1E88E5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.code-anchor {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
// Override highlight.js
|
||||
.kwd {
|
||||
color: #1E88E5 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.api-doc-code {
|
||||
font-size: 14px;
|
||||
color: #1a2326;
|
||||
|
||||
// the last .pln (white space) creates additional spacing between sections of the api doc. Remove it.
|
||||
&.no-pln {
|
||||
.pln:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.docs-content {
|
||||
// Overrides display flex from angular material.
|
||||
// This was added because Safari doesn't play nice with layout="column".
|
||||
// Look of API doc in Chrome and Firefox remains the same, and is fixed for Safari.
|
||||
.layout-xs-column {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.api-doc-code {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
p.location-badge {
|
||||
position: relative;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* API List & Filter Component
|
||||
*
|
||||
* A page that displays a formatted list of the public Angular API entities.
|
||||
* Clicking on a list item triggers navigation to the corresponding API entity document.
|
||||
* Can add/remove API entity links based on filter settings.
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||
|
||||
interface MenuItem {
|
||||
name: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
class SearchCriteria {
|
||||
query? = '';
|
||||
status? = 'all';
|
||||
type? = 'all';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-api-list',
|
||||
templateUrl: './api-list.component.html',
|
||||
styleUrls: ['./api-list.component.scss']
|
||||
})
|
||||
export class ApiListComponent implements OnInit {
|
||||
|
||||
filteredSections: Observable<ApiSection[]>;
|
||||
|
||||
showStatusMenu = false;
|
||||
showTypeMenu = false;
|
||||
|
||||
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
|
||||
private searchCriteria = new SearchCriteria();
|
||||
|
||||
status: MenuItem;
|
||||
type: MenuItem;
|
||||
|
||||
// API types
|
||||
types: MenuItem[] = [
|
||||
{ name: 'all', title: 'All' },
|
||||
{ name: 'directive', title: 'Directive' },
|
||||
{ name: 'pipe', title: 'Pipe'},
|
||||
{ name: 'decorator', title: 'Decorator' },
|
||||
{ name: 'class', title: 'Class' },
|
||||
{ name: 'interface', title: 'Interface' },
|
||||
{ name: 'function', title: 'Function' },
|
||||
{ name: 'enum', title: 'Enum' },
|
||||
{ name: 'type-alias', title: 'Type Alias' },
|
||||
{ name: 'const', title: 'Const'}
|
||||
];
|
||||
|
||||
statuses: MenuItem[] = [
|
||||
{ name: 'all', title: 'All' },
|
||||
{ name: 'stable', title: 'Stable' },
|
||||
{ name: 'deprecated', title: 'Deprecated' },
|
||||
{ name: 'experimental', title: 'Experimental' },
|
||||
{ name: 'security-risk', title: 'Security Risk' }
|
||||
];
|
||||
|
||||
@ViewChild('filter') queryEl: ElementRef;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private locationService: LocationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.filteredSections = combineLatest(
|
||||
this.apiService.sections,
|
||||
this.criteriaSubject,
|
||||
(sections, criteria) => {
|
||||
return sections.filter(section => this.filterSection(section, criteria));
|
||||
}
|
||||
);
|
||||
|
||||
this.initializeSearchCriteria();
|
||||
}
|
||||
|
||||
// Todo: may need to debounce as the original did
|
||||
// although there shouldn't be any perf consequences if we don't
|
||||
setQuery(query: string) {
|
||||
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
|
||||
}
|
||||
|
||||
setStatus(status: MenuItem) {
|
||||
this.toggleStatusMenu();
|
||||
this.status = status;
|
||||
this.setSearchCriteria({status: status.name});
|
||||
}
|
||||
|
||||
setType(type: MenuItem) {
|
||||
this.toggleTypeMenu();
|
||||
this.type = type;
|
||||
this.setSearchCriteria({type: type.name});
|
||||
}
|
||||
|
||||
toggleStatusMenu() {
|
||||
this.showStatusMenu = !this.showStatusMenu;
|
||||
}
|
||||
|
||||
toggleTypeMenu() {
|
||||
this.showTypeMenu = !this.showTypeMenu;
|
||||
}
|
||||
|
||||
//////// Private //////////
|
||||
|
||||
private filterSection(section: ApiSection, { query, status, type }: SearchCriteria) {
|
||||
let showSection = false;
|
||||
|
||||
section.items.forEach(item => {
|
||||
item.show = matchesType() && matchesStatus() && matchesQuery();
|
||||
|
||||
// show section if any of its items will be shown
|
||||
showSection = showSection || item.show;
|
||||
|
||||
function matchesQuery() {
|
||||
return !query ||
|
||||
section.name.indexOf(query) !== -1 ||
|
||||
item.name.indexOf(query) !== -1;
|
||||
}
|
||||
|
||||
function matchesStatus() {
|
||||
return status === 'all' ||
|
||||
status === item.stability ||
|
||||
(status === 'security-risk' && item.securityRisk);
|
||||
};
|
||||
|
||||
function matchesType() {
|
||||
return type === 'all' || type === item.docType;
|
||||
}
|
||||
});
|
||||
|
||||
return showSection;
|
||||
|
||||
}
|
||||
|
||||
// Get initial search criteria from URL search params
|
||||
private initializeSearchCriteria() {
|
||||
const {query, status, type} = this.locationService.search();
|
||||
|
||||
const q = (query || '').toLowerCase();
|
||||
// Hack: can't bind to query because input cursor always forced to end-of-line.
|
||||
this.queryEl.nativeElement.value = q;
|
||||
|
||||
this.status = this.statuses.find(x => x.name === status) || this.statuses[0];
|
||||
this.type = this.types.find(x => x.name === type) || this.types[0];
|
||||
|
||||
this.searchCriteria = {
|
||||
query: q,
|
||||
status: this.status.name,
|
||||
type: this.type.name
|
||||
};
|
||||
|
||||
this.criteriaSubject.next(this.searchCriteria);
|
||||
}
|
||||
|
||||
private setLocationSearch() {
|
||||
const {query, status, type} = this.searchCriteria;
|
||||
const params = {
|
||||
query: query ? query : undefined,
|
||||
status: status !== 'all' ? status : undefined,
|
||||
type: type !== 'all' ? type : undefined
|
||||
};
|
||||
|
||||
this.locationService.setSearch('API Search', params);
|
||||
}
|
||||
|
||||
private setSearchCriteria(criteria: SearchCriteria) {
|
||||
this.criteriaSubject.next(Object.assign(this.searchCriteria, criteria));
|
||||
this.setLocationSearch();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Http } from '@angular/http';
|
||||
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/takeUntil';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
export interface ApiItem {
|
||||
title: string;
|
||||
path: string;
|
||||
docType: string;
|
||||
stability: string;
|
||||
secure: string;
|
||||
securityRisk: boolean;
|
||||
barrel: string;
|
||||
|
||||
name?: string;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiSection {
|
||||
name: string;
|
||||
title: string;
|
||||
items: ApiItem[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiService implements OnDestroy {
|
||||
|
||||
private apiBase = 'content/docs/api/';
|
||||
private apiListJsonDefault = 'api-list.json';
|
||||
private firstTime = true;
|
||||
private onDestroy = new Subject();
|
||||
private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
|
||||
private _sections = this.sectionsSubject.takeUntil(this.onDestroy);
|
||||
|
||||
/**
|
||||
* Return a cached observable of API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
*/
|
||||
get sections() {
|
||||
|
||||
if (this.firstTime) {
|
||||
this.firstTime = false;
|
||||
this.fetchSections(); // TODO: get URL for fetchSections by configuration?
|
||||
|
||||
// makes sectionsSubject hot; subscribe ensures stays alive (always refCount > 0);
|
||||
this._sections.subscribe(sections => this.logger.log('ApiService got API sections') );
|
||||
}
|
||||
|
||||
return this._sections;
|
||||
};
|
||||
|
||||
constructor(private http: Http, private logger: Logger) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
* Updates `sections` observable
|
||||
*
|
||||
* @param {string} [src] - Name of the api list JSON file
|
||||
*/
|
||||
fetchSections(src?: string) {
|
||||
// TODO: get URL by configuration?
|
||||
const url = this.apiBase + (src || this.apiListJsonDefault);
|
||||
this.http.get(url)
|
||||
.takeUntil(this.onDestroy)
|
||||
.map(response => {
|
||||
const sections = response.json();
|
||||
return Object.keys(sections).map(title => {
|
||||
const items = sections[title] as ApiItem[];
|
||||
items.forEach(normalizeItem);
|
||||
return { name: title.toLowerCase(), title, items };
|
||||
});
|
||||
})
|
||||
.do(() => this.logger.log(`Got API sections from ${url}`))
|
||||
.subscribe(
|
||||
sections => this.sectionsSubject.next(sections),
|
||||
err => {
|
||||
// Todo: handle error
|
||||
this.logger.error(err);
|
||||
throw err; // rethrow for now.
|
||||
}
|
||||
);
|
||||
|
||||
function normalizeItem(item: ApiItem) {
|
||||
item.name = item.title.toLowerCase();
|
||||
// convert 'secure' property to boolean `securityRisk`
|
||||
item.securityRisk = item.secure !== 'false';
|
||||
// 'let' and 'var' doc types should be treated as 'const'
|
||||
const docType = item.docType;
|
||||
if (docType === 'let' || docType === 'var') {
|
||||
item.docType = 'const';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { ApiListComponent } from './api/api-list.component';
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
import { DocTitleComponent } from './doc-title.component';
|
||||
|
||||
/** Components that can be embedded in docs such as CodeExampleComponent, LiveExampleComponent,... */
|
||||
export const embeddedComponents: any[] = [
|
||||
CodeExampleComponent, DocTitleComponent
|
||||
ApiListComponent, CodeExampleComponent, DocTitleComponent
|
||||
];
|
||||
|
||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||
|
|
|
@ -119,4 +119,12 @@ describe('LocationService', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should ...', () => { });
|
||||
});
|
||||
|
||||
describe('setSearch', () => {
|
||||
it('should ...', () => { });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ export class LocationService {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO?: ignore if url-without-hash-or-search matches current location?
|
||||
go(url: string) {
|
||||
this.location.go(url);
|
||||
this.urlSubject.next(url);
|
||||
|
@ -28,4 +29,34 @@ export class LocationService {
|
|||
private stripLeadingSlashes(url: string) {
|
||||
return url.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
search(): { [index: string]: string; } {
|
||||
const search = {};
|
||||
const path = this.location.path();
|
||||
const q = path.indexOf('?');
|
||||
if (q > -1) {
|
||||
try {
|
||||
const params = path.substr(q + 1).split('&');
|
||||
params.forEach(p => {
|
||||
const pair = p.split('=');
|
||||
search[pair[0]] = decodeURIComponent(pair[1]);
|
||||
});
|
||||
} catch (e) { /* don't care */ }
|
||||
}
|
||||
return search;
|
||||
}
|
||||
|
||||
setSearch(label: string, params: {}) {
|
||||
if (!window || !window.history) { return; }
|
||||
|
||||
const search = Object.keys(params).reduce((acc, key) => {
|
||||
const value = params[key];
|
||||
// tslint:disable-next-line:triple-equals
|
||||
return value == undefined ? acc :
|
||||
acc += (acc ? '&' : '?') + `${key}=${encodeURIComponent(value)}`;
|
||||
}, '');
|
||||
|
||||
// this.location.replaceState doesn't let you set the history stack label
|
||||
window.history.replaceState({}, 'API Search', window.location.pathname + search);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue