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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ export interface RedocRawOptions {
disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[];
parentElementSelector?: string;
unstable_ignoreMimeParameters?: boolean;
@ -106,6 +107,22 @@ export class RedocNormalizedOptions {
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;
scrollYOffset: () => number;
hideHostname: boolean;
@ -120,6 +137,7 @@ export class RedocNormalizedOptions {
disableSearch: boolean;
onlyRequiredInSamples: boolean;
showExtensions: boolean | string[];
parentElement: Element | null;
/* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean;
@ -147,6 +165,7 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.parentElement = RedocNormalizedOptions.normalizeParentElementSelector(raw.parentElementSelector);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

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