mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-11-04 09:47:31 +03:00 
			
		
		
		
	Implement options and scrollYOffset option
This commit is contained in:
		
							parent
							
								
									6fbd47b340
								
							
						
					
					
						commit
						c2f82cdc8b
					
				| 
						 | 
				
			
			@ -3,11 +3,11 @@ import { render } from 'react-dom';
 | 
			
		|||
import { AppContainer } from 'react-hot-loader';
 | 
			
		||||
// import DevTools from 'mobx-react-devtools';
 | 
			
		||||
 | 
			
		||||
import { Redoc } from '../../src/components/Redoc/Redoc';
 | 
			
		||||
import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
 | 
			
		||||
import { AppStore } from '../../src/services/AppStore';
 | 
			
		||||
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
 | 
			
		||||
 | 
			
		||||
const renderRoot = (Component: typeof Redoc, props: { store: AppStore }) =>
 | 
			
		||||
const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
 | 
			
		||||
  render(
 | 
			
		||||
    <div>
 | 
			
		||||
      <AppContainer>
 | 
			
		||||
| 
						 | 
				
			
			@ -23,11 +23,12 @@ const swagger = window.location.search.indexOf('swagger') > -1; //compatibility
 | 
			
		|||
const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml';
 | 
			
		||||
 | 
			
		||||
let store;
 | 
			
		||||
const options = {};
 | 
			
		||||
 | 
			
		||||
async function init() {
 | 
			
		||||
  const spec = await loadAndBundleSpec(specUrl);
 | 
			
		||||
  store = new AppStore(spec, specUrl);
 | 
			
		||||
  renderRoot(Redoc, { store: store });
 | 
			
		||||
  renderRoot(Redoc, { store: store, options });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
init();
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +43,7 @@ if (module.hot) {
 | 
			
		|||
      store = AppStore.fromJS(state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderRoot(Redoc, { store: store });
 | 
			
		||||
    renderRoot(Redoc, { store: store, options });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  module.hot.accept(['../../src/components/Redoc/Redoc'], reload());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,6 +69,7 @@
 | 
			
		|||
    "webpack-node-externals": "^1.6.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/prop-types": "^15.5.2",
 | 
			
		||||
    "decko": "^1.2.0",
 | 
			
		||||
    "eventemitter3": "^2.0.3",
 | 
			
		||||
    "json-pointer": "^0.6.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +78,7 @@
 | 
			
		|||
    "mobx-react": "^4.3.3",
 | 
			
		||||
    "openapi-sampler": "^1.0.0-beta.1",
 | 
			
		||||
    "prismjs": "^1.8.1",
 | 
			
		||||
    "prop-types": "^15.6.0",
 | 
			
		||||
    "react": "^16.0.0",
 | 
			
		||||
    "react-dom": "^16.0.0",
 | 
			
		||||
    "react-dropdown": "^1.3.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								src/components/OptionsProvider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/components/OptionsProvider.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
import * as React from 'react';
 | 
			
		||||
import * as PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
 | 
			
		||||
import { ThemeInterface } from '../theme';
 | 
			
		||||
 | 
			
		||||
export interface RedocRawOptions {
 | 
			
		||||
  theme?: ThemeInterface;
 | 
			
		||||
  scrollYOffset?: number | string | Function;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OptionsProviderProps {
 | 
			
		||||
  options: RedocRawOptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class OptionsProvider extends React.Component<OptionsProviderProps> {
 | 
			
		||||
  static childContextTypes = {
 | 
			
		||||
    redocOptions: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  getChildContext() {
 | 
			
		||||
    return {
 | 
			
		||||
      redocOptions: new RedocNormalizedOptions(this.props.options),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return React.Children.only(this.props.children);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ComponentWithOptions<P = {}, S = {}> extends React.Component<P, S> {
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    redocOptions: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  get options(): RedocNormalizedOptions {
 | 
			
		||||
    return this.context.redocOptions || {};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +1,41 @@
 | 
			
		|||
import * as React from 'react';
 | 
			
		||||
import Stickyfill from 'stickyfill';
 | 
			
		||||
import * as PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { ThemeProvider } from '../../styled-components';
 | 
			
		||||
 | 
			
		||||
import { ApiInfo } from '../ApiInfo/ApiInfo';
 | 
			
		||||
import { RedocWrap, StickySidebar, ApiContent } from './elements';
 | 
			
		||||
import { RedocWrap, ApiContent } from './elements';
 | 
			
		||||
import { ApiLogo } from '../ApiLogo/ApiLogo';
 | 
			
		||||
import { SideMenu } from '../SideMenu/SideMenu';
 | 
			
		||||
import { ContentItems } from '../ContentItems/ContentItems';
 | 
			
		||||
import { AppStore } from '../../services';
 | 
			
		||||
import { OptionsProvider, RedocRawOptions } from '../OptionsProvider';
 | 
			
		||||
import { StickySidebar } from '../StickySidebar/StickySidebar';
 | 
			
		||||
 | 
			
		||||
import defaultTheme, { ThemeInterface } from '../../theme';
 | 
			
		||||
import defaultTheme from '../../theme';
 | 
			
		||||
 | 
			
		||||
export interface RedocProps {
 | 
			
		||||
  store: AppStore;
 | 
			
		||||
  options?: {
 | 
			
		||||
    theme?: ThemeInterface;
 | 
			
		||||
  };
 | 
			
		||||
  options?: RedocRawOptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const stickyfill = Stickyfill();
 | 
			
		||||
export class Redoc extends React.Component<RedocProps> {
 | 
			
		||||
  stickyElement: Element;
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    store: PropTypes.instanceOf(AppStore).isRequired,
 | 
			
		||||
    options: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.store.menu.updateOnHash();
 | 
			
		||||
    stickyfill.add(this.stickyElement);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    stickyfill.remove(this.stickyElement);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { store: { spec, menu }, options = {} } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <ThemeProvider theme={{ ...options.theme, ...defaultTheme }}>
 | 
			
		||||
        <OptionsProvider options={options}>
 | 
			
		||||
          <RedocWrap className="redoc-wrap">
 | 
			
		||||
          <StickySidebar
 | 
			
		||||
            className="menu-content"
 | 
			
		||||
            innerRef={el => {
 | 
			
		||||
              this.stickyElement = el;
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <StickySidebar className="menu-content">
 | 
			
		||||
              <ApiLogo info={spec.info} />
 | 
			
		||||
              <SideMenu menu={menu} />
 | 
			
		||||
            </StickySidebar>
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +44,7 @@ export class Redoc extends React.Component<RedocProps> {
 | 
			
		|||
              <ContentItems items={menu.items as any} />
 | 
			
		||||
            </ApiContent>
 | 
			
		||||
          </RedocWrap>
 | 
			
		||||
        </OptionsProvider>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,21 +33,6 @@ export const RedocWrap = styled.div`
 | 
			
		|||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const StickySidebar = styled.div`
 | 
			
		||||
  width: ${props => props.theme.menu.width};
 | 
			
		||||
  background-color: ${props => props.theme.menu.backgroundColor};
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  transform: translateZ(0);
 | 
			
		||||
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  position: -webkit-sticky;
 | 
			
		||||
  top: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ApiContent = styled.div`
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
  position: relative;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										68
									
								
								src/components/StickySidebar/StickySidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/components/StickySidebar/StickySidebar.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
import * as React from 'react';
 | 
			
		||||
import Stickyfill from 'stickyfill';
 | 
			
		||||
 | 
			
		||||
import { ComponentWithOptions, RedocRawOptions } from '../OptionsProvider';
 | 
			
		||||
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
 | 
			
		||||
import styled from '../../styled-components';
 | 
			
		||||
 | 
			
		||||
export interface StickySidebarProps {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  scrollYOffset?: RedocRawOptions['scrollYOffset']; // passed directly or via context
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const stickyfill = Stickyfill();
 | 
			
		||||
 | 
			
		||||
const StyledStickySidebar = styled.div`
 | 
			
		||||
  width: ${props => props.theme.menu.width};
 | 
			
		||||
  background-color: ${props => props.theme.menu.backgroundColor};
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  transform: translateZ(0);
 | 
			
		||||
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  position: -webkit-sticky;
 | 
			
		||||
  top: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export class StickySidebar extends ComponentWithOptions<StickySidebarProps> {
 | 
			
		||||
  stickyElement: Element;
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    stickyfill.add(this.refs['sticky-children']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    stickyfill.remove(this.refs['sticky-children']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get scrollYOffset() {
 | 
			
		||||
    let top;
 | 
			
		||||
    if (this.props.scrollYOffset !== undefined) {
 | 
			
		||||
      top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
 | 
			
		||||
    } else {
 | 
			
		||||
      top = this.options.scrollYOffset();
 | 
			
		||||
    }
 | 
			
		||||
    return top + 'px';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    let top = this.scrollYOffset;
 | 
			
		||||
 | 
			
		||||
    const height = `calc(100vh - ${top})`;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <StyledStickySidebar
 | 
			
		||||
        className={this.props.className}
 | 
			
		||||
        style={{ top, height }}
 | 
			
		||||
        innerRef={el => {
 | 
			
		||||
          this.stickyElement = el;
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {this.props.children}
 | 
			
		||||
      </StyledStickySidebar>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import { AppStore } from '../services/';
 | 
			
		||||
import { loadAndBundleSpec } from '../utils';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										44
									
								
								src/services/RedocNormalizedOptions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/services/RedocNormalizedOptions.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import { RedocRawOptions } from '../components/OptionsProvider';
 | 
			
		||||
import { ThemeInterface } from '../theme';
 | 
			
		||||
import { isNumeric } from '../utils/helpers';
 | 
			
		||||
 | 
			
		||||
export class RedocNormalizedOptions {
 | 
			
		||||
  theme: ThemeInterface;
 | 
			
		||||
  scrollYOffset: () => number;
 | 
			
		||||
 | 
			
		||||
  constructor(raw: RedocRawOptions) {
 | 
			
		||||
    this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static normalizeScrollYOffset(value: RedocRawOptions['scrollYOffset']): () => number {
 | 
			
		||||
    // just number is not valid selector and leads to crash so checking if isNumeric here
 | 
			
		||||
    if (typeof value === 'string' && !isNumeric(value)) {
 | 
			
		||||
      const el = document.querySelector(value);
 | 
			
		||||
      if (!el) {
 | 
			
		||||
        console.warn(
 | 
			
		||||
          'scrollYOffset value is a selector to non-existing element. Using offset 0 by default',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      const bottom = (el && el.getBoundingClientRect().bottom) || 0;
 | 
			
		||||
      return () => bottom;
 | 
			
		||||
    } else if (typeof value === 'number' || isNumeric(value)) {
 | 
			
		||||
      return () => (typeof value === 'number' ? value : parseFloat(value));
 | 
			
		||||
    } else if (typeof value === 'function') {
 | 
			
		||||
      return () => {
 | 
			
		||||
        const res = value();
 | 
			
		||||
        if (typeof res !== 'number') {
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `scrollYOffset should return number but returned value "${res}" of type ${typeof res}`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        return res;
 | 
			
		||||
      };
 | 
			
		||||
    } else if (value !== undefined) {
 | 
			
		||||
      console.warn(
 | 
			
		||||
        'Wrong value for scrollYOffset ReDoc option: should be string, number or function',
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -63,3 +63,7 @@ export function stripTrailingSlash(path: string): string {
 | 
			
		|||
export function isAbsolutePath(path: string): boolean {
 | 
			
		||||
  return /^(?:[a-z]+:)?/i.test(path);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isNumeric(n: any): n is Number {
 | 
			
		||||
  return !isNaN(parseFloat(n)) && isFinite(n);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,10 @@
 | 
			
		|||
  version "1.6.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.6.5.tgz#e222615538ea2df248c72512e1faf346af2640d6"
 | 
			
		||||
 | 
			
		||||
"@types/prop-types@^15.5.2":
 | 
			
		||||
  version "15.5.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1"
 | 
			
		||||
 | 
			
		||||
"@types/react-dom@^16.0.0":
 | 
			
		||||
  version "16.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user