Merge branch 'master' into releases

This commit is contained in:
Roman Hotsiy 2016-01-24 23:25:30 +02:00
commit 35651acd10
48 changed files with 1231 additions and 5694 deletions

View File

@ -7,10 +7,91 @@
Swagger-generated API Reference Documentation Swagger-generated API Reference Documentation
**Under development**
[Live demo](http://rebilly.github.io/ReDoc/) [Live demo](http://rebilly.github.io/ReDoc/)
## Deployment
## tl;dr
```html
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--
ReDoc uses font options from the parent element
So override default browser styles
-->
<style>
body {
margin: 0;
padding: 0;
font-family: Verdana, Geneva, sans-serif;
font-size: 14px;
color: #333;
}
</style>
</head>
<body>
<redoc spec-url='http://petstore.swagger.io/v2/swagger.json'>
</redoc>
<script src="bower_components/redoc/dist/redoc.min.js"> </script>
</body>
</html>
```
#### 1. Install redoc
Install using [bower](bower.io):
bower install redoc
or using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
npm install redoc --save
Alternatively you can just download [`redoc.min.js`](https://raw.githubusercontent.com/Rebilly/ReDoc/releases/dist/redoc.min.js).
#### 2. Reference redoc script in HTML
Then reference [`redoc.min.js`](https://raw.githubusercontent.com/Rebilly/ReDoc/releases/dist/redoc.min.js) in your HTML page:
```html
<script src="bower_components/redoc/dist/redoc.min.js"> </script>
```
For npm:
```html
<script src="node_modules/redoc/dist/redoc.min.js"> </script>
```
#### 3. Add `<redoc>` element to your page
```html
<redoc spec-url="<url to your spec>"></redoc>
```
#### 4. Enjoy :smile:
## Configuration
* `spec-url` - relative or absolute url to your spec file
* `scroll-y-offset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc.
`scroll-y-offset` can be specified in various ways:
* **number**: A fixed number of pixels to 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).
## Advanced usage
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:
```js
Redoc.init(specUrl, options)
```
`options` is javascript object with camel-cased versions of options names as the keys. For example:
```js
Redoc.init('http://petstore.swagger.io/v2/swagger.json', {
scrollYOffset: 50
})
```
-----------
## Running locally ## Running locally
1. Clone repository 1. Clone repository
`git clone https://github.com/Rebilly/ReDoc.git` `git clone https://github.com/Rebilly/ReDoc.git`

View File

@ -1,7 +1,7 @@
{ {
"name": "redoc", "name": "redoc",
"description": "Swagger-generated API Reference Documentation", "description": "Swagger-generated API Reference Documentation",
"main": "dist/redoc.full.min.js", "main": "dist/redoc.min.js",
"authors": [ "authors": [
"Roman Hotsiy" "Roman Hotsiy"
], ],

View File

@ -3,7 +3,7 @@ module.exports = {
html: 'lib/**/*.html', html: 'lib/**/*.html',
scss: 'lib/**/*.scss', scss: 'lib/**/*.scss',
sourceEntryPoint: 'lib/index.js', sourceEntryPoint: 'lib/index.js',
outputName: 'redoc.full', outputName: 'redoc',
output: 'dist/', output: 'dist/',
tmp: '.tmp/', tmp: '.tmp/',
demo: 'demo/**/*', demo: 'demo/**/*',

View File

@ -55,7 +55,7 @@ var JS_DEV_DEPS_MIN = [
gulp.task('sass', function () { gulp.task('sass', function () {
return gulp.src(paths.scss, { base: './' }) return gulp.src(paths.scss, { base: './' })
.pipe(sass.sync().on('error', sass.logError)) .pipe(sass.sync({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(gulp.dest(paths.tmp)); .pipe(gulp.dest(paths.tmp));
}); });

View File

@ -3,21 +3,21 @@
<head> <head>
<title>ReDoc</title> <title>ReDoc</title>
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<nav> <nav>
<header> ReDoc </header> <header> ReDoc </header>
<form id="schema-url-form">
<input id="schema-url-input" value='swagger.json'> <input id="schema-url-input" value='swagger.json'>
<button id="load-button"> Explore </button> <button type="submit"> Explore </button>
</nav> </form>
</nav> </nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.json'> <redoc scroll-y-offset="body > nav" spec-url='swagger.json'></redoc>
Loading...
</redoc>
<!-- ReDoc built file with all dependencies included --> <!-- ReDoc built file with all dependencies included -->
<script src="dist/redoc.full.js"> </script> <script src="dist/redoc.js"> </script>
<script src="main.js"> </script> <script src="main.js"> </script>
</body> </body>
</html> </html>

View File

@ -19,21 +19,55 @@ nav header {
float: left; float: left;
margin-left: 20px; margin-left: 20px;
font-size: 25px; font-size: 25px;
color: white; color: #00329F;
position: absolute; font-weight: bold;
} }
nav input { nav input {
width: 50%; width: 50%;
box-sizing: border-box; box-sizing: border-box;
max-width: 500px; max-width: 500px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
nav input:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
} }
nav button { nav button {
border: 1px solid #FFFFFF; background-color: #fff;
cursor: pointer; color: #333;
color: white;
background-color: #21476D;
padding: 2px 10px; padding: 2px 10px;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid #ccc;
border-radius: 4px;
}
nav button:hover {
background-color: #e6e6e6;
border-color: #adadad;
}
nav button:active {
background-color: #d4d4d4;
border-color: #8c8c8c;
} }
nav { nav {
@ -41,8 +75,25 @@ nav {
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;
text-align: center; text-align: center;
background-color: #053361; background-color: white;
border-bottom: 1px solid #ccc;
position: fixed; position: fixed;
top: 0; top: 0;
z-index: 1; z-index: 1;
box-sizing: border-box;
}
@media (min-width: 1000px) {
nav header {
position: absolute;
}
}
@media (max-width: 500px) {
nav input {
width: 70%;
}
nav header {
display: none;
}
} }

View File

@ -1,9 +1,11 @@
;(function() { ;(function() {
'use strict'; 'use strict';
var loadButton = document.getElementById('load-button'); var schemaUrlForm = document.getElementById('schema-url-form');
var schemaUrlInput = document.getElementById('schema-url-input'); var schemaUrlInput = document.getElementById('schema-url-input');
loadButton.addEventListener('click', function() { schemaUrlForm.addEventListener('submit', function(event) {
event.preventDefault();
Redoc.init(schemaUrlInput.value); Redoc.init(schemaUrlInput.value);
return false;
}) })
})(); })();

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
},{ },{
"name": "JSONP", "name": "JSONP",
"x-traitTag": true, "x-traitTag": true,
"description": "If you're writing an AJAX application, and you'd like to wrap our response with a callback, all you have to do is specify a callback parameter with any API call:\n```\n https://api.instagram.com/v1/tags/coffee/media/recent?access_token=fb2e77d.47a0479900504cb3ab4a1f626d174d2d&callback=callbackFunction\n```\nWould respond with:\n```js\ncallbackFunction({\n ...\n});\n```", "description": "If you're writing an AJAX application, and you'd like to wrap our response with a callback, all you have to do is specify a callback parameter with any API call:\n```\n https://api.instagram.com/v1/tags/coffee/media/recent?access_token=fb2e77d.47a0479900504cb3ab4a1f626d174d2d&callback=callbackFunction\n```\nWould respond with:\n```js\ncallbackFunction({\n ...\n});\n``` \n > Example of markdown blockquote",
"externalDocs": { "externalDocs": {
"description": "Find out more", "description": "Find out more",
"url": "http://swagger.io" "url": "http://swagger.io"

View File

@ -1,34 +1,26 @@
'use strict'; 'use strict';
import {Component, View, OnInit, OnDestroy, ElementRef} from 'angular2/core'; import {Directive, ElementRef} from 'angular2/core';
import {BrowserDomAdapter} from 'angular2/platform/browser'; import {BrowserDomAdapter} from 'angular2/platform/browser';
@Component({ @Directive({
selector: 'sticky-sidebar', selector: '[sticky-sidebar]',
inputs: ['scrollParent', 'scrollYOffset'] inputs: ['scrollParent', 'scrollYOffset']
}) })
@View({
template: `
<div class="sticky-sidebar">
<ng-content></ng-content>
</div>
`,
lifecycle: [OnInit, OnDestroy]
})
export default class StickySidebar { export default class StickySidebar {
constructor(elementRef, adapter) { constructor(elementRef, dom) {
this.element = elementRef.nativeElement; this.element = elementRef.nativeElement;
this.adapter = adapter; this.dom = dom;
// initial styling // initial styling
this.adapter.setStyle(this.element, 'position', 'absolute'); this.dom.setStyle(this.element, 'position', 'absolute');
this.adapter.setStyle(this.element, 'top', '0'); this.dom.setStyle(this.element, 'top', '0');
this.adapter.setStyle(this.element, 'bottom', '0'); this.dom.setStyle(this.element, 'bottom', '0');
this.adapter.setStyle(this.element, 'max-height', '100%'); this.dom.setStyle(this.element, 'max-height', '100%');
} }
bind() { bind() {
this.cancelScrollBinding = this.adapter.onAndCancel(this.scrollParent, 'scroll', () => { this.updatePosition(); }); this.cancelScrollBinding = this.dom.onAndCancel(this.scrollParent, 'scroll', () => { this.updatePosition(); });
this.updatePosition(); this.updatePosition();
} }
@ -45,13 +37,13 @@ export default class StickySidebar {
} }
stick() { stick() {
this.adapter.setStyle(this.element, 'position', 'fixed'); this.dom.setStyle(this.element, 'position', 'fixed');
this.adapter.setStyle(this.element, 'top', this.scrollYOffset() + 'px'); this.dom.setStyle(this.element, 'top', this.scrollYOffset() + 'px');
} }
unstick() { unstick() {
this.adapter.setStyle(this.element, 'position', 'absolute'); this.dom.setStyle(this.element, 'position', 'absolute');
this.adapter.setStyle(this.element, 'top', 0); this.dom.setStyle(this.element, 'top', 0);
} }
get scrollY() { get scrollY() {

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { getChildDebugElement } from 'tests/helpers'; import { getChildDebugElementByType } from 'tests/helpers';
import {Component, View, provide} from 'angular2/core'; import {Component, View, provide} from 'angular2/core';
import {BrowserDomAdapter} from 'angular2/platform/browser'; import {BrowserDomAdapter} from 'angular2/platform/browser';
@ -29,7 +29,7 @@ describe('Common components', () => {
beforeEach((done) => { beforeEach((done) => {
builder.createAsync(TestApp).then(_fixture => { builder.createAsync(TestApp).then(_fixture => {
fixture = _fixture; fixture = _fixture;
let debugEl = getChildDebugElement(fixture.debugElement, 'sticky-sidebar'); let debugEl = getChildDebugElementByType(fixture.debugElement, StickySidebar);
component = debugEl.componentInstance; component = debugEl.componentInstance;
done(); done();
}, err => done.fail(err)); }, err => done.fail(err));
@ -67,8 +67,8 @@ describe('Common components', () => {
`<div style="padding-top: 20px"> `<div style="padding-top: 20px">
<div style="height: 20px; position: fixed; top: 0;"> </div> <div style="height: 20px; position: fixed; top: 0;"> </div>
<div style="position: relative"> <div style="position: relative">
<sticky-sidebar [scrollParent]="scrollParent" [scrollYOffset]="options.scrollYOffset"> <div sticky-sidebar [scrollParent]="scrollParent" [scrollYOffset]="options.scrollYOffset">
</sticky-sidebar> </div>
</div> </div>
</div>` </div>`

View File

@ -48,8 +48,8 @@ describe('Common components', () => {
let tabs = childDebugEls.map(debugEl => debugEl.componentInstance); let tabs = childDebugEls.map(debugEl => debugEl.componentInstance);
let [tab1, tab2] = tabs; let [tab1, tab2] = tabs;
tab1.active.should.be.true; tab1.active.should.be.true();
tab2.active.should.be.false; tab2.active.should.be.false();
}); });
it('should change active tab on click', () => { it('should change active tab on click', () => {

View File

@ -6,7 +6,7 @@ $side-bar-bg-color: #FAFAFA;
$side-menu-item-color: #384248; $side-menu-item-color: #384248;
$side-menu-even-bg-color: #F0F0F0; $side-menu-even-bg-color: #F0F0F0;
$side-menu-active-bg-color: #E6E6E6; $side-menu-active-bg-color: #DEDEDE;
$side-menu-item-hpadding: 20px; $side-menu-item-hpadding: 20px;
$side-menu-item-vpadding: 5px; $side-menu-item-vpadding: 5px;
@ -16,3 +16,5 @@ $sample-panel-headers-color: #8A9094;
$sample-panel-color: #CFD2D3; $sample-panel-color: #CFD2D3;
$tree-lines-color: #7D97CE; $tree-lines-color: #7D97CE;
$side-menu-mobile-breakpoint: 1000px;

View File

@ -2,4 +2,5 @@
.api-info-header { .api-info-header {
color: $headers-color; color: $headers-color;
font-weight: normal;
} }

View File

@ -0,0 +1,160 @@
@import '../../common/styles/variables';
$lines-width: 1px;
$bullet-size: 7px;
$cell-spacing: 25px;
$cell-padding: 10px;
$bullet-margin: 10px;
$line-border: $lines-width solid $tree-lines-color;
$line-border-erase: ($lines-width + 1px) solid white;
$param-name-height: 20px;
$sub-schema-offset: ($bullet-size/2) + $bullet-margin;
.param-schema {
padding-left: $sub-schema-offset - $lines-width;
border-left: $line-border;
}
.param-wrap {
position: relative;
}
.param-schema:before {
content: "";
position: absolute;
left: $sub-schema-offset;
top: ($param-name-height/2) + $cell-padding;
bottom: 0;
border-left: $line-border;
}
.param-name {
font-size: 14px;
padding: $cell-padding $cell-spacing $cell-padding 0;
font-weight: bold;
box-sizing: border-box;
line-height: $param-name-height;
border-left: $line-border;
white-space: nowrap;
position: relative;
}
.param-info {
width: 100%;
padding: $cell-padding 0;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
}
.param {
display: flex;
}
.param-required {
color: red;
font-weight: bold;
font-size: 12px;
line-height: $param-name-height;
vertical-align: middle;
}
.param-type {
color: #999;
font-size: 12px;
line-height: $param-name-height;
vertical-align: middle;
font-weight: bold;
}
.param-type.array:before {
content: "Array of ";
color: #999;
}
.param-type.string {
color: rgba(0, 80, 0, 0.7);
}
.param-type.integer, .param-type.number {
color: rgba(74, 139, 179, 0.8);
}
.param-type.object {
color: rgba(0, 50, 159, 0.7);
}
.param-type.boolean {
color: firebrick;
}
.param-type.with-hint {
&:before, &:after {
content: "\\00a0";
}
background-color: rgba(0, 50, 159, 0.1);
padding: 0.2em 0;
font-size: 0.85em;
border-radius: 3px;
cursor: help;
}
.param-type-trivial {
margin: 10px 10px 0;
display: inline-block;
}
/* tree */
// Bullet
.param-name > span:before {
content: "";
display: inline-block;
width: $bullet-size;
height: $bullet-size;
background-color: $tree-lines-color;
margin: 0 $bullet-margin;
vertical-align: middle;
}
.param-name > span:after {
content: "";
position: absolute;
border-top: $line-border;
width: $bullet-margin;
left: 0;
top: ($param-name-height/2) + $cell-padding;
}
.param-wrap:first-of-type > .param > .param-name:before {
content: "";
display: block;
position: absolute;
left: -$lines-width;
top: 0;
border-left: $line-border-erase;
height: ($param-name-height/2) + $cell-padding;
}
.param-wrap:last-of-type > .param > .param-name {
position: static;
&:after {
content: "";
display: block;
position: absolute;
left: -$lines-width;
border-left: $line-border-erase;
top: ($param-name-height/2) + $cell-padding + $lines-width;
background-color: white;
bottom: 0;
}
}
.param-wrap:last-of-type > .param-schema {
border-left-color: transparent;
}
.param-schema .param-wrap:first-of-type .param-name:before {
display: none !important;
}

View File

@ -0,0 +1,35 @@
'use strict';
import {Component, View, ElementRef} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
import JsonSchema from './json-schema';
import {DynamicComponentLoader} from 'angular2/src/core/linker/dynamic_component_loader';
import OptionsManager from '../../options';
@Component({
selector: 'json-schema-lazy',
inputs: ['pointer']
})
@View({
template: '',
directives: [CORE_DIRECTIVES]
})
export default class JsonSchemaLazy {
constructor(elementRef, dcl) {
this.elementRef = elementRef;
this.dcl = dcl;
}
load() {
if (OptionsManager.instance().options.disableLazySchemas) return;
if (this.loaded) return;
if (this.pointer) {
this.dcl.loadNextToLocation(JsonSchema, this.elementRef).then((compRef) => {
compRef.instance.pointer = this.pointer;
});
}
this.loaded = true;
}
}
JsonSchemaLazy.parameters = [[ElementRef], [DynamicComponentLoader]];

View File

@ -0,0 +1,87 @@
'use strict';
import { getChildDebugElement } from 'tests/helpers';
import {Component, View, provide} from 'angular2/core';
import {DynamicComponentLoader} from 'angular2/src/core/linker/dynamic_component_loader';
import {
TestComponentBuilder,
inject,
beforeEach,
beforeEachProviders,
it
} from 'angular2/testing';
import JsonSchemaLazy from 'lib/components/JsonSchema/json-schema-lazy';
import SchemaManager from 'lib/utils/SchemaManager';
describe('Redoc components', () => {
describe('JsonSchemaLazy Component', () => {
let builder;
let component;
let schemaMgr = new SchemaManager();
let fixture;
let loader;
let appRef = {
instance: {}
};
beforeEachProviders(() => [
provide(SchemaManager, {useValue: schemaMgr})
]);
beforeEach(inject([TestComponentBuilder, DynamicComponentLoader], (tcb, dcl) => {
builder = tcb;
loader = dcl;
spyOn(loader, 'loadNextToLocation').and.returnValue({then: (fn) => fn(appRef)});
}));
beforeEach((done) => {
builder.createAsync(TestApp).then(_fixture => {
fixture = _fixture;
let debugEl = getChildDebugElement(fixture.debugElement, 'json-schema-lazy');
component = debugEl.componentInstance;
done();
}, err => done.fail(err));
});
afterEach(() => {
loader.loadNextToLocation.and.callThrough();
});
it('should init component', () => {
expect(component).not.toBeNull();
});
it('should run loadNextToLocation on load', () => {
component.pointer = '#/def';
fixture.detectChanges();
component.load();
expect(loader.loadNextToLocation).toHaveBeenCalled();
});
it('should not run loadNextToLocation if already loaded', () => {
component.pointer = '#/def';
fixture.detectChanges();
component.load();
component.load();
expect(loader.loadNextToLocation.calls.count()).toEqual(1);
});
it('should init json-schema with correct pointer', () => {
component.pointer = '#/def';
fixture.detectChanges();
component.load();
expect(appRef.instance.pointer).toEqual(component.pointer);
});
});
});
/** Test component that contains a Method. */
@Component({selector: 'test-app'})
@View({
directives: [JsonSchemaLazy],
providers: [SchemaManager, DynamicComponentLoader],
template:
`<json-schema-lazy></json-schema-lazy>`
})
class TestApp {
}

View File

@ -1,4 +1,7 @@
<span *ngIf="isTrivial" class="param-type param-type-trivial" [ngClass]="type">{{_displayType}}</span> <span *ngIf="isTrivial" class="param-wrap">
<span class="param-type param-type-trivial {{type}}"
[ngClass]="{'with-hint': _displayTypeHint}" title="{{_displayTypeHint}}">{{_displayType}}</span>
</span>
<div *ngIf="!isTrivial" class="params-wrap" [ngClass]="{'params-array': isArray}"> <div *ngIf="!isTrivial" class="params-wrap" [ngClass]="{'params-array': isArray}">
<div *ngFor="#prop of data.properties" class="param-wrap"> <div *ngFor="#prop of data.properties" class="param-wrap">
<div class="param" [ngClass]="{'discriminator': prop.isDiscriminator}"> <div class="param" [ngClass]="{'discriminator': prop.isDiscriminator}">
@ -7,7 +10,8 @@
</div> </div>
<div class="param-info"> <div class="param-info">
<div> <div>
<span class="param-type" [ngClass]="prop.type">{{prop._displayType}} {{prop._displayFormat}}</span> <span class="param-type {{prop.type}}" [ngClass]="{'with-hint': prop._displayTypeHint}"
title="{{prop._displayTypeHint}}"> {{prop._displayType}} {{prop._displayFormat}}</span>
<span *ngIf="prop.isRequired" class="param-required">Required</span> <span *ngIf="prop.isRequired" class="param-required">Required</span>
</div> </div>
<div class="param-description" innerHtml="{{prop.description | marked}}"></div> <div class="param-description" innerHtml="{{prop.description | marked}}"></div>

View File

@ -63,14 +63,15 @@ export default class JsonSchema extends BaseComponent {
if (!schema.properties) { if (!schema.properties) {
this.isTrivial = true; this.isTrivial = true;
this._displayType = `${schema.type} (Custom key-value pairs)`; this._displayType = schema.type;
this._displayTypeHint = 'This field may contain data of any type';
return; return;
} }
let discriminatorFieldIdx = -1; let discriminatorFieldIdx = -1;
let props = Object.keys(schema.properties).map((prop, idx) => { let props = Object.keys(schema.properties).map((prop, idx) => {
let propData = schema.properties[prop]; let propData = schema.properties[prop];
this.injectPropData(prop, propData, schema); propData = this.injectPropData(prop, propData, schema);
if (propData.isDiscriminator) discriminatorFieldIdx = idx; if (propData.isDiscriminator) discriminatorFieldIdx = idx;
return propData; return propData;
}); });
@ -97,6 +98,7 @@ export default class JsonSchema extends BaseComponent {
} }
injectPropData(prop, propData, schema) { injectPropData(prop, propData, schema) {
propData = Object.assign({}, propData);
propData._name = prop; propData._name = prop;
propData.isRequired = this.requiredMap[prop]; propData.isRequired = this.requiredMap[prop];
propData._displayType = propData.type; propData._displayType = propData.type;
@ -108,16 +110,24 @@ export default class JsonSchema extends BaseComponent {
itemType = propData.items.title || 'object'; itemType = propData.items.title || 'object';
propData._pointer = propData.items._pointer || JsonPointer.join(this.pointer, ['properties', prop, 'items']); propData._pointer = propData.items._pointer || JsonPointer.join(this.pointer, ['properties', prop, 'items']);
} }
propData._displayType = `array of ${itemType}`; propData._displayType = `${itemType}`;
propData.format = itemFormat; propData.format = itemFormat;
propData._isArray = true; propData._isArray = true;
propData.type = 'array ' + propData.items.type;
} }
if (propData.type === 'object') { if (propData.type === 'object') {
propData._displayType = propData.title || 'object'; propData._displayType = propData.title || 'object';
} }
if (!propData.type) {
propData._displayType = '< * >';
propData._displayTypeHint = 'This field may contain data of any type';
}
if (propData.format) propData._displayFormat = `<${propData.format}>`; if (propData.format) propData._displayFormat = `<${propData.format}>`;
return propData;
} }
init() { init() {

View File

@ -1,129 +1,4 @@
@import '../../common/styles/variables'; @import 'json-schema-common';
$lines-width: 1px;
$bullet-size: 7px;
$cell-spacing: 25px;
$cell-padding: 10px;
$bullet-margin: 10px;
$line-border: $lines-width solid $tree-lines-color;
$line-border-erase: ($lines-width + 1px) solid white;
$param-name-height: 20px;
$sub-schema-offset: ($bullet-size/2) + $bullet-margin;
.param-schema {
padding-left: $sub-schema-offset - $lines-width;
border-left: $line-border;
}
.param-wrap {
position: relative;
}
.param-schema:before {
content: "";
position: absolute;
left: $sub-schema-offset;
top: ($param-name-height/2) + $cell-padding;
bottom: 0;
border-left: $line-border;
}
.param-name {
font-size: 14px;
padding: $cell-padding $cell-spacing $cell-padding 0;
font-weight: bold;
box-sizing: border-box;
line-height: $param-name-height;
border-left: $line-border;
white-space: nowrap;
position: relative;
}
.param-info {
width: 100%;
padding: $cell-padding 0;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
}
.param {
display: flex;
}
.param-required {
color: red;
font-weight: bold;
font-size: 12px;
line-height: $param-name-height;
vertical-align: middle;
}
.param-type {
text-transform: capitalize;
color: #999;
font-size: 12px;
line-height: $param-name-height;
vertical-align: middle;
font-weight: bold;
}
.param-type-trivial {
margin: 10px 10px 0;
display: inline-block;
}
/* tree */
// Bullet
.param-name > span:before {
content: "";
display: inline-block;
width: $bullet-size;
height: $bullet-size;
background-color: $tree-lines-color;
margin: 0 $bullet-margin;
vertical-align: middle;
}
.param-name > span:after {
content: "";
position: absolute;
border-top: $line-border;
width: $bullet-margin;
left: 0;
top: ($param-name-height/2) + $cell-padding;
}
.param-wrap:first-of-type .param-name:before {
content: "";
display: block;
position: absolute;
left: -$lines-width;
top: 0;
border-left: $line-border-erase;
height: ($param-name-height/2) + $cell-padding;
}
.param-wrap:last-of-type > .param > .param-name:after {
content: "";
display: block;
position: absolute;
left: -$lines-width - 1px;
border-left: $line-border-erase;
top: ($param-name-height/2) + $cell-padding + $lines-width;
background-color: white;
bottom: 0;
}
.param-wrap:last-of-type > .param-schema {
border-left-color: transparent;
}
.param-schema .param-wrap:first-of-type .param-name:before {
display: none !important;
}
/* styles for array-schema for array */ /* styles for array-schema for array */
$array-marker-font-sz: 12px; $array-marker-font-sz: 12px;
@ -166,6 +41,10 @@ $array-marker-line-height: 1.5;
height: ($param-name-height/2) + $cell-padding; height: ($param-name-height/2) + $cell-padding;
} }
.params-wrap > .param > .param-schema.param-array {
border-left-color: transparent;
}
.param.discriminator { .param.discriminator {
> div { > div {
padding-bottom: 0; padding-bottom: 0;

View File

@ -0,0 +1,73 @@
'use strict';
import { getChildDebugElement } from 'tests/helpers';
import {Component, View, provide} from 'angular2/core';
import {
TestComponentBuilder,
inject,
beforeEach,
beforeEachProviders,
it
} from 'angular2/testing';
import JsonSchema from 'lib/components/JsonSchema/json-schema';
import SchemaManager from 'lib/utils/SchemaManager';
describe('Redoc components', () => {
describe('JsonSchema Component', () => {
let builder;
let component;
let schemaMgr = new SchemaManager();
let fixture;
beforeEachProviders(() => [
provide(SchemaManager, {useValue: schemaMgr})
]);
beforeEach(inject([TestComponentBuilder], (tcb) => {
builder = tcb;
}));
beforeEach((done) => {
builder.createAsync(TestApp).then(_fixture => {
fixture = _fixture;
let debugEl = getChildDebugElement(fixture.debugElement, 'json-schema');
component = debugEl.componentInstance;
done();
}, err => done.fail(err));
});
it('should init component', () => {
component.pointer = '';
schemaMgr._schema = {type: 'object'};
fixture.detectChanges();
expect(component).not.toBeNull();
});
it('should set isTrivial for non-object/array types', () => {
component.pointer = '';
schemaMgr._schema = {type: 'string'};
fixture.detectChanges();
component.isTrivial.should.be.true();
});
it('should use < * > notation for prop without type', () => {
component.pointer = '';
schemaMgr._schema = {type: 'object', properties: {
test: {}
}};
fixture.detectChanges();
component.data.properties[0]._displayType.should.be.equal('< * >');
});
});
});
/** Test component that contains a Method. */
@Component({selector: 'test-app'})
@View({
directives: [JsonSchema],
providers: [SchemaManager],
template:
`<json-schema></json-schema>`
})
class TestApp {
}

View File

@ -103,6 +103,7 @@ responses-samples {
.method-description { .method-description {
padding: 30px 0; padding: 30px 0;
margin: 0;
} }
.http-method { .http-method {

View File

@ -15,7 +15,7 @@
.tag-info h1 { .tag-info h1 {
color: $headers-color; color: $headers-color;
text-transform: capitalize; text-transform: capitalize;
font-weight: bold; font-weight: normal;
} }
.methods { .methods {

View File

@ -4,9 +4,10 @@
padding: 0.2em 0; padding: 0.2em 0;
margin: 0.5em 0; margin: 0.5em 0;
color: #253137; color: #253137;
font-weight: normal;
} }
@import '../JsonSchema/json-schema.scss'; @import '../JsonSchema/json-schema-common';
// paramters can't be multilevel so table representation works for it without javascript // paramters can't be multilevel so table representation works for it without javascript

View File

@ -1,8 +1,8 @@
<div class="redoc-wrap"> <div class="redoc-wrap">
<sticky-sidebar [scrollParent]="scrollParent" [scrollYOffset]="options.scrollYOffset"> <div class="menu-content" sticky-sidebar [scrollParent]="scrollParent" [scrollYOffset]="options.scrollYOffset">
<api-logo> </api-logo> <api-logo> </api-logo>
<side-menu> </side-menu> <side-menu> </side-menu>
</sticky-sidebar> </div>
<div id="api-content"> <div id="api-content">
<api-info> </api-info> <api-info> </api-info>
<methods-list> </methods-list> <methods-list> </methods-list>

View File

@ -16,9 +16,11 @@ import {ElementRef} from 'angular2/core';
import {BrowserDomAdapter, bootstrap} from 'angular2/platform/browser'; import {BrowserDomAdapter, bootstrap} from 'angular2/platform/browser';
import detectScollParent from 'scrollparent'; import detectScollParent from 'scrollparent';
import {isFunction} from 'angular2/src/facade/lang'; import {isFunction, isString} from 'angular2/src/facade/lang';
let optionNames = new Set(['scrollYOffset']); let optionNames = new Set(['scrollYOffset', 'disableLazySchemas']);
let dom = new BrowserDomAdapter();
@RedocComponent({ @RedocComponent({
selector: 'redoc', selector: 'redoc',
@ -41,6 +43,7 @@ export default class Redoc extends BaseComponent {
this.parseOptions(); this.parseOptions();
this.options = Object.assign({}, optionsMgr.options, this.options); this.options = Object.assign({}, optionsMgr.options, this.options);
this.normalizeOptions(); this.normalizeOptions();
optionsMgr.options = this.options;
} }
parseOptions() { parseOptions() {
@ -79,12 +82,63 @@ export default class Redoc extends BaseComponent {
} }
} }
} }
if (isString(this.options.disableLazySchemas)) this.options.disableLazySchemas = true;
}
static showLoadingAnimation() {
if (!dom.query('#redoc-loading-style')) {
let animStyle = dom.createStyleElement(`
redoc.loading {
position: relative;
display: block;
min-height:350px;
}
redoc.loading:before {
content: "Loading...";
font-size: 28px;
text-align: center;
padding-top: 40px;
color: #3F5C9C;
font-weight: bold;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
z-index: 9999;
opacity: 1;
transition: all 0.6s ease-out;
}
redoc.loading-remove:before {
opacity: 0;
}
`);
animStyle.id = 'redoc-loading-style';
dom.appendChild(dom.defaultDoc().head, animStyle);
}
let elem = dom.query('redoc');
dom.addClass(elem, 'loading');
}
static hideLoadingAnimation() {
let redocEl = dom.query('redoc');
dom.addClass(redocEl, 'loading-remove');
setTimeout(() => {
dom.removeClass(redocEl, 'loading-remove');
dom.removeClass(redocEl, 'loading');
}, 400);
} }
static init(schemaUrl, options) { static init(schemaUrl, options) {
if (Redoc.appRef) { if (Redoc.appRef) {
Redoc.dispose(); Redoc.dispose();
} }
Redoc.showLoadingAnimation();
return SchemaManager.instance().load(schemaUrl) return SchemaManager.instance().load(schemaUrl)
.then(() => { .then(() => {
(new OptionsManager()).options = options; (new OptionsManager()).options = options;
@ -92,6 +146,7 @@ export default class Redoc extends BaseComponent {
}) })
.then( .then(
(appRef) => { (appRef) => {
Redoc.hideLoadingAnimation();
Redoc.appRef = appRef; Redoc.appRef = appRef;
redocEvents.bootstrapped.next(); redocEvents.bootstrapped.next();
console.log('ReDoc bootstrapped!'); console.log('ReDoc bootstrapped!');
@ -117,6 +172,7 @@ export default class Redoc extends BaseComponent {
static dispose() { static dispose() {
let dom = new BrowserDomAdapter(); let dom = new BrowserDomAdapter();
let el = dom.query('redoc'); let el = dom.query('redoc');
let elClone;
let parent; let parent;
let nextSibling; let nextSibling;
if (el) { if (el) {
@ -124,14 +180,15 @@ export default class Redoc extends BaseComponent {
nextSibling = el.nextElementSibling; nextSibling = el.nextElementSibling;
} }
elClone = el.cloneNode(false);
if (Redoc.appRef) { if (Redoc.appRef) {
Redoc.appRef.dispose(); Redoc.appRef.dispose();
Redoc.appRef = null; Redoc.appRef = null;
// Redoc dispose removes host element, so need to restore it // Redoc dispose removes host element, so need to restore it
el = dom.createElement('redoc'); elClone.innerHTML = 'Loading...';
el.innerText = 'Loading...'; parent && parent.insertBefore(elClone, nextSibling);
parent && parent.insertBefore(el, nextSibling);
} }
} }
} }

View File

@ -5,24 +5,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
:host {
pre {
white-space: pre-wrap;
background-color: #f2f2f2;
padding: 10px;
overflow-x: auto;
line-height: normal;
}
code {
background-color: #f2f2f2;
}
p {
margin: 0;
}
}
.redoc-wrap { .redoc-wrap {
position: relative; position: relative;
} }
@ -49,17 +31,48 @@ api-info {
api-logo { api-logo {
display: block; display: block;
text-align: center; text-align: center;
@media (max-width: $side-menu-mobile-breakpoint) {
display: none;
}
} }
sticky-sidebar { [sticky-sidebar] {
width: $side-bar-width; width: $side-bar-width;
background-color: $side-bar-bg-color;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
background-color: $side-bar-bg-color;
@media (max-width: $side-menu-mobile-breakpoint) {
z-index: 1;
width: 100%;
bottom: auto !important;
}
} }
#api-content { #api-content {
margin-left: $side-bar-width; margin-left: $side-bar-width;
@media (max-width: $side-menu-mobile-breakpoint) {
padding-top: 3em;
margin-left: 0;
}
}
#api-content:after {
content: "";
position: absolute;;
left:0;
right: 0;
top: 0;
bottom: 0;
background-color: black;
opacity: 0.5;
transition: all 0.3s ease;
display: none;
}
#api-content.menu-opened:after {
display: block;;
} }
footer { footer {
@ -77,3 +90,104 @@ footer {
color: $headers-color; color: $headers-color;
} }
} }
/* global redoc styles */
:host p {
margin: 0;
margin-bottom: 1em;
}
/* markdown elements */
:host .redoc-markdown-block {
pre {
font-family: Courier, monospace;
white-space: pre-wrap;
background-color: rgba(0,0,0,0.04);
padding: 10px;
overflow-x: auto;
line-height: normal;
border-radius: 3px;
code {
background-color: transparent;
&:before, &:after {
content: none;
}
}
}
code {
font-family: Courier, monospace;
background-color: rgba(0,0,0,0.04);
padding: 0.2em 0;
font-size: 0.85em;
border-radius: 3px;
&:before, &:after {
letter-spacing: -0.2em;
content: "\\00a0";
}
}
p {
margin: 0;
margin-bottom: 1em;
}
p:last-of-type {
margin-bottom: 0;
}
blockquote {
margin: 0;
margin-bottom: 1em;
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
img {
max-width: 100%;
box-sizing: content-box;
}
ul, ol {
padding-left: 2em;
margin: 0;
margin-bottom: 1em;
}
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
border-collapse: collapse;
border-spacing: 0;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
table tr {
background-color: #fff;
border-top: 1px solid #ccc;
&:nth-child(2n) {
background-color: #f8f8f8;
}
}
table th, table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
table th {
text-align: left;
font-weight: bold;
}
}

View File

@ -105,9 +105,19 @@ describe('Redoc components', () => {
}); });
}); });
}); });
});
describe('Redoc init', () => { 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', () => { it('should return promise', () => {
let res = Redoc.init(); let res = Redoc.init();
res.should.be.instanceof(Promise); res.should.be.instanceof(Promise);
@ -129,6 +139,7 @@ describe('Redoc init', () => {
done.fail('Error handler should not been called'); done.fail('Error handler should not been called');
}); });
}); });
});
describe('Redoc dispose', () => { describe('Redoc dispose', () => {
let builder; let builder;
@ -217,7 +228,7 @@ describe('Redoc init', () => {
@View({ @View({
directives: [Redoc], directives: [Redoc],
template: template:
`<redoc></redoc>` `<redoc disable-lazy-schemas></redoc>`
}) })
class TestApp { class TestApp {
} }

View File

@ -1,6 +1,6 @@
<h2 class="responses-list-header" *ngIf="data.responses.length"> Responses </h2> <h2 class="responses-list-header" *ngIf="data.responses.length"> Responses </h2>
<zippy *ngFor="#response of data.responses" title="{{response.code}} {{response.description}}" <zippy *ngFor="#response of data.responses" title="{{response.code}} {{response.description}}"
[type]="response.type" [empty]="!response.schema"> [type]="response.type" [empty]="response.empty" (open)="lazySchema.load()">
<div *ngIf="response.headers" class="response-headers"> <div *ngIf="response.headers" class="response-headers">
<header> <header>
Headers Headers
@ -14,6 +14,8 @@
<header> <header>
Response schema Response schema
</header> </header>
<json-schema *ngIf="response.schema" class="schema type" pointer="{{response.pointer}}/schema"> <json-schema *ngIf="response.schema && !enabledLazy" class="schema type" pointer="{{response.pointer}}/schema">
</json-schema> </json-schema>
<json-schema-lazy #lazySchema pointer="{{response.schema ? response.pointer + '/schema' : null}}">
</json-schema-lazy>
</zippy> </zippy>

View File

@ -3,8 +3,10 @@
import {RedocComponent, BaseComponent} from '../base'; import {RedocComponent, BaseComponent} from '../base';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import JsonSchema from '../JsonSchema/json-schema'; import JsonSchema from '../JsonSchema/json-schema';
import JsonSchemaLazy from '../JsonSchema/json-schema-lazy';
import Zippy from '../../common/components/Zippy/zippy'; import Zippy from '../../common/components/Zippy/zippy';
import {statusCodeType} from '../../utils/helpers'; import {statusCodeType} from '../../utils/helpers';
import OptionsManager from '../../options';
function isNumeric(n) { function isNumeric(n) {
return (!isNaN(parseFloat(n)) && isFinite(n)); return (!isNaN(parseFloat(n)) && isFinite(n));
@ -14,7 +16,7 @@ function isNumeric(n) {
selector: 'responses-list', selector: 'responses-list',
templateUrl: './lib/components/ResponsesList/responses-list.html', templateUrl: './lib/components/ResponsesList/responses-list.html',
styleUrls: ['./lib/components/ResponsesList/responses-list.css'], styleUrls: ['./lib/components/ResponsesList/responses-list.css'],
directives: [JsonSchema, Zippy] directives: [JsonSchema, Zippy, JsonSchemaLazy]
}) })
export default class ResponsesList extends BaseComponent { export default class ResponsesList extends BaseComponent {
constructor(schemaMgr) { constructor(schemaMgr) {
@ -24,6 +26,7 @@ export default class ResponsesList extends BaseComponent {
prepareModel() { prepareModel() {
this.data = {}; this.data = {};
this.data.responses = []; this.data.responses = [];
this.enabledLazy = !OptionsManager.instance().options.disableLazySchemas;
let responses = this.componentSchema; let responses = this.componentSchema;
if (!responses) return; if (!responses) return;
@ -40,6 +43,7 @@ export default class ResponsesList extends BaseComponent {
resp.pointer = ref; resp.pointer = ref;
} }
resp.empty = !resp.schema;
resp.code = respCode; resp.code = respCode;
resp.type = statusCodeType(resp.code); resp.type = statusCodeType(resp.code);
if (resp.headers) { if (resp.headers) {
@ -48,6 +52,7 @@ export default class ResponsesList extends BaseComponent {
respInfo.name = k; respInfo.name = k;
return respInfo; return respInfo;
}); });
resp.empty = false;
} }
resp.extendable = resp.headers || resp.length; resp.extendable = resp.headers || resp.length;
return resp; return resp;

View File

@ -3,6 +3,7 @@
padding: 0.2em 0; padding: 0.2em 0;
margin: 0.5em 0; margin: 0.5em 0;
color: #253137; color: #253137;
font-weight: normal;
} }
.header-name { .header-name {

View File

@ -13,9 +13,10 @@ header {
margin: 5px 0; margin: 5px 0;
color: $sample-panel-headers-color; color: $sample-panel-headers-color;
text-transform: uppercase; text-transform: uppercase;
font-weight: normal;
} }
:host tabs li { :host > tabs > ul li {
font-size: 13px; font-size: 13px;
margin: 2px 0; margin: 2px 0;
padding: 2px 5px; padding: 2px 5px;

View File

@ -1,5 +1,5 @@
<div class="snippet"> <div class="snippet">
<!-- in case sample is not available for some reason --> <!-- in case sample is not available for some reason -->
<pre *ngIf="data.sample == null"> Sample unavailable </pre> <pre *ngIf="data.sample == null"> Sample unavailable </pre>
<pre>{{data.sample | json}}</pre> <pre innerHtml="{{data.sample | jsonFormatter}}"></pre>
</div> </div>

View File

@ -4,19 +4,19 @@ import {RedocComponent, BaseComponent} from '../base';
import SchemaSampler from 'json-schema-instantiator'; import SchemaSampler from 'json-schema-instantiator';
import {JsonFormatter} from '../../utils/JsonFormatterPipe';
import {ElementRef} from 'angular2/core';
@RedocComponent({ @RedocComponent({
selector: 'schema-sample', selector: 'schema-sample',
templateUrl: './lib/components/SchemaSample/schema-sample.html', templateUrl: './lib/components/SchemaSample/schema-sample.html',
styles: [` pipes: [JsonFormatter],
pre { styleUrls: ['./lib/components/SchemaSample/schema-sample.css']
background-color: transparent;
padding: 0;
}
`]
}) })
export default class SchemaSample extends BaseComponent { export default class SchemaSample extends BaseComponent {
constructor(schemaMgr) { constructor(schemaMgr, elementRef) {
super(schemaMgr); super(schemaMgr);
this.element = elementRef.nativeElement;
} }
init() { init() {
@ -44,5 +44,19 @@ export default class SchemaSample extends BaseComponent {
} }
this.data.sample = sample; this.data.sample = sample;
this.element.addEventListener('click', (event) => {
var collapsed, target = event.target;
if (event.target.className === 'collapser') {
collapsed = target.parentNode.getElementsByClassName('collapsible')[0];
if (collapsed.parentNode.classList.contains('collapsed')) {
collapsed.parentNode.classList.remove('collapsed');
} else {
collapsed.parentNode.classList.add('collapsed');
}
}
});
} }
} }
SchemaSample.parameters = SchemaSample.parameters.concat([[ElementRef]]);

View File

@ -0,0 +1,112 @@
pre {
background-color: transparent;
padding: 0;
}
:host {
.property {
//font-weight: bold;
}
.type-null {
color: gray;
}
.type-boolean {
color: firebrick;
}
.type-number {
color: #4A8BB3;
}
.type-string {
color: #66B16E;
}
.callback-function {
color: gray;
}
.collapser:after {
content: "-";
cursor: pointer;
}
.collapsed > .collapser:after {
content: "+";
cursor: pointer;
}
.ellipsis:after {
content: "";
}
.collapsible {
margin-left: 2em;
}
.hoverable {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 2px;
padding-right: 2px;
border-radius: 2px;
}
.hovered {
background-color: rgba(235, 238, 249, 1);
}
.collapser {
padding-right: 6px;
padding-left: 6px;
}
ul, .redoc-json ul {
list-style-type: none;
padding: 0px;
margin: 0px 0px 0px 26px;
}
li {
position: relative;
}
.hoverable {
transition: background-color .2s ease-out 0s;
-webkit-transition: background-color .2s ease-out 0s;
display: inline-block;
}
.hovered {
transition-delay: .2s;
-webkit-transition-delay: .2s;
}
.selected {
outline-style: solid;
outline-width: 1px;
outline-style: dotted;
}
.collapsed>.collapsible {
display: none;
}
.ellipsis {
display: none;
}
.collapsed>.ellipsis {
display: inherit;
}
.collapser {
position: absolute;
top: 1px;
left: -1.5em;
cursor: default;
user-select: none;
-webkit-user-select: none;
}
}

View File

@ -1,5 +1,13 @@
<h2 class="menu-header"> Api reference </h2> <div class="mobile-nav" (click)="toggleMobileNav()">
<div *ngFor="var cat of data.menu; #idx = index" class="menu-cat"> <span class="menu-header"> API Reference: </span>
<span class="selected-item-info">
<span class="selected-tag"> {{activeCatCaption}} </span>
<span class="selected-endpoint">{{activeItemCaption}}</span>
</span>
</div>
<div id="resources-nav">
<h2 class="menu-header"> API reference </h2>
<div *ngFor="var cat of data.menu; #idx = index" class="menu-cat">
<label class="menu-cat-header" (click)="activateAndScroll(idx, -1)" [ngClass]="{active: cat.active}"> {{cat.name}}</label> <label class="menu-cat-header" (click)="activateAndScroll(idx, -1)" [ngClass]="{active: cat.active}"> {{cat.name}}</label>
<ul class="menu-subitems" [ngClass]="{active: cat.active}"> <ul class="menu-subitems" [ngClass]="{active: cat.active}">
@ -10,4 +18,5 @@
</li> </li>
</ul> </ul>
</div>
</div> </div>

View File

@ -3,7 +3,7 @@
import {RedocComponent, BaseComponent} from '../base'; import {RedocComponent, BaseComponent} from '../base';
import {redocEvents} from '../../events'; import {redocEvents} from '../../events';
import {NgZone, ChangeDetectionStrategy} from 'angular2/core'; import {NgZone, ChangeDetectionStrategy, ElementRef} from 'angular2/core';
import {document} from 'angular2/src/facade/browser'; import {document} from 'angular2/src/facade/browser';
import {BrowserDomAdapter} from 'angular2/platform/browser'; import {BrowserDomAdapter} from 'angular2/platform/browser';
import {global} from 'angular2/src/facade/lang'; import {global} from 'angular2/src/facade/lang';
@ -27,13 +27,15 @@ const INVIEW_POSITION = {
changeDetection: ChangeDetectionStrategy.Default changeDetection: ChangeDetectionStrategy.Default
}) })
export default class SideMenu extends BaseComponent { export default class SideMenu extends BaseComponent {
constructor(schemaMgr, adapter, zone, redoc) { constructor(schemaMgr, elementRef, adapter, zone, redoc) {
super(schemaMgr); super(schemaMgr);
this.zone = zone; this.zone = zone;
this.adapter = adapter; this.adapter = adapter;
this.redoc = redoc; this.redoc = redoc;
this.scrollParent = this.redoc.scrollParent; this.scrollParent = this.redoc.scrollParent;
this.mobileNav = adapter.querySelector(elementRef.nativeElement, '.mobile-nav');
this.resourcesNav = adapter.querySelector(elementRef.nativeElement, '#resources-nav');
// for some reason constructor is not run inside zone // for some reason constructor is not run inside zone
// as workaround running it manually // as workaround running it manually
@ -45,6 +47,9 @@ export default class SideMenu extends BaseComponent {
this.prevOffsetY = null; this.prevOffsetY = null;
redocEvents.bootstrapped.subscribe(() => this.hashScroll()); redocEvents.bootstrapped.subscribe(() => this.hashScroll());
this.activeCatCaption = '';
this.activeItemCaption = '';
} }
scrollY() { scrollY() {
@ -65,7 +70,12 @@ export default class SideMenu extends BaseComponent {
bindEvents() { bindEvents() {
this.prevOffsetY = this.scrollY(); this.prevOffsetY = this.scrollY();
this.scrollYOffset = this.redoc.options.scrollYOffset;
//decorate option.scrollYOffset to account mobile nav
this.scrollYOffset = () => {
let mobileNavOffset = this.mobileNav.clientHeight;
return this.redoc.options.scrollYOffset() + mobileNavOffset;
};
this._cancel = {}; this._cancel = {};
this._cancel.scroll = this.adapter.onAndCancel(this.scrollParent, 'scroll', () => { this.scrollHandler(); }); this._cancel.scroll = this.adapter.onAndCancel(this.scrollParent, 'scroll', () => { this.scrollHandler(); });
this._cancel.hash = this.adapter.onAndCancel(global, 'hashchange', evt => this.hashScroll(evt)); this._cancel.hash = this.adapter.onAndCancel(global, 'hashchange', evt => this.hashScroll(evt));
@ -77,6 +87,9 @@ export default class SideMenu extends BaseComponent {
} }
activateAndScroll(idx, methodIdx) { activateAndScroll(idx, methodIdx) {
if (this.mobileMode()) {
this.toggleMobileNav();
}
this.activate(idx, methodIdx); this.activate(idx, methodIdx);
this.scrollToActive(); this.scrollToActive();
} }
@ -98,6 +111,10 @@ export default class SideMenu extends BaseComponent {
activate(catIdx, methodIdx) { activate(catIdx, methodIdx) {
let menu = this.data.menu; let menu = this.data.menu;
this.activeCatCaption = '';
this.activeItemCaption = '';
menu[this.activeCatIdx].active = false; menu[this.activeCatIdx].active = false;
if (menu[this.activeCatIdx].methods.length) { if (menu[this.activeCatIdx].methods.length) {
if (this.activeMethodIdx >= 0) { if (this.activeMethodIdx >= 0) {
@ -108,11 +125,13 @@ export default class SideMenu extends BaseComponent {
this.activeCatIdx = catIdx; this.activeCatIdx = catIdx;
this.activeMethodIdx = methodIdx; this.activeMethodIdx = methodIdx;
menu[catIdx].active = true; menu[catIdx].active = true;
this.activeCatCaption = menu[catIdx].name;
this.activeMethodPtr = null; this.activeMethodPtr = null;
if (menu[catIdx].methods.length && (methodIdx > -1)) { if (menu[catIdx].methods.length && (methodIdx > -1)) {
let currentItem = menu[catIdx].methods[methodIdx]; let currentItem = menu[catIdx].methods[methodIdx];
currentItem.active = true; currentItem.active = true;
this.activeMethodPtr = currentItem.pointer; this.activeMethodPtr = currentItem.pointer;
this.activeItemCaption = currentItem.summary;
} }
} }
@ -199,8 +218,26 @@ export default class SideMenu extends BaseComponent {
); );
} }
mobileMode() {
return this.mobileNav.clientHeight > 0;
}
toggleMobileNav() {
let dom = this.adapter;
let overflowParent = (this.scrollParent === global) ? dom.defaultDoc().body : this.scrollParent;
if (dom.hasStyle(this.resourcesNav, 'height')) {
dom.removeStyle(this.resourcesNav, 'height');
dom.removeStyle(overflowParent, 'overflow-y');
} else {
let viewportHeight = this.scrollParent.innerHeight || this.scrollParent.clientHeight;
let height = viewportHeight - this.mobileNav.getBoundingClientRect().bottom;
dom.setStyle(overflowParent, 'overflow-y', 'hidden');
dom.setStyle(this.resourcesNav, 'height', height + 'px');
}
}
init() { init() {
this.changeActive(CHANGE.INITIAL); this.changeActive(CHANGE.INITIAL);
} }
} }
SideMenu.parameters = SideMenu.parameters.concat([[BrowserDomAdapter], [NgZone]]); SideMenu.parameters = SideMenu.parameters.concat([[ElementRef], [BrowserDomAdapter], [NgZone]]);

View File

@ -1,4 +1,5 @@
@import '../../common/styles/variables'; @import '../../common/styles/variables';
$mobile-menu-compact-breakpoint: 550px;
.menu-header { .menu-header {
text-transform: uppercase; text-transform: uppercase;
@ -9,7 +10,6 @@
} }
.menu-cat-header { .menu-cat-header {
//font-weight: bold;
font-size: 15px; font-size: 15px;
cursor: pointer; cursor: pointer;
color: $side-menu-item-color; color: $side-menu-item-color;
@ -24,6 +24,10 @@
background-color: $side-menu-even-bg-color; background-color: $side-menu-even-bg-color;
} }
.menu-cat .menu-cat-header.active {
background-color: darken($side-menu-active-bg-color, 5%);
}
.menu-subitems { .menu-subitems {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -61,7 +65,89 @@
} }
} }
.menu-cat-header.active, .menu-subitems li.active { .menu-cat .menu-subitems {
background-color: $side-menu-active-bg-color !important; li.active, li.active:nth-of-type(even) {
font-weight: bold; background-color: $side-menu-active-bg-color;
}
}
.mobile-nav {
display: none;
height: 3em;
line-height: 3em;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
cursor: pointer;
&:after {
content: "";
display: inline-block;
width: 3em;
height: 3em;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve"><polygon fill="#010101" points="23.1 34.1 51.5 61.7 80 34.1 81.5 35 51.5 64.1 21.5 35 23.1 34.1 "/></svg>');
background-size: 70%;
background-repeat: no-repeat;
background-position: center;
float: right;
vertical-align: middle;
}
.menu-header {
padding: 0 10px 0 20px;
font-size: 0.95em;
@media (max-width: $mobile-menu-compact-breakpoint) {
display: none;
}
}
}
@media (max-width: $side-menu-mobile-breakpoint) {
.mobile-nav {
display: block;
}
#resources-nav {
height: 0;
overflow-y: auto;
transition: all 0.3s ease;
}
#resources-nav .menu-header {
display: none;
}
.menu-subitems {
height: auto;
}
}
.selected-tag {
text-transform: capitalize;
}
.selected-endpoint:before {
content: "/";
padding: 0 2px;
color: #ccc;
}
.selected-endpoint:empty:before {
display: none;
}
.selected-item-info {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
box-sizing: border-box;
max-width: 350px;
@media (max-width: $mobile-menu-compact-breakpoint) {
display: inline-block;
padding: 0 20px;
max-width: 80%;
max-width: calc(100% - 4em);
}
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import {Component, View, OnInit, OnDestroy, ChangeDetectionStrategy} from 'angular2/core'; import {Component, View, OnInit, OnDestroy, ChangeDetectionStrategy} from 'angular2/core';
import {CORE_DIRECTIVES, JsonPipe} from 'angular2/common'; import {CORE_DIRECTIVES, JsonPipe, AsyncPipe} from 'angular2/common';
import SchemaManager from '../utils/SchemaManager'; import SchemaManager from '../utils/SchemaManager';
import JsonPointer from '../utils/JsonPointer'; import JsonPointer from '../utils/JsonPointer';
import {MarkedPipe, JsonPointerEscapePipe} from '../utils/pipes'; import {MarkedPipe, JsonPointerEscapePipe} from '../utils/pipes';
@ -49,7 +49,7 @@ function snapshot(obj) {
export function RedocComponent(options) { export function RedocComponent(options) {
let inputs = safeConcat(options.inputs, commonInputs); let inputs = safeConcat(options.inputs, commonInputs);
let directives = safeConcat(options.directives, CORE_DIRECTIVES); let directives = safeConcat(options.directives, CORE_DIRECTIVES);
let pipes = safeConcat(options.pipes, [JsonPointerEscapePipe, MarkedPipe, JsonPipe]); let pipes = safeConcat(options.pipes, [JsonPointerEscapePipe, MarkedPipe, JsonPipe, AsyncPipe]);
return function decorator(target) { return function decorator(target) {
@ -104,7 +104,9 @@ export class BaseComponent {
let dereferencedCache = {}; let dereferencedCache = {};
let resolve = (schema) => { let resolve = (schema) => {
let resolvedRef;
if (schema && schema.$ref) { if (schema && schema.$ref) {
resolvedRef = schema.$ref;
let resolved = this.schemaMgr.byPointer(schema.$ref); let resolved = this.schemaMgr.byPointer(schema.$ref);
let baseName = JsonPointer.baseName(schema.$ref); let baseName = JsonPointer.baseName(schema.$ref);
if (!dereferencedCache[schema.$ref]) { if (!dereferencedCache[schema.$ref]) {
@ -114,11 +116,12 @@ export class BaseComponent {
} else { } else {
// for circular referenced save only title and type // for circular referenced save only title and type
resolved = { resolved = {
title: resolved.title title: resolved.title,
type: resolved.type
}; };
} }
dereferencedCache[schema.$ref] = true; dereferencedCache[schema.$ref] = dereferencedCache[schema.$ref] ? dereferencedCache[schema.$ref] + 1 : 1;
resolved.title = resolved.title || baseName; resolved.title = resolved.title || baseName;
@ -133,7 +136,6 @@ export class BaseComponent {
schema = schema.description ? { schema = schema.description ? {
description: schema.description description: schema.description
} : {}; } : {};
//for (var prop in schema) delete schema[prop];
Object.assign(schema, resolved); Object.assign(schema, resolved);
} }
@ -143,10 +145,11 @@ export class BaseComponent {
schema[key] = resolve(value); schema[key] = resolve(value);
} }
}); });
if (resolvedRef) dereferencedCache[resolvedRef] = dereferencedCache[resolvedRef] ? dereferencedCache[resolvedRef] - 1 : 0;
return schema; return schema;
}; };
this.componentSchema = resolve(schema); this.componentSchema = resolve(schema, 1);
} }
joinAllOf(schema = this.componentSchema, opts) { joinAllOf(schema = this.componentSchema, opts) {

View File

@ -14,12 +14,17 @@ export default class OptionsManager {
OptionsManager.prototype._instance = this; OptionsManager.prototype._instance = this;
this._defaults = { this._defaults = {
scrollYOffset: 0 scrollYOffset: 0,
disableLazySchemas: false
}; };
this._options = {}; this._options = {};
} }
static instance() {
return new OptionsManager();
}
get options() { get options() {
return this._options; return this._options;
} }

View File

@ -0,0 +1,106 @@
'use strict';
import {Pipe} from 'angular2/core';
import {isBlank} from 'angular2/src/facade/lang';
var level = 1;
const COLLAPSE_LEVEL = 2;
@Pipe({ name: 'jsonFormatter' })
export class JsonFormatter {
transform(value) {
if (isBlank(value)) return value;
return jsonToHTML(value);
}
}
function htmlEncode(t) {
return t != null ? t.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : '';
}
function decorateWithSpan(value, className) {
return '<span class="' + className + '">' + htmlEncode(value) + '</span>';
}
function valueToHTML(value) {
var valueType = typeof value, output = '';
if (value == null) {
output += decorateWithSpan('null', 'type-null');
}
else if (value && value.constructor === Array) {
level++;
output += arrayToHTML(value);
level--;
}
else if (valueType === 'object') {
level++;
output += objectToHTML(value);
level--;
}
else if (valueType === 'number') {
output += decorateWithSpan(value, 'type-number');
}
else if (valueType === 'string') {
if (/^(http|https):\/\/[^\\s]+$/.test(value)) {
output += decorateWithSpan('"', 'type-string') + '<a href="' + value + '">' + htmlEncode(value) + '</a>' + decorateWithSpan('"', 'type-string');
} else {
output += decorateWithSpan('"' + value + '"', 'type-string');
}
} else if (valueType === 'boolean') {
output += decorateWithSpan(value, 'type-boolean');
}
return output;
}
function arrayToHTML(json) {
var collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
var i, length;
var output = '<div class="collapser"></div>[<span class="ellipsis"></span><ul class="array collapsible">';
var hasContents = false;
for (i = 0, length = json.length; i < length; i++) {
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += valueToHTML(json[i]);
if (i < length - 1) {
output += ',';
}
output += '</div></li>';
}
output += '</ul>]';
if (!hasContents) {
output = '[ ]';
}
return output;
}
function objectToHTML(json) {
var collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
var i, key, length, keys = Object.keys(json);
var output = '<div class="collapser"></div>{<span class="ellipsis"></span><ul class="obj collapsible">';
var hasContents = false;
for (i = 0, length = keys.length; i < length; i++) {
key = keys[i];
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += '<span class="property">' + htmlEncode(key) + '</span>: ';
output += valueToHTML(json[key]);
if (i < length - 1) {
output += ',';
}
output += '</div></li>';
}
output += '</ul>}';
if (!hasContents) {
output = '{ }';
}
return output;
}
function jsonToHTML(json) {
level = 1;
var output = '';
output += '<div class="redoc-json">';
output += valueToHTML(json);
output += '</div>';
return output;
}

View File

@ -62,6 +62,6 @@ export class MarkedPipe {
if (!isString(value)) { if (!isString(value)) {
throw new InvalidPipeArgumentException(JsonPointerEscapePipe, value); throw new InvalidPipeArgumentException(JsonPointerEscapePipe, value);
} }
return marked(value); return `<span class="redoc-markdown-block">${marked(value)}</span>`;
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "redoc", "name": "redoc",
"description": "Swagger-generated API Reference Documentation", "description": "Swagger-generated API Reference Documentation",
"version": "0.3.0", "version": "0.4.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Rebilly/ReDoc" "url": "git://github.com/Rebilly/ReDoc"
}, },
"main": "dist/redoc.full.min.js", "main": "dist/redoc.min.js",
"scripts": { "scripts": {
"test": "gulp lint && ./build/run_tests.sh", "test": "gulp lint && ./build/run_tests.sh",
"prepublish": "gulp build", "prepublish": "gulp build",

View File

@ -65,7 +65,7 @@ function verifyNoBrowserErrors() {
function scrollToEl(selector) { function scrollToEl(selector) {
let script = ` let script = `
document.querySelector('${selector}').scrollIntoView(true); document.querySelector('${selector}').scrollIntoView(true);
window.scrollBy(0, 10); window.scrollBy(0, 200);
`; `;
return browser.driver.executeScript(script); return browser.driver.executeScript(script);

View File

@ -10,12 +10,12 @@
</redoc> </redoc>
<!-- ReDoc built file with all dependencies included --> <!-- ReDoc built file with all dependencies included -->
<script src="dist/redoc.full.min.js"> </script> <script src="dist/redoc.min.js"> </script>
<script> <script>
window.redocError = null; window.redocError = null;
/* init redoc */ /* init redoc */
var url = window.location.search.substr(5) || 'swagger.json'; var url = window.location.search.substr(5) || 'swagger.json';
Redoc.init(decodeURIComponent(url)).then(function() {}, function(err) { Redoc.init(decodeURIComponent(url), {disableLazySchemas: true}).then(function() {}, function(err) {
window.redocError = err; window.redocError = err;
}); });
</script> </script>

View File

@ -44,7 +44,8 @@ describe('Scroll sync', () => {
it('should update active menu entries on page scroll', () => { it('should update active menu entries on page scroll', () => {
scrollToEl('[tag="store"]').then(function() { scrollToEl('[tag="store"]').then(function() {
expect($('.menu-cat-header.active').getText()).toBe('STORE'); expect($('.menu-cat-header.active').getInnerHtml()).toContain('store');
expect($('.selected-tag').getInnerHtml()).toContain('store');
}); });
}); });
}); });

View File

@ -10,6 +10,13 @@ export function getChildDebugElement(parent, tagName) {
}); });
} }
/** Gets a child DebugElement by Component Type. */
export function getChildDebugElementByType(parent, type) {
return parent.query(debugEl => {
return debugEl.componentInstance instanceof type;
});
}
/** Gets a child DebugElements by tag name. */ /** Gets a child DebugElements by tag name. */
export function getChildDebugElementAll(parent, tagName) { export function getChildDebugElementAll(parent, tagName) {
return parent.queryAll(debugEl => { return parent.queryAll(debugEl => {

View File

@ -85,12 +85,12 @@ describe('Pipes', () => {
beforeEach(() => { beforeEach(() => {
unmarked = 'test\n'; unmarked = 'test\n';
marked = '<p>test</p>\n'; marked = '<span class="redoc-markdown-block"><p>test</p>\n</span>';
pipe = new MarkedPipe(); pipe = new MarkedPipe();
}); });
describe('MarkedPipe transform', () => { describe('MarkedPipe transform', () => {
it('should escpae pointer', () => { it('should wrap in markdown span', () => {
var val = pipe.transform(unmarked); var val = pipe.transform(unmarked);
val.should.be.equal(marked); val.should.be.equal(marked);
}); });