Skip to content

Commit 5bb0bdf

Browse files
committed
feat: port "copy to clipboard" / "expand/collapse all" functionality
closes #410
1 parent 199f240 commit 5bb0bdf

File tree

10 files changed

+164
-15
lines changed

10 files changed

+164
-15
lines changed

demo/playground/hmr-playground.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const swagger = window.location.search.indexOf('swagger') > -1; // compatibility
2424
const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml';
2525

2626
let store;
27-
const options: RedocRawOptions = {};
27+
const options: RedocRawOptions = { nativeScrollbars: true };
2828

2929
async function init() {
3030
const spec = await loadAndBundleSpec(specUrl);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@types/react-dom": "^16.0.0",
4040
"@types/react-hot-loader": "^3.0.3",
4141
"@types/react-tabs": "^1.0.2",
42+
"@types/react-tooltip": "^3.3.3",
4243
"@types/webpack": "^3.0.5",
4344
"@types/webpack-env": "^1.13.0",
4445
"awesome-typescript-loader": "^3.2.2",
@@ -89,8 +90,8 @@
8990
"prop-types": "^15.6.0",
9091
"react-dropdown": "^1.3.0",
9192
"react-hot-loader": "3.0.0-beta.6",
92-
"react-perfect-scrollbar": "^0.2.2",
9393
"react-tabs": "^2.0.0",
94+
"react-tooltip": "^3.4.0",
9495
"remarkable": "^1.7.1",
9596
"slugify": "^1.2.1",
9697
"stickyfill": "^1.1.1",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from 'react';
2+
import * as ReactTooltip from 'react-tooltip';
3+
4+
import { ClipboardService } from '../services/ClipboardService';
5+
6+
export interface CopyButtonWrapperProps {
7+
data: any;
8+
children: (
9+
props: {
10+
renderCopyButton: (() => React.ReactNode);
11+
},
12+
) => React.ReactNode;
13+
}
14+
15+
export class CopyButtonWrapper extends React.PureComponent<CopyButtonWrapperProps> {
16+
render() {
17+
return this.props.children({ renderCopyButton: this.renderCopyButton });
18+
}
19+
20+
copy = () => {
21+
const content =
22+
typeof this.props.data === 'string'
23+
? this.props.data
24+
: JSON.stringify(this.props.data, null, 2);
25+
ClipboardService.copyCustom(content);
26+
};
27+
28+
renderCopyButton = () => {
29+
return (
30+
<>
31+
<span
32+
onClick={this.copy}
33+
data-tip={true}
34+
data-for="copy_tooltip"
35+
data-event="click"
36+
data-event-off="mouseleave"
37+
>
38+
Copy
39+
</span>
40+
<ReactTooltip
41+
isCapture={true}
42+
id="copy_tooltip"
43+
place="top"
44+
getContent={this.getTooltipContent}
45+
type="light"
46+
effect="solid"
47+
/>
48+
</>
49+
);
50+
};
51+
52+
getTooltipContent() {
53+
return ClipboardService.isSupported() ? 'Copied' : 'Not supported in your browser';
54+
}
55+
}

src/common-elements/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './schema';
77
export * from './dropdown';
88
export * from './mixins';
99
export * from './tabs';
10+
export * from './samples';

src/common-elements/samples.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import styled from '../styled-components';
2+
3+
export const SampleControls = styled.div`
4+
opacity: 0.4;
5+
transition: opacity 0.3s ease;
6+
text-align: right;
7+
8+
> span {
9+
display: inline-block;
10+
padding: 2px 10px;
11+
cursor: pointer;
12+
13+
:hover {
14+
background: rgba(255, 255, 255, 0.1);
15+
}
16+
}
17+
`;
18+
19+
export const SampleControlsWrap = styled.div`
20+
&:hover ${SampleControls} {
21+
opacity: 1;
22+
}
23+
`;

src/components/JsonViewer/JsonViewer.tsx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import styled from '../../styled-components';
33

4+
import { SampleControls } from '../../common-elements';
5+
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
46
import { jsonToHTML } from '../../utils/jsonToHtml';
57
import { jsonStyles } from './style';
68

@@ -9,18 +11,51 @@ interface JsonProps {
911
className?: string;
1012
}
1113

14+
const JsonViewerWrap = styled.div`
15+
&:hover > ${SampleControls} {
16+
opacity: 1;
17+
}
18+
`;
19+
1220
class Json extends React.PureComponent<JsonProps> {
13-
node: HTMLElement | null;
21+
node: HTMLDivElement;
1422

1523
render() {
16-
return (
24+
return <CopyButtonWrapper data={this.props.data}>{this.renderInner}</CopyButtonWrapper>;
25+
}
26+
27+
renderInner = ({ renderCopyButton }) => (
28+
<JsonViewerWrap>
29+
<SampleControls>
30+
{renderCopyButton()}
31+
<span onClick={this.expandAll}> Expand all </span>
32+
<span onClick={this.collapseAll}> Collapse all </span>
33+
</SampleControls>
1734
<div
1835
className={this.props.className}
19-
ref={node => (this.node = node)}
36+
ref={node => (this.node = node!)}
2037
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
2138
/>
22-
);
23-
}
39+
</JsonViewerWrap>
40+
);
41+
42+
expandAll = () => {
43+
const elements = this.node.getElementsByClassName('collapsible');
44+
for (const collapsed of Array.prototype.slice.call(elements)) {
45+
(collapsed.parentNode as Element)!.classList.remove('collapsed');
46+
}
47+
};
48+
49+
collapseAll = () => {
50+
const elements = this.node.getElementsByClassName('collapsible');
51+
for (const expanded of Array.prototype.slice.call(elements)) {
52+
// const collapsed = elements[i];
53+
if ((expanded.parentNode as Element)!.classList.contains('redoc-json')) {
54+
continue;
55+
}
56+
(expanded.parentNode as Element)!.classList.add('collapsed');
57+
}
58+
};
2459

2560
clickListener = (event: MouseEvent) => {
2661
let collapsed;
@@ -44,6 +79,6 @@ class Json extends React.PureComponent<JsonProps> {
4479
}
4580
}
4681

47-
export const StyledJson = styled(Json)`
82+
export const JsonViewer = styled(Json)`
4883
${jsonStyles};
4984
`;

src/components/PayloadSamples/MediaTypeSamples.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import * as React from 'react';
22

33
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
44
import { MediaTypeModel } from '../../services/models';
5-
import { StyledJson } from '../JsonViewer/JsonViewer';
6-
import { SourceCode } from '../SourceCode/SourceCode';
5+
import { JsonViewer } from '../JsonViewer/JsonViewer';
6+
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
77
import { NoSampleLabel } from './styled.elements';
88

99
import { isJsonLike, langFromMime } from '../../utils';
@@ -19,9 +19,11 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
1919

2020
const noSample = <NoSampleLabel>No sample</NoSampleLabel>;
2121
const sampleView = isJsonLike(mimeType)
22-
? sample => <StyledJson data={sample} />
22+
? sample => <JsonViewer data={sample} />
2323
: sample =>
24-
(sample && <SourceCode lang={langFromMime(mimeType)} source={sample} />) || { noSample };
24+
(sample && <SourceCodeWithCopy lang={langFromMime(mimeType)} source={sample} />) || {
25+
noSample,
26+
};
2527

2628
const examplesNames = Object.keys(examples);
2729
if (examplesNames.length === 0) {

src/components/RequestSamples/RequestSamples.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react';
22
import * as React from 'react';
33
import { OperationModel } from '../../services/models';
44
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
5-
import { SourceCode } from '../SourceCode/SourceCode';
5+
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
66

77
import { Tab, TabList, TabPanel, Tabs } from '../../common-elements';
88

@@ -40,7 +40,7 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
4040
)}
4141
{samples.map(sample => (
4242
<TabPanel key={sample.lang}>
43-
<SourceCode lang={sample.lang} source={sample.source} />
43+
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
4444
</TabPanel>
4545
))}
4646
</Tabs>

src/components/SourceCode/SourceCode.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import * as React from 'react';
22
import styled from '../../styled-components';
33
import { highlight } from '../../utils';
44

5+
import { SampleControls, SampleControlsWrap } from '../../common-elements';
6+
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
7+
58
const StyledPre = styled.pre`
69
font-family: ${props => props.theme.code.fontFamily};
710
font-size: ${props => props.theme.code.fontSize};
811
overflow-x: auto;
912
font-size: 0.9em;
13+
margin: 0;
1014
`;
1115

1216
export interface SourceCodeProps {
@@ -20,3 +24,18 @@ export class SourceCode extends React.PureComponent<SourceCodeProps> {
2024
return <StyledPre dangerouslySetInnerHTML={{ __html: highlight(source, lang) }} />;
2125
}
2226
}
27+
28+
export class SourceCodeWithCopy extends React.PureComponent<SourceCodeProps> {
29+
render() {
30+
return (
31+
<CopyButtonWrapper data={this.props.source}>
32+
{({ renderCopyButton }) => (
33+
<SampleControlsWrap>
34+
<SampleControls>{renderCopyButton()}</SampleControls>
35+
<SourceCode lang={this.props.lang} source={this.props.source} />
36+
</SampleControlsWrap>
37+
)}
38+
</CopyButtonWrapper>
39+
);
40+
}
41+
}

yarn.lock

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@
9494
dependencies:
9595
"@types/react" "*"
9696

97+
"@types/react-tooltip@^3.3.3":
98+
version "3.3.3"
99+
resolved "https://registry.yarnpkg.com/@types/react-tooltip/-/react-tooltip-3.3.3.tgz#3b6dbb278fc8317ad04f0ce1972a0972f5450aa7"
100+
dependencies:
101+
"@types/react" "*"
102+
97103
"@types/react@*", "@types/react@^16.0.30":
98104
version "16.0.34"
99105
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e"
@@ -1091,7 +1097,7 @@ class-utils@^0.3.5:
10911097
isobject "^3.0.0"
10921098
static-extend "^0.1.1"
10931099

1094-
classnames@^2.2.0, classnames@^2.2.3:
1100+
classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.5:
10951101
version "2.2.5"
10961102
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
10971103

@@ -5848,6 +5854,13 @@ react-test-renderer@^16.0.0-0:
58485854
object-assign "^4.1.1"
58495855
prop-types "^15.6.0"
58505856

5857+
react-tooltip@^3.4.0:
5858+
version "3.4.0"
5859+
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-3.4.0.tgz#037f38f797c3e6b1b58d2534ccc8c2c76af4f52d"
5860+
dependencies:
5861+
classnames "^2.2.5"
5862+
prop-types "^15.6.0"
5863+
58515864
react@^16.2.0:
58525865
version "16.2.0"
58535866
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"

0 commit comments

Comments
 (0)