mirror of
https://github.com/Redocly/redoc.git
synced 2025-02-07 13:30:33 +03:00
Merge branch 'menu-refactor'
This commit is contained in:
commit
177bffb1c3
|
@ -113,6 +113,7 @@ ReDoc makes use of the following [vendor extensions](http://swagger.io/specifica
|
||||||
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples
|
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples
|
||||||
* [`x-nullable`](docs/redoc-vendor-extensions.md#nullable) - mark schema param as a nullable
|
* [`x-nullable`](docs/redoc-vendor-extensions.md#nullable) - mark schema param as a nullable
|
||||||
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
|
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
|
||||||
|
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
|
||||||
|
|
||||||
### `<redoc>` tag attributes
|
### `<redoc>` tag attributes
|
||||||
* `spec-url` - relative or absolute url to your spec file;
|
* `spec-url` - relative or absolute url to your spec file;
|
||||||
|
|
|
@ -7,6 +7,7 @@ const VERSION = JSON.stringify(require('../package.json').version);
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
|
performance: { hints: false },
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js', '.json', '.css'],
|
extensions: ['.ts', '.js', '.json', '.css'],
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
@ -54,6 +54,14 @@ tags:
|
||||||
description: Access to Petstore orders
|
description: Access to Petstore orders
|
||||||
- name: user
|
- name: user
|
||||||
description: Operations about user
|
description: Operations about user
|
||||||
|
x-tagGroups:
|
||||||
|
- name: General
|
||||||
|
tags:
|
||||||
|
- pet
|
||||||
|
- store
|
||||||
|
- name: User Management
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
petstore_auth:
|
petstore_auth:
|
||||||
description: |
|
description: |
|
||||||
|
|
|
@ -1,6 +1,57 @@
|
||||||
# ReDoc vendor extensions
|
# ReDoc vendor extensions
|
||||||
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions)
|
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions)
|
||||||
|
|
||||||
|
### Swagger Object vendor extensions
|
||||||
|
Extend OpenAPI root [Swagger Object](http://swagger.io/specification/#swaggerObject)
|
||||||
|
#### x-tagGroups
|
||||||
|
|
||||||
|
| Field Name | Type | Description |
|
||||||
|
| :------------- | :-----------: | :---------- |
|
||||||
|
| x-tagGroups | [ [Tag Group Object](#tagGroupObject) ] | A list of tag groups |
|
||||||
|
|
||||||
|
###### Usage in Redoc
|
||||||
|
`x-tagGroups` is used to group tags in the side menu
|
||||||
|
|
||||||
|
#### <a name="tagGroupObject"></a>Tag Group Object
|
||||||
|
Information about tags group
|
||||||
|
###### Fixed fields
|
||||||
|
| Field Name | Type | Description |
|
||||||
|
| :---------- | :--------: | :---------- |
|
||||||
|
| name | string | The group name |
|
||||||
|
| tags | [ string ] | List of tags to include in this group
|
||||||
|
|
||||||
|
###### x-tagGroups example
|
||||||
|
json
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-tagGroups": [
|
||||||
|
{
|
||||||
|
"name": "User Management",
|
||||||
|
"tags": ["Users", "API keys", "Admin"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Statistics",
|
||||||
|
"tags": ["Main Stats", "Secondary Stats"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
yaml
|
||||||
|
```yaml
|
||||||
|
x-tagGroups:
|
||||||
|
- name: User Management
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
- API keys
|
||||||
|
- Admin
|
||||||
|
- name: Statistics
|
||||||
|
tags:
|
||||||
|
- Main Stats
|
||||||
|
- Secondary Stats
|
||||||
|
```
|
||||||
|
|
||||||
|
#### <a name="logoObject"></a>Logo Object
|
||||||
|
|
||||||
### Info Object vendor extensions
|
### Info Object vendor extensions
|
||||||
Extends OpenAPI [Info Object](http://swagger.io/specification/#infoObject)
|
Extends OpenAPI [Info Object](http://swagger.io/specification/#infoObject)
|
||||||
#### x-logo
|
#### x-logo
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
import { Input, Component, OnInit, ChangeDetectionStrategy, ElementRef } from '@angular/core';
|
import { Input, HostBinding, Component, OnInit, ChangeDetectionStrategy, ElementRef } from '@angular/core';
|
||||||
import JsonPointer from '../../utils/JsonPointer';
|
import JsonPointer from '../../utils/JsonPointer';
|
||||||
import { BaseComponent, SpecManager } from '../base';
|
import { BaseComponent, SpecManager } from '../base';
|
||||||
import { SchemaHelper } from '../../services/schema-helper.service';
|
import { SchemaHelper } from '../../services/schema-helper.service';
|
||||||
import { OptionsService } from '../../services/';
|
import { OptionsService } from '../../services/';
|
||||||
|
|
||||||
|
|
||||||
|
interface MethodInfo {
|
||||||
|
apiUrl: string;
|
||||||
|
httpMethod: string;
|
||||||
|
path: string;
|
||||||
|
info: {
|
||||||
|
tags: string[];
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
bodyParam: any;
|
||||||
|
summary: any;
|
||||||
|
anchor: any;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'method',
|
selector: 'method',
|
||||||
templateUrl: './method.html',
|
templateUrl: './method.html',
|
||||||
|
@ -12,35 +26,47 @@ import { OptionsService } from '../../services/';
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class Method extends BaseComponent implements OnInit {
|
export class Method extends BaseComponent implements OnInit {
|
||||||
@Input() pointer:string;
|
@Input() pointer :string;
|
||||||
@Input() tag:string;
|
@Input() parentTagId :string;
|
||||||
@Input() posInfo: any;
|
|
||||||
|
|
||||||
hidden = true;
|
@HostBinding('attr.operation-id') operationId;
|
||||||
|
|
||||||
method:any;
|
method: MethodInfo;
|
||||||
|
|
||||||
constructor(specMgr:SpecManager, private optionsService: OptionsService, private el: ElementRef) {
|
constructor(specMgr:SpecManager, private optionsService: OptionsService) {
|
||||||
super(specMgr);
|
super(specMgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.method = {};
|
this.operationId = this.componentSchema.operationId;
|
||||||
if (this.optionsService.options.hideHostname) {
|
|
||||||
this.method.apiUrl = this.specMgr.basePath;
|
this.method = {
|
||||||
|
httpMethod: JsonPointer.baseName(this.pointer),
|
||||||
|
path: JsonPointer.baseName(this.pointer, 2),
|
||||||
|
info: {
|
||||||
|
description: this.componentSchema.description,
|
||||||
|
tags: this.filterMainTags(this.componentSchema.tags)
|
||||||
|
},
|
||||||
|
bodyParam: this.findBodyParam(),
|
||||||
|
summary: SchemaHelper.methodSummary(this.componentSchema),
|
||||||
|
apiUrl: this.getBaseUrl(),
|
||||||
|
anchor: this.buildAnchor()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAnchor() {
|
||||||
|
if (this.operationId) {
|
||||||
|
return 'operation/' + encodeURIComponent(this.componentSchema.operationId);
|
||||||
} else {
|
} else {
|
||||||
this.method.apiUrl = this.specMgr.apiUrl;
|
return this.parentTagId + encodeURIComponent(this.pointer);
|
||||||
}
|
}
|
||||||
this.method.httpMethod = JsonPointer.baseName(this.pointer);
|
}
|
||||||
this.method.path = JsonPointer.baseName(this.pointer, 2);
|
|
||||||
this.method.info = this.componentSchema;
|
getBaseUrl():string {
|
||||||
this.method.info.tags = this.filterMainTags(this.method.info.tags);
|
if (this.optionsService.options.hideHostname) {
|
||||||
this.method.bodyParam = this.findBodyParam();
|
return this.specMgr.basePath;
|
||||||
this.method.summary = SchemaHelper.methodSummary(this.componentSchema);
|
|
||||||
if (this.componentSchema.operationId) {
|
|
||||||
this.method.anchor = 'operation/' + encodeURIComponent(this.componentSchema.operationId);
|
|
||||||
} else {
|
} else {
|
||||||
this.method.anchor = this.tag + encodeURIComponent(this.pointer);
|
return this.specMgr.apiUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,14 +82,6 @@ export class Method extends BaseComponent implements OnInit {
|
||||||
return bodyParam;
|
return bodyParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(res) {
|
|
||||||
if (res) {
|
|
||||||
this.el.nativeElement.firstElementChild.removeAttribute('hidden');
|
|
||||||
} else {
|
|
||||||
this.el.nativeElement.firstElementChild.setAttribute('hidden', 'hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.preinit();
|
this.preinit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<div class="methods">
|
<div class="methods">
|
||||||
<div class="tag" *ngFor="let tag of tags;let catIdx = index; trackBy:trackByTagName">
|
<div class="tag" *ngFor="let tag of tags; trackBy:trackByTagName" [attr.section]="tag.id">
|
||||||
<div class="tag-info" [attr.section]="tag.id" *ngIf="!tag.headless">
|
<div class="tag-info" *ngIf="tag.name">
|
||||||
<h1 class="sharable-header"> <a class="share-link" href="#{{tag.id}}"></a>{{tag.name}} </h1>
|
<h1 class="sharable-header"> <a class="share-link" href="#{{tag.id}}"></a>{{tag.name}} </h1>
|
||||||
<p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p>
|
<p *ngIf="tag.description" [innerHtml]="tag.description | marked"> </p>
|
||||||
</div>
|
</div>
|
||||||
<method *lazyFor="let method of tag.methods;let show = show;" [hidden]="!show" [pointer]="method.pointer" [attr.pointer]="method.pointer"
|
<method *lazyFor="let methodItem of tag.items; let ready = ready;"
|
||||||
[attr.section]="method.tag" [tag]="method.tag" [attr.operation-id]="method.operationId"></method>
|
[hidden]="!ready" [pointer]="methodItem.metadata.pointer"
|
||||||
|
[parentTagId]="tag.id" [attr.section]="methodItem.id"></method>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,13 +42,13 @@ describe('Redoc components', () => {
|
||||||
expect(component).not.toBeNull();
|
expect(component).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get correct tags list', () => {
|
it('should build correct tags list', () => {
|
||||||
expect(component.tags).not.toBeNull();
|
expect(component.tags).not.toBeNull();
|
||||||
component.tags.should.have.lengthOf(2);
|
component.tags.should.have.lengthOf(2);
|
||||||
component.tags[0].name.should.be.equal('traitTag');
|
component.tags[0].name.should.be.equal('traitTag');
|
||||||
component.tags[0].methods.should.be.empty();
|
should.not.exist(component.tags[0].items);
|
||||||
component.tags[1].name.should.be.equal('tag1');
|
component.tags[1].name.should.be.equal('tag1');
|
||||||
component.tags[1].methods.should.have.lengthOf(2);
|
component.tags[1].items.should.have.lengthOf(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { BaseComponent, SpecManager } from '../base';
|
import { BaseComponent, SpecManager } from '../base';
|
||||||
import { SchemaHelper } from '../../services/index';
|
import { MenuService } from '../../services/index';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'methods-list',
|
selector: 'methods-list',
|
||||||
|
@ -14,20 +14,29 @@ export class MethodsList extends BaseComponent implements OnInit {
|
||||||
|
|
||||||
tags:Array<any> = [];
|
tags:Array<any> = [];
|
||||||
|
|
||||||
constructor(specMgr:SpecManager) {
|
constructor(specMgr:SpecManager, private menu: MenuService) {
|
||||||
super(specMgr);
|
super(specMgr);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let tags = SchemaHelper.buildMenuTree(this.specMgr.schema);
|
let flatMenuItems = this.menu.flatItems;
|
||||||
this.tags = tags.filter(tagInfo => !tagInfo.virtual);
|
this.tags = [];
|
||||||
this.tags.forEach(tagInfo => {
|
let emptyTag = {
|
||||||
// inject tag name into method info
|
name: '',
|
||||||
tagInfo.methods = tagInfo.methods || [];
|
items: []
|
||||||
tagInfo.methods.forEach(method => {
|
};
|
||||||
method.tag = tagInfo.id;
|
flatMenuItems.forEach(menuItem => {
|
||||||
});
|
// skip items that are not bound to swagger tags/methods
|
||||||
|
if (!menuItem.metadata) return;
|
||||||
|
|
||||||
|
if (menuItem.metadata.type === 'tag') {
|
||||||
|
this.tags.push(menuItem);
|
||||||
|
}
|
||||||
|
if (menuItem.metadata.type === 'method' && !menuItem.parent) {
|
||||||
|
emptyTag.items.push(menuItem);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (emptyTag.items.length) this.tags.push(emptyTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByTagName(_, el) {
|
trackByTagName(_, el) {
|
||||||
|
|
7
lib/components/SideMenu/side-menu-items.html
Normal file
7
lib/components/SideMenu/side-menu-items.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<li *ngFor="let item of items; let idx = index" class="menu-item"
|
||||||
|
ngClass="menu-item-depth-{{item.depth}} {{item.active ? 'active' : ''}} menu-item-for-{{item.metadata?.type}}">
|
||||||
|
<label class="menu-item-header" [ngClass]="{disabled: !item.ready}" (click)="activateItem(item)"> {{item.name}}</label>
|
||||||
|
<ul *ngIf="item.items" class="menu-subitems">
|
||||||
|
<side-menu-items [items]="item.items" (activate)="activateItem($event)"> </side-menu-items>
|
||||||
|
</ul>
|
||||||
|
</li>
|
110
lib/components/SideMenu/side-menu-items.scss
Normal file
110
lib/components/SideMenu/side-menu-items.scss
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
@import '../../shared/styles/variables';
|
||||||
|
|
||||||
|
.menu-item-header {
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba($text-color, .9);
|
||||||
|
-webkit-transition: all .15s ease-in-out;
|
||||||
|
-moz-transition: all .15s ease-in-out;
|
||||||
|
-ms-transition: all .15s ease-in-out;
|
||||||
|
-o-transition: all .15s ease-in-out;
|
||||||
|
transition: all .15s ease-in-out;
|
||||||
|
display: block;
|
||||||
|
padding: $side-menu-item-vpadding*2.5 $side-menu-item-hpadding;
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled, &.disabled:hover {
|
||||||
|
cursor: default;
|
||||||
|
color: lighten($text-color, 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
-webkit-transition: all .15s ease-in-out;
|
||||||
|
-moz-transition: all .15s ease-in-out;
|
||||||
|
-ms-transition: all .15s ease-in-out;
|
||||||
|
-o-transition: all .15s ease-in-out;
|
||||||
|
transition: all .15s ease-in-out;
|
||||||
|
list-style: none inside none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover,
|
||||||
|
.menu-item.active {
|
||||||
|
//background: darken($side-menu-active-bg-color, 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-subitems {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.929em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-weight: $light;
|
||||||
|
color: rgba($text-color, .9);
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
.active > & {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-depth-1 {
|
||||||
|
> .menu-item-header {
|
||||||
|
font-family: $headers-font, $headers-font-family;
|
||||||
|
font-weight: $light;
|
||||||
|
font-size: $h5;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not capitalize method summuary in level-1 menu
|
||||||
|
&.menu-item-for-method > .menu-item-header {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu-item-header:not(.disabled):hover,
|
||||||
|
&.active > .menu-item-header {
|
||||||
|
color: $primary-color;
|
||||||
|
background: $side-menu-active-bg-color;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
//background: $side-menu-active-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-depth-2 {
|
||||||
|
> .menu-item-header {
|
||||||
|
padding-left: 2*$side-menu-item-hpadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu-item-header:hover,
|
||||||
|
&.active > .menu-item-header {
|
||||||
|
background: darken($side-menu-active-bg-color, 6%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// group items
|
||||||
|
.menu-item-depth-0 {
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
> .menu-subitems {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu-item-header {
|
||||||
|
font-family: $headers-font, $headers-font-family;
|
||||||
|
color: rgba($text-color, .4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding-bottom: 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
//background: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,17 +7,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div #desktop id="resources-nav">
|
<div #desktop id="resources-nav">
|
||||||
<h5 class="menu-header"> API reference </h5>
|
<h5 class="menu-header"> API reference </h5>
|
||||||
<div *ngFor="let cat of categories; let idx = index" class="menu-cat">
|
<ul class="menu-root">
|
||||||
|
<side-menu-items [items]="menuItems" (activate)="activateAndScroll($event)"></side-menu-items>
|
||||||
<label class="menu-cat-header" (click)="activateAndScroll(idx, -1)" [hidden]="cat.headless"
|
</ul>
|
||||||
[ngClass]="{active: cat.active, disabled: !cat.ready }"> {{cat.name}}</label>
|
|
||||||
<ul *ngIf="cat.methods.length" class="menu-subitems" [@itemAnimation]="cat.active ? 'expanded' : 'collapsed'">
|
|
||||||
<li *ngFor="let method of cat.methods; trackBy:summary; let methIdx = index"
|
|
||||||
[ngClass]="{active: method.active, disabled: !method.ready}"
|
|
||||||
(click)="activateAndScroll(idx, methIdx)">
|
|
||||||
{{method.summary}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,11 @@ $mobile-menu-compact-breakpoint: 550px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.menu-root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-header {
|
.menu-header {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: $headers-color;
|
color: $headers-color;
|
||||||
|
@ -13,81 +18,6 @@ $mobile-menu-compact-breakpoint: 550px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-cat-header {
|
|
||||||
font-size: $h5;
|
|
||||||
font-family: $headers-font, $headers-font-family;
|
|
||||||
font-weight: $light;
|
|
||||||
cursor: pointer;
|
|
||||||
color: rgba($text-color, .6);
|
|
||||||
text-transform: uppercase;
|
|
||||||
background-color: $side-bar-bg-color;
|
|
||||||
-webkit-transition: all .15s ease-in-out;
|
|
||||||
-moz-transition: all .15s ease-in-out;
|
|
||||||
-ms-transition: all .15s ease-in-out;
|
|
||||||
-o-transition: all .15s ease-in-out;
|
|
||||||
transition: all .15s ease-in-out;
|
|
||||||
display: block;
|
|
||||||
padding: $side-menu-item-vpadding*2.5 $side-menu-item-hpadding;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
color: $primary-color;
|
|
||||||
background-color: $side-menu-active-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled, &.disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
color: lighten($text-color, 60%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-subitems {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.929em;
|
|
||||||
line-height: 1.2em;
|
|
||||||
font-weight: $light;
|
|
||||||
color: rgba($text-color, .9);
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
& li {
|
|
||||||
-webkit-transition: all .15s ease-in-out;
|
|
||||||
-moz-transition: all .15s ease-in-out;
|
|
||||||
-ms-transition: all .15s ease-in-out;
|
|
||||||
-o-transition: all .15s ease-in-out;
|
|
||||||
transition: all .15s ease-in-out;
|
|
||||||
list-style: none inside none;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: $side-menu-active-bg-color;
|
|
||||||
padding: $side-menu-item-vpadding*2 $side-menu-item-hpadding*2;
|
|
||||||
padding-left: $side-menu-item-hpadding*2;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
& li:hover,
|
|
||||||
& li.active {
|
|
||||||
background: darken($side-menu-active-bg-color, 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled, &.disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
color: lighten($text-color, 60%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.menu-subitems li.active {
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-nav {
|
.mobile-nav {
|
||||||
display: none;
|
display: none;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
|
|
|
@ -1,36 +1,49 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { Component, ElementRef, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, ElementRef, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
//import { global } from '@angular/core/src/facade/lang';
|
//import { global } from '@angular/core/src/facade/lang';
|
||||||
import { trigger, state, animate, transition, style } from '@angular/core';
|
import { trigger, state, animate, transition, style } from '@angular/core';
|
||||||
import { BaseComponent, SpecManager } from '../base';
|
import { BaseComponent, SpecManager } from '../base';
|
||||||
import { ScrollService, MenuService, OptionsService } from '../../services/index';
|
import { ScrollService, MenuService, OptionsService, MenuItem } from '../../services/';
|
||||||
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
|
import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter';
|
||||||
import { MenuCategory } from '../../services/schema-helper.service';
|
|
||||||
|
|
||||||
const global = window;
|
const global = window;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'side-menu-items',
|
||||||
|
templateUrl: './side-menu-items.html',
|
||||||
|
styleUrls: ['./side-menu-items.css']
|
||||||
|
})
|
||||||
|
export class SideMenuItems {
|
||||||
|
@Input() items: MenuItem[];
|
||||||
|
@Output() activate = new EventEmitter<MenuItem>();
|
||||||
|
|
||||||
|
activateItem(item) {
|
||||||
|
this.activate.next(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'side-menu',
|
selector: 'side-menu',
|
||||||
templateUrl: './side-menu.html',
|
templateUrl: './side-menu.html',
|
||||||
styleUrls: ['./side-menu.css'],
|
styleUrls: ['./side-menu.css'],
|
||||||
animations: [
|
animations: [
|
||||||
trigger('itemAnimation', [
|
// trigger('itemAnimation', [
|
||||||
state('collapsed, void',
|
// state('collapsed, void',
|
||||||
style({ height: '0px' })),
|
// style({ height: '0px' })),
|
||||||
state('expanded',
|
// state('expanded',
|
||||||
style({ height: '*' })),
|
// style({ height: '*' })),
|
||||||
transition('collapsed <=> expanded', [
|
// transition('collapsed <=> expanded', [
|
||||||
animate('200ms ease')
|
// animate('200ms ease')
|
||||||
])
|
// ])
|
||||||
])
|
// ])
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
|
export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
|
||||||
activeCatCaption: string;
|
activeCatCaption: string;
|
||||||
activeItemCaption: string;
|
activeItemCaption: string;
|
||||||
categories: Array<MenuCategory>;
|
menuItems: Array<MenuItem>;
|
||||||
|
|
||||||
private options: any;
|
private options: any;
|
||||||
private $element: any;
|
private $element: any;
|
||||||
|
@ -54,11 +67,18 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
|
||||||
this.menuService.changed.subscribe((evt) => this.changed(evt));
|
this.menuService.changed.subscribe((evt) => this.changed(evt));
|
||||||
}
|
}
|
||||||
|
|
||||||
changed(newItem) {
|
changed(item) {
|
||||||
if (newItem) {
|
if (!item) {
|
||||||
let {cat, item} = newItem;
|
this.activeCatCaption = '';
|
||||||
this.activeCatCaption = cat.name || '';
|
this.activeItemCaption = '';
|
||||||
this.activeItemCaption = item && item.summary || '';
|
return;
|
||||||
|
}
|
||||||
|
if (item.parent) {
|
||||||
|
this.activeItemCaption = item.name;
|
||||||
|
this.activeCatCaption = item.parent.name;
|
||||||
|
} else {
|
||||||
|
this.activeCatCaption = item.name;
|
||||||
|
this.activeItemCaption = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
//safari doesn't update bindings if not run changeDetector manually :(
|
//safari doesn't update bindings if not run changeDetector manually :(
|
||||||
|
@ -74,22 +94,17 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
|
||||||
if ($item) $item.scrollIntoView();
|
if ($item) $item.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
activateAndScroll(catIdx, methodIdx) {
|
activateAndScroll(item) {
|
||||||
if (this.mobileMode()) {
|
if (this.mobileMode) {
|
||||||
this.toggleMobileNav();
|
this.toggleMobileNav();
|
||||||
}
|
}
|
||||||
let menu = this.categories;
|
|
||||||
|
|
||||||
if (!menu[catIdx].ready) return;
|
this.menuService.activate(item.flatIdx);
|
||||||
if (menu[catIdx].methods && menu[catIdx].methods.length && (methodIdx >= 0) &&
|
|
||||||
!menu[catIdx].methods[methodIdx].ready) return;
|
|
||||||
|
|
||||||
this.menuService.activate(catIdx, methodIdx);
|
|
||||||
this.menuService.scrollToActive();
|
this.menuService.scrollToActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.categories = this.menuService.categories;
|
this.menuItems = this.menuService.items;
|
||||||
|
|
||||||
this.$mobileNav = DOM.querySelector(this.$element, '.mobile-nav');
|
this.$mobileNav = DOM.querySelector(this.$element, '.mobile-nav');
|
||||||
this.$resourcesNav = DOM.querySelector(this.$element, '#resources-nav');
|
this.$resourcesNav = DOM.querySelector(this.$element, '#resources-nav');
|
||||||
|
@ -101,7 +116,7 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mobileMode() {
|
get mobileMode() {
|
||||||
return this.$mobileNav.clientHeight > 0;
|
return this.$mobileNav.clientHeight > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { RequestSamples } from './RequestSamples/request-samples';
|
||||||
import { ResponsesList } from './ResponsesList/responses-list';
|
import { ResponsesList } from './ResponsesList/responses-list';
|
||||||
import { ResponsesSamples } from './ResponsesSamples/responses-samples';
|
import { ResponsesSamples } from './ResponsesSamples/responses-samples';
|
||||||
import { SchemaSample } from './SchemaSample/schema-sample';
|
import { SchemaSample } from './SchemaSample/schema-sample';
|
||||||
import { SideMenu } from './SideMenu/side-menu';
|
import { SideMenu, SideMenuItems } from './SideMenu/side-menu';
|
||||||
import { MethodsList } from './MethodsList/methods-list';
|
import { MethodsList } from './MethodsList/methods-list';
|
||||||
import { Method } from './Method/method';
|
import { Method } from './Method/method';
|
||||||
import { Warnings } from './Warnings/warnings';
|
import { Warnings } from './Warnings/warnings';
|
||||||
|
@ -21,9 +21,9 @@ import { Redoc } from './Redoc/redoc';
|
||||||
export const REDOC_DIRECTIVES = [
|
export const REDOC_DIRECTIVES = [
|
||||||
ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
||||||
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
||||||
LoadingBar
|
LoadingBar, SideMenuItems
|
||||||
];
|
];
|
||||||
|
|
||||||
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList,
|
||||||
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions,
|
||||||
LoadingBar }
|
LoadingBar, SideMenuItems }
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
|
|
||||||
import { MethodsList } from '../components/MethodsList/methods-list';
|
import { MethodsList } from '../components/MethodsList/methods-list';
|
||||||
import { MenuService } from './menu.service';
|
import { MenuService, MenuItem } from './menu.service';
|
||||||
import { Hash } from './hash.service';
|
import { Hash } from './hash.service';
|
||||||
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
|
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
|
||||||
import { ScrollService } from './scroll.service';
|
import { ScrollService } from './scroll.service';
|
||||||
|
@ -18,7 +18,7 @@ describe('Menu service', () => {
|
||||||
TestBed.configureTestingModule({ declarations: [ TestAppComponent, MethodsList ] });
|
TestBed.configureTestingModule({ declarations: [ TestAppComponent, MethodsList ] });
|
||||||
});
|
});
|
||||||
|
|
||||||
let menu, hashService, scroll, tasks;
|
let menu:MenuService, hashService, scroll, tasks;
|
||||||
let specMgr;
|
let specMgr;
|
||||||
|
|
||||||
beforeEach(inject([SpecManager, Hash, ScrollService, LazyTasksService],
|
beforeEach(inject([SpecManager, Hash, ScrollService, LazyTasksService],
|
||||||
|
@ -70,22 +70,113 @@ describe('Menu service', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select next/prev menu item when scrolled down/up', () => {
|
it('should select next/prev menu item when scrolled down/up', () => {
|
||||||
scroll.$scrollParent = document.querySelector('#parent');
|
// enable all items
|
||||||
menu.activeCatIdx.should.be.equal(0);
|
menu.items.forEach(item => item.ready = true);
|
||||||
menu.activeMethodIdx.should.be.equal(-1);
|
|
||||||
let nextElTop = menu.getRelativeCatOrItem(1).getBoundingClientRect().top;
|
|
||||||
|
|
||||||
|
scroll.$scrollParent = document.querySelector('#parent');
|
||||||
|
menu.activeIdx.should.be.equal(-1);
|
||||||
|
|
||||||
|
let nextElTop = menu.getEl(1).getBoundingClientRect().top;
|
||||||
scroll.$scrollParent.scrollTop = nextElTop + 1;
|
scroll.$scrollParent.scrollTop = nextElTop + 1;
|
||||||
|
|
||||||
//simulate scroll down
|
//simulate scroll down
|
||||||
spyOn(scroll, 'scrollY').and.returnValue(nextElTop + 10);
|
spyOn(scroll, 'scrollY').and.returnValue(nextElTop + 10);
|
||||||
menu.scrollUpdate(true);
|
menu.onScroll(true);
|
||||||
menu.activeCatIdx.should.be.equal(1);
|
menu.activeIdx.should.be.equal(1);
|
||||||
|
|
||||||
scroll.scrollY.and.returnValue(nextElTop - 2);
|
scroll.scrollY.and.returnValue(nextElTop - 2);
|
||||||
scroll.$scrollParent.scrollTop = nextElTop - 1;
|
scroll.$scrollParent.scrollTop = nextElTop - 1;
|
||||||
menu.scrollUpdate(false);
|
menu.onScroll(false);
|
||||||
menu.activeCatIdx.should.be.equal(0);
|
menu.activeIdx.should.be.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildMenu method', () => {
|
||||||
|
let suitSchema = {
|
||||||
|
tags: [
|
||||||
|
{name: 'tag1', description: 'info1', 'x-traitTag': true},
|
||||||
|
{name: 'tag2', description: 'info2'},
|
||||||
|
{name: 'tag4', description: 'info2', 'x-displayName': 'Tag Four'}
|
||||||
|
],
|
||||||
|
paths: {
|
||||||
|
test: {
|
||||||
|
put: {
|
||||||
|
tags: ['tag1', 'tag3'],
|
||||||
|
summary: 'test put'
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
summary: 'test get'
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
tags: ['tag4'],
|
||||||
|
summary: 'test delete'
|
||||||
|
},
|
||||||
|
// no tags
|
||||||
|
post: {
|
||||||
|
summary: 'test post'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let items:MenuItem[];
|
||||||
|
beforeEach(() => {
|
||||||
|
menu.items = null;
|
||||||
|
specMgr._schema = suitSchema;
|
||||||
|
menu.buildMenu();
|
||||||
|
items = menu.items;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return instance of Array', () => {
|
||||||
|
items.should.be.instanceof(Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Array with correct number of items', () => {
|
||||||
|
// 3 - defined tags, 1 - tag3 and 1 method item for method without tag
|
||||||
|
items.length.should.be.equal(3 + 1 + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append not defined tags to the end of list', () => {
|
||||||
|
let item = items[3];
|
||||||
|
item.name.should.be.equal('tag3');
|
||||||
|
item.items.length.should.be.equal(1);
|
||||||
|
item.items[0].name.should.be.equal('test put');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append method items without tags to the end of list', () => {
|
||||||
|
let methodItem = items[4];
|
||||||
|
methodItem.name.should.be.equal('test post');
|
||||||
|
methodItem.metadata.type.should.be.equal('method');
|
||||||
|
should.not.exist(methodItem.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map x-traitTag to empty method list', () => {
|
||||||
|
let item = items[0];
|
||||||
|
should.not.exist(item.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('methods for tag should contain valid pointer and name', () => {
|
||||||
|
for (let item of items) {
|
||||||
|
item.should.be.an.Object();
|
||||||
|
if (item.items) {
|
||||||
|
for (let subItem of item.items) {
|
||||||
|
subItem.should.have.properties(['metadata']);
|
||||||
|
let pointer = subItem.metadata.pointer;
|
||||||
|
let methSchema = specMgr.byPointer(pointer);
|
||||||
|
should.exist(methSchema);
|
||||||
|
if (methSchema.summary) {
|
||||||
|
methSchema.summary.should.be.equal(subItem.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use x-displayName to set custom names', () => {
|
||||||
|
let info = items[2];
|
||||||
|
info.id.should.be.equal('tag/tag4');
|
||||||
|
info.name.should.be.equal('Tag Four');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,97 +5,113 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
import { ScrollService, INVIEW_POSITION } from './scroll.service';
|
import { ScrollService, INVIEW_POSITION } from './scroll.service';
|
||||||
import { Hash } from './hash.service';
|
import { Hash } from './hash.service';
|
||||||
import { SpecManager } from '../utils/spec-manager';
|
import { SpecManager } from '../utils/spec-manager';
|
||||||
import { SchemaHelper, MenuCategory } from './schema-helper.service';
|
import { SchemaHelper } from './schema-helper.service';
|
||||||
import { AppStateService } from './app-state.service';
|
import { AppStateService } from './app-state.service';
|
||||||
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
|
import { LazyTasksService } from '../shared/components/LazyFor/lazy-for';
|
||||||
|
import { JsonPointer } from '../utils/JsonPointer';
|
||||||
|
import * as slugify from 'slugify';
|
||||||
|
|
||||||
|
|
||||||
const CHANGE = {
|
const CHANGE = {
|
||||||
NEXT : 1,
|
NEXT : 1,
|
||||||
BACK : -1,
|
BACK : -1,
|
||||||
INITIAL : 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface TagGroup {
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
items?: Array<MenuItem>;
|
||||||
|
parent?: MenuItem;
|
||||||
|
|
||||||
|
active?: boolean;
|
||||||
|
ready?: boolean;
|
||||||
|
|
||||||
|
level?: number;
|
||||||
|
flatIdx?: number;
|
||||||
|
|
||||||
|
metadata?: any;
|
||||||
|
isGroup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MenuService {
|
export class MenuService {
|
||||||
|
|
||||||
changed: EventEmitter<any> = new EventEmitter();
|
changed: EventEmitter<any> = new EventEmitter();
|
||||||
ready: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
|
||||||
categories: Array<MenuCategory>;
|
|
||||||
|
|
||||||
activeCatIdx: number = 0;
|
items: MenuItem[];
|
||||||
activeMethodIdx: number = -1;
|
activeIdx: number = -1;
|
||||||
|
|
||||||
|
private _flatItems: MenuItem[];
|
||||||
private _hashSubscription: Subscription;
|
private _hashSubscription: Subscription;
|
||||||
|
private _scrollSubscription: Subscription;
|
||||||
|
private _tagsWithMethods: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private hash:Hash,
|
private hash:Hash,
|
||||||
private tasks: LazyTasksService,
|
private tasks: LazyTasksService,
|
||||||
private scrollService: ScrollService,
|
private scrollService: ScrollService,
|
||||||
private appState: AppStateService,
|
private appState: AppStateService,
|
||||||
specMgr:SpecManager
|
private specMgr:SpecManager
|
||||||
) {
|
) {
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
this.categories = SchemaHelper.buildMenuTree(specMgr.schema);
|
this.buildMenu();
|
||||||
|
|
||||||
scrollService.scroll.subscribe((evt) => {
|
this._scrollSubscription = scrollService.scroll.subscribe((evt) => {
|
||||||
this.scrollUpdate(evt.isScrolledDown);
|
this.onScroll(evt.isScrolledDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
//this.changeActive(CHANGE.INITIAL);
|
|
||||||
|
|
||||||
this._hashSubscription = this.hash.value.subscribe((hash) => {
|
this._hashSubscription = this.hash.value.subscribe((hash) => {
|
||||||
if (hash == undefined) return;
|
this.onHashChange(hash);
|
||||||
this.setActiveByHash(hash);
|
|
||||||
if (!this.tasks.empty) {
|
|
||||||
this.tasks.start(this.activeCatIdx, this.activeMethodIdx, this);
|
|
||||||
this.scrollService.setStickElement(this.getCurrentMethodEl());
|
|
||||||
if (hash) this.scrollToActive();
|
|
||||||
this.appState.stopLoading();
|
|
||||||
} else {
|
|
||||||
if (hash) this.scrollToActive();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enableItem(catIdx, methodIdx, skipUpdate = false) {
|
get flatItems():MenuItem[] {
|
||||||
let cat = this.categories[catIdx];
|
if (!this._flatItems) {
|
||||||
cat.ready = true;
|
this._flatItems = this.flatMenu();
|
||||||
if (cat.methods.length) cat.methods[methodIdx].ready = true;
|
}
|
||||||
let prevCat = this.categories[catIdx - 1];
|
return this._flatItems;
|
||||||
if (prevCat && !prevCat.ready && (prevCat.virtual || !prevCat.methods.length)) {
|
}
|
||||||
this.enableItem(catIdx - 1, -1, true);
|
|
||||||
|
enableItem(idx) {
|
||||||
|
let item = this.flatItems[idx];
|
||||||
|
item.ready = true;
|
||||||
|
if (item.parent) {
|
||||||
|
item.parent.ready = true;
|
||||||
|
idx = item.parent.flatIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if previous items can be enabled
|
||||||
|
let prevItem = this.flatItems[idx -= 1];
|
||||||
|
while(prevItem && (!prevItem.metadata || !prevItem.items)) {
|
||||||
|
prevItem.ready = true;
|
||||||
|
prevItem = this.flatItems[idx -= 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipUpdate) return;
|
|
||||||
this.changed.next();
|
this.changed.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeMethodPtr() {
|
onScroll(isScrolledDown) {
|
||||||
let cat = this.categories[this.activeCatIdx];
|
|
||||||
let ptr = null;
|
|
||||||
if (cat && cat.methods.length) {
|
|
||||||
let mtd = cat.methods[this.activeMethodIdx];
|
|
||||||
ptr = mtd && mtd.pointer || null;
|
|
||||||
}
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollUpdate(isScrolledDown) {
|
|
||||||
let stable = false;
|
let stable = false;
|
||||||
while(!stable) {
|
while(!stable) {
|
||||||
let $activeMethodHost = this.getCurrentMethodEl();
|
|
||||||
if (!$activeMethodHost) return;
|
|
||||||
var elementInViewPos = this.scrollService.getElementPos($activeMethodHost);
|
|
||||||
if(isScrolledDown) {
|
if(isScrolledDown) {
|
||||||
//&& elementInViewPos === INVIEW_POSITION.BELLOW
|
let $nextEl = this.getEl(this.activeIdx + 1);
|
||||||
let $nextEl = this.getRelativeCatOrItem(1);
|
|
||||||
if (!$nextEl) return;
|
if (!$nextEl) return;
|
||||||
let nextInViewPos = this.scrollService.getElementPos($nextEl, true);
|
let nextInViewPos = this.scrollService.getElementPos($nextEl, true);
|
||||||
if (elementInViewPos === INVIEW_POSITION.BELLOW && nextInViewPos === INVIEW_POSITION.ABOVE) {
|
if (nextInViewPos === INVIEW_POSITION.ABOVE) {
|
||||||
stable = this.changeActive(CHANGE.NEXT);
|
stable = this.changeActive(CHANGE.NEXT);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let $currentEl = this.getCurrentEl();
|
||||||
|
if (!$currentEl) return;
|
||||||
|
var elementInViewPos = this.scrollService.getElementPos($currentEl);
|
||||||
if(!isScrolledDown && elementInViewPos === INVIEW_POSITION.ABOVE ) {
|
if(!isScrolledDown && elementInViewPos === INVIEW_POSITION.ABOVE ) {
|
||||||
stable = this.changeActive(CHANGE.BACK);
|
stable = this.changeActive(CHANGE.BACK);
|
||||||
continue;
|
continue;
|
||||||
|
@ -104,136 +120,291 @@ export class MenuService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRelativeCatOrItem(offset: number = 0) {
|
onHashChange(hash?: string) {
|
||||||
let ptr, cat;
|
if (hash == undefined) return;
|
||||||
cat = this.categories[this.activeCatIdx];
|
let activated = this.activateByHash(hash);
|
||||||
if (cat.methods.length === 0) {
|
if (!this.tasks.empty) {
|
||||||
ptr = null;
|
this.tasks.start(this.activeIdx, this);
|
||||||
cat = this.categories[this.activeCatIdx + Math.sign(offset)] || cat;
|
this.scrollService.setStickElement(this.getCurrentEl());
|
||||||
|
if (activated) this.scrollToActive();
|
||||||
|
this.appState.stopLoading();
|
||||||
} else {
|
} else {
|
||||||
let cat = this.categories[this.activeCatIdx];
|
if (activated) this.scrollToActive();
|
||||||
let idx = this.activeMethodIdx + offset;
|
}
|
||||||
if ((idx >= cat.methods.length - 1) || idx < 0) {
|
}
|
||||||
cat = this.categories[this.activeCatIdx + Math.sign(offset)] || cat;
|
|
||||||
idx = offset > 0 ? -1 : cat.methods.length - 1;
|
getEl(flatIdx:number):Element {
|
||||||
|
if (flatIdx < 0) return null;
|
||||||
|
let currentItem = this.flatItems[flatIdx];
|
||||||
|
if (!currentItem) return;
|
||||||
|
if (currentItem.isGroup) currentItem = this.flatItems[flatIdx + 1];
|
||||||
|
|
||||||
|
let selector = '';
|
||||||
|
while(currentItem) {
|
||||||
|
if (currentItem.id) {
|
||||||
|
selector = `[section="${currentItem.id}"] ` + selector;
|
||||||
|
// We only need to go up the chain for methods that
|
||||||
|
// might have multiple tags. For headers/subheaders
|
||||||
|
// we need to siply early terminate.
|
||||||
|
if (!currentItem.metadata) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ptr = cat.methods[idx] && cat.methods[idx].pointer;
|
currentItem = currentItem.parent;
|
||||||
}
|
}
|
||||||
|
selector = selector.trim();
|
||||||
return this.getMethodElByPtr(ptr, cat.id);
|
return selector ? document.querySelector(selector) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentMethodEl() {
|
getCurrentEl():Element {
|
||||||
return this.getMethodElByPtr(this.activeMethodPtr,
|
return this.getEl(this.activeIdx);
|
||||||
this.categories[this.activeCatIdx].id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMethodElByPtr(ptr, section) {
|
deactivate(idx) {
|
||||||
let selector = ptr ? `[pointer="${ptr}"][section="${section}"]` : `[section="${section}"]`;
|
if (idx < 0) return;
|
||||||
return document.querySelector(selector);
|
|
||||||
|
let item = this.flatItems[idx];
|
||||||
|
item.active = false;
|
||||||
|
while (item.parent) {
|
||||||
|
item.parent.active = false;
|
||||||
|
item = item.parent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMethodElByOperId(operationId) {
|
activate(idx, force = false) {
|
||||||
let selector =`[operation-id="${operationId}"]`;
|
let item = this.flatItems[idx];
|
||||||
return document.querySelector(selector);
|
if (!force && item && !item.ready) return;
|
||||||
|
|
||||||
|
this.deactivate(this.activeIdx);
|
||||||
|
this.activeIdx = idx;
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
item.active = true;
|
||||||
|
|
||||||
|
let cItem = item;
|
||||||
|
while (cItem.parent) {
|
||||||
|
cItem.parent.active = true;
|
||||||
|
cItem = cItem.parent;
|
||||||
|
}
|
||||||
|
this.changed.next(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
activate(catIdx, methodIdx) {
|
changeActive(offset = 1):boolean {
|
||||||
if (catIdx < 0) return;
|
let noChange = (this.activeIdx <= 0 && offset === -1) ||
|
||||||
|
(this.activeIdx === this.flatItems.length - 1 && offset === 1);
|
||||||
let menu = this.categories;
|
this.activate(this.activeIdx + offset);
|
||||||
|
return noChange;
|
||||||
menu[this.activeCatIdx].active = false;
|
|
||||||
if (menu[this.activeCatIdx].methods.length) {
|
|
||||||
if (this.activeMethodIdx >= 0) {
|
|
||||||
menu[this.activeCatIdx].methods[this.activeMethodIdx].active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeCatIdx = catIdx;
|
|
||||||
this.activeMethodIdx = methodIdx;
|
|
||||||
menu[catIdx].active = true;
|
|
||||||
let currentItem;
|
|
||||||
if (menu[catIdx].methods.length && (methodIdx > -1)) {
|
|
||||||
currentItem = menu[catIdx].methods[methodIdx];
|
|
||||||
currentItem.active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changed.next({cat: menu[catIdx], item: currentItem});
|
|
||||||
}
|
|
||||||
|
|
||||||
_calcActiveIndexes(offset) {
|
|
||||||
let menu = this.categories;
|
|
||||||
let catCount = menu.length;
|
|
||||||
if (!catCount) return [0, -1];
|
|
||||||
let catLength = menu[this.activeCatIdx].methods.length;
|
|
||||||
|
|
||||||
let resMethodIdx = this.activeMethodIdx + offset;
|
|
||||||
let resCatIdx = this.activeCatIdx;
|
|
||||||
|
|
||||||
if (resMethodIdx > catLength - 1) {
|
|
||||||
resCatIdx++;
|
|
||||||
resMethodIdx = -1;
|
|
||||||
}
|
|
||||||
if (resMethodIdx < -1) {
|
|
||||||
let prevCatIdx = --resCatIdx;
|
|
||||||
catLength = menu[Math.max(prevCatIdx, 0)].methods.length;
|
|
||||||
resMethodIdx = catLength - 1;
|
|
||||||
}
|
|
||||||
if (resCatIdx > catCount - 1) {
|
|
||||||
resCatIdx = catCount - 1;
|
|
||||||
resMethodIdx = catLength - 1;
|
|
||||||
}
|
|
||||||
if (resCatIdx < 0) {
|
|
||||||
resCatIdx = 0;
|
|
||||||
resMethodIdx = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [resCatIdx, resMethodIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
changeActive(offset = 1) {
|
|
||||||
let [catIdx, methodIdx] = this._calcActiveIndexes(offset);
|
|
||||||
this.activate(catIdx, methodIdx);
|
|
||||||
return (methodIdx === 0 && catIdx === 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToActive() {
|
scrollToActive() {
|
||||||
this.scrollService.scrollTo(this.getCurrentMethodEl());
|
let $el = this.getCurrentEl();
|
||||||
|
if ($el) this.scrollService.scrollTo($el);
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveByHash(hash) {
|
activateByHash(hash):boolean {
|
||||||
if (!hash) {
|
if (!hash) return;
|
||||||
if (this.categories[0].headless) {
|
let idx = 0;
|
||||||
this.activate(0, 0);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let catIdx, methodIdx;
|
|
||||||
hash = hash.substr(1);
|
hash = hash.substr(1);
|
||||||
let namespace = hash.split('/')[0];
|
let namespace = hash.split('/')[0];
|
||||||
let ptr = decodeURIComponent(hash.substr(namespace.length + 1));
|
let ptr = decodeURIComponent(hash.substr(namespace.length + 1));
|
||||||
if (namespace === 'section' || namespace === 'tag') {
|
if (namespace === 'section' || namespace === 'tag') {
|
||||||
let sectionId = ptr.split('/')[0];
|
let sectionId = ptr.split('/')[0];
|
||||||
catIdx = this.categories.findIndex(cat => cat.id === namespace + '/' + sectionId);
|
|
||||||
let cat = this.categories[catIdx];
|
|
||||||
ptr = ptr.substr(sectionId.length) || null;
|
ptr = ptr.substr(sectionId.length) || null;
|
||||||
methodIdx = cat.methods.findIndex(method => method.pointer === ptr);
|
|
||||||
} else {
|
let searchId;
|
||||||
catIdx = this.categories.findIndex(cat => {
|
if (namespace === 'section') {
|
||||||
if (!cat.methods.length) return false;
|
searchId = hash;
|
||||||
methodIdx = cat.methods.findIndex(method => method.operationId === ptr || method.pointer === ptr);
|
} else {
|
||||||
if (methodIdx >= 0) {
|
searchId = ptr || (namespace + '/' + sectionId);;
|
||||||
return true;
|
}
|
||||||
} else {
|
|
||||||
return false;
|
idx = this.flatItems.findIndex(item => item.id === searchId);
|
||||||
}
|
if (idx < 0) this.tryScrollToId(searchId);
|
||||||
|
} else if (namespace === 'operation') {
|
||||||
|
idx = this.flatItems.findIndex(item => {
|
||||||
|
return item.metadata && item.metadata.operationId === ptr;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.activate(catIdx, methodIdx);
|
this.activate(idx, true);
|
||||||
|
return idx >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryScrollToId(id) {
|
||||||
|
let $el = document.querySelector(`[section="${id}"]`);
|
||||||
|
if ($el) this.scrollService.scrollTo($el);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarkdownItems() {
|
||||||
|
let schema = this.specMgr.schema;
|
||||||
|
for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) {
|
||||||
|
let id = 'section/' + slugify(header);
|
||||||
|
let item = {
|
||||||
|
name: header,
|
||||||
|
id: id,
|
||||||
|
items: null
|
||||||
|
};
|
||||||
|
item.items = this.getMarkdownSubheaders(item);
|
||||||
|
|
||||||
|
this.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMarkdownSubheaders(parent: MenuItem):MenuItem[] {
|
||||||
|
let res = [];
|
||||||
|
|
||||||
|
let schema = this.specMgr.schema;
|
||||||
|
for (let subheader of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-subheaders'] || []))) {
|
||||||
|
let parts = subheader.split('/');
|
||||||
|
let header = parts[0];
|
||||||
|
if (parent.name !== header) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = parts[1];
|
||||||
|
let id = parent.id + '/' + slugify(name);
|
||||||
|
|
||||||
|
let subItem = {
|
||||||
|
name: name,
|
||||||
|
id: id,
|
||||||
|
parent: parent
|
||||||
|
};
|
||||||
|
res.push(subItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMethodsItems(parent: MenuItem, tag:any):MenuItem[] {
|
||||||
|
if (!tag.methods || !tag.methods.length) return null;
|
||||||
|
|
||||||
|
let res = [];
|
||||||
|
for (let method of tag.methods) {
|
||||||
|
let subItem = {
|
||||||
|
name: SchemaHelper.methodSummary(method),
|
||||||
|
id: method._pointer,
|
||||||
|
description: method.description,
|
||||||
|
metadata: {
|
||||||
|
type: 'method',
|
||||||
|
pointer: method._pointer,
|
||||||
|
operationId: method.operationId
|
||||||
|
},
|
||||||
|
parent: parent
|
||||||
|
};
|
||||||
|
res.push(subItem);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagsItems(parent: MenuItem, tagGroup:TagGroup = null):MenuItem[] {
|
||||||
|
let schema = this.specMgr.schema;
|
||||||
|
|
||||||
|
let tags;
|
||||||
|
if (!tagGroup) {
|
||||||
|
// all tags
|
||||||
|
tags = Object.keys(this._tagsWithMethods);
|
||||||
|
} else {
|
||||||
|
tags = tagGroup.tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = tags.map(k => {
|
||||||
|
if (!this._tagsWithMethods[k]) {
|
||||||
|
console.warn(`Non-existing tag "${k}" is specified in tag group "${tagGroup.name}"`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this._tagsWithMethods[k].used = true;
|
||||||
|
return this._tagsWithMethods[k];
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = [];
|
||||||
|
for (let tag of tags || []) {
|
||||||
|
if (!tag) continue;
|
||||||
|
let id = 'tag/' + slugify(tag.name);
|
||||||
|
let item: MenuItem;
|
||||||
|
|
||||||
|
// don't put empty tag into menu, instead put their methods
|
||||||
|
if (tag.name === '') {
|
||||||
|
let items = this.getMethodsItems(null, tag);
|
||||||
|
res.push(...items);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = {
|
||||||
|
name: tag['x-displayName'] || tag.name,
|
||||||
|
id: id,
|
||||||
|
description: tag.description,
|
||||||
|
metadata: { type: 'tag' },
|
||||||
|
parent: parent,
|
||||||
|
items: null
|
||||||
|
};
|
||||||
|
item.items = this.getMethodsItems(item, tag);
|
||||||
|
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagGroupsItems(parent: MenuItem, groups: TagGroup[]):MenuItem[] {
|
||||||
|
let res = [];
|
||||||
|
for (let group of groups) {
|
||||||
|
let item;
|
||||||
|
item = {
|
||||||
|
name: group.name,
|
||||||
|
id: null,
|
||||||
|
description: '',
|
||||||
|
parent: parent,
|
||||||
|
isGroup: true,
|
||||||
|
items: null
|
||||||
|
};
|
||||||
|
item.items = this.getTagsItems(item, group);
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
this.checkAllTagsUsedInGroups();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAllTagsUsedInGroups() {
|
||||||
|
for (let tag of Object.keys(this._tagsWithMethods)) {
|
||||||
|
if (!this._tagsWithMethods[tag].used) {
|
||||||
|
console.warn(`Tag "${tag}" is not added to any group`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMenu() {
|
||||||
|
this._tagsWithMethods = SchemaHelper.getTagsWithMethods(this.specMgr.schema);
|
||||||
|
|
||||||
|
this.items = this.items || [];
|
||||||
|
this.addMarkdownItems();
|
||||||
|
if (this.specMgr.schema['x-tagGroups']) {
|
||||||
|
this.items.push(...this.getTagGroupsItems(null, this.specMgr.schema['x-tagGroups']));
|
||||||
|
} else {
|
||||||
|
this.items.push(...this.getTagsItems(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flatMenu():MenuItem[] {
|
||||||
|
let menu = this.items;
|
||||||
|
let res = [];
|
||||||
|
let curDepth = 1;
|
||||||
|
|
||||||
|
let recursive = (items) => {
|
||||||
|
for (let item of items) {
|
||||||
|
res.push(item);
|
||||||
|
item.depth = item.isGroup ? 0 : curDepth;
|
||||||
|
item.flatIdx = res.length - 1;
|
||||||
|
if (item.items) {
|
||||||
|
if (!item.isGroup) curDepth++;
|
||||||
|
recursive(item.items);
|
||||||
|
if (!item.isGroup) curDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recursive(menu);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this._hashSubscription.unsubscribe();
|
this._hashSubscription.unsubscribe();
|
||||||
|
this._scrollSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,100 +3,6 @@ import { SchemaHelper } from './schema-helper.service';
|
||||||
import { SpecManager } from '../utils/spec-manager';
|
import { SpecManager } from '../utils/spec-manager';
|
||||||
|
|
||||||
describe('Spec Helper', () => {
|
describe('Spec Helper', () => {
|
||||||
describe('buildMenuTree method', () => {
|
|
||||||
let suitSchema = {
|
|
||||||
tags: [
|
|
||||||
{name: 'tag1', description: 'info1', 'x-traitTag': true},
|
|
||||||
{name: 'tag2', description: 'info2'},
|
|
||||||
{name: 'tag4', description: 'info2', 'x-displayName': 'Tag Four'}
|
|
||||||
],
|
|
||||||
paths: {
|
|
||||||
test: {
|
|
||||||
put: {
|
|
||||||
tags: ['tag1', 'tag3'],
|
|
||||||
summary: 'test put'
|
|
||||||
},
|
|
||||||
get: {
|
|
||||||
tags: ['tag1', 'tag2'],
|
|
||||||
summary: 'test get'
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
tags: ['tag4'],
|
|
||||||
summary: 'test delete'
|
|
||||||
},
|
|
||||||
// no tags
|
|
||||||
post: {
|
|
||||||
summary: 'test post'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let menuTree;
|
|
||||||
let specMgr;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
specMgr = new SpecManager();
|
|
||||||
specMgr._schema = suitSchema;
|
|
||||||
menuTree = SchemaHelper.buildMenuTree(suitSchema);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return instance of Array', () => {
|
|
||||||
menuTree.should.be.instanceof(Array);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return Array with correct number of items', () => {
|
|
||||||
//3 - defined tags, 1 - tag3 and 1 [other] tag for no-tags method
|
|
||||||
menuTree.length.should.be.equal(3 + 1 + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should append not defined tags to the end of list', () => {
|
|
||||||
let info = menuTree[3];
|
|
||||||
info.name.should.be.equal('tag3');
|
|
||||||
info.methods.length.should.be.equal(1);
|
|
||||||
info.methods[0].summary.should.be.equal('test put');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should append methods without tags to [other] tag', () => {
|
|
||||||
let info = menuTree[4];
|
|
||||||
info.name.should.be.equal('');
|
|
||||||
info.methods.length.should.be.equal(1);
|
|
||||||
info.methods[0].summary.should.be.equal('test post');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should map x-traitTag to empty section', () => {
|
|
||||||
let info = menuTree[0];
|
|
||||||
info.empty.should.be.true();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should map x-traitTag to empty methods list', () => {
|
|
||||||
let info = menuTree[0];
|
|
||||||
info.methods.should.be.empty();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('methods for tag should contain valid pointer and summary', () => {
|
|
||||||
for (let entr of menuTree) {
|
|
||||||
let info = entr;
|
|
||||||
info.should.be.an.Object();
|
|
||||||
info.methods.should.be.an.Array();
|
|
||||||
for (let methodInfo of info.methods) {
|
|
||||||
methodInfo.should.have.properties(['pointer', 'summary']);
|
|
||||||
let methSchema = specMgr.byPointer(methodInfo.pointer);
|
|
||||||
expect(methSchema).not.toBeNull();
|
|
||||||
if (methSchema.summary) {
|
|
||||||
methSchema.summary.should.be.equal(methodInfo.summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use x-displayName to set custom names', () => {
|
|
||||||
let info = menuTree[2];
|
|
||||||
info.id.should.be.equal('tag/tag4');
|
|
||||||
info.name.should.be.equal('Tag Four');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('injectors', () => {
|
describe('injectors', () => {
|
||||||
it('should autodetect type if not-specified', () => {
|
it('should autodetect type if not-specified', () => {
|
||||||
spyOn(console, 'warn').and.stub();
|
spyOn(console, 'warn').and.stub();
|
||||||
|
|
|
@ -9,28 +9,6 @@ interface PropertyPreprocessOptions {
|
||||||
skipReadOnly?: boolean;
|
skipReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuMethod {
|
|
||||||
active: boolean;
|
|
||||||
summary: string;
|
|
||||||
tag: string;
|
|
||||||
pointer: string;
|
|
||||||
operationId: string;
|
|
||||||
ready: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuCategory {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
active?: boolean;
|
|
||||||
methods?: Array<MenuMethod>;
|
|
||||||
description?: string;
|
|
||||||
empty?: string;
|
|
||||||
virtual?: boolean;
|
|
||||||
ready: boolean;
|
|
||||||
headless: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// global var for this module
|
// global var for this module
|
||||||
var specMgrInstance;
|
var specMgrInstance;
|
||||||
|
|
||||||
|
@ -300,30 +278,11 @@ export class SchemaHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static buildMenuTree(schema):Array<MenuCategory> {
|
static getTagsWithMethods(schema) {
|
||||||
var catIdx = 0;
|
let tags = {};
|
||||||
let tag2MethodMapping = {};
|
|
||||||
|
|
||||||
for (let header of (<Array<string>>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) {
|
|
||||||
let id = 'section/' + slugify(header);
|
|
||||||
tag2MethodMapping[id] = {
|
|
||||||
name: header, id: id, virtual: true, methods: [], idx: catIdx
|
|
||||||
};
|
|
||||||
catIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let tag of schema.tags || []) {
|
for (let tag of schema.tags || []) {
|
||||||
let id = 'tag/' + slugify(tag.name);
|
tags[tag.name] = tag;
|
||||||
tag2MethodMapping[id] = {
|
tag.methods = [];
|
||||||
name: tag['x-displayName'] || tag.name,
|
|
||||||
id: id,
|
|
||||||
description: tag.description,
|
|
||||||
headless: tag.name === '',
|
|
||||||
empty: !!tag['x-traitTag'],
|
|
||||||
methods: [],
|
|
||||||
idx: catIdx
|
|
||||||
};
|
|
||||||
catIdx++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let paths = schema.paths;
|
let paths = schema.paths;
|
||||||
|
@ -331,39 +290,29 @@ export class SchemaHelper {
|
||||||
let methods = Object.keys(paths[path]).filter((k) => swaggerMethods.has(k));
|
let methods = Object.keys(paths[path]).filter((k) => swaggerMethods.has(k));
|
||||||
for (let method of methods) {
|
for (let method of methods) {
|
||||||
let methodInfo = paths[path][method];
|
let methodInfo = paths[path][method];
|
||||||
let tags = methodInfo.tags;
|
let methodTags = methodInfo.tags;
|
||||||
|
|
||||||
if (!tags || !tags.length) {
|
// empty tag
|
||||||
tags = [''];
|
if (!(methodTags && methodTags.length)) {
|
||||||
|
methodTags = [''];
|
||||||
}
|
}
|
||||||
let methodPointer = JsonPointer.compile(['paths', path, method]);
|
let methodPointer = JsonPointer.compile(['paths', path, method]);
|
||||||
let methodSummary = SchemaHelper.methodSummary(methodInfo);
|
for (let tagName of methodTags) {
|
||||||
for (let tag of tags) {
|
let tag = tags[tagName];
|
||||||
let id = 'tag/' + slugify(tag);
|
if (!tag) {
|
||||||
let tagDetails = tag2MethodMapping[id];
|
tag = {
|
||||||
if (!tagDetails) {
|
name: tagName,
|
||||||
tagDetails = {
|
|
||||||
name: tag,
|
|
||||||
id: id,
|
|
||||||
headless: tag === '',
|
|
||||||
idx: catIdx
|
|
||||||
};
|
};
|
||||||
tag2MethodMapping[id] = tagDetails;
|
tags[tagName] = tag;
|
||||||
catIdx++;
|
|
||||||
}
|
}
|
||||||
if (tagDetails.empty) continue;
|
if (tag['x-traitTag']) continue;
|
||||||
if (!tagDetails.methods) tagDetails.methods = [];
|
if (!tag.methods) tag.methods = [];
|
||||||
tagDetails.methods.push({
|
tag.methods.push(methodInfo);
|
||||||
pointer: methodPointer,
|
methodInfo._pointer = methodPointer;
|
||||||
summary: methodSummary,
|
|
||||||
operationId: methodInfo.operationId,
|
|
||||||
tag: tag,
|
|
||||||
idx: tagDetails.methods.length,
|
|
||||||
catIdx: tagDetails.idx
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(tag2MethodMapping).map(tag => tag2MethodMapping[tag]);
|
|
||||||
|
return tags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { OptionsService } from '../../../services/options.service';
|
||||||
import { isSafari } from '../../../utils/helpers';
|
import { isSafari } from '../../../utils/helpers';
|
||||||
|
|
||||||
export class LazyForRow {
|
export class LazyForRow {
|
||||||
constructor(public $implicit: any, public index: number, public show: boolean) {}
|
constructor(public $implicit: any, public index: number, public ready: boolean) {}
|
||||||
|
|
||||||
get first(): boolean { return this.index === 0; }
|
get first(): boolean { return this.index === 0; }
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ export class LazyTasksService {
|
||||||
}
|
}
|
||||||
|
|
||||||
addTasks(tasks:any[], callback:Function) {
|
addTasks(tasks:any[], callback:Function) {
|
||||||
tasks.forEach((task) => {
|
tasks.forEach((task, idx) => {
|
||||||
let taskCopy = Object.assign({_callback: callback}, task);
|
let taskCopy = Object.assign({_callback: callback, idx: idx}, task);
|
||||||
this._tasks.push(taskCopy);
|
this._tasks.push(taskCopy);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ export class LazyTasksService {
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
task._callback(task.idx, true);
|
task._callback(task.idx, true);
|
||||||
this._current++;
|
this._current++;
|
||||||
this.menuService.enableItem(task.catIdx, task.idx);
|
this.menuService.enableItem(task.flatIdx);
|
||||||
this.loadProgress.next(this._current / this._tasks.length * 100);
|
this.loadProgress.next(this._current / this._tasks.length * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,39 +72,30 @@ export class LazyTasksService {
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
task._callback(task.idx, false).then(() => {
|
task._callback(task.idx, false).then(() => {
|
||||||
this._current++;
|
this._current++;
|
||||||
this.menuService.enableItem(task.catIdx, task.idx);
|
this.menuService.enableItem(task.flatIdx);
|
||||||
setTimeout(()=> this.nextTask());
|
setTimeout(()=> this.nextTask());
|
||||||
this.loadProgress.next(this._current / this._tasks.length * 100);
|
this.loadProgress.next(this._current / this._tasks.length * 100);
|
||||||
}).catch(err => console.error(err));
|
}).catch(err => console.error(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTasks(catIdx, metIdx) {
|
sortTasks(center) {
|
||||||
let idxMap = {};
|
let idxMap = {};
|
||||||
this._tasks.forEach((task, idx) => {
|
|
||||||
idxMap[task.catIdx + '_' + task.idx] = idx;
|
|
||||||
});
|
|
||||||
metIdx = metIdx < 0 ? 0 : metIdx;
|
|
||||||
let destIdx = idxMap[catIdx + '_' + metIdx] || 0;
|
|
||||||
this._tasks.sort((a, b) => {
|
this._tasks.sort((a, b) => {
|
||||||
let aIdx = idxMap[a.catIdx + '_' + a.idx];
|
return Math.abs(a.flatIdx - center) - Math.abs(b.flatIdx - center);
|
||||||
let bIdx = idxMap[b.catIdx + '_' + b.idx];
|
|
||||||
return Math.abs(aIdx - destIdx) - Math.abs(bIdx - destIdx);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
start(catIdx, metIdx, menuService) {
|
start(idx, menuService) {
|
||||||
this.menuService = menuService;
|
this.menuService = menuService;
|
||||||
let syncCount = 5;
|
let syncCount = 5;
|
||||||
// I know this is bad practice to detect browsers but there is an issue on Safari only
|
// I know this is a bad practice to detect browsers but there is an issue in Safari only
|
||||||
// http://stackoverflow.com/questions/40692365/maintaining-scroll-position-while-inserting-elements-above-glitching-only-in-sa
|
// http://stackoverflow.com/questions/40692365/maintaining-scroll-position-while-inserting-elements-above-glitching-only-in-sa
|
||||||
if (isSafari && this.optionsService.options.$scrollParent === window) {
|
if (isSafari && this.optionsService.options.$scrollParent === window) {
|
||||||
syncCount = (metIdx >= 0) ?
|
syncCount = this._tasks.findIndex(task => task.flatIdx === idx);
|
||||||
this._tasks.findIndex(task => (task.catIdx === catIdx) && (task.idx === metIdx))
|
|
||||||
: this._tasks.findIndex(task => task.catIdx === catIdx);
|
|
||||||
syncCount += 1;
|
syncCount += 1;
|
||||||
} else {
|
} else {
|
||||||
this.sortTasks(catIdx, metIdx);
|
this.sortTasks(idx);
|
||||||
}
|
}
|
||||||
if (this.allSync) syncCount = this._tasks.length;
|
if (this.allSync) syncCount = this._tasks.length;
|
||||||
for (var i = this._current; i < syncCount; i++) {
|
for (var i = this._current; i < syncCount; i++) {
|
||||||
|
@ -141,8 +132,8 @@ export class LazyFor {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIteration(idx: number, sync: boolean):Promise<void> {
|
nextIteration(idx: number, sync: boolean):Promise<void> {
|
||||||
const view = this._viewContainer.createEmbeddedView(
|
const view = this._viewContainer.createEmbeddedView(this._template,
|
||||||
this._template, new LazyForRow(this.lazyForOf[idx], idx, sync), idx < this.prevIdx ? 0 : undefined);
|
new LazyForRow(this.lazyForOf[idx], idx, sync), idx < this.prevIdx ? 0 : undefined);
|
||||||
this.prevIdx = idx;
|
this.prevIdx = idx;
|
||||||
view.context.index = idx;
|
view.context.index = idx;
|
||||||
(<any>view as ChangeDetectorRef).markForCheck();
|
(<any>view as ChangeDetectorRef).markForCheck();
|
||||||
|
@ -154,7 +145,7 @@ export class LazyFor {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.scroll.saveScroll();
|
this.scroll.saveScroll();
|
||||||
|
|
||||||
view.context.show = true;
|
view.context.ready = true;
|
||||||
(<any>view as ChangeDetectorRef).markForCheck();
|
(<any>view as ChangeDetectorRef).markForCheck();
|
||||||
(<any>view as ChangeDetectorRef).detectChanges();
|
(<any>view as ChangeDetectorRef).detectChanges();
|
||||||
|
|
||||||
|
@ -165,6 +156,7 @@ export class LazyFor {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (!this.lazyForOf) return;
|
||||||
this.lazyTasks.addTasks(this.lazyForOf, this.nextIteration.bind(this))
|
this.lazyTasks.addTasks(this.lazyForOf, this.nextIteration.bind(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,6 @@ export class StickySidebar implements OnInit, OnDestroy {
|
||||||
this.$redocEl = this.$element.offsetParent.parentNode || DOM.defaultDoc().body;
|
this.$redocEl = this.$element.offsetParent.parentNode || DOM.defaultDoc().body;
|
||||||
this.bind();
|
this.bind();
|
||||||
requestAnimationFrame(() => this.updatePosition());
|
requestAnimationFrame(() => this.updatePosition());
|
||||||
//this.updatePosition()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
|
@ -22,6 +22,8 @@ const md = new Remarkable({
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MdRenderer {
|
export class MdRenderer {
|
||||||
public firstLevelHeadings: string[] = [];
|
public firstLevelHeadings: string[] = [];
|
||||||
|
public secondLevelHeadings: string[] = [];
|
||||||
|
public currentHeading: string = null;
|
||||||
|
|
||||||
private _origRules:any = {};
|
private _origRules:any = {};
|
||||||
private _preProcessors:Function[] = [];
|
private _preProcessors:Function[] = [];
|
||||||
|
@ -44,19 +46,27 @@ export class MdRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
headingOpenRule(tokens, idx) {
|
headingOpenRule(tokens, idx) {
|
||||||
if (tokens[idx].hLevel !== 1 ) {
|
if (tokens[idx].hLevel > 2 ) {
|
||||||
return this._origRules.open(tokens, idx);
|
return this._origRules.open(tokens, idx);
|
||||||
} else {
|
} else {
|
||||||
let content = tokens[idx + 1].content;
|
let content = tokens[idx + 1].content;
|
||||||
this.firstLevelHeadings.push(content);
|
if (tokens[idx].hLevel === 1 ) {
|
||||||
let contentSlug = slugify(content);
|
this.firstLevelHeadings.push(content);
|
||||||
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` +
|
this.currentHeading = content;
|
||||||
`<a class="share-link" href="#section/${contentSlug}"></a>`;
|
let contentSlug = slugify(content);
|
||||||
|
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` +
|
||||||
|
`<a class="share-link" href="#section/${contentSlug}"></a>`;
|
||||||
|
} else if (tokens[idx].hLevel === 2 ) {
|
||||||
|
this.secondLevelHeadings.push(this.currentHeading + `/` + content);
|
||||||
|
let contentSlug = slugify(this.currentHeading) + `/` + slugify(content);
|
||||||
|
return `<h${tokens[idx].hLevel} section="section/${contentSlug}">` +
|
||||||
|
`<a class="share-link" href="#section/${contentSlug}"></a>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headingCloseRule(tokens, idx) {
|
headingCloseRule(tokens, idx) {
|
||||||
if (tokens[idx].hLevel !== 1 ) {
|
if (tokens[idx].hLevel > 2 ) {
|
||||||
return this._origRules.close(tokens, idx);
|
return this._origRules.close(tokens, idx);
|
||||||
} else {
|
} else {
|
||||||
return `</h${tokens[idx].hLevel}>\n`;
|
return `</h${tokens[idx].hLevel}>\n`;
|
||||||
|
|
|
@ -76,6 +76,7 @@ export class SpecManager {
|
||||||
}
|
}
|
||||||
this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description);
|
this._schema.info['x-redoc-html-description'] = mdRender.renderMd(this._schema.info.description);
|
||||||
this._schema.info['x-redoc-markdown-headers'] = mdRender.firstLevelHeadings;
|
this._schema.info['x-redoc-markdown-headers'] = mdRender.firstLevelHeadings;
|
||||||
|
this._schema.info['x-redoc-markdown-subheaders'] = mdRender.secondLevelHeadings;
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>ReDoc</title>
|
<title>ReDoc</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="margin:0">
|
||||||
<redoc>
|
<redoc >
|
||||||
Loading...
|
Loading...
|
||||||
</redoc>
|
</redoc>
|
||||||
|
|
||||||
|
|
|
@ -56,16 +56,16 @@ describe('Scroll sync', () => {
|
||||||
|
|
||||||
it('should update active menu entries on page scroll forwards', () => {
|
it('should update active menu entries on page scroll forwards', () => {
|
||||||
scrollToEl('[section="tag/store"]').then(() => {
|
scrollToEl('[section="tag/store"]').then(() => {
|
||||||
expect($('.menu-cat-header.active').getInnerHtml()).toContain('store');
|
expect($('.menu-item.active > .menu-item-header').getInnerHtml()).toContain('store');
|
||||||
expect($('.selected-tag').getInnerHtml()).toContain('store');
|
expect($('.selected-tag').getInnerHtml()).toContain('store');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update active menu entries on page scroll backwards', () => {
|
it('should update active menu entries on page scroll backwards', () => {
|
||||||
scrollToEl('[operation-id="getPetById"]').then(() => {
|
scrollToEl('[operation-id="getPetById"]').then(() => {
|
||||||
expect($('.menu-cat-header.active').getInnerHtml()).toContain('pet');
|
expect($('.menu-item.menu-item-depth-1.active .menu-item-header').getInnerHtml()).toContain('pet');
|
||||||
expect($('.selected-tag').getInnerHtml()).toContain('pet');
|
expect($('.selected-tag').getInnerHtml()).toContain('pet');
|
||||||
expect($('.menu-cat li.active').getInnerHtml()).toContain('Find pet by ID');
|
expect($('.menu-item.menu-item-depth-2.active .menu-item-header').getInnerHtml()).toContain('Find pet by ID');
|
||||||
expect($('.selected-endpoint').getInnerHtml()).toContain('Find pet by ID');
|
expect($('.selected-endpoint').getInnerHtml()).toContain('Find pet by ID');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -85,7 +85,7 @@ describe('Language tabs sync', () => {
|
||||||
// check if correct item
|
// check if correct item
|
||||||
expect($item.getText()).toContain('PHP');
|
expect($item.getText()).toContain('PHP');
|
||||||
var EC = protractor.ExpectedConditions;
|
var EC = protractor.ExpectedConditions;
|
||||||
browser.wait(EC.elementToBeClickable($item), 2000);
|
browser.wait(EC.elementToBeClickable($item), 5000);
|
||||||
$item.click().then(() => {
|
$item.click().then(() => {
|
||||||
expect($('[operation-id="updatePet"] li.active').getText()).toContain('PHP');
|
expect($('[operation-id="updatePet"] li.active').getText()).toContain('PHP');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
|
|
||||||
/** Gets a child DebugElement by tag name. */
|
/** Gets a child DebugElement by tag name. */
|
||||||
export function getChildDebugElement(parent, tagName) {
|
export function getChildDebugElement(parent, tagName) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user