Continue menu refactor

This commit is contained in:
Roman Hotsiy 2016-12-25 14:24:58 +02:00
parent 97949a17f9
commit c41ffe8209
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
8 changed files with 226 additions and 180 deletions

View File

@ -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; private 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();
} }

View File

@ -4,8 +4,8 @@
<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.items; let show = show;" [hidden]="!show" <method *lazyFor="let methodItem of tag.items; let ready = ready;"
[pointer]="method.metadata.pointer" [attr.section]="method.id" [hidden]="!ready" [pointer]="methodItem.metadata.pointer"
[attr.operation-id]="method.metadata.operationId"></method> [parentTagId]="tag.id" [attr.section]="methodItem.id"></method>
</div> </div>
</div> </div>

View File

@ -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,18 +14,19 @@ 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 flatMenuItems = SchemaHelper.flatMenu(SchemaHelper.buildMenuTree(this.specMgr.schema)); let flatMenuItems = this.menu.flatItems;
this.tags = []; this.tags = [];
let emptyTag = { let emptyTag = {
name: '', name: '',
items: [] items: []
} }
flatMenuItems.forEach(menuItem => { flatMenuItems.forEach(menuItem => {
// skip items that are not bound to swagger tags/methods
if (!menuItem.metadata) return; if (!menuItem.metadata) return;
if (menuItem.metadata.type === 'tag') { if (menuItem.metadata.type === 'tag') {

View File

@ -53,7 +53,7 @@
} }
} }
.menu-item-level-0 { .menu-item-level-1 {
> .menu-item-header { > .menu-item-header {
font-family: $headers-font, $headers-font-family; font-family: $headers-font, $headers-font-family;
font-weight: $light; font-weight: $light;
@ -61,7 +61,7 @@
text-transform: uppercase; text-transform: uppercase;
} }
> .menu-item-header:hover, > .menu-item-header:not(.disabled):hover,
&.active > .menu-item-header { &.active > .menu-item-header {
color: $primary-color; color: $primary-color;
background: $side-menu-active-bg-color; background: $side-menu-active-bg-color;
@ -71,7 +71,7 @@
} }
} }
.menu-item-level-1 { .menu-item-level-2 {
> .menu-item-header { > .menu-item-header {
padding-left: 2*$side-menu-item-hpadding; padding-left: 2*$side-menu-item-hpadding;
} }

View File

@ -69,9 +69,17 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
} }
changed(item) { changed(item) {
if (item) { if (!item) {
this.activeCatCaption = item.name || ''; this.activeCatCaption = '';
this.activeItemCaption = item.parent && item.parent.name || ''; 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 :( //safari doesn't update bindings if not run changeDetector manually :(
@ -88,12 +96,10 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
} }
activateAndScroll(item) { activateAndScroll(item) {
if (this.mobileMode()) { if (this.mobileMode) {
this.toggleMobileNav(); this.toggleMobileNav();
} }
//if (!this.flatItems[idx].ready) return; // TODO: move inside next statement
this.menuService.activate(item.flatIdx); this.menuService.activate(item.flatIdx);
this.menuService.scrollToActive(); this.menuService.scrollToActive();
} }
@ -111,7 +117,7 @@ export class SideMenu extends BaseComponent implements OnInit, OnDestroy {
}; };
} }
mobileMode() { get mobileMode() {
return this.$mobileNav.clientHeight > 0; return this.$mobileNav.clientHeight > 0;
} }

View File

@ -15,53 +15,45 @@ import * as slugify from 'slugify';
const CHANGE = { const CHANGE = {
NEXT : 1, NEXT : 1,
BACK : -1, BACK : -1,
INITIAL : 0
}; };
@Injectable() @Injectable()
export class MenuService { export class MenuService {
changed: EventEmitter<any> = new EventEmitter(); changed: EventEmitter<any> = new EventEmitter();
ready: BehaviorSubject<boolean> = new BehaviorSubject(false);
items: Array<MenuItem>;
flatItems: Array<MenuItem>;
activeCatIdx: number = 0;
activeMethodIdx: number = -1;
items: MenuItem[];
activeIdx: number = -1; activeIdx: number = -1;
private _flatItems: MenuItem[];
private _hashSubscription: Subscription; private _hashSubscription: Subscription;
private _scrollSubscription: Subscription;
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.items = SchemaHelper.buildMenuTree(specMgr.schema); this.buildMenu();
this.flatItems = SchemaHelper.flatMenu(this.items);
scrollService.scroll.subscribe((evt) => { this._scrollSubscription = scrollService.scroll.subscribe((evt) => {
this.scrollUpdate(evt.isScrolledDown); this.onScroll(evt.isScrolledDown);
}); });
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.activeIdx, this);
this.scrollService.setStickElement(this.getCurrentEl());
if (hash) this.scrollToActive();
this.appState.stopLoading();
} else {
if (hash) this.scrollToActive();
}
}); });
} }
get flatItems():MenuItem[] {
if (!this._flatItems) {
this._flatItems = this.flatMenu();
}
return this._flatItems;
}
enableItem(idx) { enableItem(idx) {
let item = this.flatItems[idx]; let item = this.flatItems[idx];
item.ready = true; item.ready = true;
@ -70,6 +62,7 @@ export class MenuService {
idx = item.parent.flatIdx; idx = item.parent.flatIdx;
} }
// check if previous items can be enabled
let prevItem = this.flatItems[idx -= 1]; let prevItem = this.flatItems[idx -= 1];
while(prevItem && (!prevItem.metadata || !prevItem.items)) { while(prevItem && (!prevItem.metadata || !prevItem.items)) {
prevItem.ready = true; prevItem.ready = true;
@ -79,11 +72,10 @@ export class MenuService {
this.changed.next(); this.changed.next();
} }
scrollUpdate(isScrolledDown) { onScroll(isScrolledDown) {
let stable = false; let stable = false;
while(!stable) { while(!stable) {
if(isScrolledDown) { if(isScrolledDown) {
//&& elementInViewPos === INVIEW_POSITION.BELLOW
let $nextEl = this.getEl(this.activeIdx + 1); let $nextEl = this.getEl(this.activeIdx + 1);
if (!$nextEl) return; if (!$nextEl) return;
let nextInViewPos = this.scrollService.getElementPos($nextEl, true); let nextInViewPos = this.scrollService.getElementPos($nextEl, true);
@ -103,8 +95,21 @@ export class MenuService {
} }
} }
getEl(flatIdx) { onHashChange(hash?: string) {
if (flatIdx < 0) flatIdx = 0; 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 currentItem = this.flatItems[flatIdx];
let selector = ''; let selector = '';
while(currentItem) { while(currentItem) {
@ -115,35 +120,37 @@ export class MenuService {
return selector ? document.querySelector(selector) : null; return selector ? document.querySelector(selector) : null;
} }
getCurrentEl() { getCurrentEl():Element {
return this.getEl(this.activeIdx); return this.getEl(this.activeIdx);
} }
deactivate(idx) { deactivate(idx) {
if (idx < 0) return; if (idx < 0) return;
let prevItem = this.flatItems[idx]; let item = this.flatItems[idx];
prevItem.active = false; item.active = false;
if (prevItem.parent) { if (item.parent) {
prevItem.parent.active = false; 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.deactivate(this.activeIdx);
this.activeIdx = idx; this.activeIdx = idx;
if (idx < 0) return; if (idx < 0) return;
let currentItem = this.flatItems[this.activeIdx]; item.active = true;
currentItem.active = true; if (item.parent) {
if (currentItem.parent) { item.parent.active = true;
currentItem.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) || let noChange = (this.activeIdx <= 0 && offset === -1) ||
(this.activeIdx === this.flatItems.length - 1 && offset === 1); (this.activeIdx === this.flatItems.length - 1 && offset === 1);
this.activate(this.activeIdx + offset); this.activate(this.activeIdx + offset);
@ -151,10 +158,11 @@ export class MenuService {
} }
scrollToActive() { scrollToActive() {
this.scrollService.scrollTo(this.getCurrentEl()); let $el = this.getCurrentEl();
if ($el) this.scrollService.scrollTo($el);
} }
setActiveByHash(hash) { activateByHash(hash):boolean {
if (!hash) return; if (!hash) return;
let idx = 0; let idx = 0;
hash = hash.substr(1); hash = hash.substr(1);
@ -163,17 +171,118 @@ export class MenuService {
if (namespace === 'section' || namespace === 'tag') { if (namespace === 'section' || namespace === 'tag') {
let sectionId = ptr.split('/')[0]; let sectionId = ptr.split('/')[0];
ptr = ptr.substr(sectionId.length) || null; 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); idx = this.flatItems.findIndex(item => item.id === searchId);
if (idx < 0) this.tryScrollToId(searchId);
} else if (namespace === 'operation') { } else if (namespace === 'operation') {
idx = this.flatItems.findIndex(item => { idx = this.flatItems.findIndex(item => {
return item.metadata && item.metadata.operationId === ptr 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 (<Array<string>>(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() { destroy() {
this._hashSubscription.unsubscribe(); this._hashSubscription.unsubscribe();
this._scrollSubscription.unsubscribe();
} }
} }

View File

@ -9,15 +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 MenuItem { export interface MenuItem {
id: string; id: string;
@ -305,7 +296,7 @@ export class SchemaHelper {
} }
} }
static getTags(schema) { static getTagsWithMethods(schema) {
let tags = {}; let tags = {};
for (let tag of schema.tags || []) { for (let tag of schema.tags || []) {
tags[tag.name] = tag; tags[tag.name] = tag;
@ -319,10 +310,11 @@ export class SchemaHelper {
let methodInfo = paths[path][method]; let methodInfo = paths[path][method];
let methodTags = methodInfo.tags; let methodTags = methodInfo.tags;
// empty tag
if (!(methodTags && methodTags.length)) { if (!(methodTags && methodTags.length)) {
methodTags = ['']; methodTags = [''];
} }
let methodPointer = JsonPointer.compile([path, method]); let methodPointer = JsonPointer.compile(['paths', path, method]);
for (let tagName of methodTags) { for (let tagName of methodTags) {
let tag = tags[tagName]; let tag = tags[tagName];
if (!tag) { if (!tag) {
@ -341,84 +333,4 @@ export class SchemaHelper {
return Object.keys(tags).map(k => tags[k]); 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 (<Array<string>>(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;
}
} }

View File

@ -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; }
@ -145,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();