diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index eaf024bc..f266854d 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -65,7 +65,7 @@ export class TagItem extends React.Component {

- + {name}

{description !== undefined && } diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 060966d8..75c8fd92 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -51,7 +51,7 @@ export class Operation extends React.Component {

- + {summary} {deprecated && Deprecated }

{options.pathInMiddlePanel && } diff --git a/src/components/SideMenu/MenuItem.tsx b/src/components/SideMenu/MenuItem.tsx index 92cf1499..9d03725a 100644 --- a/src/components/SideMenu/MenuItem.tsx +++ b/src/components/SideMenu/MenuItem.tsx @@ -40,7 +40,7 @@ export class MenuItem extends React.Component { render() { const { item, withoutChildren } = this.props; return ( - + {item.type === 'operation' ? ( ) : ( diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index b213166b..5542942c 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -2,6 +2,7 @@ import { observe } from 'mobx'; import { OpenAPISpec } from '../types'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; +import { HistoryService } from './HistoryService'; import { MarkerService } from './MarkerService'; import { MenuStore } from './MenuStore'; import { SpecStore } from './models'; @@ -63,6 +64,10 @@ export class AppStore { this.rawOptions = options; this.options = new RedocNormalizedOptions(options); this.scroll = new ScrollService(this.options); + + // update position statically based on hash (in case of SSR) + MenuStore.updateOnHash(HistoryService.hash, this.scroll); + this.spec = new SpecStore(spec, specUrl, this.options); this.menu = new MenuStore(this.spec, this.scroll); diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index e347f457..d1eb5a41 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -5,7 +5,7 @@ import { GroupModel, OperationModel, SpecStore } from './models'; import { HistoryService } from './HistoryService'; import { ScrollService } from './ScrollService'; -import { flattenByProp } from '../utils'; +import { flattenByProp, normalizeHash } from '../utils'; import { GROUP_DEPTH } from './MenuBuilder'; export type MenuItemGroupType = 'group' | 'tag' | 'section'; @@ -24,7 +24,6 @@ export interface IMenuItem { deprecated?: boolean; type: MenuItemType; - getHash(): string; deactivate(): void; activate(): void; } @@ -35,6 +34,17 @@ export const SECTION_ATTR = 'data-section-id'; * Stores all side-menu related information */ export class MenuStore { + /** + * Statically try update scroll position + * Used before hydrating from server-side rendered html to scroll page faster + */ + static updateOnHash(hash: string = HistoryService.hash, scroll: ScrollService) { + if (!hash) { + return; + } + scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${normalizeHash(hash)}"]`); + } + /** * active item absolute index (when flattened). -1 means nothing is selected */ @@ -127,32 +137,13 @@ export class MenuStore { return false; } let item: IMenuItem | undefined; - hash = hash.substr(1); - const namespace = hash.split('/')[0]; - let ptr = decodeURIComponent(hash.substr(namespace.length + 1)); - if (namespace === 'section' || namespace === 'tag') { - const sectionId = ptr.split('/')[0]; - ptr = ptr.substr(sectionId.length); + hash = normalizeHash(hash); - let searchId; - if (namespace === 'section') { - searchId = hash; - } else { - searchId = ptr || namespace + '/' + sectionId; - } - - item = this.flatItems.find(i => i.id === searchId); - if (item === undefined) { - this._scrollService.scrollIntoViewBySelector(`[${SECTION_ATTR}="${searchId}"]`); - return false; - } - } else if (namespace === 'operation') { - item = this.flatItems.find(i => { - return (i as OperationModel).operationId === ptr; - }); - } + item = this.flatItems.find(i => i.id === hash); if (item) { this.activateAndScroll(item, false); + } else { + this._scrollService.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`); } return item !== undefined; } @@ -216,7 +207,7 @@ export class MenuStore { this.activeItemIdx = item.absoluteIdx!; if (updateHash) { - HistoryService.update(item.getHash(), rewriteHistory); + HistoryService.update(item.id, rewriteHistory); } while (item !== undefined) { diff --git a/src/services/models/Group.model.ts b/src/services/models/Group.model.ts index 89d4e878..45b76359 100644 --- a/src/services/models/Group.model.ts +++ b/src/services/models/Group.model.ts @@ -52,8 +52,4 @@ export class GroupModel implements IMenuItem { } this.active = false; } - - getHash() { - return this.id; - } } diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index 03690ff3..9808f62b 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -62,7 +62,11 @@ export class OperationModel implements IMenuItem { parent: GroupModel | undefined, options: RedocNormalizedOptions, ) { - this.id = operationSpec._$ref; + this.id = + operationSpec.operationId !== undefined + ? 'operation/' + operationSpec.operationId + : this.parent !== undefined ? this.parent.id + operationSpec._$ref : operationSpec._$ref; + this.name = getOperationSummary(operationSpec); this.description = operationSpec.description; @@ -130,12 +134,6 @@ export class OperationModel implements IMenuItem { deactivate() { this.active = false; } - - getHash() { - return this.operationId !== undefined - ? 'operation/' + this.operationId - : this.parent !== undefined ? this.parent.id + this.id : this.id; - } } function isNumeric(n) { diff --git a/src/standalone.tsx b/src/standalone.tsx index ac6025c9..558ef40e 100644 --- a/src/standalone.tsx +++ b/src/standalone.tsx @@ -76,9 +76,11 @@ export function hydrate( const store = AppStore.fromJS(state); debugTimeEnd('Redoc create store'); - debugTime('Redoc hydrate'); - hydrateComponent(, element, callback); - debugTimeEnd('Redoc hydrate'); + setTimeout(() => { + debugTime('Redoc hydrate'); + hydrateComponent(, element, callback); + debugTimeEnd('Redoc hydrate'); + }, 0); } /** diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 5f50b85f..4b069cd9 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -24,6 +24,10 @@ export function html2Str(html: string): string { .join(' '); } +export function normalizeHash(hash: string): string { + return hash.startsWith('#') ? hash.substr(1) : hash; +} + // scrollIntoViewIfNeeded polyfill if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {