feat: port "copy to clipboard" / "expand/collapse all" functionality

closes #410
This commit is contained in:
Roman Hotsiy 2018-01-23 18:34:27 +02:00
parent 199f240e7c
commit 5bb0bdfd29
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
10 changed files with 164 additions and 15 deletions

View File

@ -24,7 +24,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 = {};
const options: RedocRawOptions = { nativeScrollbars: true };
async function init() {
const spec = await loadAndBundleSpec(specUrl);

View File

@ -39,6 +39,7 @@
"@types/react-dom": "^16.0.0",
"@types/react-hot-loader": "^3.0.3",
"@types/react-tabs": "^1.0.2",
"@types/react-tooltip": "^3.3.3",
"@types/webpack": "^3.0.5",
"@types/webpack-env": "^1.13.0",
"awesome-typescript-loader": "^3.2.2",
@ -89,8 +90,8 @@
"prop-types": "^15.6.0",
"react-dropdown": "^1.3.0",
"react-hot-loader": "3.0.0-beta.6",
"react-perfect-scrollbar": "^0.2.2",
"react-tabs": "^2.0.0",
"react-tooltip": "^3.4.0",
"remarkable": "^1.7.1",
"slugify": "^1.2.1",
"stickyfill": "^1.1.1",

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import * as ReactTooltip from 'react-tooltip';
import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps {
data: any;
children: (
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
}
export class CopyButtonWrapper extends React.PureComponent<CopyButtonWrapperProps> {
render() {
return this.props.children({ renderCopyButton: this.renderCopyButton });
}
copy = () => {
const content =
typeof this.props.data === 'string'
? this.props.data
: JSON.stringify(this.props.data, null, 2);
ClipboardService.copyCustom(content);
};
renderCopyButton = () => {
return (
<>
<span
onClick={this.copy}
data-tip={true}
data-for="copy_tooltip"
data-event="click"
data-event-off="mouseleave"
>
Copy
</span>
<ReactTooltip
isCapture={true}
id="copy_tooltip"
place="top"
getContent={this.getTooltipContent}
type="light"
effect="solid"
/>
</>
);
};
getTooltipContent() {
return ClipboardService.isSupported() ? 'Copied' : 'Not supported in your browser';
}
}

View File

@ -7,3 +7,4 @@ export * from './schema';
export * from './dropdown';
export * from './mixins';
export * from './tabs';
export * from './samples';

View File

@ -0,0 +1,23 @@
import styled from '../styled-components';
export const SampleControls = styled.div`
opacity: 0.4;
transition: opacity 0.3s ease;
text-align: right;
> span {
display: inline-block;
padding: 2px 10px;
cursor: pointer;
:hover {
background: rgba(255, 255, 255, 0.1);
}
}
`;
export const SampleControlsWrap = styled.div`
&:hover ${SampleControls} {
opacity: 1;
}
`;

View File

@ -1,6 +1,8 @@
import * as React from 'react';
import styled from '../../styled-components';
import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { jsonToHTML } from '../../utils/jsonToHtml';
import { jsonStyles } from './style';
@ -9,18 +11,51 @@ interface JsonProps {
className?: string;
}
const JsonViewerWrap = styled.div`
&:hover > ${SampleControls} {
opacity: 1;
}
`;
class Json extends React.PureComponent<JsonProps> {
node: HTMLElement | null;
node: HTMLDivElement;
render() {
return (
return <CopyButtonWrapper data={this.props.data}>{this.renderInner}</CopyButtonWrapper>;
}
renderInner = ({ renderCopyButton }) => (
<JsonViewerWrap>
<SampleControls>
{renderCopyButton()}
<span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span>
</SampleControls>
<div
className={this.props.className}
ref={node => (this.node = node)}
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
/>
);
}
</JsonViewerWrap>
);
expandAll = () => {
const elements = this.node.getElementsByClassName('collapsible');
for (const collapsed of Array.prototype.slice.call(elements)) {
(collapsed.parentNode as Element)!.classList.remove('collapsed');
}
};
collapseAll = () => {
const elements = this.node.getElementsByClassName('collapsible');
for (const expanded of Array.prototype.slice.call(elements)) {
// const collapsed = elements[i];
if ((expanded.parentNode as Element)!.classList.contains('redoc-json')) {
continue;
}
(expanded.parentNode as Element)!.classList.add('collapsed');
}
};
clickListener = (event: MouseEvent) => {
let collapsed;
@ -44,6 +79,6 @@ class Json extends React.PureComponent<JsonProps> {
}
}
export const StyledJson = styled(Json)`
export const JsonViewer = styled(Json)`
${jsonStyles};
`;

View File

@ -2,8 +2,8 @@ import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { StyledJson } from '../JsonViewer/JsonViewer';
import { SourceCode } from '../SourceCode/SourceCode';
import { JsonViewer } from '../JsonViewer/JsonViewer';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { NoSampleLabel } from './styled.elements';
import { isJsonLike, langFromMime } from '../../utils';
@ -19,9 +19,11 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
const sampleView = isJsonLike(mimeType)
? sample => <StyledJson data={sample} />
? sample => <JsonViewer data={sample} />
: sample =>
(sample && <SourceCode lang={langFromMime(mimeType)} source={sample} />) || { noSample };
(sample && <SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />) || {
noSample,
};
const examplesNames = Object.keys(examples);
if (examplesNames.length === 0) {

View File

@ -2,7 +2,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel } from '../../services/models';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCode } from '../SourceCode/SourceCode';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { Tab, TabList, TabPanel, Tabs } from '../../common-elements';
@ -40,7 +40,7 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
)}
{samples.map(sample => (
<TabPanel key={sample.lang}>
<SourceCode lang={sample.lang} source={sample.source} />
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
</TabPanel>
))}
</Tabs>

View File

@ -2,11 +2,15 @@ import * as React from 'react';
import styled from '../../styled-components';
import { highlight } from '../../utils';
import { SampleControls, SampleControlsWrap } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
const StyledPre = styled.pre`
font-family: ${props => props.theme.code.fontFamily};
font-size: ${props => props.theme.code.fontSize};
overflow-x: auto;
font-size: 0.9em;
margin: 0;
`;
export interface SourceCodeProps {
@ -20,3 +24,18 @@ export class SourceCode extends React.PureComponent<SourceCodeProps> {
return <StyledPre dangerouslySetInnerHTML={{ __html: highlight(source, lang) }} />;
}
}
export class SourceCodeWithCopy extends React.PureComponent<SourceCodeProps> {
render() {
return (
<CopyButtonWrapper data={this.props.source}>
{({ renderCopyButton }) => (
<SampleControlsWrap>
<SampleControls>{renderCopyButton()}</SampleControls>
<SourceCode lang={this.props.lang} source={this.props.source} />
</SampleControlsWrap>
)}
</CopyButtonWrapper>
);
}
}

View File

@ -94,6 +94,12 @@
dependencies:
"@types/react" "*"
"@types/react-tooltip@^3.3.3":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@types/react-tooltip/-/react-tooltip-3.3.3.tgz#3b6dbb278fc8317ad04f0ce1972a0972f5450aa7"
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.0.30":
version "16.0.34"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e"
@ -1091,7 +1097,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.0, classnames@^2.2.3:
classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
@ -5848,6 +5854,13 @@ react-test-renderer@^16.0.0-0:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-tooltip@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-3.4.0.tgz#037f38f797c3e6b1b58d2534ccc8c2c76af4f52d"
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"