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';
|
||||
|
||||
let store;
|
||||
const options: RedocRawOptions = {};
|
||||
const options: RedocRawOptions = { nativeScrollbars: true };
|
||||
|
||||
async function init() {
|
||||
const spec = await loadAndBundleSpec(specUrl);
|
||||
|
|
|
@ -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",
|
||||
|
|
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 './mixins';
|
||||
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 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};
|
||||
`;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user