Implement options and scrollYOffset option

This commit is contained in:
Roman Hotsiy 2017-11-20 18:02:49 +02:00
parent 6fbd47b340
commit c2f82cdc8b
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
10 changed files with 190 additions and 47 deletions

View File

@ -3,11 +3,11 @@ import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader'; import { AppContainer } from 'react-hot-loader';
// import DevTools from 'mobx-react-devtools'; // 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 { AppStore } from '../../src/services/AppStore';
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
const renderRoot = (Component: typeof Redoc, props: { store: AppStore }) => const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
render( render(
<div> <div>
<AppContainer> <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'; const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml';
let store; let store;
const options = {};
async function init() { async function init() {
const spec = await loadAndBundleSpec(specUrl); const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl); store = new AppStore(spec, specUrl);
renderRoot(Redoc, { store: store }); renderRoot(Redoc, { store: store, options });
} }
init(); init();
@ -42,7 +43,7 @@ if (module.hot) {
store = AppStore.fromJS(state); store = AppStore.fromJS(state);
} }
renderRoot(Redoc, { store: store }); renderRoot(Redoc, { store: store, options });
}; };
module.hot.accept(['../../src/components/Redoc/Redoc'], reload()); module.hot.accept(['../../src/components/Redoc/Redoc'], reload());

View File

@ -69,6 +69,7 @@
"webpack-node-externals": "^1.6.0" "webpack-node-externals": "^1.6.0"
}, },
"dependencies": { "dependencies": {
"@types/prop-types": "^15.5.2",
"decko": "^1.2.0", "decko": "^1.2.0",
"eventemitter3": "^2.0.3", "eventemitter3": "^2.0.3",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
@ -77,6 +78,7 @@
"mobx-react": "^4.3.3", "mobx-react": "^4.3.3",
"openapi-sampler": "^1.0.0-beta.1", "openapi-sampler": "^1.0.0-beta.1",
"prismjs": "^1.8.1", "prismjs": "^1.8.1",
"prop-types": "^15.6.0",
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0", "react-dom": "^16.0.0",
"react-dropdown": "^1.3.0", "react-dropdown": "^1.3.0",

View 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 || {};
}
}

View File

@ -1,48 +1,41 @@
import * as React from 'react'; import * as React from 'react';
import Stickyfill from 'stickyfill'; import * as PropTypes from 'prop-types';
import { ThemeProvider } from '../../styled-components'; import { ThemeProvider } from '../../styled-components';
import { ApiInfo } from '../ApiInfo/ApiInfo'; import { ApiInfo } from '../ApiInfo/ApiInfo';
import { RedocWrap, StickySidebar, ApiContent } from './elements'; import { RedocWrap, ApiContent } from './elements';
import { ApiLogo } from '../ApiLogo/ApiLogo'; import { ApiLogo } from '../ApiLogo/ApiLogo';
import { SideMenu } from '../SideMenu/SideMenu'; import { SideMenu } from '../SideMenu/SideMenu';
import { ContentItems } from '../ContentItems/ContentItems'; import { ContentItems } from '../ContentItems/ContentItems';
import { AppStore } from '../../services'; 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 { export interface RedocProps {
store: AppStore; store: AppStore;
options?: { options?: RedocRawOptions;
theme?: ThemeInterface;
};
} }
const stickyfill = Stickyfill();
export class Redoc extends React.Component<RedocProps> { export class Redoc extends React.Component<RedocProps> {
stickyElement: Element; static propTypes = {
store: PropTypes.instanceOf(AppStore).isRequired,
options: PropTypes.object,
};
componentDidMount() { componentDidMount() {
this.props.store.menu.updateOnHash(); this.props.store.menu.updateOnHash();
stickyfill.add(this.stickyElement);
}
componentWillUnmount() {
stickyfill.remove(this.stickyElement);
} }
render() { render() {
const { store: { spec, menu }, options = {} } = this.props; const { store: { spec, menu }, options = {} } = this.props;
return ( return (
<ThemeProvider theme={{ ...options.theme, ...defaultTheme }}> <ThemeProvider theme={{ ...options.theme, ...defaultTheme }}>
<OptionsProvider options={options}>
<RedocWrap className="redoc-wrap"> <RedocWrap className="redoc-wrap">
<StickySidebar <StickySidebar className="menu-content">
className="menu-content"
innerRef={el => {
this.stickyElement = el;
}}
>
<ApiLogo info={spec.info} /> <ApiLogo info={spec.info} />
<SideMenu menu={menu} /> <SideMenu menu={menu} />
</StickySidebar> </StickySidebar>
@ -51,6 +44,7 @@ export class Redoc extends React.Component<RedocProps> {
<ContentItems items={menu.items as any} /> <ContentItems items={menu.items as any} />
</ApiContent> </ApiContent>
</RedocWrap> </RedocWrap>
</OptionsProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@ -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` export const ApiContent = styled.div`
z-index: 10; z-index: 10;
position: relative; position: relative;

View 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>
);
}
}

View File

@ -1,4 +1,5 @@
import { Component } from 'react'; import { Component } from 'react';
import { AppStore } from '../services/'; import { AppStore } from '../services/';
import { loadAndBundleSpec } from '../utils'; import { loadAndBundleSpec } from '../utils';

View 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;
}
}

View File

@ -63,3 +63,7 @@ export function stripTrailingSlash(path: string): string {
export function isAbsolutePath(path: string): boolean { export function isAbsolutePath(path: string): boolean {
return /^(?:[a-z]+:)?/i.test(path); return /^(?:[a-z]+:)?/i.test(path);
} }
export function isNumeric(n: any): n is Number {
return !isNaN(parseFloat(n)) && isFinite(n);
}

View File

@ -67,6 +67,10 @@
version "1.6.5" version "1.6.5"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.6.5.tgz#e222615538ea2df248c72512e1faf346af2640d6" 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": "@types/react-dom@^16.0.0":
version "16.0.3" version "16.0.3"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64"