'Copy to Clipboard' functionality (#163)
* Adds 'Copy to Clipboard' functionality to all code blocks in readOnly
* Show on hover, move to SC
* 💚
* PR feedback
This commit is contained in:
parent
ca35cee841
commit
38c8a46d32
|
@ -0,0 +1,36 @@
|
||||||
|
// @flow
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
|
class CopyToClipboard extends PureComponent {
|
||||||
|
props: {
|
||||||
|
text: string,
|
||||||
|
children?: React.Element<any>,
|
||||||
|
onClick?: () => void,
|
||||||
|
onCopy: (string, boolean) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
onClick = (ev: SyntheticEvent) => {
|
||||||
|
const { text, onCopy, children } = this.props;
|
||||||
|
const elem = React.Children.only(children);
|
||||||
|
const result = copy(text, {
|
||||||
|
debug: __DEV__,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onCopy) {
|
||||||
|
onCopy(text, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem && elem.props && typeof elem.props.onClick === 'function') {
|
||||||
|
elem.props.onClick(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { text: _text, onCopy: _onCopy, children, ...rest } = this.props;
|
||||||
|
const elem = React.Children.only(children);
|
||||||
|
return React.cloneElement(elem, { ...rest, onClick: this.onClick });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyToClipboard;
|
|
@ -0,0 +1,3 @@
|
||||||
|
// @flow
|
||||||
|
import CopyToClipboard from './CopyToClipboard';
|
||||||
|
export default CopyToClipboard;
|
|
@ -77,24 +77,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
|
||||||
pre {
|
|
||||||
background: #efefef;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #dedede;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 0 .5em;
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 3px solid #efefef;
|
border-left: 3px solid #efefef;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
|
|
@ -1,13 +1,40 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import CopyButton from './CopyButton';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
import type { Props } from '../types';
|
import type { Props } from '../types';
|
||||||
|
|
||||||
export default function Code({ children, attributes }: Props) {
|
export default function Code({ children, node, readOnly, attributes }: Props) {
|
||||||
return (
|
return (
|
||||||
<pre>
|
<Container>
|
||||||
<code {...attributes}>
|
{readOnly && <CopyButton text={node.text} />}
|
||||||
{children}
|
<Pre>
|
||||||
</code>
|
<code {...attributes}>
|
||||||
</pre>
|
{children}
|
||||||
|
</code>
|
||||||
|
</Pre>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Pre = styled.pre`
|
||||||
|
padding: .5em 1em;
|
||||||
|
background: ${color.smoke};
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid ${color.smokeDark};
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import CopyToClipboard from 'components/CopyToClipboard';
|
||||||
|
|
||||||
|
@observer class CopyButton extends Component {
|
||||||
|
@observable copied: boolean = false;
|
||||||
|
copiedTimeout: ?number;
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearTimeout(this.copiedTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCopy = () => {
|
||||||
|
this.copied = true;
|
||||||
|
this.copiedTimeout = setTimeout(() => (this.copied = false), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<StyledCopyToClipboard onCopy={this.handleCopy} {...this.props}>
|
||||||
|
<span>{this.copied ? 'Copied!' : 'Copy to clipboard'}</span>
|
||||||
|
</StyledCopyToClipboard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledCopyToClipboard = styled(CopyToClipboard)`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 50ms ease-in-out;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
background: ${color.slateLight};
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${color.slate};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default CopyButton;
|
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
|
|
||||||
|
const InlineCode = styled.code`
|
||||||
|
padding: .25em;
|
||||||
|
background: ${color.smoke};
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid ${color.smokeDark};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default InlineCode;
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Code from './components/Code';
|
import Code from './components/Code';
|
||||||
|
import InlineCode from './components/InlineCode';
|
||||||
import Image from './components/Image';
|
import Image from './components/Image';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import ListItem from './components/ListItem';
|
import ListItem from './components/ListItem';
|
||||||
|
@ -13,7 +14,7 @@ const createSchema = () => {
|
||||||
return {
|
return {
|
||||||
marks: {
|
marks: {
|
||||||
bold: (props: Props) => <strong>{props.children}</strong>,
|
bold: (props: Props) => <strong>{props.children}</strong>,
|
||||||
code: (props: Props) => <code>{props.children}</code>,
|
code: (props: Props) => <InlineCode>{props.children}</InlineCode>,
|
||||||
italic: (props: Props) => <em>{props.children}</em>,
|
italic: (props: Props) => <em>{props.children}</em>,
|
||||||
underlined: (props: Props) => <u>{props.children}</u>,
|
underlined: (props: Props) => <u>{props.children}</u>,
|
||||||
deleted: (props: Props) => <del>{props.children}</del>,
|
deleted: (props: Props) => <del>{props.children}</del>,
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
"boundless-popover": "^1.0.4",
|
"boundless-popover": "^1.0.4",
|
||||||
"bugsnag": "^1.7.0",
|
"bugsnag": "^1.7.0",
|
||||||
"classnames": "2.2.3",
|
"classnames": "2.2.3",
|
||||||
|
"copy-to-clipboard": "^3.0.6",
|
||||||
"css-loader": "0.23.1",
|
"css-loader": "0.23.1",
|
||||||
"debug": "2.2.0",
|
"debug": "2.2.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -1874,6 +1874,12 @@ cookies@~0.7.0:
|
||||||
depd "~1.1.0"
|
depd "~1.1.0"
|
||||||
keygrip "~1.0.1"
|
keygrip "~1.0.1"
|
||||||
|
|
||||||
|
copy-to-clipboard@^3.0.6:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.6.tgz#13c09bfea0408a5dc5bb987fee3b3986518c9d69"
|
||||||
|
dependencies:
|
||||||
|
toggle-selection "^1.0.3"
|
||||||
|
|
||||||
copy-to@^2.0.1:
|
copy-to@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
|
resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
|
||||||
|
@ -8569,6 +8575,10 @@ to-space-case@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-no-case "^1.0.0"
|
to-no-case "^1.0.0"
|
||||||
|
|
||||||
|
toggle-selection@^1.0.3:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.5.tgz#726c703de607193a73c32c7df49cd24950fc574f"
|
||||||
|
|
||||||
topo@1.x.x:
|
topo@1.x.x:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5"
|
resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5"
|
||||||
|
|
Reference in New Issue