beta version

This commit is contained in:
Andrei Sorescu 2019-02-12 15:09:20 +02:00
parent 12cfb6ebde
commit aa80c7d6e0
6 changed files with 175 additions and 33 deletions

View File

@ -22,7 +22,7 @@ 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: RedocRawOptions = { nativeScrollbars: false }; const options: RedocRawOptions = { nativeScrollbars: false, parentElementSelector: '#redoc-container' };
async function init() { async function init() {
const spec = await loadAndBundleSpec(specUrl); const spec = await loadAndBundleSpec(specUrl);

View File

@ -41,7 +41,7 @@ export class Redoc extends React.Component<RedocProps> {
<ThemeProvider theme={options.theme}> <ThemeProvider theme={options.theme}>
<StoreProvider value={this.props.store}> <StoreProvider value={this.props.store}>
<OptionsProvider value={options}> <OptionsProvider value={options}>
<RedocWrap className="redoc-wrap"> <RedocWrap ref={menu.scroll.setRedocElement} className="redoc-wrap">
<StickyResponsiveSidebar menu={menu} className="menu-content"> <StickyResponsiveSidebar menu={menu} className="menu-content">
<ApiLogo info={spec.info} /> <ApiLogo info={spec.info} />
{(!options.disableSearch && ( {(!options.disableSearch && (
@ -58,8 +58,8 @@ export class Redoc extends React.Component<RedocProps> {
<ApiContentWrap className="api-content"> <ApiContentWrap className="api-content">
<ApiInfo store={store} /> <ApiInfo store={store} />
<ContentItems items={menu.items as any} /> <ContentItems items={menu.items as any} />
<BackgroundStub />
</ApiContentWrap> </ApiContentWrap>
<BackgroundStub />
</RedocWrap> </RedocWrap>
</OptionsProvider> </OptionsProvider>
</StoreProvider> </StoreProvider>

View File

@ -8,7 +8,6 @@ export const RedocWrap = styled.div`
line-height: ${theme.typography.lineHeight}; line-height: ${theme.typography.lineHeight};
color: ${theme.colors.text.primary}; color: ${theme.colors.text.primary};
display: flex; display: flex;
position: relative;
text-align: left; text-align: left;
-webkit-font-smoothing: ${theme.typography.smoothing}; -webkit-font-smoothing: ${theme.typography.smoothing};
@ -43,10 +42,11 @@ export const BackgroundStub = styled.div`
top: 0; top: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: -1;
width: ${({ theme }) => { width: ${({ theme }) => {
if (theme.rightPanel.width.endsWith('%')) { if (theme.rightPanel.width.endsWith('%')) {
const percents = parseInt(theme.rightPanel.width, 10); const percents = parseInt(theme.rightPanel.width, 10);
return `calc((100% - ${theme.menu.width}) * ${percents / 100})`; return `calc(100% * ${percents / 100})`;
} else { } else {
return theme.rightPanel.width; return theme.rightPanel.width;
} }

View File

@ -1,5 +1,6 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { createPortal } from 'react-dom';
import { MenuStore } from '../../services/MenuStore'; import { MenuStore } from '../../services/MenuStore';
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions'; import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
@ -66,9 +67,10 @@ const FloatingButton = styled.div`
`}; `};
bottom: 44px; bottom: 44px;
box-sizing: content-box;
width: 60px;
height: 60px; height: 60px;
width: 20px;
padding: 0 20px; padding: 0 20px;
@media print { @media print {
@ -76,11 +78,101 @@ const FloatingButton = styled.div`
} }
`; `;
export interface FloatingButtonProps {
onClick?: () => void;
parentElement: Element;
}
@observer
class PortaledFloatingButton extends React.Component<FloatingButtonProps> {
portalRoot: HTMLElement;
floatingButtonRef: HTMLElement;
constructor(props) {
super(props);
this.portalRoot = document.createElement('div');
this.portalRoot.classList.add('portalled-element');
}
private setFloatingButtonRef = (node) => {
this.floatingButtonRef = node;
}
componentWillUnmount() {
document.body.removeChild(this.portalRoot);
}
componentDidMount() {
const { parentElement } = this.props;
document.body.appendChild(this.portalRoot);
this.floatingButtonRef.style.top = `${parentElement.getBoundingClientRect().bottom - 88}px`;
this.floatingButtonRef.style.left = `${parentElement.getBoundingClientRect().right - 88}px`;
}
render() {
return createPortal(
<FloatingButton ref={this.setFloatingButtonRef} {...this.props} />,
this.portalRoot,
);
}
}
@observer
class PortaledStickySidebar extends React.Component<FloatingButtonProps> {
portalRoot: HTMLElement;
stickySidebarRef: HTMLElement;
constructor(props) {
super(props);
this.portalRoot = document.createElement('div');
this.portalRoot.classList.add('portalled-element');
}
componentWillUnmount() {
document.body.removeChild(this.portalRoot);
}
private setStickySidebarRef = (node) => {
this.stickySidebarRef = node;
}
componentDidMount() {
const { parentElement } = this.props;
document.body.appendChild(this.portalRoot);
this.stickySidebarRef.style.top = `${parentElement.getBoundingClientRect().top}px`;
this.stickySidebarRef.style.left = `${parentElement.getBoundingClientRect().left}px`;
this.stickySidebarRef.style.width = `${parentElement.clientWidth}px`;
this.stickySidebarRef.style.zIndex = '99';
}
render() {
return createPortal(
<StyledStickySidebar ref={this.setStickySidebarRef} {...this.props} />,
this.portalRoot,
);
}
}
@observer @observer
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> { export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> {
stickyElement: Element; stickyElement: Element;
portalRoot: Element;
constructor(props) {
super(props);
this.portalRoot = document.createElement('div');
this.portalRoot.classList.add('portalled-element');
}
componentDidMount() { componentDidMount() {
document.body.appendChild(this.portalRoot);
if (stickyfill) { if (stickyfill) {
stickyfill.add(this.stickyElement); stickyfill.add(this.stickyElement);
} }
@ -102,37 +194,52 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
return top + 'px'; return top + 'px';
} }
render() { getSidebarStyles = options => {
const open = this.props.menu.sideBarOpened; if (options.parentElement instanceof Element) {
return {
const style = options => { height: options.parentElement.offsetHeight,
};
} else {
const top = this.getScrollYOffset(options); const top = this.getScrollYOffset(options);
return { return {
top, top,
height: `calc(100vh - ${top})`, height: `calc(100vh - ${top})`,
}; };
}; }
}
setStickyElementRef = el => {
this.stickyElement = el as any;
}
render() {
const open = this.props.menu.sideBarOpened;
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => {
<> const CondFloating = options.parentElement ? PortaledFloatingButton :
<StyledStickySidebar FloatingButton;
open={open} const CondSidebar = options.parentElement && window.innerWidth < 800 ? PortaledStickySidebar :
className={this.props.className} StyledStickySidebar;
style={style(options)}
// tslint:disable-next-line return (
ref={el => { <>
this.stickyElement = el as any; <CondSidebar
}} open={open}
> className={this.props.className}
{this.props.children} style={this.getSidebarStyles(options)}
</StyledStickySidebar> ref={this.setStickyElementRef}
<FloatingButton onClick={this.toggleNavMenu}> parentElement={options.parentElement}
<AnimatedChevronButton open={open} /> >
</FloatingButton> {this.props.children}
</> </CondSidebar>
)} <CondFloating onClick={this.toggleNavMenu} parentElement={options.parentElement}>
<AnimatedChevronButton open={open} />
</CondFloating>
</>
)
}}
</OptionsContext.Consumer> </OptionsContext.Consumer>
); );
} }

View File

@ -20,6 +20,7 @@ export interface RedocRawOptions {
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
parentElementSelector?: string;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
@ -106,6 +107,22 @@ export class RedocNormalizedOptions {
return value; return value;
} }
static normalizeParentElementSelector(value: RedocRawOptions['parentElementSelector']): Element | null {
if (typeof value === 'string') {
try {
return querySelector(value);
} catch (e) {
console.warn(
'Invalid selector for parent element.',
);
}
return null;
}
return null;
}
theme: ResolvedThemeInterface; theme: ResolvedThemeInterface;
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
@ -120,6 +137,7 @@ export class RedocNormalizedOptions {
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
parentElement: Element | null;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -147,6 +165,7 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.parentElement = RedocNormalizedOptions.normalizeParentElementSelector(raw.parentElementSelector);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

@ -7,11 +7,13 @@ import { RedocNormalizedOptions } from './RedocNormalizedOptions';
const EVENT = 'scroll'; const EVENT = 'scroll';
export class ScrollService { export class ScrollService {
private _scrollParent: Window | HTMLElement | undefined; private _scrollParent: Window | HTMLElement | Element | null | undefined;
private _emiter: EventEmitter; private _emiter: EventEmitter;
private _prevOffsetY: number = 0; private _prevOffsetY: number = 0;
private _redocContainer: Window | HTMLElement | Element | null | undefined;
constructor(private options: RedocNormalizedOptions) { constructor(private options: RedocNormalizedOptions) {
this._scrollParent = IS_BROWSER ? window : undefined; const browserContainer = options.parentElement || window;
this._scrollParent = IS_BROWSER ? browserContainer : undefined;
this._emiter = new EventEmitter(); this._emiter = new EventEmitter();
this.bind(); this.bind();
} }
@ -31,8 +33,8 @@ export class ScrollService {
} }
scrollY(): number { scrollY(): number {
if (typeof HTMLElement !== 'undefined' && this._scrollParent instanceof HTMLElement) { if (typeof HTMLElement !== 'undefined' && this._scrollParent instanceof Element && this._redocContainer instanceof Element) {
return this._scrollParent.scrollTop; return this._scrollParent.getBoundingClientRect().top - this._redocContainer.getBoundingClientRect().top;
} else if (this._scrollParent !== undefined) { } else if (this._scrollParent !== undefined) {
return (this._scrollParent as Window).pageYOffset; return (this._scrollParent as Window).pageYOffset;
} else { } else {
@ -44,6 +46,10 @@ export class ScrollService {
if (el === null) { if (el === null) {
return; return;
} }
if (this._scrollParent instanceof Element) {
return el.getBoundingClientRect().top - this._scrollParent.getBoundingClientRect().top > this.options.scrollYOffset();
}
return el.getBoundingClientRect().top > this.options.scrollYOffset(); return el.getBoundingClientRect().top > this.options.scrollYOffset();
} }
@ -51,6 +57,11 @@ export class ScrollService {
if (el === null) { if (el === null) {
return; return;
} }
if (this._scrollParent instanceof Element) {
const topDistance = el.getBoundingClientRect().top - this._scrollParent.getBoundingClientRect().top;
return (topDistance > 0 ? Math.floor(topDistance) : Math.ceil(topDistance)) <= this.options.scrollYOffset();
}
const top = el.getBoundingClientRect().top; const top = el.getBoundingClientRect().top;
return (top > 0 ? Math.floor(top) : Math.ceil(top)) <= this.options.scrollYOffset(); return (top > 0 ? Math.floor(top) : Math.ceil(top)) <= this.options.scrollYOffset();
} }
@ -76,6 +87,11 @@ export class ScrollService {
this.scrollIntoView(element); this.scrollIntoView(element);
} }
@bind
setRedocElement(node: HTMLDivElement) {
this._redocContainer = node;
}
@bind @bind
@Throttle(100) @Throttle(100)
handleScroll() { handleScroll() {