fix: scroll to section sooner when SSR + simplify item ids

This commit is contained in:
Roman Hotsiy 2018-03-22 17:46:30 +02:00
parent 28c487d2f1
commit d1d80422a4
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
9 changed files with 39 additions and 43 deletions

View File

@ -65,7 +65,7 @@ export class TagItem extends React.Component<ContentItemProps> {
<Row> <Row>
<MiddlePanel key="middle"> <MiddlePanel key="middle">
<H1> <H1>
<ShareLink href={'#' + this.props.item.getHash()} /> <ShareLink href={'#' + this.props.item.id} />
{name} {name}
</H1> </H1>
{description !== undefined && <Markdown source={description} />} {description !== undefined && <Markdown source={description} />}

View File

@ -51,7 +51,7 @@ export class Operation extends React.Component<OperationProps> {
<OperationRow> <OperationRow>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink href={'#' + operation.getHash()} /> <ShareLink href={'#' + operation.id} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
</H2> </H2>
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />} {options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}

View File

@ -40,7 +40,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
render() { render() {
const { item, withoutChildren } = this.props; const { item, withoutChildren } = this.props;
return ( return (
<MenuItemLi onClick={this.activate} depth={item.depth} innerRef={this.saveRef}> <MenuItemLi onClick={this.activate} depth={item.depth} innerRef={this.saveRef} data-item-id={item.id}>
{item.type === 'operation' ? ( {item.type === 'operation' ? (
<OperationMenuItemContent item={item as OperationModel} /> <OperationMenuItemContent item={item as OperationModel} />
) : ( ) : (

View File

@ -2,6 +2,7 @@ import { observe } from 'mobx';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { HistoryService } from './HistoryService';
import { MarkerService } from './MarkerService'; import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore'; import { MenuStore } from './MenuStore';
import { SpecStore } from './models'; import { SpecStore } from './models';
@ -63,6 +64,10 @@ export class AppStore {
this.rawOptions = options; this.rawOptions = options;
this.options = new RedocNormalizedOptions(options); this.options = new RedocNormalizedOptions(options);
this.scroll = new ScrollService(this.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.spec = new SpecStore(spec, specUrl, this.options);
this.menu = new MenuStore(this.spec, this.scroll); this.menu = new MenuStore(this.spec, this.scroll);

View File

@ -5,7 +5,7 @@ import { GroupModel, OperationModel, SpecStore } from './models';
import { HistoryService } from './HistoryService'; import { HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { flattenByProp } from '../utils'; import { flattenByProp, normalizeHash } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -24,7 +24,6 @@ export interface IMenuItem {
deprecated?: boolean; deprecated?: boolean;
type: MenuItemType; type: MenuItemType;
getHash(): string;
deactivate(): void; deactivate(): void;
activate(): void; activate(): void;
} }
@ -35,6 +34,17 @@ export const SECTION_ATTR = 'data-section-id';
* Stores all side-menu related information * Stores all side-menu related information
*/ */
export class MenuStore { 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 * active item absolute index (when flattened). -1 means nothing is selected
*/ */
@ -127,32 +137,13 @@ export class MenuStore {
return false; return false;
} }
let item: IMenuItem | undefined; let item: IMenuItem | undefined;
hash = hash.substr(1); hash = normalizeHash(hash);
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);
let searchId; item = this.flatItems.find(i => i.id === hash);
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;
});
}
if (item) { if (item) {
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} else {
this._scrollService.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`);
} }
return item !== undefined; return item !== undefined;
} }
@ -216,7 +207,7 @@ export class MenuStore {
this.activeItemIdx = item.absoluteIdx!; this.activeItemIdx = item.absoluteIdx!;
if (updateHash) { if (updateHash) {
HistoryService.update(item.getHash(), rewriteHistory); HistoryService.update(item.id, rewriteHistory);
} }
while (item !== undefined) { while (item !== undefined) {

View File

@ -52,8 +52,4 @@ export class GroupModel implements IMenuItem {
} }
this.active = false; this.active = false;
} }
getHash() {
return this.id;
}
} }

View File

@ -62,7 +62,11 @@ export class OperationModel implements IMenuItem {
parent: GroupModel | undefined, parent: GroupModel | undefined,
options: RedocNormalizedOptions, 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.name = getOperationSummary(operationSpec);
this.description = operationSpec.description; this.description = operationSpec.description;
@ -130,12 +134,6 @@ export class OperationModel implements IMenuItem {
deactivate() { deactivate() {
this.active = false; this.active = false;
} }
getHash() {
return this.operationId !== undefined
? 'operation/' + this.operationId
: this.parent !== undefined ? this.parent.id + this.id : this.id;
}
} }
function isNumeric(n) { function isNumeric(n) {

View File

@ -76,9 +76,11 @@ export function hydrate(
const store = AppStore.fromJS(state); const store = AppStore.fromJS(state);
debugTimeEnd('Redoc create store'); debugTimeEnd('Redoc create store');
debugTime('Redoc hydrate'); setTimeout(() => {
hydrateComponent(<Redoc store={store} />, element, callback); debugTime('Redoc hydrate');
debugTimeEnd('Redoc hydrate'); hydrateComponent(<Redoc store={store} />, element, callback);
debugTimeEnd('Redoc hydrate');
}, 0);
} }
/** /**

View File

@ -24,6 +24,10 @@ export function html2Str(html: string): string {
.join(' '); .join(' ');
} }
export function normalizeHash(hash: string): string {
return hash.startsWith('#') ? hash.substr(1) : hash;
}
// scrollIntoViewIfNeeded polyfill // scrollIntoViewIfNeeded polyfill
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) { if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {