From aa80c7d6e0cc0c5f8364ce51b665e5baf3a0831c Mon Sep 17 00:00:00 2001 From: Andrei Sorescu Date: Tue, 12 Feb 2019 15:09:20 +0200 Subject: [PATCH] beta version --- demo/playground/hmr-playground.tsx | 2 +- src/components/Redoc/Redoc.tsx | 4 +- src/components/Redoc/styled.elements.tsx | 4 +- .../StickySidebar/StickyResponsiveSidebar.tsx | 155 +++++++++++++++--- src/services/RedocNormalizedOptions.ts | 19 +++ src/services/ScrollService.ts | 24 ++- 6 files changed, 175 insertions(+), 33 deletions(-) diff --git a/demo/playground/hmr-playground.tsx b/demo/playground/hmr-playground.tsx index 66d9389b..2f6c7d30 100644 --- a/demo/playground/hmr-playground.tsx +++ b/demo/playground/hmr-playground.tsx @@ -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); diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 2b43e77d..e069a6eb 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -41,7 +41,7 @@ export class Redoc extends React.Component { - + {(!options.disableSearch && ( @@ -58,8 +58,8 @@ export class Redoc extends React.Component { + - diff --git a/src/components/Redoc/styled.elements.tsx b/src/components/Redoc/styled.elements.tsx index ebb30df1..ae19baf0 100644 --- a/src/components/Redoc/styled.elements.tsx +++ b/src/components/Redoc/styled.elements.tsx @@ -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; } diff --git a/src/components/StickySidebar/StickyResponsiveSidebar.tsx b/src/components/StickySidebar/StickyResponsiveSidebar.tsx index 5a9f1817..afd7270e 100644 --- a/src/components/StickySidebar/StickyResponsiveSidebar.tsx +++ b/src/components/StickySidebar/StickyResponsiveSidebar.tsx @@ -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 { + 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( + , + this.portalRoot, + ); + } +} + +@observer +class PortaledStickySidebar extends React.Component { + 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( + , + this.portalRoot, + ); + } +} + @observer export class StickyResponsiveSidebar extends React.Component { 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 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 ( - {options => ( - <> - { - this.stickyElement = el as any; - }} - > - {this.props.children} - - - - - - )} + {options => { + const CondFloating = options.parentElement ? PortaledFloatingButton : + FloatingButton; + const CondSidebar = options.parentElement && window.innerWidth < 800 ? PortaledStickySidebar : + StyledStickySidebar; + + return ( + <> + + {this.props.children} + + + + + + ) + }} ); } diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index d445b667..3846abc9 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -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); diff --git a/src/services/ScrollService.ts b/src/services/ScrollService.ts index 3e946c37..2e61cefd 100644 --- a/src/services/ScrollService.ts +++ b/src/services/ScrollService.ts @@ -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() {