mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-22 08:36:33 +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 { 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());
|
||||||
|
|
|
@ -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",
|
||||||
|
|
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,56 +1,50 @@
|
||||||
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 }}>
|
||||||
<RedocWrap className="redoc-wrap">
|
<OptionsProvider options={options}>
|
||||||
<StickySidebar
|
<RedocWrap className="redoc-wrap">
|
||||||
className="menu-content"
|
<StickySidebar className="menu-content">
|
||||||
innerRef={el => {
|
<ApiLogo info={spec.info} />
|
||||||
this.stickyElement = el;
|
<SideMenu menu={menu} />
|
||||||
}}
|
</StickySidebar>
|
||||||
>
|
<ApiContent className="api-content">
|
||||||
<ApiLogo info={spec.info} />
|
<ApiInfo info={spec.info} externalDocs={spec.externalDocs} />
|
||||||
<SideMenu menu={menu} />
|
<ContentItems items={menu.items as any} />
|
||||||
</StickySidebar>
|
</ApiContent>
|
||||||
<ApiContent className="api-content">
|
</RedocWrap>
|
||||||
<ApiInfo info={spec.info} externalDocs={spec.externalDocs} />
|
</OptionsProvider>
|
||||||
<ContentItems items={menu.items as any} />
|
|
||||||
</ApiContent>
|
|
||||||
</RedocWrap>
|
|
||||||
</ThemeProvider>
|
</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`
|
export const ApiContent = styled.div`
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: relative;
|
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 { Component } from 'react';
|
||||||
|
|
||||||
import { AppStore } from '../services/';
|
import { AppStore } from '../services/';
|
||||||
import { loadAndBundleSpec } from '../utils';
|
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 {
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user