mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-29 03:53:43 +03:00
feat: port "copy to clipboard" / "expand/collapse all" functionality
closes #410
This commit is contained in:
parent
199f240e7c
commit
5bb0bdfd29
|
@ -24,7 +24,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 = {};
|
const options: RedocRawOptions = { nativeScrollbars: true };
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const spec = await loadAndBundleSpec(specUrl);
|
const spec = await loadAndBundleSpec(specUrl);
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"@types/react-dom": "^16.0.0",
|
"@types/react-dom": "^16.0.0",
|
||||||
"@types/react-hot-loader": "^3.0.3",
|
"@types/react-hot-loader": "^3.0.3",
|
||||||
"@types/react-tabs": "^1.0.2",
|
"@types/react-tabs": "^1.0.2",
|
||||||
|
"@types/react-tooltip": "^3.3.3",
|
||||||
"@types/webpack": "^3.0.5",
|
"@types/webpack": "^3.0.5",
|
||||||
"@types/webpack-env": "^1.13.0",
|
"@types/webpack-env": "^1.13.0",
|
||||||
"awesome-typescript-loader": "^3.2.2",
|
"awesome-typescript-loader": "^3.2.2",
|
||||||
|
@ -89,8 +90,8 @@
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"react-dropdown": "^1.3.0",
|
"react-dropdown": "^1.3.0",
|
||||||
"react-hot-loader": "3.0.0-beta.6",
|
"react-hot-loader": "3.0.0-beta.6",
|
||||||
"react-perfect-scrollbar": "^0.2.2",
|
|
||||||
"react-tabs": "^2.0.0",
|
"react-tabs": "^2.0.0",
|
||||||
|
"react-tooltip": "^3.4.0",
|
||||||
"remarkable": "^1.7.1",
|
"remarkable": "^1.7.1",
|
||||||
"slugify": "^1.2.1",
|
"slugify": "^1.2.1",
|
||||||
"stickyfill": "^1.1.1",
|
"stickyfill": "^1.1.1",
|
||||||
|
|
55
src/common-elements/CopyButtonWrapper.tsx
Normal file
55
src/common-elements/CopyButtonWrapper.tsx
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export * from './schema';
|
||||||
export * from './dropdown';
|
export * from './dropdown';
|
||||||
export * from './mixins';
|
export * from './mixins';
|
||||||
export * from './tabs';
|
export * from './tabs';
|
||||||
|
export * from './samples';
|
||||||
|
|
23
src/common-elements/samples.tsx
Normal file
23
src/common-elements/samples.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -1,6 +1,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from '../../styled-components';
|
import styled from '../../styled-components';
|
||||||
|
|
||||||
|
import { SampleControls } from '../../common-elements';
|
||||||
|
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
|
||||||
import { jsonToHTML } from '../../utils/jsonToHtml';
|
import { jsonToHTML } from '../../utils/jsonToHtml';
|
||||||
import { jsonStyles } from './style';
|
import { jsonStyles } from './style';
|
||||||
|
|
||||||
|
@ -9,18 +11,51 @@ interface JsonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const JsonViewerWrap = styled.div`
|
||||||
|
&:hover > ${SampleControls} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
class Json extends React.PureComponent<JsonProps> {
|
class Json extends React.PureComponent<JsonProps> {
|
||||||
node: HTMLElement | null;
|
node: HTMLDivElement;
|
||||||
|
|
||||||
render() {
|
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
|
<div
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
ref={node => (this.node = node)}
|
ref={node => (this.node = node!)}
|
||||||
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
|
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) => {
|
clickListener = (event: MouseEvent) => {
|
||||||
let collapsed;
|
let collapsed;
|
||||||
|
@ -44,6 +79,6 @@ class Json extends React.PureComponent<JsonProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledJson = styled(Json)`
|
export const JsonViewer = styled(Json)`
|
||||||
${jsonStyles};
|
${jsonStyles};
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
|
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
|
||||||
import { MediaTypeModel } from '../../services/models';
|
import { MediaTypeModel } from '../../services/models';
|
||||||
import { StyledJson } from '../JsonViewer/JsonViewer';
|
import { JsonViewer } from '../JsonViewer/JsonViewer';
|
||||||
import { SourceCode } from '../SourceCode/SourceCode';
|
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
|
||||||
import { NoSampleLabel } from './styled.elements';
|
import { NoSampleLabel } from './styled.elements';
|
||||||
|
|
||||||
import { isJsonLike, langFromMime } from '../../utils';
|
import { isJsonLike, langFromMime } from '../../utils';
|
||||||
|
@ -19,9 +19,11 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
|
||||||
|
|
||||||
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
|
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
|
||||||
const sampleView = isJsonLike(mimeType)
|
const sampleView = isJsonLike(mimeType)
|
||||||
? sample => <StyledJson data={sample} />
|
? sample => <JsonViewer data={sample} />
|
||||||
: sample =>
|
: sample =>
|
||||||
(sample && <SourceCode lang={langFromMime(mimeType)} source={sample} />) || { noSample };
|
(sample && <SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />) || {
|
||||||
|
noSample,
|
||||||
|
};
|
||||||
|
|
||||||
const examplesNames = Object.keys(examples);
|
const examplesNames = Object.keys(examples);
|
||||||
if (examplesNames.length === 0) {
|
if (examplesNames.length === 0) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { observer } from 'mobx-react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { OperationModel } from '../../services/models';
|
import { OperationModel } from '../../services/models';
|
||||||
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
|
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
|
||||||
import { SourceCode } from '../SourceCode/SourceCode';
|
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from '../../common-elements';
|
import { Tab, TabList, TabPanel, Tabs } from '../../common-elements';
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
|
||||||
)}
|
)}
|
||||||
{samples.map(sample => (
|
{samples.map(sample => (
|
||||||
<TabPanel key={sample.lang}>
|
<TabPanel key={sample.lang}>
|
||||||
<SourceCode lang={sample.lang} source={sample.source} />
|
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -2,11 +2,15 @@ import * as React from 'react';
|
||||||
import styled from '../../styled-components';
|
import styled from '../../styled-components';
|
||||||
import { highlight } from '../../utils';
|
import { highlight } from '../../utils';
|
||||||
|
|
||||||
|
import { SampleControls, SampleControlsWrap } from '../../common-elements';
|
||||||
|
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
|
||||||
|
|
||||||
const StyledPre = styled.pre`
|
const StyledPre = styled.pre`
|
||||||
font-family: ${props => props.theme.code.fontFamily};
|
font-family: ${props => props.theme.code.fontFamily};
|
||||||
font-size: ${props => props.theme.code.fontSize};
|
font-size: ${props => props.theme.code.fontSize};
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
margin: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface SourceCodeProps {
|
export interface SourceCodeProps {
|
||||||
|
@ -20,3 +24,18 @@ export class SourceCode extends React.PureComponent<SourceCodeProps> {
|
||||||
return <StyledPre dangerouslySetInnerHTML={{ __html: highlight(source, lang) }} />;
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -94,6 +94,12 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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":
|
"@types/react@*", "@types/react@^16.0.30":
|
||||||
version "16.0.34"
|
version "16.0.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e"
|
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"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
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"
|
version "2.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
|
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"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.0"
|
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:
|
react@^16.2.0:
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user