mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-11-04 09:47:31 +03:00 
			
		
		
		
	fix: scroll to section sooner when SSR + simplify item ids
This commit is contained in:
		
							parent
							
								
									28c487d2f1
								
							
						
					
					
						commit
						d1d80422a4
					
				| 
						 | 
					@ -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} />}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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} />}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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} />
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,8 +52,4 @@ export class GroupModel implements IMenuItem {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.active = false;
 | 
					    this.active = false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  getHash() {
 | 
					 | 
				
			||||||
    return this.id;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setTimeout(() => {
 | 
				
			||||||
    debugTime('Redoc hydrate');
 | 
					    debugTime('Redoc hydrate');
 | 
				
			||||||
    hydrateComponent(<Redoc store={store} />, element, callback);
 | 
					    hydrateComponent(<Redoc store={store} />, element, callback);
 | 
				
			||||||
    debugTimeEnd('Redoc hydrate');
 | 
					    debugTimeEnd('Redoc hydrate');
 | 
				
			||||||
 | 
					  }, 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user