diff --git a/lib/components/Method/method.ts b/lib/components/Method/method.ts index 82332307..977f7a1d 100644 --- a/lib/components/Method/method.ts +++ b/lib/components/Method/method.ts @@ -1,10 +1,24 @@ '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 { BaseComponent, SpecManager } from '../base'; import { SchemaHelper } from '../../services/schema-helper.service'; import { OptionsService } from '../../services/'; + +interface MethodInfo { + apiUrl: string; + httpMethod: string; + path: string; + info: { + tags: string[]; + description: string; + }; + bodyParam: any; + summary: any; + anchor: any; +} + @Component({ selector: 'method', templateUrl: './method.html', @@ -12,35 +26,47 @@ import { OptionsService } from '../../services/'; changeDetection: ChangeDetectionStrategy.OnPush }) export class Method extends BaseComponent implements OnInit { - @Input() pointer:string; - @Input() tag:string; - @Input() posInfo: any; + @Input() pointer :string; + @Input() parentTagId :string; - hidden = true; + @HostBinding('attr.operation-id') operationId; - method:any; + private method: MethodInfo; - constructor(specMgr:SpecManager, private optionsService: OptionsService, private el: ElementRef) { + constructor(specMgr:SpecManager, private optionsService: OptionsService) { super(specMgr); } init() { - this.method = {}; - if (this.optionsService.options.hideHostname) { - this.method.apiUrl = this.specMgr.basePath; + this.operationId = this.componentSchema.operationId; + + 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 { - 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; - this.method.info.tags = this.filterMainTags(this.method.info.tags); - this.method.bodyParam = this.findBodyParam(); - this.method.summary = SchemaHelper.methodSummary(this.componentSchema); - if (this.componentSchema.operationId) { - this.method.anchor = 'operation/' + encodeURIComponent(this.componentSchema.operationId); + } + + getBaseUrl():string { + if (this.optionsService.options.hideHostname) { + return this.specMgr.basePath; } 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; } - show(res) { - if (res) { - this.el.nativeElement.firstElementChild.removeAttribute('hidden'); - } else { - this.el.nativeElement.firstElementChild.setAttribute('hidden', 'hidden'); - } - } - ngOnInit() { this.preinit(); } diff --git a/lib/components/MethodsList/methods-list.html b/lib/components/MethodsList/methods-list.html index f80a471d..18a833c8 100644 --- a/lib/components/MethodsList/methods-list.html +++ b/lib/components/MethodsList/methods-list.html @@ -4,8 +4,8 @@

{{tag.name}}

