Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/theme/CodeBlock/Buttons/CopyButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Swizzled from @docusaurus/theme-classic to strip leading shell prompts
* (`$ `) from text written to the clipboard. The prompt remains visible
* in the rendered code block, but the copied text is the runnable command.
*/
import React, {useCallback, useState, useRef, useEffect} from 'react';
import clsx from 'clsx';
import {translate} from '@docusaurus/Translate';
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
import Button from '@theme/CodeBlock/Buttons/Button';
import IconCopy from '@theme/Icon/Copy';
import IconSuccess from '@theme/Icon/Success';
import styles from './styles.module.css';

function stripShellPrompts(code) {
if (typeof code !== 'string' || code.length === 0) {
return code;
}
const lines = code.split('\n');
const nonEmpty = lines.filter((line) => line.trim().length > 0);
if (nonEmpty.length === 0) {
return code;
}
const allPrompted = nonEmpty.every((line) => /^\s*\$\s+/.test(line));
if (!allPrompted) {
return code;
}
return lines
.map((line) =>
line.trim().length === 0 ? line : line.replace(/^(\s*)\$\s+/, '$1'),
)
.join('\n');
}

function title() {
return translate({
id: 'theme.CodeBlock.copy',
message: 'Copy',
description: 'The copy button label on code blocks',
});
}

function ariaLabel(isCopied) {
return isCopied
? translate({
id: 'theme.CodeBlock.copied',
message: 'Copied',
description: 'The copied button label on code blocks',
})
: translate({
id: 'theme.CodeBlock.copyButtonAriaLabel',
message: 'Copy code to clipboard',
description: 'The ARIA label for copy code blocks button',
});
}

async function copyToClipboard(text) {
if (navigator.clipboard) {
return navigator.clipboard.writeText(text);
}
const {default: copy} = await import('copy-text-to-clipboard');
return copy(text);
}

function useCopyButton() {
const {
metadata: {code},
} = useCodeBlockContext();
const [isCopied, setIsCopied] = useState(false);
const copyTimeout = useRef(undefined);
const copyCode = useCallback(() => {
copyToClipboard(stripShellPrompts(code)).then(() => {
setIsCopied(true);
copyTimeout.current = window.setTimeout(() => {
setIsCopied(false);
}, 1000);
});
}, [code]);
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
return {copyCode, isCopied};
}

export default function CopyButton({className}) {
const {copyCode, isCopied} = useCopyButton();
return (
<Button
aria-label={ariaLabel(isCopied)}
title={title()}
className={clsx(
className,
styles.copyButton,
isCopied && styles.copyButtonCopied,
)}
onClick={copyCode}>
<span className={styles.copyButtonIcons} aria-hidden="true">
<IconCopy className={styles.copyButtonIcon} />
<IconSuccess className={styles.copyButtonSuccessIcon} />
</span>
</Button>
);
}
47 changes: 47 additions & 0 deletions src/theme/CodeBlock/Buttons/CopyButton/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

:global(.theme-code-block:hover) .copyButtonCopied {
opacity: 1 !important;
}

.copyButtonIcons {
position: relative;
width: 1.125rem;
height: 1.125rem;
}

.copyButtonIcon,
.copyButtonSuccessIcon {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all var(--ifm-transition-fast) ease;
}

.copyButtonSuccessIcon {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}

.copyButtonCopied .copyButtonIcon {
transform: scale(0.33);
opacity: 0;
}

.copyButtonCopied .copyButtonSuccessIcon {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}
Loading