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
**Under development**
[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
1. Clone repository
`git clone https://github.com/Rebilly/ReDoc.git`

View File

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

View File

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

View File

@ -55,7 +55,7 @@ var JS_DEV_DEPS_MIN = [
gulp.task('sass', function () {
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));
});

View File

@ -3,21 +3,21 @@
<head>
<title>ReDoc</title>
<link rel="stylesheet" href="main.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<nav>
<header> ReDoc </header>
<input id="schema-url-input" value='swagger.json'>
<button id="load-button"> Explore </button>
</nav>
<form id="schema-url-form">
<input id="schema-url-input" value='swagger.json'>
<button type="submit"> Explore </button>
</form>
</nav>
<redoc scroll-y-offset="body > nav" spec-url='swagger.json'>
Loading...
</redoc>
<redoc scroll-y-offset="body > nav" spec-url='swagger.json'></redoc>
<!-- 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>
</body>
</html>

View File

@ -19,21 +19,55 @@ nav header {
float: left;
margin-left: 20px;
font-size: 25px;
color: white;
position: absolute;
color: #00329F;
font-weight: bold;
}
nav input {
width: 50%;
box-sizing: border-box;
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 {
border: 1px solid #FFFFFF;
cursor: pointer;
color: white;
background-color: #21476D;
background-color: #fff;
color: #333;
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 {
@ -41,8 +75,25 @@ nav {
height: 50px;
line-height: 50px;
text-align: center;
background-color: #053361;
background-color: white;
border-bottom: 1px solid #ccc;
position: fixed;
top: 0;
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() {
'use strict';
var loadButton = document.getElementById('load-button');
var schemaUrlForm = document.getElementById('schema-url-form');
var schemaUrlInput = document.getElementById('schema-url-input');
loadButton.addEventListener('click', function() {
schemaUrlForm.addEventListener('submit', function(event) {
event.preventDefault();
Redoc.init(schemaUrlInput.value);
return false;
})
})();

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
},{
"name": "JSONP",
"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": {
"description": "Find out more",
"url": "http://swagger.io"

View File

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

View File

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

View File

@ -48,8 +48,8 @@ describe('Common components', () => {
let tabs = childDebugEls.map(debugEl => debugEl.componentInstance);
let [tab1, tab2] = tabs;
tab1.active.should.be.true;
tab2.active.should.be.false;
tab1.active.should.be.true();
tab2.active.should.be.false();
});
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-even-bg-color: #F0F0F0;
$side-menu-active-bg-color: #E6E6E6;
$side-menu-active-bg-color: #DEDEDE;
$side-menu-item-hpadding: 20px;
$side-menu-item-vpadding: 5px;
@ -16,3 +16,5 @@ $sample-panel-headers-color: #8A9094;
$sample-panel-color: #CFD2D3;
$tree-lines-color: #7D97CE;
$side-menu-mobile-breakpoint: 1000px;

View File

@ -2,4 +2,5 @@
.api-info-header {
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 *ngFor="#prop of data.properties" class="param-wrap">
<div class="param" [ngClass]="{'discriminator': prop.isDiscriminator}">
@ -7,7 +10,8 @@
</div>
<div class="param-info">
<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>
</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) {
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;
}
let discriminatorFieldIdx = -1;
let props = Object.keys(schema.properties).map((prop, idx) => {
let propData = schema.properties[prop];
this.injectPropData(prop, propData, schema);
propData = this.injectPropData(prop, propData, schema);
if (propData.isDiscriminator) discriminatorFieldIdx = idx;
return propData;
});
@ -97,6 +98,7 @@ export default class JsonSchema extends BaseComponent {
}
injectPropData(prop, propData, schema) {
propData = Object.assign({}, propData);
propData._name = prop;
propData.isRequired = this.requiredMap[prop];
propData._displayType = propData.type;
@ -108,16 +110,24 @@ export default class JsonSchema extends BaseComponent {
itemType = propData.items.title || 'object';
propData._pointer = propData.items._pointer || JsonPointer.join(this.pointer, ['properties', prop, 'items']);
}
propData._displayType = `array of ${itemType}`;
propData._displayType = `${itemType}`;
propData.format = itemFormat;
propData._isArray = true;
propData.type = 'array ' + propData.items.type;
}
if (propData.type === '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}>`;
return propData;
}
init() {

View File

@ -1,129 +1,4 @@
@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 {
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;
}
@import 'json-schema-common';
/* styles for array-schema for array */
$array-marker-font-sz: 12px;
@ -166,6 +41,10 @@ $array-marker-line-height: 1.5;
height: ($param-name-height/2) + $cell-padding;
}
.params-wrap > .param > .param-schema.param-array {
border-left-color: transparent;
}
.param.discriminator {
> div {
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 {
padding: 30px 0;
margin: 0;
}
.http-method {

View File

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

View File

@ -4,9 +4,10 @@
padding: 0.2em 0;
margin: 0.5em 0;
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

View File

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

View File

@ -16,9 +16,11 @@ import {ElementRef} from 'angular2/core';
import {BrowserDomAdapter, bootstrap} from 'angular2/platform/browser';
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({
selector: 'redoc',
@ -41,6 +43,7 @@ export default class Redoc extends BaseComponent {
this.parseOptions();
this.options = Object.assign({}, optionsMgr.options, this.options);
this.normalizeOptions();
optionsMgr.options = this.options;
}
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) {
if (Redoc.appRef) {
Redoc.dispose();
}
Redoc.showLoadingAnimation();
return SchemaManager.instance().load(schemaUrl)
.then(() => {
(new OptionsManager()).options = options;
@ -92,6 +146,7 @@ export default class Redoc extends BaseComponent {
})
.then(
(appRef) => {
Redoc.hideLoadingAnimation();
Redoc.appRef = appRef;
redocEvents.bootstrapped.next();
console.log('ReDoc bootstrapped!');
@ -117,6 +172,7 @@ export default class Redoc extends BaseComponent {
static dispose() {
let dom = new BrowserDomAdapter();
let el = dom.query('redoc');
let elClone;
let parent;
let nextSibling;
if (el) {
@ -124,14 +180,15 @@ export default class Redoc extends BaseComponent {
nextSibling = el.nextElementSibling;
}
elClone = el.cloneNode(false);
if (Redoc.appRef) {
Redoc.appRef.dispose();
Redoc.appRef = null;
// Redoc dispose removes host element, so need to restore it
el = dom.createElement('redoc');
el.innerText = 'Loading...';
parent && parent.insertBefore(el, nextSibling);
elClone.innerHTML = 'Loading...';
parent && parent.insertBefore(elClone, nextSibling);
}
}
}

View File

@ -5,24 +5,6 @@
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 {
position: relative;
}
@ -49,17 +31,48 @@ api-info {
api-logo {
display: block;
text-align: center;
@media (max-width: $side-menu-mobile-breakpoint) {
display: none;
}
}
sticky-sidebar {
[sticky-sidebar] {
width: $side-bar-width;
background-color: $side-bar-bg-color;
overflow-y: auto;
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 {
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 {
@ -77,3 +90,104 @@ footer {
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,28 +105,39 @@ describe('Redoc components', () => {
});
});
});
});
describe('Redoc init', () => {
it('should return promise', () => {
let res = Redoc.init();
res.should.be.instanceof(Promise);
});
it('should reject promise for not specifed url', (done) => {
let res = Redoc.init();
res.then(() => { done.fail('Should not been called'); }, () => {
done();
describe('Redoc init', () => {
let dom = new BrowserDomAdapter();
let elem;
beforeEach(() => {
elem = dom.createElement('redoc');
dom.defaultDoc().body.appendChild(elem);
});
});
//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.json');
res.then(() => { done(); }, () => {
done.fail('Error handler should not been called');
afterEach(() => {
dom.defaultDoc().body.removeChild(elem);
});
it('should return promise', () => {
let res = Redoc.init();
res.should.be.instanceof(Promise);
});
it('should reject promise for not specifed url', (done) => {
let res = Redoc.init();
res.then(() => { done.fail('Should not been called'); }, () => {
done();
});
});
//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.json');
res.then(() => { done(); }, () => {
done.fail('Error handler should not been called');
});
});
});
@ -217,7 +228,7 @@ describe('Redoc init', () => {
@View({
directives: [Redoc],
template:
`<redoc></redoc>`
`<redoc disable-lazy-schemas></redoc>`
})
class TestApp {
}

View File

@ -1,6 +1,6 @@
<h2 class="responses-list-header" *ngIf="data.responses.length"> Responses </h2>
<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">
<header>
Headers
@ -14,6 +14,8 @@
<header>
Response schema
</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-lazy #lazySchema pointer="{{response.schema ? response.pointer + '/schema' : null}}">
</json-schema-lazy>
</zippy>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,19 +4,19 @@ import {RedocComponent, BaseComponent} from '../base';
import SchemaSampler from 'json-schema-instantiator';
import {JsonFormatter} from '../../utils/JsonFormatterPipe';
import {ElementRef} from 'angular2/core';
@RedocComponent({
selector: 'schema-sample',
templateUrl: './lib/components/SchemaSample/schema-sample.html',
styles: [`
pre {
background-color: transparent;
padding: 0;
}
`]
pipes: [JsonFormatter],
styleUrls: ['./lib/components/SchemaSample/schema-sample.css']
})
export default class SchemaSample extends BaseComponent {
constructor(schemaMgr) {
constructor(schemaMgr, elementRef) {
super(schemaMgr);
this.element = elementRef.nativeElement;
}
init() {
@ -44,5 +44,19 @@ export default class SchemaSample extends BaseComponent {
}
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,13 +1,22 @@
<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>
<ul class="menu-subitems" [ngClass]="{active: cat.active}">
<li *ngFor="var method of cat.methods; var methIdx = index"
[ngClass]="{active: method.active}"
(click)="activateAndScroll(idx, methIdx)">
{{method.summary}}
</li>
</ul>
<div class="mobile-nav" (click)="toggleMobileNav()">
<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>
<ul class="menu-subitems" [ngClass]="{active: cat.active}">
<li *ngFor="var method of cat.methods; var methIdx = index"
[ngClass]="{active: method.active}"
(click)="activateAndScroll(idx, methIdx)">
{{method.summary}}
</li>
</ul>
</div>
</div>

View File

@ -3,7 +3,7 @@
import {RedocComponent, BaseComponent} from '../base';
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 {BrowserDomAdapter} from 'angular2/platform/browser';
import {global} from 'angular2/src/facade/lang';
@ -27,13 +27,15 @@ const INVIEW_POSITION = {
changeDetection: ChangeDetectionStrategy.Default
})
export default class SideMenu extends BaseComponent {
constructor(schemaMgr, adapter, zone, redoc) {
constructor(schemaMgr, elementRef, adapter, zone, redoc) {
super(schemaMgr);
this.zone = zone;
this.adapter = adapter;
this.redoc = redoc;
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
// as workaround running it manually
@ -45,6 +47,9 @@ export default class SideMenu extends BaseComponent {
this.prevOffsetY = null;
redocEvents.bootstrapped.subscribe(() => this.hashScroll());
this.activeCatCaption = '';
this.activeItemCaption = '';
}
scrollY() {
@ -65,7 +70,12 @@ export default class SideMenu extends BaseComponent {
bindEvents() {
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.scroll = this.adapter.onAndCancel(this.scrollParent, 'scroll', () => { this.scrollHandler(); });
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) {
if (this.mobileMode()) {
this.toggleMobileNav();
}
this.activate(idx, methodIdx);
this.scrollToActive();
}
@ -98,6 +111,10 @@ export default class SideMenu extends BaseComponent {
activate(catIdx, methodIdx) {
let menu = this.data.menu;
this.activeCatCaption = '';
this.activeItemCaption = '';
menu[this.activeCatIdx].active = false;
if (menu[this.activeCatIdx].methods.length) {
if (this.activeMethodIdx >= 0) {
@ -108,11 +125,13 @@ export default class SideMenu extends BaseComponent {
this.activeCatIdx = catIdx;
this.activeMethodIdx = methodIdx;
menu[catIdx].active = true;
this.activeCatCaption = menu[catIdx].name;
this.activeMethodPtr = null;
if (menu[catIdx].methods.length && (methodIdx > -1)) {
let currentItem = menu[catIdx].methods[methodIdx];
currentItem.active = true;
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() {
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';
$mobile-menu-compact-breakpoint: 550px;
.menu-header {
text-transform: uppercase;
@ -9,7 +10,6 @@
}
.menu-cat-header {
//font-weight: bold;
font-size: 15px;
cursor: pointer;
color: $side-menu-item-color;
@ -24,6 +24,10 @@
background-color: $side-menu-even-bg-color;
}
.menu-cat .menu-cat-header.active {
background-color: darken($side-menu-active-bg-color, 5%);
}
.menu-subitems {
margin: 0;
padding: 0;
@ -61,7 +65,89 @@
}
}
.menu-cat-header.active, .menu-subitems li.active {
background-color: $side-menu-active-bg-color !important;
font-weight: bold;
.menu-cat .menu-subitems {
li.active, li.active:nth-of-type(even) {
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';
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 JsonPointer from '../utils/JsonPointer';
import {MarkedPipe, JsonPointerEscapePipe} from '../utils/pipes';
@ -49,7 +49,7 @@ function snapshot(obj) {
export function RedocComponent(options) {
let inputs = safeConcat(options.inputs, commonInputs);
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) {
@ -104,7 +104,9 @@ export class BaseComponent {
let dereferencedCache = {};
let resolve = (schema) => {
let resolvedRef;
if (schema && schema.$ref) {
resolvedRef = schema.$ref;
let resolved = this.schemaMgr.byPointer(schema.$ref);
let baseName = JsonPointer.baseName(schema.$ref);
if (!dereferencedCache[schema.$ref]) {
@ -114,11 +116,12 @@ export class BaseComponent {
} else {
// for circular referenced save only title and type
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;
@ -133,7 +136,6 @@ export class BaseComponent {
schema = schema.description ? {
description: schema.description
} : {};
//for (var prop in schema) delete schema[prop];
Object.assign(schema, resolved);
}
@ -143,10 +145,11 @@ export class BaseComponent {
schema[key] = resolve(value);
}
});
if (resolvedRef) dereferencedCache[resolvedRef] = dereferencedCache[resolvedRef] ? dereferencedCache[resolvedRef] - 1 : 0;
return schema;
};
this.componentSchema = resolve(schema);
this.componentSchema = resolve(schema, 1);
}
joinAllOf(schema = this.componentSchema, opts) {

View File

@ -14,12 +14,17 @@ export default class OptionsManager {
OptionsManager.prototype._instance = this;
this._defaults = {
scrollYOffset: 0
scrollYOffset: 0,
disableLazySchemas: false
};
this._options = {};
}
static instance() {
return new OptionsManager();
}
get 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)) {
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",
"description": "Swagger-generated API Reference Documentation",
"version": "0.3.0",
"version": "0.4.0",
"repository": {
"type": "git",
"url": "git://github.com/Rebilly/ReDoc"
},
"main": "dist/redoc.full.min.js",
"main": "dist/redoc.min.js",
"scripts": {
"test": "gulp lint && ./build/run_tests.sh",
"prepublish": "gulp build",

View File

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

View File

@ -10,12 +10,12 @@
</redoc>
<!-- ReDoc built file with all dependencies included -->
<script src="dist/redoc.full.min.js"> </script>
<script src="dist/redoc.min.js"> </script>
<script>
window.redocError = null;
/* init redoc */
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;
});
</script>

View File

@ -44,7 +44,8 @@ describe('Scroll sync', () => {
it('should update active menu entries on page scroll', () => {
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. */
export function getChildDebugElementAll(parent, tagName) {
return parent.queryAll(debugEl => {

View File

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