- + diff --git a/lib/components/MethodsList/methods-list.ts b/lib/components/MethodsList/methods-list.ts index 3db51eb8..9542bf51 100644 --- a/lib/components/MethodsList/methods-list.ts +++ b/lib/components/MethodsList/methods-list.ts @@ -1,7 +1,7 @@ 'use strict'; import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { BaseComponent, SpecManager } from '../base'; -import { SchemaHelper } from '../../services/index'; +import { MenuService } from '../../services/index'; @Component({ selector: 'methods-list', @@ -14,18 +14,19 @@ export class MethodsList extends BaseComponent implements OnInit { tags:Array = []; - constructor(specMgr:SpecManager) { + constructor(specMgr:SpecManager, private menu: MenuService) { super(specMgr); } init() { - let flatMenuItems = SchemaHelper.flatMenu(SchemaHelper.buildMenuTree(this.specMgr.schema)); + let flatMenuItems = this.menu.flatItems; this.tags = []; let emptyTag = { name: '', items: [] } flatMenuItems.forEach(menuItem => { + // skip items that are not bound to swagger tags/methods if (!menuItem.metadata) return; if (menuItem.metadata.type === 'tag') { diff --git a/lib/components/SideMenu/side-menu-items.scss b/lib/components/SideMenu/side-menu-items.scss index f59ce73c..db89e9ac 100644 --- a/lib/components/SideMenu/side-menu-items.scss +++ b/lib/components/SideMenu/side-menu-items.scss @@ -53,7 +53,7 @@ } } -.menu-item-level-0 { +.menu-item-level-1 { > .menu-item-header { font-family: $headers-font, $headers-font-family; font-weight: $light; @@ -61,7 +61,7 @@ text-transform: uppercase; } - > .menu-item-header:hover, + > .menu-item-header:not(.disabled):hover, &.active > .menu-item-header { color: $primary-color; background: $side-menu-active-bg-color; @@ -71,7 +71,7 @@ } } -.menu-item-level-1 { +.menu-item-level-2 { > .menu-item-header { padding-left: 2*$side-menu-item-hpadding; } diff --git a/lib/components/SideMenu/side-menu.ts b/lib/components/SideMenu/side-menu.ts index fc3d1925..c46fcca9 100644 --- a/lib/components/SideMenu/side-menu.ts +++ b/lib/components/SideMenu/side-menu.ts @@ -69,9 +69,17 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy { } changed(item) { - if (item) { - this.activeCatCaption = item.name || ''; - this.activeItemCaption = item.parent && item.parent.name || ''; + if (!item) { + this.activeCatCaption = ''; + this.activeItemCaption = ''; + 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 :( @@ -88,12 +96,10 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy { } activateAndScroll(item) { - if (this.mobileMode()) { + if (this.mobileMode) { this.toggleMobileNav(); } - //if (!this.flatItems[idx].ready) return; // TODO: move inside next statement - this.menuService.activate(item.flatIdx); this.menuService.scrollToActive(); } @@ -111,7 +117,7 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy { }; } - mobileMode() { + get mobileMode() { return this.$mobileNav.clientHeight > 0; } diff --git a/lib/services/menu.service.ts b/lib/services/menu.service.ts index 3bd5f88e..e4fd784d 100644 --- a/lib/services/menu.service.ts +++ b/lib/services/menu.service.ts @@ -15,53 +15,45 @@ import * as slugify from 'slugify'; const CHANGE = { NEXT : 1, BACK : -1, - INITIAL : 0 }; @Injectable() export class MenuService { - changed: EventEmitter = new EventEmitter(); - ready: BehaviorSubject = new BehaviorSubject(false); - items: Array; - flatItems: Array; - - activeCatIdx: number = 0; - activeMethodIdx: number = -1; + items: MenuItem[]; activeIdx: number = -1; + private _flatItems: MenuItem[]; private _hashSubscription: Subscription; + private _scrollSubscription: Subscription; constructor( private hash:Hash, private tasks: LazyTasksService, private scrollService: ScrollService, private appState: AppStateService, - specMgr:SpecManager + private specMgr:SpecManager ) { this.hash = hash; - this.items = SchemaHelper.buildMenuTree(specMgr.schema); - this.flatItems = SchemaHelper.flatMenu(this.items); + this.buildMenu(); - scrollService.scroll.subscribe((evt) => { - this.scrollUpdate(evt.isScrolledDown); + this._scrollSubscription = scrollService.scroll.subscribe((evt) => { + this.onScroll(evt.isScrolledDown); }); this._hashSubscription = this.hash.value.subscribe((hash) => { - if (hash == undefined) return; - this.setActiveByHash(hash); - if (!this.tasks.empty) { - this.tasks.start(this.activeIdx, this); - this.scrollService.setStickElement(this.getCurrentEl()); - if (hash) this.scrollToActive(); - this.appState.stopLoading(); - } else { - if (hash) this.scrollToActive(); - } + this.onHashChange(hash); }); } + get flatItems():MenuItem[] { + if (!this._flatItems) { + this._flatItems = this.flatMenu(); + } + return this._flatItems; + } + enableItem(idx) { let item = this.flatItems[idx]; item.ready = true; @@ -70,6 +62,7 @@ export class MenuService { 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; @@ -79,11 +72,10 @@ export class MenuService { this.changed.next(); } - scrollUpdate(isScrolledDown) { + onScroll(isScrolledDown) { let stable = false; while(!stable) { if(isScrolledDown) { - //&& elementInViewPos === INVIEW_POSITION.BELLOW let $nextEl = this.getEl(this.activeIdx + 1); if (!$nextEl) return; let nextInViewPos = this.scrollService.getElementPos($nextEl, true); @@ -103,8 +95,21 @@ export class MenuService { } } - getEl(flatIdx) { - if (flatIdx < 0) flatIdx = 0; + onHashChange(hash?: string) { + if (hash == undefined) return; + let activated = this.activateByHash(hash); + if (!this.tasks.empty) { + this.tasks.start(this.activeIdx, this); + this.scrollService.setStickElement(this.getCurrentEl()); + if (activated) this.scrollToActive(); + this.appState.stopLoading(); + } else { + if (activated) this.scrollToActive(); + } + } + + getEl(flatIdx:number):Element { + if (flatIdx < 0) return null; let currentItem = this.flatItems[flatIdx]; let selector = ''; while(currentItem) { @@ -115,35 +120,37 @@ export class MenuService { return selector ? document.querySelector(selector) : null; } - getCurrentEl() { + getCurrentEl():Element { return this.getEl(this.activeIdx); } deactivate(idx) { if (idx < 0) return; - let prevItem = this.flatItems[idx]; - prevItem.active = false; - if (prevItem.parent) { - prevItem.parent.active = false; + let item = this.flatItems[idx]; + item.active = false; + if (item.parent) { + item.parent.active = false; } } - activate(idx) { + activate(idx, force = false) { + let item = this.flatItems[idx]; + if (!force && item && !item.ready) return; + this.deactivate(this.activeIdx); this.activeIdx = idx; if (idx < 0) return; - let currentItem = this.flatItems[this.activeIdx]; - currentItem.active = true; - if (currentItem.parent) { - currentItem.parent.active = true; + item.active = true; + if (item.parent) { + item.parent.active = true; } - this.changed.next(currentItem); + this.changed.next(item); } - changeActive(offset = 1) { + changeActive(offset = 1):boolean { let noChange = (this.activeIdx <= 0 && offset === -1) || (this.activeIdx === this.flatItems.length - 1 && offset === 1); this.activate(this.activeIdx + offset); @@ -151,10 +158,11 @@ export class MenuService { } scrollToActive() { - this.scrollService.scrollTo(this.getCurrentEl()); + let $el = this.getCurrentEl(); + if ($el) this.scrollService.scrollTo($el); } - setActiveByHash(hash) { + activateByHash(hash):boolean { if (!hash) return; let idx = 0; hash = hash.substr(1); @@ -163,17 +171,118 @@ export class MenuService { if (namespace === 'section' || namespace === 'tag') { let sectionId = ptr.split('/')[0]; ptr = ptr.substr(sectionId.length) || null; - let searchId = ptr || (namespace + '/' + sectionId); + + let searchId; + if (namespace === 'section') { + searchId = hash; + } else { + searchId = ptr || (namespace + '/' + sectionId);; + } + 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(idx); + 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 (>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) { + let id = 'section/' + slugify(header); + let item = { + name: header, + id: id + } + this.items.push(item); + } + } + + addTagsAndOperationItems() { + let schema = this.specMgr.schema; + let menu = this.items; + + let tags = SchemaHelper.getTagsWithMethods(schema); + for (let tag of tags || []) { + let id = 'tag/' + slugify(tag.name); + let item: MenuItem; + let items: MenuItem[]; + + // don't put empty tag into menu, instead put their methods + if (tag.name !== '') { + item = { + name: tag['x-displayName'] || tag.name, + id: id, + description: tag.description, + metadata: { type: 'tag' } + }; + if (tag.methods && tag.methods.length) { + item.items = items = []; + } + } else { + item = null; + items = menu; + } + + if (items) { + 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: item + } + items.push(subItem); + } + } + + if (item) menu.push(item); + } + } + + buildMenu() { + this.items = this.items || []; + this.addMarkdownItems(); + this.addTagsAndOperationItems(); + } + + flatMenu():MenuItem[] { + let menu = this.items; + let res = []; + let level = 1; + + let recursive = function(items) { + for (let item of items) { + res.push(item); + item.level = item.level || level; + item.flatIdx = res.length - 1; + if (item.items) { + level++; + recursive(item.items); + level--; + } + } + } + recursive(menu); + return res; } destroy() { this._hashSubscription.unsubscribe(); + this._scrollSubscription.unsubscribe(); } } diff --git a/lib/services/schema-helper.service.ts b/lib/services/schema-helper.service.ts index 6a307b92..7a0f426b 100644 --- a/lib/services/schema-helper.service.ts +++ b/lib/services/schema-helper.service.ts @@ -9,15 +9,6 @@ interface PropertyPreprocessOptions { skipReadOnly?: boolean; } -export interface MenuMethod { - active: boolean; - summary: string; - tag: string; - pointer: string; - operationId: string; - ready: boolean; -} - export interface MenuItem { id: string; @@ -305,7 +296,7 @@ export class SchemaHelper { } } - static getTags(schema) { + static getTagsWithMethods(schema) { let tags = {}; for (let tag of schema.tags || []) { tags[tag.name] = tag; @@ -319,10 +310,11 @@ export class SchemaHelper { let methodInfo = paths[path][method]; let methodTags = methodInfo.tags; + // empty tag if (!(methodTags && methodTags.length)) { methodTags = ['']; } - let methodPointer = JsonPointer.compile([path, method]); + let methodPointer = JsonPointer.compile(['paths', path, method]); for (let tagName of methodTags) { let tag = tags[tagName]; if (!tag) { @@ -341,84 +333,4 @@ export class SchemaHelper { return Object.keys(tags).map(k => tags[k]); } - - static buildMenuTree(schema):MenuItem[] { - let tags = SchemaHelper.getTags(schema); - - let menu = []; - - // markdown menu items - - for (let header of (>(schema.info && schema.info['x-redoc-markdown-headers'] || []))) { - let id = 'section/' + slugify(header); - let item = { - name: header, - id: id - } - menu.push(item); - } - - // tag menu items - for (let tag of tags || []) { - let id = 'tag/' + slugify(tag.name); - let item:MenuItem; - let items:MenuItem[]; - - // don't put empty tag into menu, instead put all methods - if (tag.name !== '') { - item = { - name: tag['x-displayName'] || tag.name, - id: id, - description: tag.description, - metadata: { type: 'tag' } - }; - if (tag.methods && tag.methods.length) { - item.items = items = []; - } - } else { - item = null; - items = menu; - } - - if (items) { - for (let method of tag.methods) { - let subItem = { - name: SchemaHelper.methodSummary(method), - id: method._pointer, - description: method.description, - metadata: { - type: 'method', - pointer: '/paths' + method._pointer, - operationId: method.operationId - }, - parent: item - } - items.push(subItem); - } - } - - if (item) menu.push(item); - } - return menu; - } - - static flatMenu(menu: MenuItem[]):MenuItem[] { - let res = []; - let level = 0; - - let recursive = function(items) { - for (let item of items) { - res.push(item); - item.level = item.level || level; - item.flatIdx = res.length - 1; - if (item.items) { - level++; - recursive(item.items); - level--; - } - } - } - recursive(menu); - return res; - } } diff --git a/lib/shared/components/LazyFor/lazy-for.ts b/lib/shared/components/LazyFor/lazy-for.ts index f1d3ea47..e0028689 100644 --- a/lib/shared/components/LazyFor/lazy-for.ts +++ b/lib/shared/components/LazyFor/lazy-for.ts @@ -17,7 +17,7 @@ import { OptionsService } from '../../../services/options.service'; import { isSafari } from '../../../utils/helpers'; 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; } @@ -145,7 +145,7 @@ export class LazyFor { requestAnimationFrame(() => { this.scroll.saveScroll(); - view.context.show = true; + view.context.ready = true; (view as ChangeDetectorRef).markForCheck(); (view as ChangeDetectorRef).detectChanges();