Skip to content

Commit 7f3bf8b

Browse files
committed
feat: Add copy to clipboard Heading
1 parent 7045d96 commit 7f3bf8b

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useThemeConfig } from '@docusaurus/theme-common';
2+
import { translate } from '@docusaurus/Translate';
3+
import useBrokenLinks from '@docusaurus/useBrokenLinks';
4+
import clsx from 'clsx';
5+
import React from 'react';
6+
7+
import { LinkIcon } from '@apify/ui-icons';
8+
9+
import styles from './styles.module.css';
10+
import { useCopyToClipboard } from './useCopyToClipboard';
11+
12+
export default function Heading({ as: As, id, ...props }) {
13+
const brokenLinks = useBrokenLinks();
14+
const {
15+
navbar: { hideOnScroll },
16+
} = useThemeConfig();
17+
18+
const [isCopied, handleClick] = useCopyToClipboard({
19+
text: id ?? '',
20+
transform: (text) => {
21+
const url = new URL(window.location.href);
22+
url.hash = `#${text}`;
23+
return url.toString();
24+
},
25+
});
26+
27+
// H1 headings shouldn't have the copy to clipboard button
28+
if (As === 'h1') {
29+
return <As {...props} />;
30+
}
31+
32+
// Register the anchor ID so Docusaurus can scroll to it
33+
if (id) {
34+
brokenLinks.collectAnchor(id);
35+
}
36+
37+
const anchorTitle = translate(
38+
{
39+
id: 'theme.common.headingLinkTitle',
40+
message: 'Direct link to {heading}',
41+
description: 'Title for link to heading',
42+
},
43+
{
44+
heading: typeof props.children === 'string' ? props.children : id,
45+
},
46+
);
47+
48+
return (
49+
<As
50+
{...props}
51+
className={clsx(
52+
'anchor',
53+
hideOnScroll
54+
? styles.anchorWithHideOnScrollNavbar
55+
: styles.anchorWithStickyNavbar,
56+
props.className,
57+
)}
58+
id={id}>
59+
{props.children}
60+
<a
61+
onClick={handleClick}
62+
href={`#${id}`}
63+
className={clsx(styles.headingCopyIcon, isCopied && styles.copied)}
64+
aria-label={anchorTitle}
65+
>
66+
<LinkIcon size="16" />
67+
</a>
68+
</As>
69+
);
70+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.anchorWithStickyNavbar {
2+
scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem);
3+
}
4+
5+
.anchorWithHideOnScrollNavbar {
6+
scroll-margin-top: 0.5rem;
7+
}
8+
9+
.headingCopyIcon {
10+
display: none;
11+
transform: translateX(0.25rem);
12+
color: var(--ifm-color-emphasis-700);
13+
text-decoration: none;
14+
}
15+
16+
.headingCopyIcon svg {
17+
stroke: var(--ifm-color-primary);
18+
max-height: 1em !important;
19+
}
20+
21+
h2:hover .headingCopyIcon,
22+
h3:hover .headingCopyIcon,
23+
h4:hover .headingCopyIcon,
24+
h5:hover .headingCopyIcon,
25+
h6:hover .headingCopyIcon {
26+
display: inline-block;
27+
}
28+
29+
.headingCopyIcon.copied {
30+
display: inline-block !important;
31+
}
32+
33+
.headingCopyIcon.copied svg {
34+
display: none;
35+
}
36+
37+
.headingCopyIcon.copied::after {
38+
content: 'Copied!';
39+
font-size: 12px;
40+
color: var(--ifm-font-color-secondary);
41+
font-weight: 600;
42+
margin-left: 8px;
43+
vertical-align: middle;
44+
line-height: 1;
45+
}
46+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useState } from 'react';
2+
3+
const HIDE_COPIED_AFTER = 2000;
4+
5+
export const useCopyToClipboard = ({ text, transform }) => {
6+
const [isCopied, setIsCopied] = useState(false);
7+
8+
const handleClick = async () => {
9+
try {
10+
const textToCopy = transform ? transform(text) : text;
11+
await navigator.clipboard.writeText(textToCopy);
12+
setIsCopied(true);
13+
setTimeout(() => setIsCopied(false), HIDE_COPIED_AFTER);
14+
} catch (err) {
15+
console.error('Failed to copy link:', err);
16+
}
17+
};
18+
19+
return [isCopied, handleClick];
20+
};

0 commit comments

Comments
 (0)