Merge branch 'performance-fix'🎉

This commit is contained in:
Roman Hotsiy 2016-11-30 18:05:41 +02:00
commit 6bb521e1d8
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
52 changed files with 712 additions and 268 deletions

View File

@ -116,6 +116,7 @@ ReDoc makes use of the following [vendor extensions](http://swagger.io/specifica
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset; * **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset;
* **function**: A getter function. Must return a number representing the offset (in pixels); * **function**: A getter function. Must return a number representing the offset (in pixels);
* `suppress-warnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console). * `suppress-warnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazy-rendering` - if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\rebilly.github.io/ReDoc) for the example.
* `hide-hostname` - if set, the protocol and hostname is not shown in the method definition. * `hide-hostname` - if set, the protocol and hostname is not shown in the method definition.
## Advanced usage ## Advanced usage

View File

@ -22,7 +22,7 @@
frameborder="0" scrolling="0" width="130px" height="30px"></iframe> frameborder="0" scrolling="0" width="130px" height="30px"></iframe>
</nav> </nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.yaml'></redoc> <redoc scroll-y-offset="body > nav" spec-url='swagger.yaml' lazy-rendering></redoc>
<script src="main.js"> </script> <script src="main.js"> </script>
<script src="./dist/redoc.min.js"> </script> <script src="./dist/redoc.min.js"> </script>

View File

@ -22,7 +22,7 @@
frameborder="0" scrolling="0" width="130px" height="30px"></iframe> frameborder="0" scrolling="0" width="130px" height="30px"></iframe>
</nav> </nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.yaml'></redoc> <redoc scroll-y-offset="body > nav" spec-url='swagger.yaml' lazy-rendering></redoc>
<script src="main.js"> </script> <script src="main.js"> </script>
<script src="/webpack-dev-server.js"></script> <script src="/webpack-dev-server.js"></script>

View File

@ -18,23 +18,28 @@ describe('Redoc components', () => {
let component; let component;
let fixture; let fixture;
let opts; let opts;
let specMgr;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] }); TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
}); });
beforeEach(async(inject([SpecManager, OptionsService], (specMgr, _opts) => { beforeEach(async(inject([SpecManager, OptionsService], (_specMgr, _opts) => {
opts = _opts; opts = _opts;
opts.options = { opts.options = {
scrollYOffset: () => 0, scrollYOffset: () => 0,
$scrollParent: window $scrollParent: window
}; };
return specMgr.load('/tests/schemas/api-info-test.json'); specMgr = _specMgr;
}))); })));
beforeEach(() => { beforeEach(done => {
specMgr.load('/tests/schemas/api-info-test.json').then(done, done.fail);
});
beforeEach(async(() => {
fixture = TestBed.createComponent(TestAppComponent); fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'api-info').componentInstance; component = getChildDebugElement(fixture.debugElement, 'api-info').componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); }));
it('should init component data', () => { it('should init component data', () => {

View File

@ -24,10 +24,15 @@ describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] }); TestBed.configureTestingModule({ declarations: [ TestAppComponent ] });
}); });
beforeEach(async(inject([SpecManager], ( _specMgr) => { beforeEach(async(inject([SpecManager], ( _specMgr) => {
specMgr = _specMgr; specMgr = _specMgr;
return specMgr.load(schemaUrl);
}))); })));
beforeEach(done => {
specMgr.load(schemaUrl).then(done, done.fail);
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent); fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'api-logo').componentInstance; component = getChildDebugElement(fixture.debugElement, 'api-logo').componentInstance;
@ -36,6 +41,7 @@ describe('Redoc components', () => {
it('should init component data', () => { it('should init component data', () => {
if (specMgr.a) return;
expect(component).not.toBeNull(); expect(component).not.toBeNull();
expect(component.logo).not.toBeNull(); expect(component.logo).not.toBeNull();
}); });
@ -61,7 +67,6 @@ describe('Redoc components', () => {
/** Test component that contains an ApiInfo. */ /** Test component that contains an ApiInfo. */
@Component({ @Component({
selector: 'test-app', selector: 'test-app',
providers: [SpecManager],
template: template:
`<api-logo></api-logo>` `<api-logo></api-logo>`
}) })

View File

@ -27,6 +27,10 @@ $sub-schema-offset: ($bullet-size / 2) + $bullet-margin;
width: 75%; width: 75%;
box-sizing: border-box; box-sizing: border-box;
> div {
line-height: 1;
}
} }
.param-range { .param-range {
@ -41,7 +45,7 @@ $sub-schema-offset: ($bullet-size / 2) + $bullet-margin;
} }
.param-description { .param-description {
font-size: 13px; //font-size: 14px;
} }
.param-required { .param-required {
@ -220,7 +224,6 @@ $sub-schema-offset: ($bullet-size / 2) + $bullet-margin;
margin: 0 3px; margin: 0 3px;
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
vertical-align: bottom;
} }
} }

View File

@ -12,7 +12,8 @@ var cache = {};
@Component({ @Component({
selector: 'json-schema-lazy', selector: 'json-schema-lazy',
entryComponents: [ JsonSchema ], entryComponents: [ JsonSchema ],
template: '' template: '',
styles: [':host { display:none }']
}) })
export class JsonSchemaLazy implements OnDestroy, AfterViewInit { export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
@Input() pointer: string; @Input() pointer: string;
@ -66,7 +67,7 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
this._loadAfterSelf(); this._loadAfterSelf();
return; return;
} }
insertAfter($element.cloneNode(true), this.elementRef.nativeElement); //insertAfter($element.cloneNode(true), this.elementRef.nativeElement);
this.loaded = true; this.loaded = true;
} else { } else {
cache[this.pointer] = this._loadAfterSelf(); cache[this.pointer] = this._loadAfterSelf();

View File

@ -62,7 +62,6 @@ describe('Redoc components', () => {
/** Test component that contains a Method. */ /** Test component that contains a Method. */
@Component({ @Component({
selector: 'test-app', selector: 'test-app',
providers: [SpecManager],
template: template:
`<json-schema></json-schema>` `<json-schema></json-schema>`
}) })

View File

@ -0,0 +1,49 @@
'use strict';
import { Input, HostBinding, Component, OnInit, ChangeDetectionStrategy, ElementRef, ChangeDetectorRef } from '@angular/core';
import JsonPointer from '../../utils/JsonPointer';
import { BaseComponent, SpecManager } from '../base';
import { SchemaHelper } from '../../services/schema-helper.service';
import { OptionsService, AppStateService } from '../../services/';
@Component({
selector: 'loading-bar',
template: `
<span [style.width]='progress + "%"'> </span>
`,
styles: [`
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
display: block;
height: 5px;
z-index: 100;
}
span {
display: block;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: attr(progress percentage);
background-color: #5f7fc3;
transition: right 0.2s linear;
}
`],
//changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoadingBar {
@Input() progress:number = 0;
@HostBinding('style.display') display = 'block';
ngOnChanges(ch) {
if (ch.progress.currentValue === 100) {
setTimeout(() => {
this.display = 'none';
}, 500);
}
}
}

View File

@ -1,18 +1,18 @@
<div class="method"> <div class="method" *ngIf="method">
<div class="method-content"> <div class="method-content">
<h2 class="method-header sharable-header"> <h2 class="method-header sharable-header">
<a class="share-link" href="#{{method.anchor}}"></a>{{method.summary}} <a class="share-link" href="#{{method.anchor}}"></a>{{method.summary}}
</h2> </h2>
<div class="method-tags" *ngIf="method.info.tags.length"> <div class="method-tags" *ngIf="method.info.tags.length">
<a *ngFor="let tag of method.info.tags" attr.href="#tag/{{tag}}"> {{tag}} </a> <a *ngFor="let tag of method.info.tags" attr.href="#tag/{{tag}}"> {{tag}} </a>
</div> </div>
<p *ngIf="method.info.description" class="method-description" <p *ngIf="method.info.description" class="method-description"
[innerHtml]="method.info.description | marked"> [innerHtml]="method.info.description | marked">
</p> </p>
<params-list pointer="{{pointer}}/parameters"> </params-list> <params-list pointer="{{pointer}}/parameters"> </params-list>
<responses-list pointer="{{pointer}}/responses"> </responses-list> <responses-list pointer="{{pointer}}/responses"> </responses-list>
</div> </div>
<div class="method-samples"> <div class="method-samples">
<h4 class="method-params-subheader">Definition</h4> <h4 class="method-params-subheader">Definition</h4>
<div class="method-endpoint"> <div class="method-endpoint">
@ -30,5 +30,5 @@
<br> <br>
<responses-samples pointer="{{pointer}}/responses"> </responses-samples> <responses-samples pointer="{{pointer}}/responses"> </responses-samples>
</div> </div>
</div>
</div> </div>
<div>

View File

@ -6,18 +6,20 @@
display: block; display: block;
border-bottom: 1px solid rgba(127, 127, 127, 0.25); border-bottom: 1px solid rgba(127, 127, 127, 0.25);
margin-top: 1em; margin-top: 1em;
transform: translateZ(0);
z-index: 2;
} }
:host:last-of-type { // :host:last-of-type {
border-bottom: 0; // border-bottom: 0;
} // }
.method-header { .method-header {
margin-bottom: .9em; margin-bottom: calc(1em - 6px);
} }
.method-endpoint { .method-endpoint {
margin: 0 0 2em 0; //margin: 0 0 2px 0;
padding: 10px 20px; padding: 10px 20px;
border-radius: $border-radius*2; border-radius: $border-radius*2;
background-color: darken($black, 2%); background-color: darken($black, 2%);
@ -31,8 +33,8 @@
padding-top: 1px; padding-top: 1px;
padding-bottom: 0; padding-bottom: 0;
margin: 0; margin: 0;
font-size: .8em; font-size: 12/14em;
color: $black; color: $black;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
border-radius: $border-radius; border-radius: $border-radius;

View File

@ -11,6 +11,7 @@ import { getChildDebugElement } from '../../../tests/helpers';
import { Method } from './method'; import { Method } from './method';
import { SpecManager } from '../../utils/spec-manager';; import { SpecManager } from '../../utils/spec-manager';;
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {
@ -19,12 +20,17 @@ describe('Redoc components', () => {
describe('Method Component', () => { describe('Method Component', () => {
let builder; let builder;
let component; let component;
let specMgr;
beforeEach(async(inject([SpecManager], ( specMgr) => { beforeEach(async(inject([SpecManager, LazyTasksService], (_specMgr, lazyTasks) => {
lazyTasks.sync = true;
return specMgr.load('/tests/schemas/extended-petstore.yml'); specMgr = _specMgr;
}))); })));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
});
beforeEach(() => { beforeEach(() => {
let fixture = TestBed.createComponent(TestAppComponent); let fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'method').componentInstance; component = getChildDebugElement(fixture.debugElement, 'method').componentInstance;
@ -53,7 +59,6 @@ describe('Redoc components', () => {
/** Test component that contains a Method. */ /** Test component that contains a Method. */
@Component({ @Component({
selector: 'test-app', selector: 'test-app',
providers: [SpecManager],
template: template:
`<method pointer='#/paths/~1user~1{username}/put'></method>` `<method pointer='#/paths/~1user~1{username}/put'></method>`
}) })

View File

@ -1,9 +1,9 @@
'use strict'; 'use strict';
import { Input, Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Input, Component, OnInit, ChangeDetectionStrategy, ElementRef, ChangeDetectorRef } from '@angular/core';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import { BaseComponent, SpecManager } from '../base'; import { BaseComponent, SpecManager } from '../base';
import { SchemaHelper } from '../../services/schema-helper.service'; import { SchemaHelper } from '../../services/schema-helper.service';
import { OptionsService } from '../../services/options.service'; import { OptionsService, AppStateService } from '../../services/';
@Component({ @Component({
selector: 'method', selector: 'method',
@ -14,10 +14,14 @@ import { OptionsService } from '../../services/options.service';
export class Method extends BaseComponent implements OnInit { export class Method extends BaseComponent implements OnInit {
@Input() pointer:string; @Input() pointer:string;
@Input() tag:string; @Input() tag:string;
@Input() posInfo: any;
hidden = true;
method:any; method:any;
constructor(specMgr:SpecManager, private optionsService: OptionsService) { constructor(specMgr:SpecManager, private optionsService: OptionsService, private chDetector: ChangeDetectorRef,
private appState: AppStateService, private el: ElementRef) {
super(specMgr); super(specMgr);
} }
@ -53,6 +57,14 @@ export class Method extends BaseComponent implements OnInit {
return bodyParam; return bodyParam;
} }
show(res) {
if (res) {
this.el.nativeElement.firstElementChild.removeAttribute('hidden');
} else {
this.el.nativeElement.firstElementChild.setAttribute('hidden', 'hidden');
}
}
ngOnInit() { ngOnInit() {
this.preinit(); this.preinit();
} }

View File

@ -1,10 +1,10 @@
<div class="methods"> <div class="methods">
<div class="tag" *ngFor="let tag of tags;trackBy:trackByTagName"> <div class="tag" *ngFor="let tag of tags;let catIdx = index; trackBy:trackByTagName">
<div class="tag-info" [attr.section]="tag.id" *ngIf="!tag.headless"> <div class="tag-info" [attr.section]="tag.id" *ngIf="!tag.headless">
<h1 class="sharable-header"> <a class="share-link" href="#tag/{{tag.name | encodeURIComponent}}"></a>{{tag.name}} </h1> <h1 class="sharable-header"> <a class="share-link" href="#tag/{{tag.name | encodeURIComponent}}"></a>{{tag.name}} </h1>
<p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p> <p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p>
</div> </div>
<method *ngFor="let method of tag.methods;trackBy:trackByPointer" [pointer]="method.pointer" [attr.pointer]="method.pointer" <method *lazyFor="let method of tag.methods;let show = show;" [hidden]="!show" [pointer]="method.pointer" [attr.pointer]="method.pointer"
[attr.section]="method.tag" [tag]="method.tag" [attr.operation-id]="method.operationId"></method> [attr.section]="method.tag" [tag]="method.tag" [attr.operation-id]="method.operationId"></method>
</div> </div>
</div> </div>

View File

@ -4,6 +4,11 @@
display: block; display: block;
overflow: hidden; overflow: hidden;
} }
:host [hidden] {
display: none;
}
.tag-info { .tag-info {
padding: $section-spacing; padding: $section-spacing;
box-sizing: border-box; box-sizing: border-box;

View File

@ -21,11 +21,16 @@ describe('Redoc components', () => {
let builder; let builder;
let component; let component;
let fixture; let fixture;
let specMgr;
beforeEach(async(inject([SpecManager], ( specMgr) => { beforeEach(async(inject([SpecManager], (_specMgr) => {
specMgr = _specMgr;
return specMgr.load('/tests/schemas/methods-list-component.json');
}))); })));
beforeEach(done => {
specMgr.load('/tests/schemas/methods-list-component.json').then(done, done.fail);
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent); fixture = TestBed.createComponent(TestAppComponent);
component = getChildDebugElement(fixture.debugElement, 'methods-list').componentInstance; component = getChildDebugElement(fixture.debugElement, 'methods-list').componentInstance;

View File

@ -2,10 +2,14 @@
$hint-color: #999999; $hint-color: #999999;
:host {
display: block;
}
.param-list-header { .param-list-header {
border-bottom: 1px solid rgba($text-color, .3); border-bottom: 1px solid rgba($text-color, .3);
padding: 0.2em 0; // padding: 0.2em 0;
margin: 3.5em 0 .8em 0; margin: 3em 0 1em 0;
color: rgba($text-color, .5); color: rgba($text-color, .5);
font-weight: normal; font-weight: normal;
text-transform: uppercase; text-transform: uppercase;

View File

@ -2,7 +2,11 @@
<h1>Oops... ReDoc failed to render this spec</h1> <h1>Oops... ReDoc failed to render this spec</h1>
<div class='redoc-error-details'>{{error.message}}</div> <div class='redoc-error-details'>{{error.message}}</div>
</div> </div>
<loading-bar *ngIf="options.lazyRendering" [progress]="loadingProgress"> </loading-bar>
<div class="redoc-wrap" *ngIf="specLoaded && !error"> <div class="redoc-wrap" *ngIf="specLoaded && !error">
<div class="background">
<div class="background-actual"> </div>
</div>
<div class="menu-content" sticky-sidebar [scrollParent]="options.$scrollParent" [scrollYOffset]="options.scrollYOffset"> <div class="menu-content" sticky-sidebar [scrollParent]="options.$scrollParent" [scrollYOffset]="options.scrollYOffset">
<api-logo> </api-logo> <api-logo> </api-logo>
<side-menu> </side-menu> <side-menu> </side-menu>

View File

@ -29,6 +29,7 @@
} }
.redoc-wrap { .redoc-wrap {
z-index: 0;
position: relative; position: relative;
font-family: $base-font, $base-font-family; font-family: $base-font, $base-font-family;
font-size: $em-size; font-size: $em-size;
@ -36,11 +37,17 @@
color: $text-color; color: $text-color;
} }
.menu-content {
overflow: hidden;
}
[sticky-sidebar] { [sticky-sidebar] {
width: $side-bar-width; width: $side-bar-width;
background-color: $side-bar-bg-color; background-color: $side-bar-bg-color;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
transform: translateZ(0);
z-index: 75;
@media (max-width: $side-menu-mobile-breakpoint) { @media (max-width: $side-menu-mobile-breakpoint) {
z-index: 1; z-index: 1;
@ -51,22 +58,33 @@
.api-content { .api-content {
margin-left: $side-bar-width; margin-left: $side-bar-width;
z-index: 50;
position: relative; position: relative;
// height: 100vh;
// overflow: scroll;
top: 0;
@media (max-width: $side-menu-mobile-breakpoint) { @media (max-width: $side-menu-mobile-breakpoint) {
padding-top: 3em; padding-top: 3em;
margin-left: 0; margin-left: 0;
} }
} }
.api-content:before { .background {
content: ""; position: fixed;
background: $samples-panel-bg-color;
height: 100%;
width: $samples-panel-width;
top: 0; top: 0;
bottom: 0;
right: 0; right: 0;
position: absolute; left: $side-bar-width;
z-index: -1; z-index: 1;
&-actual {
background: $samples-panel-bg-color;
left: 100% - $samples-panel-width;
right: 0;
top: 0;
bottom: 0;
position: absolute;
}
@media (max-width: $right-panel-squash-breakpoint) { @media (max-width: $right-panel-squash-breakpoint) {
display: none; display: none;
@ -98,7 +116,8 @@
font-family: $headers-font, $headers-font-family; font-family: $headers-font, $headers-font-family;
color: $secondary-color; color: $secondary-color;
font-weight: $headers-font-weight; font-weight: $headers-font-weight;
line-height: 1.4em; line-height: 1.5;
margin-bottom: 0.5em;
} }
} }
@ -107,7 +126,7 @@
h2 { font-size: $h2; } h2 { font-size: $h2; }
h3 { font-size: $h3; } h3 { font-size: $h3; }
h4 { font-size: $h4; } h4 { font-size: $h4; }
h5 { font-size: $h5; } h5 { font-size: $h5; line-height: 20px; }
p { p {
font-family: $base-font, $base-font-family; font-family: $base-font, $base-font-family;

View File

@ -29,9 +29,11 @@ describe('Redoc components', () => {
optsMgr = _optsMgr; optsMgr = _optsMgr;
specMgr = _specMgr; specMgr = _specMgr;
return specMgr.load('/tests/schemas/extended-petstore.yml');
}))); })));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
})
it('should init component', () => { it('should init component', () => {
let fixture = TestBed.createComponent(TestAppComponent); let fixture = TestBed.createComponent(TestAppComponent);

View File

@ -16,14 +16,15 @@ import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/spec-manager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService, Hash, MenuService, AppStateService } from '../../services/index'; import { OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/index';
import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';
import { CustomErrorHandler } from '../../utils/'; import { CustomErrorHandler } from '../../utils/';
@Component({ @Component({
selector: 'redoc', selector: 'redoc',
templateUrl: './redoc.html', templateUrl: './redoc.html',
styleUrls: ['./redoc.css'], styleUrls: ['./redoc.css'],
changeDetection: ChangeDetectionStrategy.OnPush //changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class Redoc extends BaseComponent implements OnInit { export class Redoc extends BaseComponent implements OnInit {
static _preOptions: any; static _preOptions: any;
@ -34,6 +35,8 @@ export class Redoc extends BaseComponent implements OnInit {
specLoaded: boolean; specLoaded: boolean;
options: any; options: any;
loadingProgress: number;
@Input() specUrl: string; @Input() specUrl: string;
@HostBinding('class.loading') specLoading: boolean = false; @HostBinding('class.loading') specLoading: boolean = false;
@HostBinding('class.loading-remove') specLoadingRemove: boolean = false; @HostBinding('class.loading-remove') specLoadingRemove: boolean = false;
@ -43,9 +46,12 @@ export class Redoc extends BaseComponent implements OnInit {
optionsMgr: OptionsService, optionsMgr: OptionsService,
elementRef: ElementRef, elementRef: ElementRef,
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
private appState: AppStateService private appState: AppStateService,
private lazyTasksService: LazyTasksService,
private hash: Hash
) { ) {
super(specMgr); super(specMgr);
SchemaHelper.setSpecManager(specMgr);
// merge options passed before init // merge options passed before init
optionsMgr.options = Redoc._preOptions || {}; optionsMgr.options = Redoc._preOptions || {};
@ -56,14 +62,22 @@ export class Redoc extends BaseComponent implements OnInit {
if (scrollParent === DOM.defaultDoc().body) scrollParent = window; if (scrollParent === DOM.defaultDoc().body) scrollParent = window;
optionsMgr.options.$scrollParent = scrollParent; optionsMgr.options.$scrollParent = scrollParent;
this.options = optionsMgr.options; this.options = optionsMgr.options;
this.lazyTasksService.allSync = !this.options.lazyRendering;
} }
hideLoadingAnimation() { hideLoadingAnimation() {
this.specLoadingRemove = true; requestAnimationFrame(() => {
setTimeout(() => {
this.specLoadingRemove = true; this.specLoadingRemove = true;
this.specLoading = false; setTimeout(() => {
}, 400); this.specLoadingRemove = false;
this.specLoading = false;
}, 400);
});
}
showLoadingAnimation() {
this.specLoading = true;
this.specLoadingRemove = false;
} }
load() { load() {
@ -71,19 +85,30 @@ export class Redoc extends BaseComponent implements OnInit {
throw err; throw err;
}); });
this.appState.loading.subscribe(loading => {
if (loading) {
this.showLoadingAnimation();
} else {
this.hideLoadingAnimation();
}
});
this.specMgr.spec.subscribe((spec) => { this.specMgr.spec.subscribe((spec) => {
if (!spec) { if (!spec) {
this.specLoading = true; this.appState.startLoading();
this.specLoaded = false;
} else { } else {
this.specLoaded = true;
this.hideLoadingAnimation();
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
this.changeDetector.detectChanges();
this.specLoaded = true;
setTimeout(() => {
this.hash.start();
});
} }
}); });
} }
ngOnInit() { ngOnInit() {
this.lazyTasksService.loadProgress.subscribe(progress => this.loadingProgress = progress)
this.appState.error.subscribe(_err => { this.appState.error.subscribe(_err => {
if (!_err) return; if (!_err) return;

View File

@ -6,31 +6,26 @@
} }
.action-buttons { .action-buttons {
display: block;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
transform: translateY(100%); transform: translateY(100%);
z-index: 3;
> span { position: relative;
float: right; height: 2em;
} line-height: 2em;
padding-right: 10px;
text-align: right;
margin-top: -1em;
> span > a { > span > a {
padding: 2px 10px; padding: 2px 10px;
color: #ffffff; color: #ffffff;
cursor: pointer; cursor: pointer;
background-color: darken($black, 4%);
&:hover { &:hover {
background-color: $black; background-color: lighten($black, 15%);
} }
} }
&:after {
display: block;
content: '';
clear: both;
}
} }
.code-sample:hover > .action-buttons { .code-sample:hover > .action-buttons {
@ -45,6 +40,7 @@ header {
color: $sample-panel-headers-color; color: $sample-panel-headers-color;
text-transform: uppercase; text-transform: uppercase;
font-weight: normal; font-weight: normal;
margin-top: 20px;
} }
:host /deep/ > tabs > ul li { :host /deep/ > tabs > ul li {
@ -53,7 +49,7 @@ header {
border-radius: $border-radius; border-radius: $border-radius;
margin: 2px 0; margin: 2px 0;
padding: 3px 10px 2px 10px; padding: 3px 10px 2px 10px;
line-height: 1.25; line-height: 16px;
color: $sample-panel-headers-color; color: $sample-panel-headers-color;
&:hover { &:hover {

View File

@ -22,7 +22,7 @@ header {
margin: 2px 0; margin: 2px 0;
padding: 2px 8px 3px 8px; padding: 2px 8px 3px 8px;
color: $sample-panel-headers-color; color: $sample-panel-headers-color;
line-height: 1.25; line-height: 16px;
&:hover { &:hover {
color: #ffffff; color: #ffffff;

View File

@ -2,9 +2,9 @@
<!-- in case sample is not available for some reason --> <!-- in case sample is not available for some reason -->
<pre *ngIf="sample == undefined"> Sample unavailable </pre> <pre *ngIf="sample == undefined"> Sample unavailable </pre>
<div class="action-buttons"> <div class="action-buttons">
<span> <a *ngIf="enableButtons" (click)="collapseAll()">Collapse all</a> </span> <span copy-button [copyText]="sample" class="hint--top-left hint--inversed"> <a>Copy</a> </span>
<span> <a *ngIf="enableButtons" (click)="expandAll()">Expand all</a> </span> <span> <a *ngIf="enableButtons" (click)="expandAll()">Expand all</a> </span>
<span copy-button [copyText]="sample | json" class="hint--top hint--inversed"> <a>Copy</a> </span> <span> <a *ngIf="enableButtons" (click)="collapseAll()">Collapse all</a> </span>
</div> </div>
<pre [innerHtml]="sample | jsonFormatter"></pre> <pre [innerHtml]="sample | jsonFormatter"></pre>
</div> </div>

View File

@ -13,46 +13,26 @@ pre {
} }
.action-buttons { .action-buttons {
display: block;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
transform: translateY(100%); transform: translateY(100%);
z-index: 1; z-index: 3;
position: relative; position: relative;
height: 2em;
> span { line-height: 2em;
float: right; padding-right: 10px;
text-align: right;
&:last-child > a:before { margin-top: -1em;
display: none;
}
}
> span > a { > span > a {
padding: 2px 10px; padding: 2px 10px;
color: #ffffff; color: #ffffff;
cursor: pointer; cursor: pointer;
&:before {
content: '|';
display: inline-block;
transform: translateX(-10px);
}
&:first-child {
margin-right: 0;
}
&:hover { &:hover {
background-color: $black; background-color: lighten($black, 15%);
} }
} }
&:after {
display: block;
content: '';
clear: both;
}
} }
.snippet:hover .action-buttons { .snippet:hover .action-buttons {
@ -135,6 +115,7 @@ pre {
li { li {
position: relative; position: relative;
display: block;
} }
.hoverable { .hoverable {

View File

@ -10,10 +10,10 @@
<div *ngFor="let cat of categories; let idx = index" class="menu-cat"> <div *ngFor="let cat of categories; let idx = index" class="menu-cat">
<label class="menu-cat-header" (click)="activateAndScroll(idx, -1)" [hidden]="cat.headless" <label class="menu-cat-header" (click)="activateAndScroll(idx, -1)" [hidden]="cat.headless"
[ngClass]="{active: cat.active}"> {{cat.name}}</label> [ngClass]="{active: cat.active, disabled: !cat.ready }"> {{cat.name}}</label>
<ul class="menu-subitems" [@itemAnimation]="cat.active ? 'expanded' : 'collapsed'"> <ul class="menu-subitems" [@itemAnimation]="cat.active ? 'expanded' : 'collapsed'">
<li *ngFor="let method of cat.methods; trackBy:summary; let methIdx = index" <li *ngFor="let method of cat.methods; trackBy:summary; let methIdx = index"
[ngClass]="{active: method.active}" [ngClass]="{active: method.active, disabled: !method.ready}"
(click)="activateAndScroll(idx, methIdx)"> (click)="activateAndScroll(idx, methIdx)">
{{method.summary}} {{method.summary}}
</li> </li>

View File

@ -38,6 +38,11 @@ $mobile-menu-compact-breakpoint: 550px;
&[hidden] { &[hidden] {
display: none; display: none;
} }
&.disabled, &.disabled:hover {
cursor: default;
color: lighten($text-color, 60%);
}
} }
.menu-subitems { .menu-subitems {
@ -72,6 +77,11 @@ $mobile-menu-compact-breakpoint: 550px;
& li.active { & li.active {
background: darken($side-menu-active-bg-color, 6%); background: darken($side-menu-active-bg-color, 6%);
} }
&.disabled, &.disabled:hover {
cursor: default;
color: lighten($text-color, 60%);
}
} }

View File

@ -19,23 +19,28 @@ let testOptions;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] }); TestBed.configureTestingModule({ declarations: [ TestAppComponent, MethodsList ] });
}); });
describe('SideMenu Component', () => { describe('SideMenu Component', () => {
let builder; let builder;
let component; let component;
let fixture; let fixture;
let specMgr;
beforeEach(async(inject([SpecManager, OptionsService], beforeEach(inject([SpecManager, OptionsService],
( specMgr, opts) => { (_specMgr, opts) => {
testOptions = opts; testOptions = opts;
testOptions.options = { testOptions.options = {
scrollYOffset: () => 0, scrollYOffset: () => 0,
$scrollParent: window $scrollParent: window
}; };
return specMgr.load('/tests/schemas/extended-petstore.yml'); specMgr = _specMgr;
}))); }));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TestAppComponent); fixture = TestBed.createComponent(TestAppComponent);

View File

@ -38,6 +38,8 @@ export class SideMenu extends BaseComponent implements OnInit {
private $resourcesNav: any; private $resourcesNav: any;
private $scrollParent: any; private $scrollParent: any;
private firstChange = true;
constructor(specMgr:SpecManager, elementRef:ElementRef, constructor(specMgr:SpecManager, elementRef:ElementRef,
private scrollService:ScrollService, private menuService:MenuService, private hash:Hash, private scrollService:ScrollService, private menuService:MenuService, private hash:Hash,
optionsService:OptionsService, private detectorRef:ChangeDetectorRef) { optionsService:OptionsService, private detectorRef:ChangeDetectorRef) {
@ -52,20 +54,37 @@ export class SideMenu extends BaseComponent implements OnInit {
this.menuService.changed.subscribe((evt) => this.changed(evt)); this.menuService.changed.subscribe((evt) => this.changed(evt));
} }
changed({cat, item}) { changed(newItem) {
this.activeCatCaption = cat.name || ''; if (newItem) {
this.activeItemCaption = item && item.summary || ''; let {cat, item} = newItem;
this.activeCatCaption = cat.name || '';
this.activeItemCaption = item && item.summary || '';
}
//safari doesn't update bindings if not run changeDetector manually :( //safari doesn't update bindings if not run changeDetector manually :(
this.detectorRef.detectChanges(); this.detectorRef.detectChanges();
if (this.firstChange) {
this.scrollActiveIntoView();
this.firstChange = false;
}
} }
activateAndScroll(idx, methodIdx) { scrollActiveIntoView() {
let $item = this.$element.querySelector('li.active, label.active');
if ($item) $item.scrollIntoView();
}
activateAndScroll(catIdx, methodIdx) {
if (this.mobileMode()) { if (this.mobileMode()) {
this.toggleMobileNav(); this.toggleMobileNav();
} }
this.menuService.activate(idx, methodIdx); let menu = this.categories;
if (!menu[catIdx].ready) return;
if (menu[catIdx].methods && menu[catIdx].methods.length && (methodIdx >= 0) &&
!menu[catIdx].methods[methodIdx].ready) return;
this.menuService.activate(catIdx, methodIdx);
this.menuService.scrollToActive(); this.menuService.scrollToActive();
} }

View File

@ -14,13 +14,16 @@ import { MethodsList } from './MethodsList/methods-list';
import { Method } from './Method/method'; import { Method } from './Method/method';
import { Warnings } from './Warnings/warnings'; import { Warnings } from './Warnings/warnings';
import { SecurityDefinitions } from './SecurityDefinitions/security-definitions'; import { SecurityDefinitions } from './SecurityDefinitions/security-definitions';
import { LoadingBar } from './LoadingBar/loading-bar';
import { Redoc } from './Redoc/redoc'; import { Redoc } from './Redoc/redoc';
export const REDOC_DIRECTIVES = [ export const REDOC_DIRECTIVES = [
ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
LoadingBar
]; ];
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions } ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
LoadingBar }

View File

@ -1,3 +1,6 @@
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'core-js/es6/symbol'; import 'core-js/es6/symbol';
import 'core-js/es6/object'; import 'core-js/es6/object';
import 'core-js/es6/function'; import 'core-js/es6/function';
@ -18,9 +21,6 @@ import 'core-js/es6/reflect';
// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709 // see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
// import 'core-js/es6/promise'; // import 'core-js/es6/promise';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
// Typescript emit helpers polyfill // Typescript emit helpers polyfill
import 'ts-helpers'; import 'ts-helpers';

View File

@ -1,10 +1,11 @@
import { NgModule, ErrorHandler } from '@angular/core'; import { NgModule, ErrorHandler } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Redoc, SecurityDefinitions, REDOC_DIRECTIVES } from './components/index'; import { Redoc, SecurityDefinitions, Method, REDOC_DIRECTIVES } from './components/index';
import { REDOC_COMMON_DIRECTIVES, DynamicNg2Wrapper } from './shared/components/index'; import { REDOC_COMMON_DIRECTIVES, DynamicNg2Wrapper } from './shared/components/index';
import { REDOC_PIPES, KeysPipe } from './utils/pipes'; import { REDOC_PIPES, KeysPipe } from './utils/pipes';
import { CustomErrorHandler } from './utils/' import { CustomErrorHandler } from './utils/'
import { LazyTasksService } from './shared/components/LazyFor/lazy-for';
import { import {
OptionsService, OptionsService,
@ -22,7 +23,7 @@ import { SpecManager } from './utils/spec-manager';
imports: [ CommonModule ], imports: [ CommonModule ],
declarations: [ REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES, REDOC_PIPES ], declarations: [ REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES, REDOC_PIPES ],
bootstrap: [ Redoc ], bootstrap: [ Redoc ],
entryComponents: [ SecurityDefinitions, DynamicNg2Wrapper ], entryComponents: [ SecurityDefinitions, DynamicNg2Wrapper, Method ],
providers: [ providers: [
SpecManager, SpecManager,
ScrollService, ScrollService,
@ -33,6 +34,7 @@ import { SpecManager } from './utils/spec-manager';
AppStateService, AppStateService,
ComponentParser, ComponentParser,
ContentProjector, ContentProjector,
LazyTasksService,
{ provide: ErrorHandler, useClass: CustomErrorHandler }, { provide: ErrorHandler, useClass: CustomErrorHandler },
{ provide: COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': SecurityDefinitions} } { provide: COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': SecurityDefinitions} }
], ],

View File

@ -1,11 +1,24 @@
'use strict'; 'use strict';
import { Injectable } from '@angular/core'; import { Injectable, NgZone } from '@angular/core';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Injector } from '@angular/core';
@Injectable() @Injectable()
export class AppStateService { export class AppStateService {
samplesLanguage = new Subject<string>(); samplesLanguage = new Subject<string>();
error = new BehaviorSubject<any>(null); error = new BehaviorSubject<any>(null);
loading = new Subject<boolean>();
startLoading() {
this.loading.next(true);
}
stopLoading() {
this.loading.next(false);
}
constructor(private injector: Injector, private zone: NgZone) {
}
} }

View File

@ -4,20 +4,18 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { SpecManager } from '../utils/spec-manager';
describe('Hash Service', () => { describe('Hash Service', () => {
let specMgr = new SpecManager();
let hashService; let hashService;
beforeEach(inject([Hash], (_hash) => hashService = _hash)); beforeEach(inject([Hash], (_hash) => {
hashService = _hash;
}));
it('should trigger changed event after ReDoc bootstrapped', (done) => { it('should trigger changed event when method start is called', () => {
spyOn(hashService.value, 'next').and.callThrough(); spyOn(hashService.value, 'next').and.stub();
specMgr.spec.next({}); hashService.start();
setTimeout(() => { expect(hashService.value.next).toHaveBeenCalled();
expect(hashService.value.next).toHaveBeenCalled(); hashService.value.next.and.callThrough();
done();
});
}); });
}); });

View File

@ -3,20 +3,16 @@ import { Injectable } from '@angular/core';
import { PlatformLocation } from '@angular/common'; import { PlatformLocation } from '@angular/common';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { SpecManager } from '../utils/spec-manager';
@Injectable() @Injectable()
export class Hash { export class Hash {
public value = new BehaviorSubject<string>(''); public value = new BehaviorSubject<string | null>(null);
constructor(private specMgr: SpecManager, private location: PlatformLocation) { constructor(private location: PlatformLocation) {
this.bind(); this.bind();
}
this.specMgr.spec.subscribe((spec) => { start() {
if (!spec) return; this.value.next(this.hash);
setTimeout(() => {
this.value.next(this.hash);
});
});
} }
get hash() { get hash() {

View File

@ -1,57 +1,52 @@
'use strict'; 'use strict';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { import {
inject, inject,
async,
TestBed TestBed
} from '@angular/core/testing'; } from '@angular/core/testing';
import { MethodsList } from '../components/MethodsList/methods-list';
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { ScrollService } from './scroll.service'; import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
import { ScrollService } from './scroll.service';
import { SchemaHelper } from './schema-helper.service';
import { SpecManager } from '../utils/spec-manager';; import { SpecManager } from '../utils/spec-manager';;
describe('Menu service', () => { describe('Menu service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ declarations: [ TestAppComponent ] }); TestBed.configureTestingModule({ declarations: [ TestAppComponent, MethodsList ] });
}); });
let menu, hashService, scroll; let menu, hashService, scroll, tasks;
let specMgr; let specMgr;
beforeEach(async(inject([SpecManager, Hash, ScrollService], beforeEach(inject([SpecManager, Hash, ScrollService, LazyTasksService],
( _specMgr, _hash, _scroll, _menu) => { ( _specMgr, _hash, _scroll, _tasks) => {
hashService = _hash; hashService = _hash;
scroll = _scroll; scroll = _scroll;
tasks = _tasks;
specMgr = _specMgr; specMgr = _specMgr;
return specMgr.load('/tests/schemas/extended-petstore.yml'); SchemaHelper.setSpecManager(specMgr);
}))); }));
beforeEach(done => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
});
beforeEach(() => { beforeEach(() => {
menu = new MenuService(hashService, scroll, specMgr); menu = TestBed.get(MenuService);
let fixture = TestBed.createComponent(TestAppComponent); let fixture = TestBed.createComponent(TestAppComponent);
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should run hashScroll when hash changed', (done) => {
spyOn(menu, 'hashScroll').and.callThrough();
hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled();
menu.hashScroll.and.callThrough();
done();
});
hashService.value.next('nonFalsy');
});
it('should scroll to method when location hash is present [jp]', (done) => { it('should scroll to method when location hash is present [jp]', (done) => {
let hash = '#tag/pet/paths/~1pet~1findByStatus/get'; let hash = '#tag/pet/paths/~1pet~1findByStatus/get';
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'scrollToActive').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.value.subscribe((hash) => { hashService.value.subscribe((hash) => {
if (!hash) return; if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.scrollToActive).toHaveBeenCalled();
let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1]; let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1];
expect(scrollY).toBeGreaterThan(0); expect(scrollY).toBeGreaterThan(0);
(<jasmine.Spy>window.scrollTo).and.callThrough(); (<jasmine.Spy>window.scrollTo).and.callThrough();
@ -59,14 +54,14 @@ describe('Menu service', () => {
}); });
hashService.value.next(hash); hashService.value.next(hash);
}); });
//
it('should scroll to method when location hash is present [operation]', (done) => { it('should scroll to method when location hash is present [operation]', (done) => {
let hash = '#operation/getPetById'; let hash = '#operation/getPetById';
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'scrollToActive').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.value.subscribe((hash) => { hashService.value.subscribe((hash) => {
if (!hash) return; if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.scrollToActive).toHaveBeenCalled();
let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1]; let scrollY = (<jasmine.Spy>window.scrollTo).calls.argsFor(0)[1];
expect(scrollY).toBeGreaterThan(0); expect(scrollY).toBeGreaterThan(0);
done(); done();

View File

@ -1,9 +1,12 @@
'use strict'; 'use strict';
import { Injectable, EventEmitter } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ScrollService, INVIEW_POSITION } from './scroll.service'; import { ScrollService, INVIEW_POSITION } from './scroll.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { SpecManager } from '../utils/spec-manager'; import { SpecManager } from '../utils/spec-manager';
import { SchemaHelper, MenuCategory } from './schema-helper.service'; import { SchemaHelper, MenuCategory } from './schema-helper.service';
import { AppStateService } from './app-state.service';
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
const CHANGE = { const CHANGE = {
NEXT : 1, NEXT : 1,
@ -14,13 +17,19 @@ const CHANGE = {
@Injectable() @Injectable()
export class MenuService { export class MenuService {
changed: EventEmitter<any> = new EventEmitter(); changed: EventEmitter<any> = new EventEmitter();
ready: BehaviorSubject<boolean> = new BehaviorSubject(false);
categories: Array<MenuCategory>; categories: Array<MenuCategory>;
activeCatIdx: number = 0; activeCatIdx: number = 0;
activeMethodIdx: number = -1; activeMethodIdx: number = -1;
activeMethodPtr: string;
constructor(private hash:Hash, private scrollService:ScrollService, specMgr:SpecManager) { constructor(
private hash:Hash,
private tasks: LazyTasksService,
private scrollService: ScrollService,
private appState: AppStateService,
specMgr:SpecManager
) {
this.hash = hash; this.hash = hash;
this.categories = SchemaHelper.buildMenuTree(specMgr.schema); this.categories = SchemaHelper.buildMenuTree(specMgr.schema);
@ -28,13 +37,45 @@ export class MenuService {
this.scrollUpdate(evt.isScrolledDown); this.scrollUpdate(evt.isScrolledDown);
}); });
this.changeActive(CHANGE.INITIAL); //this.changeActive(CHANGE.INITIAL);
this.hash.value.subscribe((hash) => { this.hash.value.subscribe((hash) => {
this.hashScroll(hash); if (hash == undefined) return;
this.setActiveByHash(hash);
if (!this.tasks.empty) {
this.tasks.start(this.activeCatIdx, this.activeMethodIdx, this);
this.scrollService.setStickElement(this.getCurrentMethodEl());
if (hash) this.scrollToActive();
this.appState.stopLoading();
} else {
if (hash) this.scrollToActive();
}
}); });
} }
enableItem(catIdx, methodIdx, skipUpdate = false) {
let cat = this.categories[catIdx];
cat.ready = true;
if (cat.methods.length) cat.methods[methodIdx].ready = true;
let prevCat = this.categories[catIdx - 1];
if (prevCat && !prevCat.ready && (prevCat.virtual || !prevCat.methods.length)) {
this.enableItem(catIdx - 1, -1, true);
}
if (skipUpdate) return;
this.changed.next();
}
get activeMethodPtr() {
let cat = this.categories[this.activeCatIdx];
let ptr = null;
if (cat && cat.methods.length) {
let mtd = cat.methods[this.activeMethodIdx];
ptr = mtd && mtd.pointer || null;
}
return ptr;
}
scrollUpdate(isScrolledDown) { scrollUpdate(isScrolledDown) {
let stable = false; let stable = false;
while(!stable) { while(!stable) {
@ -44,6 +85,7 @@ export class MenuService {
if(isScrolledDown) { if(isScrolledDown) {
//&& elementInViewPos === INVIEW_POSITION.BELLOW //&& elementInViewPos === INVIEW_POSITION.BELLOW
let $nextEl = this.getRelativeCatOrItem(1); let $nextEl = this.getRelativeCatOrItem(1);
if (!$nextEl) return;
let nextInViewPos = this.scrollService.getElementPos($nextEl, true); let nextInViewPos = this.scrollService.getElementPos($nextEl, true);
if (elementInViewPos === INVIEW_POSITION.BELLOW && nextInViewPos === INVIEW_POSITION.ABOVE) { if (elementInViewPos === INVIEW_POSITION.BELLOW && nextInViewPos === INVIEW_POSITION.ABOVE) {
stable = this.changeActive(CHANGE.NEXT); stable = this.changeActive(CHANGE.NEXT);
@ -93,6 +135,8 @@ export class MenuService {
} }
activate(catIdx, methodIdx) { activate(catIdx, methodIdx) {
if (catIdx < 0) return;
let menu = this.categories; let menu = this.categories;
menu[this.activeCatIdx].active = false; menu[this.activeCatIdx].active = false;
@ -105,12 +149,10 @@ export class MenuService {
this.activeCatIdx = catIdx; this.activeCatIdx = catIdx;
this.activeMethodIdx = methodIdx; this.activeMethodIdx = methodIdx;
menu[catIdx].active = true; menu[catIdx].active = true;
this.activeMethodPtr = null;
let currentItem; let currentItem;
if (menu[catIdx].methods.length && (methodIdx > -1)) { if (menu[catIdx].methods.length && (methodIdx > -1)) {
currentItem = menu[catIdx].methods[methodIdx]; currentItem = menu[catIdx].methods[methodIdx];
currentItem.active = true; currentItem.active = true;
this.activeMethodPtr = currentItem.pointer;
} }
this.changed.next({cat: menu[catIdx], item: currentItem}); this.changed.next({cat: menu[catIdx], item: currentItem});
@ -156,24 +198,31 @@ export class MenuService {
this.scrollService.scrollTo(this.getCurrentMethodEl()); this.scrollService.scrollTo(this.getCurrentMethodEl());
} }
hashScroll(hash) { setActiveByHash(hash) {
if (!hash) return; if (!hash) {
return;
let $el; }
let catIdx, methodIdx;
hash = hash.substr(1); hash = hash.substr(1);
let namespace = hash.split('/')[0]; let namespace = hash.split('/')[0];
let ptr = decodeURIComponent(hash.substr(namespace.length + 1)); let ptr = decodeURIComponent(hash.substr(namespace.length + 1));
if (namespace === 'operation') { if (namespace === 'section' || namespace === 'tag') {
$el = this.getMethodElByOperId(ptr);
} else if (namespace === 'tag') {
let sectionId = ptr.split('/')[0]; let sectionId = ptr.split('/')[0];
catIdx = this.categories.findIndex(cat => cat.id === namespace + '/' + sectionId);
let cat = this.categories[catIdx];
ptr = ptr.substr(sectionId.length) || null; ptr = ptr.substr(sectionId.length) || null;
sectionId = namespace + (sectionId ? '/' + sectionId : ''); methodIdx = cat.methods.findIndex(method => method.pointer === ptr);
$el = this.getMethodElByPtr(ptr, sectionId);
} else { } else {
$el = this.getMethodElByPtr(null, namespace + '/' + ptr); catIdx = this.categories.findIndex(cat => {
if (!cat.methods.length) return false;
methodIdx = cat.methods.findIndex(method => method.operationId === ptr || method.pointer === ptr);
if (methodIdx >= 0) {
return true;
} else {
return false;
}
});
} }
this.activate(catIdx, methodIdx);
if ($el) this.scrollService.scrollTo($el);
} }
} }

View File

@ -8,25 +8,43 @@ const defaults = {
disableLazySchemas: false disableLazySchemas: false
}; };
const OPTION_NAMES = new Set(['scrollYOffset', 'disableLazySchemas', 'specUrl', 'suppressWarnings', 'hideHostname']); const OPTION_NAMES = new Set([
'scrollYOffset',
'disableLazySchemas',
'specUrl',
'suppressWarnings',
'hideHostname',
'lazyRendering'
]);
interface Options {
scrollYOffset?: any;
disableLazySchemas?: boolean;
specUrl?: string;
suppressWarnings?: boolean;
hideHostname?: boolean;
lazyRendering?: boolean;
$scrollParent?: HTMLElement | Window;
}
@Injectable() @Injectable()
export class OptionsService { export class OptionsService {
private _options: any; private _options: Options;
constructor() { constructor() {
this._options = defaults; this._options = defaults;
this._normalizeOptions();
} }
get options() { get options():Options {
return this._options; return this._options;
} }
set options(opts) { set options(opts:Options) {
this._options = Object.assign(this._options, opts); this._options = Object.assign(this._options, opts);
} }
parseOptions(el) { parseOptions(el:HTMLElement):void {
let parsedOpts; let parsedOpts;
let attributesMap = DOM.attributeMap(el); let attributesMap = DOM.attributeMap(el);
parsedOpts = {}; parsedOpts = {};
@ -46,7 +64,7 @@ export class OptionsService {
this._normalizeOptions(); this._normalizeOptions();
} }
_normalizeOptions() { _normalizeOptions():void {
// modify scrollYOffset to always be a function // modify scrollYOffset to always be a function
if (!isFunction(this._options.scrollYOffset)) { if (!isFunction(this._options.scrollYOffset)) {
if (isFinite(this._options.scrollYOffset)) { if (isFinite(this._options.scrollYOffset)) {
@ -70,5 +88,6 @@ export class OptionsService {
if (isString(this._options.disableLazySchemas)) this._options.disableLazySchemas = true; if (isString(this._options.disableLazySchemas)) this._options.disableLazySchemas = true;
if (isString(this._options.suppressWarnings)) this._options.suppressWarnings = true; if (isString(this._options.suppressWarnings)) this._options.suppressWarnings = true;
if (isString(this._options.hideHostname)) this._options.hideHostname = true; if (isString(this._options.hideHostname)) this._options.hideHostname = true;
if (isString(this._options.lazyRendering)) this._options.lazyRendering = true;
} }
} }

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { SpecManager } from '../utils/spec-manager';
import { methods as swaggerMethods, keywordTypes } from '../utils/swagger-defs'; import { methods as swaggerMethods, keywordTypes } from '../utils/swagger-defs';
import { WarningsService } from './warnings.service'; import { WarningsService } from './warnings.service';
import * as slugify from 'slugify'; import * as slugify from 'slugify';
@ -15,6 +14,8 @@ export interface MenuMethod {
summary: string; summary: string;
tag: string; tag: string;
pointer: string; pointer: string;
operationId: string;
ready: boolean;
} }
export interface MenuCategory { export interface MenuCategory {
@ -26,8 +27,12 @@ export interface MenuCategory {
description?: string; description?: string;
empty?: string; empty?: string;
virtual?: boolean; virtual?: boolean;
ready: boolean;
} }
// global var for this module
var specMgrInstance;
const injectors = { const injectors = {
notype: { notype: {
check: (propertySchema) => !propertySchema.type, check: (propertySchema) => !propertySchema.type,
@ -186,8 +191,8 @@ const injectors = {
parentPtr = JsonPointer.dirName(hostPointer, 3); parentPtr = JsonPointer.dirName(hostPointer, 3);
} }
let parentParam = SpecManager.instance().byPointer(parentPtr); let parentParam = specMgrInstance.byPointer(parentPtr);
let root = SpecManager.instance().schema; let root =specMgrInstance.schema;
injectTo._produces = parentParam && parentParam.produces || root.produces; injectTo._produces = parentParam && parentParam.produces || root.produces;
injectTo._consumes = parentParam && parentParam.consumes || root.consumes; injectTo._consumes = parentParam && parentParam.consumes || root.consumes;
injectTo._widgetType = 'file'; injectTo._widgetType = 'file';
@ -196,6 +201,10 @@ const injectors = {
}; };
export class SchemaHelper { export class SchemaHelper {
static setSpecManager(specMgr) {
specMgrInstance = specMgr;
}
static preprocess(schema, pointer, hostPointer?) { static preprocess(schema, pointer, hostPointer?) {
//propertySchema = Object.assign({}, propertySchema); //propertySchema = Object.assign({}, propertySchema);
if (schema['x-redoc-schema-precompiled']) { if (schema['x-redoc-schema-precompiled']) {
@ -291,13 +300,15 @@ export class SchemaHelper {
} }
static buildMenuTree(schema):Array<MenuCategory> { static buildMenuTree(schema):Array<MenuCategory> {
var catIdx = 0;
let tag2MethodMapping = {}; let tag2MethodMapping = {};
for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) { for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) {
let id = 'section/' + slugify(header); let id = 'section/' + slugify(header);
tag2MethodMapping[id] = { tag2MethodMapping[id] = {
name: header, id: id, virtual: true, methods: [] name: header, id: id, virtual: true, methods: [], idx: catIdx
}; };
catIdx++;
} }
for (let tag of schema.tags || []) { for (let tag of schema.tags || []) {
@ -309,7 +320,9 @@ export class SchemaHelper {
headless: tag.name === '', headless: tag.name === '',
empty: !!tag['x-traitTag'], empty: !!tag['x-traitTag'],
methods: [], methods: [],
idx: catIdx
}; };
catIdx++;
} }
let paths = schema.paths; let paths = schema.paths;
@ -331,9 +344,11 @@ export class SchemaHelper {
tagDetails = { tagDetails = {
name: tag, name: tag,
id: id, id: id,
headless: tag === '' headless: tag === '',
idx: catIdx
}; };
tag2MethodMapping[id] = tagDetails; tag2MethodMapping[id] = tagDetails;
catIdx++;
} }
if (tagDetails.empty) continue; if (tagDetails.empty) continue;
if (!tagDetails.methods) tagDetails.methods = []; if (!tagDetails.methods) tagDetails.methods = [];
@ -341,7 +356,9 @@ export class SchemaHelper {
pointer: methodPointer, pointer: methodPointer,
summary: methodSummary, summary: methodSummary,
operationId: methodInfo.operationId, operationId: methodInfo.operationId,
tag: tag tag: tag,
idx: tagDetails.methods.length,
catIdx: tagDetails.idx
}); });
} }
} }

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Injectable, EventEmitter, Output } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { BrowserDomAdapter as DOM } from '../utils/browser-adapter'; import { BrowserDomAdapter as DOM } from '../utils/browser-adapter';
import { OptionsService } from './options.service'; import { OptionsService } from './options.service';
import { throttle } from '../utils/helpers'; import { throttle } from '../utils/helpers';
@ -14,14 +14,19 @@ export const INVIEW_POSITION = {
export class ScrollService { export class ScrollService {
scrollYOffset: any; scrollYOffset: any;
$scrollParent: any; $scrollParent: any;
@Output() scroll = new EventEmitter(); scroll = new EventEmitter();
private prevOffsetY: number; private prevOffsetY: number;
private _cancel:any; private _cancel:any;
constructor(optionsService:OptionsService) { private _savedPosition:number;
private _stickElement: HTMLElement;
constructor(private optionsService:OptionsService) {
this.scrollYOffset = () => optionsService.options.scrollYOffset(); this.scrollYOffset = () => optionsService.options.scrollYOffset();
this.$scrollParent = optionsService.options.$scrollParent; this.$scrollParent = optionsService.options.$scrollParent || window;
this.scroll = new EventEmitter(); this.scroll = new EventEmitter();
this.bind(); this.bind();
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
} }
scrollY() { scrollY() {
@ -42,20 +47,45 @@ export class ScrollService {
return INVIEW_POSITION.INVIEW; return INVIEW_POSITION.INVIEW;
} }
scrollTo($el, offset:number = 0) { scrollToPos(posY: number) {
// TODO: rewrite this to use offsetTop as more reliable solution
let subjRect = $el.getBoundingClientRect();
let posY = this.scrollY() + subjRect.top - this.scrollYOffset() + offset + 1;
if (this.$scrollParent.scrollTo) { if (this.$scrollParent.scrollTo) {
this.$scrollParent.scrollTo(0, posY); this.$scrollParent.scrollTo(0, Math.floor(posY));
} else { } else {
this.$scrollParent.scrollTop = posY; this.$scrollParent.scrollTop = posY;
} }
} }
scrollTo($el, offset:number = 0) {
if (!$el) return;
// TODO: rewrite this to use offsetTop as more reliable solution
let subjRect = $el.getBoundingClientRect();
let posY = this.scrollY() + subjRect.top - this.scrollYOffset() + offset + 1;
this.scrollToPos(posY);
return posY;
}
saveScroll() {
let $el = this._stickElement;
if (!$el) return;
let offsetParent = $el.offsetParent;
this._savedPosition = $el.offsetTop + (<any>offsetParent).offsetTop;
}
setStickElement($el) {
this._stickElement = $el;
}
restoreScroll() {
let $el = this._stickElement;
if (!$el) return;
let offsetParent = $el.offsetParent;
let currentPosition = $el.offsetTop + (<any>offsetParent).offsetTop;
let newY = this.scrollY() + (currentPosition - this._savedPosition);
this.scrollToPos(newY);
}
relativeScrollPos($el) { relativeScrollPos($el) {
let subjRect = $el.getBoundingClientRect(); let subjRect = $el.getBoundingClientRect();
return - subjRect.top + this.scrollYOffset() - 1; return -subjRect.top + this.scrollYOffset() - 1;
} }
scrollHandler(evt) { scrollHandler(evt) {

View File

@ -27,7 +27,7 @@ export class CopyButton implements OnInit {
onClick() { onClick() {
let copied; let copied;
if (this.copyText) { if (this.copyText) {
copied = Clipboard.copyCustom(this.copyText); copied = Clipboard.copyCustom(JSON.stringify(this.copyText));
} else { } else {
copied = Clipboard.copyElement(this.copyElement); copied = Clipboard.copyElement(this.copyElement);
} }

View File

@ -0,0 +1,172 @@
'use strict';
import {
Directive,
Input,
TemplateRef,
ChangeDetectorRef,
ViewContainerRef,
Injectable,
NgZone
} from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ScrollService } from '../../../services/scroll.service';
import { OptionsService } from '../../../services/options.service';
import { isSafari } from '../../../utils/helpers';
export class LazyForRow {
constructor(public $implicit: any, public index: number, public show: boolean) {}
get first(): boolean { return this.index === 0; }
get even(): boolean { return this.index % 2 === 0; }
get odd(): boolean { return !this.even; }
}
@Injectable()
export class LazyTasksService {
private _tasks = [];
private _current: number = 0;
private _syncCount: number = 0;
private menuService;
public loadProgress = new BehaviorSubject<number>(0);
public allSync = false;
constructor(public optionsService: OptionsService, private zone: NgZone) {
}
get empty() {
return this._current === this._tasks.length;
}
set syncCount(n: number) {
this._syncCount = n;
}
set lazy(sync:boolean) {
this.allSync = sync;
}
addTasks(tasks:any[], callback:Function) {
tasks.forEach((task) => {
let taskCopy = Object.assign({_callback: callback}, task);
this._tasks.push(taskCopy);
});
}
nextTaskSync() {
let task = this._tasks[this._current];
if (!task) return;
task._callback(task.idx, true);
this._current++;
this.menuService.enableItem(task.catIdx, task.idx);
this.loadProgress.next(this._current / this._tasks.length * 100);
}
nextTask() {
requestAnimationFrame(() => {
let task = this._tasks[this._current];
if (!task) return;
task._callback(task.idx, false).then(() => {
this._current++;
this.menuService.enableItem(task.catIdx, task.idx);
setTimeout(()=> this.nextTask());
this.loadProgress.next(this._current / this._tasks.length * 100);
}).catch(err => console.error(err));
});
}
sortTasks(catIdx, metIdx) {
let idxMap = {};
this._tasks.forEach((task, idx) => {
idxMap[task.catIdx + '_' + task.idx] = idx;
});
metIdx = metIdx < 0 ? 0 : metIdx;
let destIdx = idxMap[catIdx + '_' + metIdx] || 0;
this._tasks.sort((a, b) => {
let aIdx = idxMap[a.catIdx + '_' + a.idx];
let bIdx = idxMap[b.catIdx + '_' + b.idx];
return Math.abs(aIdx - destIdx) - Math.abs(bIdx - destIdx);
})
}
start(catIdx, metIdx, menuService) {
this.menuService = menuService;
let syncCount = 5;
// I know this is bad practice to detect browsers but there is an issue on Safari only
// http://stackoverflow.com/questions/40692365/maintaining-scroll-position-while-inserting-elements-above-glitching-only-in-sa
if (isSafari && this.optionsService.options.$scrollParent === window) {
syncCount = (metIdx >= 0) ?
this._tasks.findIndex(task => (task.catIdx === catIdx) && (task.idx === metIdx))
: this._tasks.findIndex(task => task.catIdx === catIdx);
syncCount += 1;
} else {
this.sortTasks(catIdx, metIdx);
}
if (this.allSync) syncCount = this._tasks.length;
for (var i = this._current; i < syncCount; i++) {
this.nextTaskSync();
}
this.nextTask();
}
}
@Injectable()
export class LazyTasksServiceSync extends LazyTasksService {
constructor(optionsService: OptionsService, zone: NgZone) {
super(optionsService, zone);
this.allSync = true;
}
}
@Directive({
selector: '[lazyFor][lazyForOf]'
})
export class LazyFor {
@Input() lazyForOf: any;
prevIdx = null;
private _viewRef;
constructor(
public _template: TemplateRef<LazyForRow>,
public cdr: ChangeDetectorRef,
public _viewContainer: ViewContainerRef,
public lazyTasks: LazyTasksService,
public scroll: ScrollService
){
}
nextIteration(idx: number, sync: boolean):Promise<void> {
const view = this._viewContainer.createEmbeddedView(
this._template, new LazyForRow(this.lazyForOf[idx], idx, sync), idx < this.prevIdx ? 0 : undefined);
this.prevIdx = idx;
view.context.index = idx;
(<any>view as ChangeDetectorRef).markForCheck();
(<any>view as ChangeDetectorRef).detectChanges();
if (sync) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
requestAnimationFrame(() => {
this.scroll.saveScroll();
view.context.show = true;
(<any>view as ChangeDetectorRef).markForCheck();
(<any>view as ChangeDetectorRef).detectChanges();
this.scroll.restoreScroll();
resolve();
});
});
}
ngOnInit() {
this.lazyTasks.addTasks(this.lazyForOf, this.nextIteration.bind(this))
}
}

View File

@ -83,7 +83,7 @@ export class StickySidebar implements OnInit, OnDestroy {
// FIXME use more reliable code // FIXME use more reliable code
this.$redocEl = this.$element.offsetParent.parentNode || DOM.defaultDoc().body; this.$redocEl = this.$element.offsetParent.parentNode || DOM.defaultDoc().body;
this.bind(); this.bind();
setTimeout(() => this.updatePosition()); requestAnimationFrame(() => this.updatePosition());
//this.updatePosition() //this.updatePosition()
} }

View File

@ -69,6 +69,9 @@ export class Tabs implements OnInit {
</div> </div>
`, `,
styles: [` styles: [`
:host {
display: block;
}
.tab-wrap { .tab-wrap {
display: none; display: none;
} }

View File

@ -12,8 +12,8 @@ $zippy-redirect-bg-color: rgba($zippy-redirect-color, .08);
:host { :host {
// performance optimization // performance optimization
transform: translate3d(0, 0, 0); // transform: translate3d(0, 0, 0);
backface-visibility: hidden; // backface-visibility: hidden;
overflow: hidden; overflow: hidden;
display: block; display: block;
} }

View File

@ -6,9 +6,11 @@ import { Zippy } from './Zippy/zippy';
import { CopyButton } from './CopyButton/copy-button.directive'; import { CopyButton } from './CopyButton/copy-button.directive';
import { SelectOnClick } from './SelectOnClick/select-on-click.directive'; import { SelectOnClick } from './SelectOnClick/select-on-click.directive';
import { DynamicNg2Viewer, DynamicNg2Wrapper } from './DynamicNg2Viewer/dynamic-ng2-viewer.component'; import { DynamicNg2Viewer, DynamicNg2Wrapper } from './DynamicNg2Viewer/dynamic-ng2-viewer.component';
import { LazyFor, LazyTasksService, LazyTasksServiceSync } from './LazyFor/lazy-for';
export const REDOC_COMMON_DIRECTIVES = [ export const REDOC_COMMON_DIRECTIVES = [
DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper, LazyFor
]; ];
export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper } export { DropDown, StickySidebar, Tabs, Tab, Zippy, CopyButton, SelectOnClick, DynamicNg2Viewer, DynamicNg2Wrapper, LazyFor }
export { LazyTasksService, LazyTasksServiceSync }

View File

@ -22,7 +22,7 @@ $base-font: Roboto;
$base-font-family: sans-serif; $base-font-family: sans-serif;
$base-font-weight: $light; $base-font-weight: $light;
$base-font-size: 1em; $base-font-size: 1em;
$base-line-height: 1.55em; $base-line-height: 1.5em;
$text-color: $black; $text-color: $black;
// Heading Font // Heading Font

View File

@ -74,3 +74,7 @@ export function throttle(fn, threshhold, scope) {
} }
}; };
} }
export const isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0
|| (function (p) { return p.toString() === '[object SafariRemoteNotification]'; })(!window['safari']
|| safari.pushNotification);

View File

@ -13,24 +13,10 @@ export class SpecManager {
public basePath: string; public basePath: string;
public spec = new BehaviorSubject<any|null>(null); public spec = new BehaviorSubject<any|null>(null);
private _instance: any;
private _url: string; private _url: string;
private parser: any; private parser: any;
static instance() {
return new SpecManager();
}
constructor() {
if (SpecManager.prototype._instance) {
return SpecManager.prototype._instance;
}
SpecManager.prototype._instance = this;
}
load(urlOrObject: string|Object) { load(urlOrObject: string|Object) {
this.schema = null;
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
this.parser = new JsonSchemaRefParser(); this.parser = new JsonSchemaRefParser();
this.parser.bundle(urlOrObject, {http: {withCredentials: false}}) this.parser.bundle(urlOrObject, {http: {withCredentials: false}})

View File

@ -19,4 +19,11 @@ declare var AOT: any;
interface ErrorStackTraceLimit { interface ErrorStackTraceLimit {
stackTraceLimit: number; stackTraceLimit: number;
} }
interface History {
scrollRestoration: "auto"|"manual";
}
interface Window {
HTMLElement: any
}
declare var safari: any;
interface ErrorConstructor extends ErrorStackTraceLimit {} interface ErrorConstructor extends ErrorStackTraceLimit {}

View File

@ -48,6 +48,7 @@ beforeEach(function() {
services.OptionsService, services.OptionsService,
services.ComponentParser, services.ComponentParser,
services.ContentProjector, services.ContentProjector,
{ provide: sharedComponents.LazyTasksService, useClass: sharedComponents.LazyTasksServiceSync },
{ provide: ErrorHandler, useClass: services.CustomErrorHandler }, { provide: ErrorHandler, useClass: services.CustomErrorHandler },
{ provide: services.COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': components.SecurityDefinitions }} { provide: services.COMPONENT_PARSER_ALLOWED, useValue: { 'security-definitions': components.SecurityDefinitions }}
], ],
@ -60,6 +61,14 @@ beforeEach(function() {
}); });
}); });
// afterEach(function() {
// TestBed.resetTestingModule();
// });
// afterEach(function() {
// TestBed.resetTestEnvironment();
// })
var testContext = require.context('..', true, /\.spec\.ts/); var testContext = require.context('..', true, /\.spec\.ts/);

View File

@ -9,11 +9,6 @@ describe('Utils', () => {
specMgr = new SpecManager(); specMgr = new SpecManager();
}); });
it('Should be a singleton', ()=> {
(new SpecManager()).should.be.equal(specMgr);
SpecManager.instance().should.be.equal(specMgr);
});
it('load should return a promise', ()=> { it('load should return a promise', ()=> {
specMgr.load('/tests/schemas/extended-petstore.yml').should.be.instanceof(Promise); specMgr.load('/tests/schemas/extended-petstore.yml').should.be.instanceof(Promise);
}); });
@ -35,15 +30,10 @@ describe('Utils', () => {
}); });
describe('Schema manager basic functionality', ()=> { describe('Schema manager basic functionality', ()=> {
beforeAll(function (done) { beforeEach(function (done) {
specMgr.load('/tests/schemas/extended-petstore.yml').then(() => { specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
done();
}, () => {
throw new Error('Error handler should not be called');
});
}); });
it('should contain non-empty schema', ()=> { it('should contain non-empty schema', ()=> {
specMgr.schema.should.be.an.Object(); specMgr.schema.should.be.an.Object();
specMgr.schema.should.be.not.empty(); specMgr.schema.should.be.not.empty();
@ -68,9 +58,9 @@ describe('Utils', () => {
it('should substitute api host when spec host is undefined', () => { it('should substitute api host when spec host is undefined', () => {
specMgr._schema.host = undefined; specMgr._schema.host = undefined;
specMgr._url = 'https://petstore.swagger.io/v2'; specMgr._url = 'http://petstore.swagger.io/v2';
specMgr.init(); specMgr.init();
specMgr.apiUrl.should.be.equal('https://petstore.swagger.io/v2'); specMgr.apiUrl.should.be.equal('http://petstore.swagger.io/v2');
}); });
describe('byPointer method', () => { describe('byPointer method', () => {
@ -88,7 +78,7 @@ describe('Utils', () => {
}); });
describe('getTagsMap method', () => { describe('getTagsMap method', () => {
beforeAll(function () { beforeEach(function () {
specMgr._schema = { specMgr._schema = {
tags: [ tags: [
{name: 'tag1', description: 'info1'}, {name: 'tag1', description: 'info1'},
@ -114,12 +104,8 @@ describe('Utils', () => {
}); });
describe('getMethodParams method', () => { describe('getMethodParams method', () => {
beforeAll((done:any) => { beforeEach((done:any) => {
specMgr.load('/tests/schemas/schema-mgr-methodparams.json').then(() => { specMgr.load('/tests/schemas/schema-mgr-methodparams.json').then(done, done.fail);
done();
}, () => {
done(new Error('Error handler should not be called'));
});
}); });
it('should propagate path parameters', () => { it('should propagate path parameters', () => {
@ -163,12 +149,8 @@ describe('Utils', () => {
}); });
describe('findDerivedDefinitions method', () => { describe('findDerivedDefinitions method', () => {
beforeAll((done:any) => { beforeEach((done) => {
specMgr.load('/tests/schemas/extended-petstore.yml').then(() => { specMgr.load('/tests/schemas/extended-petstore.yml').then(done, done.fail);
done();
}, () => {
done(new Error('Error handler should not be called'));
});
}); });
it('should find derived definitions for Pet', () => { it('should find derived definitions for Pet', () => {