mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-10 06:42:31 +02:00
feat(v2): CodeBlock copy button (#1643)
* feat(v2): CodeBlock copy button * fix: live theme editor breaking bug
This commit is contained in:
parent
4faa608edd
commit
7b7d1e6161
7 changed files with 164 additions and 21 deletions
|
@ -11,6 +11,7 @@
|
||||||
"@mdx-js/mdx": "^1.0.20",
|
"@mdx-js/mdx": "^1.0.20",
|
||||||
"@mdx-js/react": "^1.0.20",
|
"@mdx-js/react": "^1.0.20",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"clipboard": "^2.0.4",
|
||||||
"infima": "0.2.0-alpha.2",
|
"infima": "0.2.0-alpha.2",
|
||||||
"prism-react-renderer": "^0.1.6",
|
"prism-react-renderer": "^0.1.6",
|
||||||
"react-toggle": "^4.0.2"
|
"react-toggle": "^4.0.2"
|
||||||
|
|
|
@ -5,15 +5,44 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useEffect, useState, useRef} from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Highlight, {defaultProps} from 'prism-react-renderer';
|
import Highlight, {defaultProps} from 'prism-react-renderer';
|
||||||
import nightOwlTheme from 'prism-react-renderer/themes/nightOwl';
|
import nightOwlTheme from 'prism-react-renderer/themes/nightOwl';
|
||||||
|
import Clipboard from 'clipboard';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
export default ({children, className: languageClassName}) => {
|
export default ({children, className: languageClassName}) => {
|
||||||
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
const target = useRef(null);
|
||||||
|
const button = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let clipboard;
|
||||||
|
|
||||||
|
if (button.current) {
|
||||||
|
clipboard = new Clipboard(button.current, {
|
||||||
|
target: () => target.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (clipboard) {
|
||||||
|
clipboard.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [button.current, target.current]);
|
||||||
|
|
||||||
const language =
|
const language =
|
||||||
languageClassName && languageClassName.replace(/language-/, '');
|
languageClassName && languageClassName.replace(/language-/, '');
|
||||||
|
|
||||||
|
const handleCopyCode = () => {
|
||||||
|
window.getSelection().empty();
|
||||||
|
setShowCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => setShowCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Highlight
|
<Highlight
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
|
@ -21,7 +50,11 @@ export default ({children, className: languageClassName}) => {
|
||||||
code={children.trim()}
|
code={children.trim()}
|
||||||
language={language}>
|
language={language}>
|
||||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
||||||
<pre className={classnames(className, styles.codeBlock)} style={style}>
|
<div className={styles.codeBlockWrapper}>
|
||||||
|
<pre
|
||||||
|
ref={target}
|
||||||
|
className={classnames(className, styles.codeBlock)}
|
||||||
|
style={style}>
|
||||||
{tokens.map((line, i) => (
|
{tokens.map((line, i) => (
|
||||||
<div key={i} {...getLineProps({line, key: i})}>
|
<div key={i} {...getLineProps({line, key: i})}>
|
||||||
{line.map((token, key) => (
|
{line.map((token, key) => (
|
||||||
|
@ -30,6 +63,15 @@ export default ({children, className: languageClassName}) => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</pre>
|
</pre>
|
||||||
|
<button
|
||||||
|
ref={button}
|
||||||
|
type="button"
|
||||||
|
aria-label="Copy code to clipboard"
|
||||||
|
className={styles.copyButton}
|
||||||
|
onClick={handleCopyCode}>
|
||||||
|
{showCopied ? 'Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,3 +6,31 @@
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeBlockWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlockWrapper:hover > .copyButton {
|
||||||
|
bottom: calc(var(--ifm-pre-padding) - 2px);
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--ifm-pre-padding);
|
||||||
|
bottom: calc(var(--ifm-pre-padding) - 4px);
|
||||||
|
padding: 4px 8px;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in-out, visibility 200ms ease-in-out,
|
||||||
|
bottom 200ms ease-in-out;
|
||||||
|
border: 1px solid rgb(214, 222, 235);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 12px;
|
||||||
|
background: rgb(1, 22, 39);
|
||||||
|
color: rgb(214, 222, 235);
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"clipboard": "^2.0.4",
|
||||||
"prism-react-renderer": "^0.1.6",
|
"prism-react-renderer": "^0.1.6",
|
||||||
"react-live": "^2.1.2",
|
"react-live": "^2.1.2",
|
||||||
"react-loadable": "^5.5.0",
|
"react-loadable": "^5.5.0",
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useEffect, useState, useRef} from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import LoadableVisibility from 'react-loadable-visibility/react-loadable';
|
import LoadableVisibility from 'react-loadable-visibility/react-loadable';
|
||||||
import Highlight, {defaultProps} from 'prism-react-renderer';
|
import Highlight, {defaultProps} from 'prism-react-renderer';
|
||||||
import nightOwlTheme from 'prism-react-renderer/themes/nightOwl';
|
import nightOwlTheme from 'prism-react-renderer/themes/nightOwl';
|
||||||
|
import Clipboard from 'clipboard';
|
||||||
import Loading from '@theme/Loading';
|
import Loading from '@theme/Loading';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
@ -20,6 +21,26 @@ const Playground = LoadableVisibility({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ({children, className: languageClassName, live, ...props}) => {
|
export default ({children, className: languageClassName, live, ...props}) => {
|
||||||
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
const target = useRef(null);
|
||||||
|
const button = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let clipboard;
|
||||||
|
|
||||||
|
if (button.current) {
|
||||||
|
clipboard = new Clipboard(button.current, {
|
||||||
|
target: () => target.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (clipboard) {
|
||||||
|
clipboard.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [button.current, target.current]);
|
||||||
|
|
||||||
if (live) {
|
if (live) {
|
||||||
return (
|
return (
|
||||||
<Playground
|
<Playground
|
||||||
|
@ -30,8 +51,17 @@ export default ({children, className: languageClassName, live, ...props}) => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const language =
|
const language =
|
||||||
languageClassName && languageClassName.replace(/language-/, '');
|
languageClassName && languageClassName.replace(/language-/, '');
|
||||||
|
|
||||||
|
const handleCopyCode = () => {
|
||||||
|
window.getSelection().empty();
|
||||||
|
setShowCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => setShowCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Highlight
|
<Highlight
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
|
@ -39,7 +69,11 @@ export default ({children, className: languageClassName, live, ...props}) => {
|
||||||
code={children.trim()}
|
code={children.trim()}
|
||||||
language={language}>
|
language={language}>
|
||||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
||||||
<pre className={classnames(className, styles.codeBlock)} style={style}>
|
<div className={styles.codeBlockWrapper}>
|
||||||
|
<pre
|
||||||
|
ref={target}
|
||||||
|
className={classnames(className, styles.codeBlock)}
|
||||||
|
style={style}>
|
||||||
{tokens.map((line, i) => (
|
{tokens.map((line, i) => (
|
||||||
<div key={i} {...getLineProps({line, key: i})}>
|
<div key={i} {...getLineProps({line, key: i})}>
|
||||||
{line.map((token, key) => (
|
{line.map((token, key) => (
|
||||||
|
@ -48,6 +82,15 @@ export default ({children, className: languageClassName, live, ...props}) => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</pre>
|
</pre>
|
||||||
|
<button
|
||||||
|
ref={button}
|
||||||
|
type="button"
|
||||||
|
aria-label="Copy code to clipboard"
|
||||||
|
className={styles.copyButton}
|
||||||
|
onClick={handleCopyCode}>
|
||||||
|
{showCopied ? 'Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,3 +6,31 @@
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeBlockWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlockWrapper:hover > .copyButton {
|
||||||
|
bottom: calc(var(--ifm-pre-padding) - 2px);
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--ifm-pre-padding);
|
||||||
|
bottom: calc(var(--ifm-pre-padding) - 4px);
|
||||||
|
padding: 4px 8px;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in-out, visibility 200ms ease-in-out,
|
||||||
|
bottom 200ms ease-in-out;
|
||||||
|
border: 1px solid rgb(214, 222, 235);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 12px;
|
||||||
|
background: rgb(1, 22, 39);
|
||||||
|
color: rgb(214, 222, 235);
|
||||||
|
}
|
||||||
|
|
|
@ -3799,7 +3799,7 @@ cli-width@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||||
|
|
||||||
clipboard@^2.0.0:
|
clipboard@^2.0.0, clipboard@^2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d"
|
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d"
|
||||||
integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==
|
integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue