Refactor + minor fixes/optimizations

This commit is contained in:
Roman Hotsiy 2016-10-23 20:18:42 +03:00
parent 76035c84d7
commit 17e7bf9fd8
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
41 changed files with 251 additions and 391 deletions

View File

@ -11,7 +11,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ApiInfo } from './api-info'; import { ApiInfo } from './api-info';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {
describe('ApiInfo Component', () => { describe('ApiInfo Component', () => {

View File

@ -12,7 +12,7 @@ import { OptionsService, MenuService } from '../../services/index';
export class ApiInfo extends BaseComponent implements OnInit { export class ApiInfo extends BaseComponent implements OnInit {
info: any = {}; info: any = {};
specUrl: String; specUrl: String;
constructor(specMgr:SpecManager, private optionsService:OptionsService, private menuServ: MenuService) { constructor(specMgr:SpecManager, private optionsService: OptionsService) {
super(specMgr); super(specMgr);
} }

View File

@ -10,7 +10,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { ApiLogo } from './api-logo'; import { ApiLogo } from './api-logo';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {

View File

@ -5,7 +5,7 @@ import { Component, ElementRef, ViewContainerRef, OnDestroy, Input,
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
import { OptionsService } from '../../services/options.service'; import { OptionsService } from '../../services/options.service';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
var cache = {}; var cache = {};
@ -38,7 +38,7 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
var componentFactory = this.resolver.resolveComponentFactory(JsonSchema); var componentFactory = this.resolver.resolveComponentFactory(JsonSchema);
let contextInjector = this.location.parentInjector; let contextInjector = this.location.parentInjector;
let compRef = this.location.createComponent(componentFactory, null, contextInjector, null); let compRef = this.location.createComponent(componentFactory, null, contextInjector, null);
this.initComponent(compRef.instance); this.projectComponentInputs(compRef.instance);
this._renderer.setElementAttribute(compRef.location.nativeElement, 'class', this.location.element.nativeElement.className); this._renderer.setElementAttribute(compRef.location.nativeElement, 'class', this.location.element.nativeElement.className);
compRef.changeDetectorRef.detectChanges(); compRef.changeDetectorRef.detectChanges();
this.loaded = true; this.loaded = true;
@ -58,7 +58,6 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
this.pointer = this.normalizePointer(); this.pointer = this.normalizePointer();
if (cache[this.pointer]) { if (cache[this.pointer]) {
let compRef = cache[this.pointer]; let compRef = cache[this.pointer];
setTimeout( ()=> {
let $element = compRef.location.nativeElement; let $element = compRef.location.nativeElement;
// skip caching view with descendant schemas // skip caching view with descendant schemas
@ -69,13 +68,12 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
} }
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();
} }
} }
initComponent(instance:JsonSchema) { projectComponentInputs(instance:JsonSchema) {
Object.assign(instance, this); Object.assign(instance, this);
} }

View File

@ -10,7 +10,7 @@ import { getChildDebugElement } from '../../../tests/helpers';
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
import { SpecManager } from '../../utils/SpecManager';; import { SpecManager } from '../../utils/spec-manager';;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -10,7 +10,7 @@ import {
import { getChildDebugElement } from '../../../tests/helpers'; import { getChildDebugElement } from '../../../tests/helpers';
import { Method } from './method'; import { Method } from './method';
import { SpecManager } from '../../utils/SpecManager';; import { SpecManager } from '../../utils/spec-manager';;
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -11,7 +11,7 @@ import { getChildDebugElement } from '../../../tests/helpers';
import { MethodsList } from './methods-list'; import { MethodsList } from './methods-list';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
describe('Redoc components', () => { describe('Redoc components', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -1,4 +1,8 @@
<div class="redoc-wrap" *ngIf="specLoaded"> <div class="redoc-error" *ngIf="error">
<h1>Oops... ReDoc failed to render this spec</h1>
<div class='redoc-error-details'>{{_error.message}}</div>
</div>
<div class="redoc-wrap" *ngIf="specLoaded && !error">
<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

@ -11,7 +11,7 @@ import {
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Redoc } from './redoc'; import { Redoc } from './redoc';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService } from '../../services/index'; import { OptionsService } from '../../services/index';
let optsMgr:OptionsService; let optsMgr:OptionsService;
@ -46,123 +46,6 @@ describe('Redoc components', () => {
fixture.destroy(); fixture.destroy();
}); });
}); });
// describe('Redoc init', () => {
// let dom = new BrowserDomAdapter();
// let elem;
// beforeEach(() => {
// elem = dom.createElement('redoc');
// dom.defaultDoc().body.appendChild(elem);
// });
//
// afterEach(() => {
// dom.defaultDoc().body.removeChild(elem);
// });
//
// it('should return promise', () => {
// let res = Redoc.init().catch(() => {/**/});
// res.should.be.instanceof(Promise);
// });
//
// it('should hide loading animation and display message in case of error', async(() => {
// spyOn(Redoc, 'hideLoadingAnimation').and.callThrough();
// spyOn(Redoc, 'displayError').and.callThrough();
// let res = Redoc.init();
// return res.catch(() => {
// expect(Redoc.hideLoadingAnimation).toHaveBeenCalled();
// expect(Redoc.displayError).toHaveBeenCalled();
// });
// }));
//
// //skip because of PhantomJS crashes on this testcase
// xit('should init redoc', (done) => {
// var node = document.createElement('redoc');
// document.body.appendChild(node);
// let res = Redoc.init('/tests/schemas/extended-petstore.yml');
// res.then(() => { done(); }, () => {
// done.fail('Error handler should not been called');
// });
// });
// });
//
// describe('Redoc destroy', () => {
// let builder;
// let fixture;
// let element;
// let dom;
// let destroySpy;
//
// beforeEach(async(inject([SpecManager, OptionsService, BrowserDomAdapter],
// ( specMgr, opts, _dom) => {
//
// optsMgr = opts;
// dom = _dom;
// return specMgr.load('/tests/schemas/extended-petstore.yml');
// })));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(TestAppComponent);
// element = getChildDebugElement(fixture.debugElement, 'methods-list').nativeElement;
// destroySpy = jasmine.createSpy('spy');
// Redoc.appRef = <ComponentRef<any>>{
// destroy: destroySpy
// };
// fixture.detectChanges();
// });
//
// afterEach(()=> {
// fixture.destroy();
// Redoc.appRef = null;
// });
//
// it('should call componentRef.destroy', () => {
// Redoc.destroy();
// expect(destroySpy).toHaveBeenCalled();
// });
//
// it('should create new host element', () => {
// element.parentElement.removeChild(element);
// Redoc.destroy();
// expect(dom.query('redoc')).not.toBeNull();
// dom.query('redoc').should.not.be.equal(element);
// });
//
// it('should set to null appRef', () => {
// Redoc.destroy();
// expect(Redoc.appRef).toBeNull();
// });
// });
//
// describe('Redoc autoInit', () => {
// const testURL = 'testurl';
// let dom = new BrowserDomAdapter();
// //let redocInitSpy;
// let elem: HTMLElement;
//
// beforeEach(() => {
// spyOn(Redoc, 'init').and.stub();
// elem = dom.createElement('redoc');
// dom.defaultDoc().body.appendChild(elem);
// dom.setAttribute(elem, 'spec-url', testURL);
// });
//
// it('should call Redoc.init with url from param spec-url', () => {
// Redoc.autoInit();
// expect(Redoc.init).toHaveBeenCalled();
// expect((<jasmine.Spy>Redoc.init).calls.argsFor(0)).toEqual([testURL]);
// });
//
// it('should not call Redoc.init when spec-url param is not provided', () => {
// dom.removeAttribute(elem, 'spec-url');
// Redoc.autoInit();
// expect(Redoc.init).not.toHaveBeenCalled();
// });
//
// afterEach(() => {
// (<jasmine.Spy>Redoc.init).and.callThrough();
// dom.defaultDoc().body.removeChild(elem);
// });
// });
}); });
/** Test component that contains a Redoc. */ /** Test component that contains a Redoc. */

View File

@ -1,15 +1,23 @@
'use strict'; 'use strict';
import { ElementRef, ComponentRef, ChangeDetectorRef, Input, import { ElementRef,
Component, OnInit, ChangeDetectionStrategy} from '@angular/core'; ComponentRef,
ChangeDetectorRef,
Input,
Component,
OnInit,
ChangeDetectionStrategy,
HostBinding
} from '@angular/core';
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter'; import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
import { BaseComponent } from '../base'; import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
import { OptionsService, RedocEventsService } from '../../services/index'; import { OptionsService, Hash, MenuService, AppStateService } from '../../services/index';
import { CustomErrorHandler } from '../../utils/';
@Component({ @Component({
selector: 'redoc', selector: 'redoc',
@ -18,45 +26,29 @@ import { OptionsService, RedocEventsService } from '../../services/index';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class Redoc extends BaseComponent implements OnInit { export class Redoc extends BaseComponent implements OnInit {
static appRef: ComponentRef<any>;
static _preOptions: any; static _preOptions: any;
specLoaded: boolean;
public options: any;
private element: any; private element: any;
error: any;
specLoaded: boolean;
options: any;
@Input() specUrl: string; @Input() specUrl: string;
@HostBinding('class.loading') specLoading: boolean = false;
@HostBinding('class.loading-remove') specLoadingRemove: boolean = false;
// TODO: refactor in separate component/service constructor(
showLoadingAnimation() { specMgr: SpecManager,
DOM.addClass(this.element, 'loading'); optionsMgr: OptionsService,
} elementRef: ElementRef,
private changeDetector: ChangeDetectorRef,
hideLoadingAnimation() { private appState: AppStateService
DOM.addClass(this.element, 'loading-remove'); ) {
setTimeout(() => {
DOM.removeClass(this.element, 'loading-remove');
DOM.removeClass(this.element, 'loading');
}, 400);
}
static displayError(err, elem?) {
let redocEl = elem || DOM.query('redoc');
if (!redocEl) return;
let heading = 'Oops... ReDoc failed to render this spec';
let details = err.message;
let erroHtml = `<div class="redoc-error">
<h1>${heading}</h1>
<div class='redoc-error-details'>${details}</div>`;
redocEl.innerHTML = erroHtml;
}
constructor(specMgr: SpecManager, optionsMgr:OptionsService, elementRef:ElementRef,
public events:RedocEventsService, private changeDetector: ChangeDetectorRef) {
super(specMgr); super(specMgr);
// merge options passed before init // merge options passed before init
optionsMgr.options = Redoc._preOptions || {}; optionsMgr.options = Redoc._preOptions || {};
this.element = elementRef.nativeElement; this.element = elementRef.nativeElement;
//parse options (top level component doesn't support inputs) //parse options (top level component doesn't support inputs)
optionsMgr.parseOptions( this.element ); optionsMgr.parseOptions( this.element );
@ -66,22 +58,34 @@ export class Redoc extends BaseComponent implements OnInit {
this.options = optionsMgr.options; this.options = optionsMgr.options;
} }
hideLoadingAnimation() {
this.specLoadingRemove = true;
setTimeout(() => {
this.specLoadingRemove = true;
this.specLoading = false;
}, 400);
}
load() { load() {
this.showLoadingAnimation(); this.specMgr.load(this.options.specUrl);
SpecManager.instance().load(this.options.specUrl).then(() => { this.specMgr.spec.subscribe((spec) => {
if (!spec) {
this.specLoading = true;
this.specLoaded = false;
} else {
this.specLoaded = true; this.specLoaded = true;
this.hideLoadingAnimation();
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
//this.changeDetector.detectChanges(); }
this.events.bootstrapped.next({}); });
this.hideLoadingAnimation();
}).catch((err) => {
this.hideLoadingAnimation();
Redoc.displayError(err, this.element);
throw err;
})
} }
ngOnInit() { ngOnInit() {
this.appState.error.subscribe(_err => {
this.error = _err;
this.changeDetector.detectChanges();
})
if (this.specUrl) { if (this.specUrl) {
this.options.specUrl = this.specUrl; this.options.specUrl = this.specUrl;
} }

View File

@ -1,12 +1,14 @@
'use strict'; 'use strict';
import { Component, ViewChildren, QueryList, EventEmitter, Input, import { Component, ViewChildren, QueryList, Input,
ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core'; ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BaseComponent, SpecManager } from '../base'; import { BaseComponent, SpecManager } from '../base';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import { Tabs } from '../../shared/components/index'; import { Tabs } from '../../shared/components/index';
import { RedocEventsService } from '../../services/index'; import { AppStateService } from '../../services/index';
@Component({ @Component({
selector: 'request-samples', selector: 'request-samples',
@ -21,18 +23,17 @@ export class RequestSamples extends BaseComponent implements OnInit {
@HostBinding('attr.hidden') hidden; @HostBinding('attr.hidden') hidden;
childTabs: Tabs; childTabs: Tabs;
selectedLang: EventEmitter<any>; selectedLang: Subject<any>;
samples: Array<any>; samples: Array<any>;
constructor(specMgr:SpecManager, public events:RedocEventsService) { constructor(specMgr:SpecManager, public appState:AppStateService) {
super(specMgr); super(specMgr);
this.selectedLang = this.events.samplesLanguageChanged; this.selectedLang = this.appState.samplesLanguage;
} }
changeLangNotify(lang) { changeLangNotify(lang) {
this.events.samplesLanguageChanged.next(lang); this.selectedLang.next(lang);
} }
init() { init() {

View File

@ -0,0 +1,2 @@
<h1> Hello world </h1>
<h2> {{message}} {{pointer}} </h2>

View File

@ -0,0 +1,28 @@
@import '../../shared/styles/variables';
.api-info-header {
font-weight: normal;
}
:host > div {
width: 60%;
padding: 40px;
box-sizing: border-box;
@media (max-width: $right-panel-squash-breakpoint) {
width: 100%;
}
}
a.openapi-button {
padding: 3px 8px 4px 8px;
color: $primary-color;
border: 1px solid $primary-color;
margin-left: 0.5em;
font-weight: normal;
}
:host /deep/ [section] {
padding-top: 60px;
margin-top: 20px;
}

View File

@ -0,0 +1,25 @@
'use strict';
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { SpecManager, BaseComponent } from '../base';
@Component({
selector: 'security-definitions',
styleUrls: ['./security-definitions.css'],
templateUrl: './security-definitions.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SecurityDefinitions extends BaseComponent implements OnInit {
info: any = {};
specUrl: String;
message: string = 'Roman';
constructor(specMgr:SpecManager) {
super(specMgr);
}
init() {
}
ngOnInit() {
this.preinit();
}
}

View File

@ -13,7 +13,7 @@ import { TestBed } from '@angular/core/testing';
import { MethodsList, SideMenu } from '../index'; import { MethodsList, SideMenu } from '../index';
import { SpecManager } from '../../utils/SpecManager'; import { SpecManager } from '../../utils/spec-manager';
let testOptions; let testOptions;

View File

@ -103,7 +103,6 @@ export class SideMenu extends BaseComponent implements OnInit {
destroy() { destroy() {
this.scrollService.unbind(); this.scrollService.unbind();
this.hash.unbind();
} }
ngOnInit() { ngOnInit() {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { BaseComponent } from '../components/base'; import { BaseComponent } from '../components/base';
describe('Redoc components', () => { describe('Redoc components', () => {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { OnInit, OnDestroy } from '@angular/core'; import { OnInit, OnDestroy } from '@angular/core';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
export { SpecManager }; export { SpecManager };

View File

@ -13,13 +13,14 @@ import { SideMenu } from './SideMenu/side-menu';
import { MethodsList } from './MethodsList/methods-list'; 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 { 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 ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions
]; ];
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc } ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions }

View File

@ -3,7 +3,7 @@ import './components/Redoc/redoc-initial-styles.css';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { Redoc } from './components/index'; import { Redoc } from './components/index';
import { SpecManager } from './utils/SpecManager'; import { SpecManager } from './utils/spec-manager';
import { BrowserDomAdapter as DOM } from './utils/browser-adapter'; import { BrowserDomAdapter as DOM } from './utils/browser-adapter';
import { disableDebugTools } from '@angular/platform-browser'; import { disableDebugTools } from '@angular/platform-browser';
@ -34,7 +34,7 @@ export function init(specUrl:string, options:any = {}) {
moduleRef = appRef; moduleRef = appRef;
console.log('ReDoc initialized!'); console.log('ReDoc initialized!');
}).catch(err => { }).catch(err => {
Redoc.displayError(err); //Redoc.displayError(err);
throw err; throw err;
}); });
}; };

View File

@ -1,26 +1,29 @@
import { NgModule } from '@angular/core'; import { NgModule, ErrorHandler } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Redoc, REDOC_DIRECTIVES } from './components/index'; import { Redoc, SecurityDefinitions, REDOC_DIRECTIVES } from './components/index';
import { REDOC_COMMON_DIRECTIVES } from './shared/components/index'; import { REDOC_COMMON_DIRECTIVES } from './shared/components/index';
import { REDOC_PIPES } from './utils/pipes'; import { REDOC_PIPES } from './utils/pipes';
import { CustomErrorHandler } from './utils/'
import { OptionsService, RedocEventsService, MenuService, import { OptionsService, MenuService,
ScrollService, Hash, WarningsService } from './services/index'; ScrollService, Hash, WarningsService, AppStateService } from './services/';
import { SpecManager } from './utils/SpecManager'; import { SpecManager } from './utils/spec-manager';
@NgModule({ @NgModule({
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],
providers: [ providers: [
SpecManager, SpecManager,
RedocEventsService,
ScrollService, ScrollService,
Hash, Hash,
MenuService, MenuService,
WarningsService, WarningsService,
OptionsService OptionsService,
AppStateService,
{ provide: ErrorHandler, useClass: CustomErrorHandler }
], ],
exports: [Redoc] exports: [Redoc]
}) })

View File

@ -0,0 +1,11 @@
'use strict';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class AppStateService {
samplesLanguage = new Subject<string>();
error = new BehaviorSubject<any>(null);
}

View File

@ -1,8 +1,9 @@
'use strict'; 'use strict';
var isSupported = document.queryCommandSupported && document.queryCommandSupported('copy');
export class Clipboard { export class Clipboard {
static isSupported():boolean { static isSupported():boolean {
return document.queryCommandSupported && document.queryCommandSupported('copy'); return isSupported;
} }
static selectElement(element:any):void { static selectElement(element:any):void {

View File

@ -1,8 +0,0 @@
'use strict';
import { EventEmitter, Output } from '@angular/core';
export class RedocEventsService {
@Output() bootstrapped = new EventEmitter();
@Output() samplesLanguageChanged = new EventEmitter();
}

View File

@ -1,25 +1,22 @@
'use strict'; 'use strict';
import {
inject
} from '@angular/core/testing';
import { RedocEventsService } from './events.service';
import { Hash } from './hash.service'; import { Hash } from './hash.service';
import { SpecManager } from '../utils/spec-manager';
describe('Hash Service', () => { describe('Hash Service', () => {
let events = new RedocEventsService(); let specMgr = new SpecManager();
let hashService; let hashService;
beforeEach(() => { beforeEach(inject([Hash], (_hash) => hashService = _hash));
hashService = new Hash(events);
});
afterEach(() => {
hashService.unbind();
});
it('should trigger changed event after ReDoc bootstrapped', (done) => { it('should trigger changed event after ReDoc bootstrapped', (done) => {
spyOn(hashService.changed, 'next').and.callThrough(); spyOn(hashService.value, 'next').and.callThrough();
events.bootstrapped.next({}); specMgr.spec.next({});
setTimeout(() => { setTimeout(() => {
expect(hashService.changed.next).toHaveBeenCalled(); expect(hashService.value.next).toHaveBeenCalled();
done(); done();
}); });
}); });

View File

@ -1,32 +1,31 @@
'use strict'; 'use strict';
import { Injectable, EventEmitter, Output } from '@angular/core'; import { Injectable } from '@angular/core';
import { BrowserDomAdapter as DOM } from '../utils/browser-adapter'; import { PlatformLocation } from '@angular/common';
import { global } from '@angular/core/src/facade/lang';
import { RedocEventsService } from './events.service'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { SpecManager } from '../utils/spec-manager';
@Injectable() @Injectable()
export class Hash { export class Hash {
@Output() changed = new EventEmitter(); public value = new BehaviorSubject<string>('');
private _cancel: any; constructor(private specMgr: SpecManager, private location: PlatformLocation) {
constructor(private events:RedocEventsService) {
this.bind(); this.bind();
events.bootstrapped.subscribe(() => this.changed.next(this.hash)); this.specMgr.spec.subscribe((spec) => {
} if (!spec) return;
setTimeout(() => {
get hash() { this.value.next(this.hash);
return DOM.getLocation().hash; });
}
bind() {
this._cancel = DOM.onAndCancel(global, 'hashchange', (evt) => {
this.changed.next(this.hash);
evt.preventDefault();
}); });
} }
unbind() { get hash() {
this._cancel(); return this.location.hash;
}
bind() {
this.location.onHashChange(() => {
this.value.next(this.hash);
});
} }
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
export * from './events.service'; export * from './app-state.service';
export * from './options.service'; export * from './options.service';
export * from './menu.service'; export * from './menu.service';
export * from './scroll.service'; export * from './scroll.service';

View File

@ -9,7 +9,7 @@ import {
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 { ScrollService } from './scroll.service';
import { SpecManager } from '../utils/SpecManager';; import { SpecManager } from '../utils/spec-manager';;
describe('Menu service', () => { describe('Menu service', () => {
beforeEach(() => { beforeEach(() => {
@ -36,39 +36,42 @@ describe('Menu service', () => {
it('should run hashScroll when hash changed', (done) => { it('should run hashScroll when hash changed', (done) => {
spyOn(menu, 'hashScroll').and.callThrough(); spyOn(menu, 'hashScroll').and.callThrough();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).toHaveBeenCalled();
menu.hashScroll.and.callThrough(); menu.hashScroll.and.callThrough();
done(); done();
}); });
hashService.changed.next(); 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, 'hashScroll').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).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();
done(); done();
}); });
hashService.changed.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, 'hashScroll').and.callThrough();
spyOn(window, 'scrollTo').and.stub(); spyOn(window, 'scrollTo').and.stub();
hashService.changed.subscribe(() => { hashService.value.subscribe((hash) => {
if (!hash) return;
expect(menu.hashScroll).toHaveBeenCalled(); expect(menu.hashScroll).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();
}); });
hashService.changed.next(hash); hashService.value.next(hash);
}); });
it('should select next/prev menu item when scrolled down/up', () => { it('should select next/prev menu item when scrolled down/up', () => {

View File

@ -2,7 +2,7 @@
import { Injectable, EventEmitter } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
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/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { SchemaHelper, MenuCategory } from './schema-helper.service'; import { SchemaHelper, MenuCategory } from './schema-helper.service';
const CHANGE = { const CHANGE = {
@ -30,7 +30,7 @@ export class MenuService {
this.changeActive(CHANGE.INITIAL); this.changeActive(CHANGE.INITIAL);
this.hash.changed.subscribe((hash) => { this.hash.value.subscribe((hash) => {
this.hashScroll(hash); this.hashScroll(hash);
}); });
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SchemaHelper } from './schema-helper.service'; import { SchemaHelper } from './schema-helper.service';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
describe('Spec Helper', () => { describe('Spec Helper', () => {
describe('buildMenuTree method', () => { describe('buildMenuTree method', () => {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { SpecManager } from '../utils/SpecManager'; 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';

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SchemaNormalizer } from './schema-normalizer.service'; import { SchemaNormalizer } from './schema-normalizer.service';
import { SpecManager } from '../utils/SpecManager';; import { SpecManager } from '../utils/spec-manager';;
describe('Spec Helper', () => { describe('Spec Helper', () => {
let specMgr:SpecManager = new SpecManager(); let specMgr:SpecManager = new SpecManager();

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SpecManager } from '../utils/SpecManager'; import { SpecManager } from '../utils/spec-manager';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { defaults } from '../utils/helpers'; import { defaults } from '../utils/helpers';
import { WarningsService } from './warnings.service'; import { WarningsService } from './warnings.service';

View File

@ -18,7 +18,6 @@ export class ScrollService {
private prevOffsetY: number; private prevOffsetY: number;
private _cancel:any; private _cancel:any;
constructor(optionsService:OptionsService) { constructor(optionsService:OptionsService) {
//events.bootstrapped.subscribe(() => this.hashScroll());
this.scrollYOffset = () => optionsService.options.scrollYOffset(); this.scrollYOffset = () => optionsService.options.scrollYOffset();
this.$scrollParent = optionsService.options.$scrollParent; this.$scrollParent = optionsService.options.$scrollParent;
this.scroll = new EventEmitter(); this.scroll = new EventEmitter();
@ -31,11 +30,12 @@ export class ScrollService {
/* returns 1 if element if above the view, 0 if in view and -1 below the view */ /* returns 1 if element if above the view, 0 if in view and -1 below the view */
getElementPos($el) { getElementPos($el) {
if (Math.floor($el.getBoundingClientRect().top) > this.scrollYOffset()) { let scrollYOffset = this.scrollYOffset();
if (Math.floor($el.getBoundingClientRect().top) > scrollYOffset) {
return INVIEW_POSITION.ABOVE; return INVIEW_POSITION.ABOVE;
} }
if ($el.getBoundingClientRect().bottom <= this.scrollYOffset()) { if ($el.getBoundingClientRect().bottom <= scrollYOffset) {
return INVIEW_POSITION.BELLOW; return INVIEW_POSITION.BELLOW;
} }
return INVIEW_POSITION.INVIEW; return INVIEW_POSITION.INVIEW;

View File

@ -1,10 +0,0 @@
/*eslint no-unused-vars: 0*/
/*eslint strict: 0*/
var $buoop = { vs: {i:9, f:25, o:12.1, s:7}, c:2 };
function $buo_f(){
var e = document.createElement('script');
e.src = '//browser-update.org/update.min.js';
document.body.appendChild(e);
}
try {document.addEventListener('DOMContentLoaded', $buo_f, false);}
catch(e){window['attachEvent']('onload', $buo_f);}

View File

@ -0,0 +1,12 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { AppStateService } from '../services/app-state.service';
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
constructor(private appState: AppStateService) {
}
handleError(error) {
console.log(error);
this.appState.error.next(error);
}
}

View File

@ -1,103 +0,0 @@
'use strict';
var Remarkable = require('remarkable');
var md = new Remarkable({
html: true,
linkify: true,
breaks: false,
typographer: false,
highlight: function (str, lang) {
if (lang === 'json')
lang = 'js';
var grammar = Prism.languages[lang];
if (!grammar)
return str;
return Prism.highlight(str, grammar);
}
});
function renderMd(rawText, headersHandler) {
var _origRule;
if (headersHandler) {
_origRule = {
open: md.renderer.rules.heading_open,
close: md.renderer.rules.heading_close
};
md.renderer.rules.heading_open = function (tokens, idx) {
if (tokens[idx].hLevel !== 1) {
return _origRule.open(tokens, idx);
}
else {
return headersHandler.open(tokens, idx);
}
};
md.renderer.rules.heading_close = function (tokens, idx) {
if (tokens[idx].hLevel !== 1) {
return _origRule.close(tokens, idx);
}
else {
return headersHandler.close(tokens, idx);
}
};
}
var res = md.render(rawText);
if (headersHandler) {
md.renderer.rules.heading_open = _origRule.open;
md.renderer.rules.heading_close = _origRule.close;
}
return res;
}
exports.renderMd = renderMd;
function statusCodeType(statusCode) {
if (statusCode < 100 || statusCode > 599) {
throw new Error('invalid HTTP code');
}
var res = 'success';
if (statusCode >= 300 && statusCode < 400) {
res = 'redirect';
}
else if (statusCode >= 400) {
res = 'error';
}
else if (statusCode < 200) {
res = 'info';
}
return res;
}
exports.statusCodeType = statusCodeType;
function defaults(target, src) {
var props = Object.keys(src);
var index = -1, length = props.length;
while (++index < length) {
var key = props[index];
if (target[key] === undefined) {
target[key] = src[key];
}
}
return target;
}
exports.defaults = defaults;
function safePush(obj, prop, val) {
if (!obj[prop])
obj[prop] = [];
obj[prop].push(val);
}
exports.safePush = safePush;
function throttle(fn, threshhold, scope) {
threshhold = threshhold || 250;
var last, deferTimer;
return function () {
var context = scope || this;
var now = +new Date, args = arguments;
if (last && now < last + threshhold) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
}
else {
last = now;
fn.apply(context, args);
}
};
}
exports.throttle = throttle;

1
lib/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './custom-error-handler';

View File

@ -5,10 +5,13 @@ import { JsonPointer } from './JsonPointer';
import { renderMd, safePush } from './helpers'; import { renderMd, safePush } from './helpers';
import * as slugify from 'slugify'; import * as slugify from 'slugify';
import { parse as urlParse, resolve as urlResolve } from 'url'; import { parse as urlParse, resolve as urlResolve } from 'url';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class SpecManager { export class SpecManager {
public _schema: any = {}; public _schema: any = {};
public apiUrl: string; public apiUrl: string;
public spec = new BehaviorSubject<any|null>(null);
private _instance: any; private _instance: any;
private _url: string; private _url: string;
@ -24,17 +27,19 @@ export class SpecManager {
SpecManager.prototype._instance = this; SpecManager.prototype._instance = this;
} }
load(url) { load(urlOrObject: string|Object) {
this.schema = null;
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
this._schema = {}; JsonSchemaRefParser.bundle(urlOrObject, {http: {withCredentials: false}})
JsonSchemaRefParser.bundle(url, {http: {withCredentials: false}})
.then(schema => { .then(schema => {
try { if (typeof urlOrObject === 'string') {
this._url = url; this._url = urlOrObject;
}
this._schema = schema; this._schema = schema;
try {
this.init(); this.init();
resolve(this._schema); resolve(this._schema);
this.spec.next(this._schema);
} catch(err) { } catch(err) {
reject(err); reject(err);
} }
@ -88,6 +93,11 @@ export class SpecManager {
return this._schema; return this._schema;
} }
set schema(val:any) {
this._schema = val;
this.spec.next(this._schema);
}
byPointer(pointer) { byPointer(pointer) {
let res = null; let res = null;
try { try {

View File

@ -19,11 +19,11 @@ require('zone.js/dist/jasmine-patch');
require('../lib/vendor'); require('../lib/vendor');
var TestBed = require('@angular/core/testing').TestBed; var TestBed = require('@angular/core/testing').TestBed;
var ErrorHandler = require('@angular/core').ErrorHandler;
var BrowserDynamicTestingModule = require('@angular/platform-browser-dynamic/testing').BrowserDynamicTestingModule; var BrowserDynamicTestingModule = require('@angular/platform-browser-dynamic/testing').BrowserDynamicTestingModule;
var platformBrowserDynamicTesting = require('@angular/platform-browser-dynamic/testing').platformBrowserDynamicTesting; var platformBrowserDynamicTesting = require('@angular/platform-browser-dynamic/testing').platformBrowserDynamicTesting;
var services = require('../lib/services/index'); var services = require('../lib/services/index');
var SpecManager = require('../lib/utils/SpecManager').SpecManager; var SpecManager = require('../lib/utils/spec-manager').SpecManager;
var BrowserDomAdapter = require('@angular/platform-browser/src/browser/browser_adapter').BrowserDomAdapter;
var REDOC_PIPES = require('../lib/utils/pipes').REDOC_PIPES; var REDOC_PIPES = require('../lib/utils/pipes').REDOC_PIPES;
var REDOC_COMMON_DIRECTIVES = require('../lib/shared/components/index').REDOC_COMMON_DIRECTIVES; var REDOC_COMMON_DIRECTIVES = require('../lib/shared/components/index').REDOC_COMMON_DIRECTIVES;
var REDOC_DIRECTIVES = require('../lib/components/index').REDOC_DIRECTIVES; var REDOC_DIRECTIVES = require('../lib/components/index').REDOC_DIRECTIVES;
@ -36,15 +36,14 @@ TestBed.initTestEnvironment(
beforeEach(function() { beforeEach(function() {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
BrowserDomAdapter,
SpecManager, SpecManager,
BrowserDomAdapter, services.AppStateService,
services.RedocEventsService,
services.ScrollService, services.ScrollService,
services.Hash, services.Hash,
services.MenuService, services.MenuService,
services.WarningsService, services.WarningsService,
services.OptionsService services.OptionsService,
{ provide: ErrorHandler, useClass: services.CustomErrorHandler }
], ],
declarations: [REDOC_PIPES, REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES] declarations: [REDOC_PIPES, REDOC_DIRECTIVES, REDOC_COMMON_DIRECTIVES]
}); });

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { SpecManager } from '../../lib/utils/SpecManager'; import { SpecManager } from '../../lib/utils/spec-manager';
describe('Utils', () => { describe('Utils', () => {
describe('Schema manager', () => { describe('Schema manager', () => {
let specMgr; let specMgr;