From de00c7868a539139f4badffd362673abaf4ea9e5 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Fri, 23 Dec 2016 00:41:36 +0200 Subject: [PATCH 01/16] Split menu generation in two stages --- lib/services/schema-helper.service.ts | 171 +++++++++++++++++--------- 1 file changed, 113 insertions(+), 58 deletions(-) diff --git a/lib/services/schema-helper.service.ts b/lib/services/schema-helper.service.ts index cfdb42e2..6a307b92 100644 --- a/lib/services/schema-helper.service.ts +++ b/lib/services/schema-helper.service.ts @@ -18,17 +18,22 @@ export interface MenuMethod { ready: boolean; } -export interface MenuCategory { - name: string; +export interface MenuItem { id: string; - active?: boolean; - methods?: Array; + name: string; description?: string; - empty?: string; - virtual?: boolean; - ready: boolean; - headless: boolean; + + items?: Array; + parent?: MenuItem; + + active?: boolean; + ready?: boolean; + + level?: number; + flatIdx?: number; + + metadata?: any; } // global var for this module @@ -300,30 +305,11 @@ export class SchemaHelper { } } - static buildMenuTree(schema):Array { - var catIdx = 0; - let tag2MethodMapping = {}; - - for (let header of (>(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++; - } - + static getTags(schema) { + let tags = {}; for (let tag of schema.tags || []) { - let id = 'tag/' + slugify(tag.name); - tag2MethodMapping[id] = { - name: tag['x-displayName'] || tag.name, - id: id, - description: tag.description, - headless: tag.name === '', - empty: !!tag['x-traitTag'], - methods: [], - idx: catIdx - }; - catIdx++; + tags[tag.name] = tag; + tag.methods = []; } let paths = schema.paths; @@ -331,39 +317,108 @@ export class SchemaHelper { let methods = Object.keys(paths[path]).filter((k) => swaggerMethods.has(k)); for (let method of methods) { let methodInfo = paths[path][method]; - let tags = methodInfo.tags; + let methodTags = methodInfo.tags; - if (!tags || !tags.length) { - tags = ['']; + if (!(methodTags && methodTags.length)) { + methodTags = ['']; } - let methodPointer = JsonPointer.compile(['paths', path, method]); - let methodSummary = SchemaHelper.methodSummary(methodInfo); - for (let tag of tags) { - let id = 'tag/' + slugify(tag); - let tagDetails = tag2MethodMapping[id]; - if (!tagDetails) { - tagDetails = { - name: tag, - id: id, - headless: tag === '', - idx: catIdx + let methodPointer = JsonPointer.compile([path, method]); + for (let tagName of methodTags) { + let tag = tags[tagName]; + if (!tag) { + tag = { + name: tagName, }; - tag2MethodMapping[id] = tagDetails; - catIdx++; + tags[tagName] = tag; } - if (tagDetails.empty) continue; - if (!tagDetails.methods) tagDetails.methods = []; - tagDetails.methods.push({ - pointer: methodPointer, - summary: methodSummary, - operationId: methodInfo.operationId, - tag: tag, - idx: tagDetails.methods.length, - catIdx: tagDetails.idx - }); + if (tag['x-traitTag']) continue; + if (!tag.methods) tag.methods = []; + tag.methods.push(methodInfo); + methodInfo._pointer = methodPointer; } } } - return Object.keys(tag2MethodMapping).map(tag => tag2MethodMapping[tag]); + + 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; } } From ac0681c8a9c3f93b8256abbdf9f62288180b348e Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Fri, 23 Dec 2016 00:42:58 +0200 Subject: [PATCH 02/16] Update side menu to work with new menu structure --- lib/components/SideMenu/side-menu-items.html | 7 ++ lib/components/SideMenu/side-menu-items.scss | 83 ++++++++++++++++++++ lib/components/SideMenu/side-menu.html | 16 +--- lib/components/SideMenu/side-menu.scss | 80 ++----------------- lib/components/SideMenu/side-menu.ts | 58 ++++++++------ lib/components/index.ts | 6 +- 6 files changed, 135 insertions(+), 115 deletions(-) create mode 100644 lib/components/SideMenu/side-menu-items.html create mode 100644 lib/components/SideMenu/side-menu-items.scss diff --git a/lib/components/SideMenu/side-menu-items.html b/lib/components/SideMenu/side-menu-items.html new file mode 100644 index 00000000..d286695d --- /dev/null +++ b/lib/components/SideMenu/side-menu-items.html @@ -0,0 +1,7 @@ + diff --git a/lib/components/SideMenu/side-menu-items.scss b/lib/components/SideMenu/side-menu-items.scss new file mode 100644 index 00000000..f59ce73c --- /dev/null +++ b/lib/components/SideMenu/side-menu-items.scss @@ -0,0 +1,83 @@ +@import '../../shared/styles/variables'; + +.menu-item-header { + cursor: pointer; + color: rgba($text-color, .6); + -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-level-0 { + > .menu-item-header { + font-family: $headers-font, $headers-font-family; + font-weight: $light; + font-size: $h5; + text-transform: uppercase; + } + + > .menu-item-header:hover, + &.active > .menu-item-header { + color: $primary-color; + background: $side-menu-active-bg-color; + } + &.active { + background: $side-menu-active-bg-color; + } +} + +.menu-item-level-1 { + > .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%); + } +} diff --git a/lib/components/SideMenu/side-menu.html b/lib/components/SideMenu/side-menu.html index 3328bd3b..638ff025 100644 --- a/lib/components/SideMenu/side-menu.html +++ b/lib/components/SideMenu/side-menu.html @@ -7,17 +7,7 @@
- +
diff --git a/lib/components/SideMenu/side-menu.scss b/lib/components/SideMenu/side-menu.scss index 22144b21..2cd849c2 100644 --- a/lib/components/SideMenu/side-menu.scss +++ b/lib/components/SideMenu/side-menu.scss @@ -6,6 +6,11 @@ $mobile-menu-compact-breakpoint: 550px; box-sizing: border-box; } +ul.menu-root { + margin: 0; + padding: 0; +} + .menu-header { text-transform: uppercase; color: $headers-color; @@ -13,81 +18,6 @@ $mobile-menu-compact-breakpoint: 550px; 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 { display: none; height: 3em; diff --git a/lib/components/SideMenu/side-menu.ts b/lib/components/SideMenu/side-menu.ts index 63a36c59..fc3d1925 100644 --- a/lib/components/SideMenu/side-menu.ts +++ b/lib/components/SideMenu/side-menu.ts @@ -1,36 +1,50 @@ '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 { trigger, state, animate, transition, style } from '@angular/core'; import { BaseComponent, SpecManager } from '../base'; import { ScrollService, MenuService, OptionsService } from '../../services/index'; import { BrowserDomAdapter as DOM } from '../../utils/browser-adapter'; -import { MenuCategory } from '../../services/schema-helper.service'; +import { MenuItem } from '../../services/schema-helper.service'; 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(); + + activateItem(item) { + this.activate.next(item); + } +} + @Component({ selector: 'side-menu', templateUrl: './side-menu.html', styleUrls: ['./side-menu.css'], animations: [ - trigger('itemAnimation', [ - state('collapsed, void', - style({ height: '0px' })), - state('expanded', - style({ height: '*' })), - transition('collapsed <=> expanded', [ - animate('200ms ease') - ]) - ]) + // trigger('itemAnimation', [ + // state('collapsed, void', + // style({ height: '0px' })), + // state('expanded', + // style({ height: '*' })), + // transition('collapsed <=> expanded', [ + // animate('200ms ease') + // ]) + // ]) ], }) export class SideMenu extends BaseComponent implements OnInit, OnDestroy { activeCatCaption: string; activeItemCaption: string; - categories: Array; + menuItems: Array; private options: any; private $element: any; @@ -54,11 +68,10 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy { this.menuService.changed.subscribe((evt) => this.changed(evt)); } - changed(newItem) { - if (newItem) { - let {cat, item} = newItem; - this.activeCatCaption = cat.name || ''; - this.activeItemCaption = item && item.summary || ''; + changed(item) { + if (item) { + this.activeCatCaption = item.name || ''; + this.activeItemCaption = item.parent && item.parent.name || ''; } //safari doesn't update bindings if not run changeDetector manually :( @@ -74,22 +87,19 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy { if ($item) $item.scrollIntoView(); } - activateAndScroll(catIdx, methodIdx) { + activateAndScroll(item) { if (this.mobileMode()) { this.toggleMobileNav(); } - let menu = this.categories; - if (!menu[catIdx].ready) return; - if (menu[catIdx].methods && menu[catIdx].methods.length && (methodIdx >= 0) && - !menu[catIdx].methods[methodIdx].ready) return; + //if (!this.flatItems[idx].ready) return; // TODO: move inside next statement - this.menuService.activate(catIdx, methodIdx); + this.menuService.activate(item.flatIdx); this.menuService.scrollToActive(); } init() { - this.categories = this.menuService.categories; + this.menuItems = this.menuService.items; this.$mobileNav = DOM.querySelector(this.$element, '.mobile-nav'); this.$resourcesNav = DOM.querySelector(this.$element, '#resources-nav'); diff --git a/lib/components/index.ts b/lib/components/index.ts index ed445ecb..4b4ffc32 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -9,7 +9,7 @@ import { RequestSamples } from './RequestSamples/request-samples'; import { ResponsesList } from './ResponsesList/responses-list'; import { ResponsesSamples } from './ResponsesSamples/responses-samples'; 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 { Method } from './Method/method'; import { Warnings } from './Warnings/warnings'; @@ -21,9 +21,9 @@ import { Redoc } from './Redoc/redoc'; export const REDOC_DIRECTIVES = [ ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, - LoadingBar + LoadingBar, SideMenuItems ]; export { ApiInfo, ApiLogo, JsonSchema, JsonSchemaLazy, ParamsList, RequestSamples, ResponsesList, ResponsesSamples, SchemaSample, SideMenu, MethodsList, Method, Warnings, Redoc, SecurityDefinitions, -LoadingBar } +LoadingBar, SideMenuItems } From 398aa60a46e086d4a676ed7bc76c19759c60167b Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Fri, 23 Dec 2016 00:43:19 +0200 Subject: [PATCH 03/16] Update Method list to work with new menu structure --- lib/components/MethodsList/methods-list.html | 9 ++++---- lib/components/MethodsList/methods-list.ts | 24 +++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/components/MethodsList/methods-list.html b/lib/components/MethodsList/methods-list.html index 61caa17f..f80a471d 100644 --- a/lib/components/MethodsList/methods-list.html +++ b/lib/components/MethodsList/methods-list.html @@ -1,10 +1,11 @@
-
-
+
+

{{tag.name}}

- +
diff --git a/lib/components/MethodsList/methods-list.ts b/lib/components/MethodsList/methods-list.ts index ec130328..3db51eb8 100644 --- a/lib/components/MethodsList/methods-list.ts +++ b/lib/components/MethodsList/methods-list.ts @@ -19,15 +19,23 @@ export class MethodsList extends BaseComponent implements OnInit { } init() { - let tags = SchemaHelper.buildMenuTree(this.specMgr.schema); - this.tags = tags.filter(tagInfo => !tagInfo.virtual); - this.tags.forEach(tagInfo => { - // inject tag name into method info - tagInfo.methods = tagInfo.methods || []; - tagInfo.methods.forEach(method => { - method.tag = tagInfo.id; - }); + let flatMenuItems = SchemaHelper.flatMenu(SchemaHelper.buildMenuTree(this.specMgr.schema)); + this.tags = []; + let emptyTag = { + name: '', + items: [] + } + flatMenuItems.forEach(menuItem => { + 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) { From 97949a17f915f777b4feb316879d085e323fcc03 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Fri, 23 Dec 2016 00:43:56 +0200 Subject: [PATCH 04/16] Adapt menu service and lazy-tasks to new menu strcuture --- lib/services/menu.service.ts | 202 ++++++++-------------- lib/shared/components/LazyFor/lazy-for.ts | 34 ++-- 2 files changed, 84 insertions(+), 152 deletions(-) diff --git a/lib/services/menu.service.ts b/lib/services/menu.service.ts index f76b85fd..3bd5f88e 100644 --- a/lib/services/menu.service.ts +++ b/lib/services/menu.service.ts @@ -5,9 +5,12 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { ScrollService, INVIEW_POSITION } from './scroll.service'; import { Hash } from './hash.service'; import { SpecManager } from '../utils/spec-manager'; -import { SchemaHelper, MenuCategory } from './schema-helper.service'; +import { SchemaHelper, MenuItem } from './schema-helper.service'; import { AppStateService } from './app-state.service'; import { LazyTasksService } from '../shared/components/LazyFor/lazy-for'; +import { JsonPointer } from '../utils/JsonPointer'; +import * as slugify from 'slugify'; + const CHANGE = { NEXT : 1, @@ -20,11 +23,14 @@ export class MenuService { changed: EventEmitter = new EventEmitter(); ready: BehaviorSubject = new BehaviorSubject(false); - categories: Array; + items: Array; + flatItems: Array; activeCatIdx: number = 0; activeMethodIdx: number = -1; + activeIdx: number = -1; + private _hashSubscription: Subscription; constructor( @@ -35,20 +41,19 @@ export class MenuService { specMgr:SpecManager ) { this.hash = hash; - this.categories = SchemaHelper.buildMenuTree(specMgr.schema); + this.items = SchemaHelper.buildMenuTree(specMgr.schema); + this.flatItems = SchemaHelper.flatMenu(this.items); scrollService.scroll.subscribe((evt) => { this.scrollUpdate(evt.isScrolledDown); }); - //this.changeActive(CHANGE.INITIAL); - this._hashSubscription = this.hash.value.subscribe((hash) => { if (hash == undefined) return; this.setActiveByHash(hash); if (!this.tasks.empty) { - this.tasks.start(this.activeCatIdx, this.activeMethodIdx, this); - this.scrollService.setStickElement(this.getCurrentMethodEl()); + this.tasks.start(this.activeIdx, this); + this.scrollService.setStickElement(this.getCurrentEl()); if (hash) this.scrollToActive(); this.appState.stopLoading(); } else { @@ -57,45 +62,39 @@ export class MenuService { }); } - enableItem(catIdx, methodIdx, skipUpdate = false) { - let cat = this.categories[catIdx]; - cat.ready = true; - if (cat.methods.length) cat.methods[methodIdx].ready = true; - let prevCat = this.categories[catIdx - 1]; - 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; + } + + 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(); } - get activeMethodPtr() { - 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; while(!stable) { - let $activeMethodHost = this.getCurrentMethodEl(); - if (!$activeMethodHost) return; - var elementInViewPos = this.scrollService.getElementPos($activeMethodHost); if(isScrolledDown) { //&& elementInViewPos === INVIEW_POSITION.BELLOW - let $nextEl = this.getRelativeCatOrItem(1); + let $nextEl = this.getEl(this.activeIdx + 1); if (!$nextEl) return; 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); continue; } } + let $currentEl = this.getCurrentEl(); + if (!$currentEl) return; + var elementInViewPos = this.scrollService.getElementPos($currentEl); if(!isScrolledDown && elementInViewPos === INVIEW_POSITION.ABOVE ) { stable = this.changeActive(CHANGE.BACK); continue; @@ -104,133 +103,74 @@ export class MenuService { } } - getRelativeCatOrItem(offset: number = 0) { - let ptr, cat; - cat = this.categories[this.activeCatIdx]; - if (cat.methods.length === 0) { - ptr = null; - cat = this.categories[this.activeCatIdx + Math.sign(offset)] || cat; - } else { - let cat = this.categories[this.activeCatIdx]; - 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; - } - ptr = cat.methods[idx] && cat.methods[idx].pointer; + getEl(flatIdx) { + if (flatIdx < 0) flatIdx = 0; + let currentItem = this.flatItems[flatIdx]; + let selector = ''; + while(currentItem) { + selector = `[section="${currentItem.id}"] ` + selector + currentItem = currentItem.parent; } - - return this.getMethodElByPtr(ptr, cat.id); + selector = selector.trim(); + return selector ? document.querySelector(selector) : null; } - getCurrentMethodEl() { - return this.getMethodElByPtr(this.activeMethodPtr, - this.categories[this.activeCatIdx].id); + getCurrentEl() { + return this.getEl(this.activeIdx); } - getMethodElByPtr(ptr, section) { - let selector = ptr ? `[pointer="${ptr}"][section="${section}"]` : `[section="${section}"]`; - return document.querySelector(selector); + deactivate(idx) { + if (idx < 0) return; + + let prevItem = this.flatItems[idx]; + prevItem.active = false; + if (prevItem.parent) { + prevItem.parent.active = false; + } } - getMethodElByOperId(operationId) { - let selector =`[operation-id="${operationId}"]`; - return document.querySelector(selector); - } + activate(idx) { + this.deactivate(this.activeIdx); + this.activeIdx = idx; + if (idx < 0) return; - activate(catIdx, methodIdx) { - if (catIdx < 0) return; - - let menu = this.categories; - - 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; + let currentItem = this.flatItems[this.activeIdx]; + currentItem.active = true; + if (currentItem.parent) { + currentItem.parent.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]; + this.changed.next(currentItem); } changeActive(offset = 1) { - let [catIdx, methodIdx] = this._calcActiveIndexes(offset); - this.activate(catIdx, methodIdx); - return (methodIdx === 0 && catIdx === 0); + let noChange = (this.activeIdx <= 0 && offset === -1) || + (this.activeIdx === this.flatItems.length - 1 && offset === 1); + this.activate(this.activeIdx + offset); + return noChange; } scrollToActive() { - this.scrollService.scrollTo(this.getCurrentMethodEl()); + this.scrollService.scrollTo(this.getCurrentEl()); } setActiveByHash(hash) { - if (!hash) { - if (this.categories[0].headless) { - this.activate(0, 0); - } - return; - } - let catIdx, methodIdx; + if (!hash) return; + let idx = 0; hash = hash.substr(1); let namespace = hash.split('/')[0]; let ptr = decodeURIComponent(hash.substr(namespace.length + 1)); if (namespace === 'section' || namespace === 'tag') { 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; - methodIdx = cat.methods.findIndex(method => method.pointer === ptr); - } else { - catIdx = this.categories.findIndex(cat => { - if (!cat.methods.length) return false; - methodIdx = cat.methods.findIndex(method => method.operationId === ptr || method.pointer === ptr); - if (methodIdx >= 0) { - return true; - } else { - return false; - } - }); + let searchId = ptr || (namespace + '/' + sectionId); + idx = this.flatItems.findIndex(item => item.id === searchId); + } else if (namespace === 'operation') { + idx = this.flatItems.findIndex(item => { + return item.metadata && item.metadata.operationId === ptr + }) } - this.activate(catIdx, methodIdx); + this.activate(idx); } destroy() { diff --git a/lib/shared/components/LazyFor/lazy-for.ts b/lib/shared/components/LazyFor/lazy-for.ts index 6beb8353..f1d3ea47 100644 --- a/lib/shared/components/LazyFor/lazy-for.ts +++ b/lib/shared/components/LazyFor/lazy-for.ts @@ -51,8 +51,8 @@ export class LazyTasksService { } addTasks(tasks:any[], callback:Function) { - tasks.forEach((task) => { - let taskCopy = Object.assign({_callback: callback}, task); + tasks.forEach((task, idx) => { + let taskCopy = Object.assign({_callback: callback, idx: idx}, task); this._tasks.push(taskCopy); }); } @@ -62,7 +62,7 @@ export class LazyTasksService { if (!task) return; task._callback(task.idx, true); this._current++; - this.menuService.enableItem(task.catIdx, task.idx); + this.menuService.enableItem(task.flatIdx); this.loadProgress.next(this._current / this._tasks.length * 100); } @@ -72,39 +72,30 @@ export class LazyTasksService { if (!task) return; task._callback(task.idx, false).then(() => { this._current++; - this.menuService.enableItem(task.catIdx, task.idx); + this.menuService.enableItem(task.flatIdx); setTimeout(()=> this.nextTask()); this.loadProgress.next(this._current / this._tasks.length * 100); }).catch(err => console.error(err)); }); } - sortTasks(catIdx, metIdx) { + sortTasks(center) { 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) => { - let aIdx = idxMap[a.catIdx + '_' + a.idx]; - let bIdx = idxMap[b.catIdx + '_' + b.idx]; - return Math.abs(aIdx - destIdx) - Math.abs(bIdx - destIdx); + return Math.abs(a.flatIdx - center) - Math.abs(b.flatIdx - center); }) } - start(catIdx, metIdx, menuService) { + start(idx, menuService) { this.menuService = menuService; 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 if (isSafari && this.optionsService.options.$scrollParent === window) { - syncCount = (metIdx >= 0) ? - this._tasks.findIndex(task => (task.catIdx === catIdx) && (task.idx === metIdx)) - : this._tasks.findIndex(task => task.catIdx === catIdx); + syncCount = this._tasks.findIndex(task => task.idx === idx); syncCount += 1; } else { - this.sortTasks(catIdx, metIdx); + this.sortTasks(idx); } if (this.allSync) syncCount = this._tasks.length; for (var i = this._current; i < syncCount; i++) { @@ -141,8 +132,8 @@ export class LazyFor { } nextIteration(idx: number, sync: boolean):Promise { - const view = this._viewContainer.createEmbeddedView( - this._template, new LazyForRow(this.lazyForOf[idx], idx, sync), idx < this.prevIdx ? 0 : undefined); + const view = this._viewContainer.createEmbeddedView(this._template, + new LazyForRow(this.lazyForOf[idx], idx, sync), idx < this.prevIdx ? 0 : undefined); this.prevIdx = idx; view.context.index = idx; (view as ChangeDetectorRef).markForCheck(); @@ -165,6 +156,7 @@ export class LazyFor { } ngOnInit() { + if (!this.lazyForOf) return; this.lazyTasks.addTasks(this.lazyForOf, this.nextIteration.bind(this)) } } From c41ffe8209a55a28baa9060f6d2a2c80dc92c374 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Sun, 25 Dec 2016 14:24:58 +0200 Subject: [PATCH 05/16] Continue menu refactor --- lib/components/Method/method.ts | 74 ++++--- lib/components/MethodsList/methods-list.html | 6 +- lib/components/MethodsList/methods-list.ts | 7 +- lib/components/SideMenu/side-menu-items.scss | 6 +- lib/components/SideMenu/side-menu.ts | 20 +- lib/services/menu.service.ts | 195 +++++++++++++++---- lib/services/schema-helper.service.ts | 94 +-------- lib/shared/components/LazyFor/lazy-for.ts | 4 +- 8 files changed, 226 insertions(+), 180 deletions(-) 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(); From 90ae0448ebf02c98a5954d7ac52a86b9e6bb2caa Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Sun, 25 Dec 2016 17:14:31 +0200 Subject: [PATCH 06/16] Add support for grouping items in menu --- lib/components/SideMenu/side-menu-items.html | 2 +- lib/components/SideMenu/side-menu-items.scss | 32 +++- lib/components/SideMenu/side-menu.ts | 3 +- lib/services/menu.service.ts | 162 +++++++++++++------ lib/services/schema-helper.service.ts | 20 +-- 5 files changed, 145 insertions(+), 74 deletions(-) diff --git a/lib/components/SideMenu/side-menu-items.html b/lib/components/SideMenu/side-menu-items.html index d286695d..088baaa1 100644 --- a/lib/components/SideMenu/side-menu-items.html +++ b/lib/components/SideMenu/side-menu-items.html @@ -1,5 +1,5 @@