Skip to content

Commit 263cc3f

Browse files
feat: Add copy button to highlighted code lines (#12983)
* feat: Add copy button to highlighted code lines * more subtle copying * fix key error * remove unused variables * [getsentry/action-github-commit] Auto commit * cleanup * remove class name * check icon, remove leftovers * remove global css class * revert accidental color change * [getsentry/action-github-commit] Auto commit --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 78c6161 commit 263cc3f

File tree

4 files changed

+154
-6
lines changed

4 files changed

+154
-6
lines changed

src/components/codeBlock/code-blocks.module.scss

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,6 @@
7474
}
7575
}
7676

77-
:global(.highlight-line) {
78-
background-color: rgba(239, 239, 239, 0.06);
79-
/* Set highlight bg color */
80-
border-left: 4px solid var(--brandPink);
81-
}
8277

8378
/* Diff highlighting, classes provided by rehype-prism-plus */
8479
/* Set inserted line (+) color */

src/components/codeBlock/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Clipboard} from 'react-feather';
55

66
import styles from './code-blocks.module.scss';
77

8+
import {makeHighlightBlocks} from '../codeHighlights';
89
import {makeKeywordsClickable} from '../codeKeywords';
910

1011
export interface CodeBlockProps {
@@ -57,7 +58,9 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) {
5758
<div className={styles.copied} style={{opacity: showCopied ? 1 : 0}}>
5859
Copied
5960
</div>
60-
<div ref={codeRef}>{makeKeywordsClickable(children)}</div>
61+
<div ref={codeRef}>
62+
{makeKeywordsClickable(makeHighlightBlocks(children, language))}
63+
</div>
6164
</div>
6265
);
6366
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use client';
2+
3+
import {Children, cloneElement, ReactElement, useEffect, useRef, useState} from 'react';
4+
import {Check, Clipboard} from 'react-feather';
5+
import styled from '@emotion/styled';
6+
import * as Sentry from '@sentry/nextjs';
7+
8+
import {cleanCodeSnippet, useCleanSnippetInClipboard} from '../codeBlock';
9+
10+
type ChildrenItem = ReturnType<typeof Children.toArray>[number] | React.ReactNode;
11+
12+
export function makeHighlightBlocks(
13+
children: React.ReactNode,
14+
language: string | undefined
15+
) {
16+
const items = Children.toArray(children);
17+
18+
let highlightedLineElements: ReactElement[] = [];
19+
let highlightElementGroupCounter = 0;
20+
21+
return items.reduce((arr: ChildrenItem[], child) => {
22+
if (typeof child !== 'object') {
23+
arr.push(child);
24+
return arr;
25+
}
26+
27+
const element = child as ReactElement;
28+
const classes = element.props.className;
29+
30+
const isCodeLine = classes && classes.includes('code-line');
31+
if (!isCodeLine) {
32+
const updatedChild = cloneElement(
33+
child as ReactElement,
34+
element.props,
35+
makeHighlightBlocks((child as ReactElement).props.children, language)
36+
);
37+
arr.push(updatedChild);
38+
return arr;
39+
}
40+
41+
const isHighlightedLine = isCodeLine && classes.includes('highlight-line');
42+
43+
if (isHighlightedLine) {
44+
highlightedLineElements.push(element);
45+
} else {
46+
if (highlightedLineElements.length > 0) {
47+
arr.push(
48+
<HighlightBlock key={highlightElementGroupCounter} language={language}>
49+
{...highlightedLineElements}
50+
</HighlightBlock>
51+
);
52+
highlightedLineElements = [];
53+
++highlightElementGroupCounter;
54+
}
55+
arr.push(child);
56+
}
57+
58+
return arr;
59+
}, [] as ChildrenItem[]);
60+
}
61+
62+
export function HighlightBlock({
63+
children,
64+
language,
65+
}: {
66+
children: React.ReactNode;
67+
language: string | undefined;
68+
}) {
69+
const codeRef = useRef<HTMLDivElement>(null);
70+
71+
useCleanSnippetInClipboard(codeRef, {language});
72+
73+
// Show the copy button after js has loaded
74+
// otherwise the copy button will not work
75+
const [showCopyButton, setShowCopyButton] = useState(false);
76+
useEffect(() => {
77+
setShowCopyButton(true);
78+
}, []);
79+
80+
const [copied, setCopied] = useState(false);
81+
82+
async function copyCodeOnClick() {
83+
if (codeRef.current === null) {
84+
return;
85+
}
86+
87+
const code = cleanCodeSnippet(codeRef.current.innerText, {language});
88+
89+
try {
90+
setCopied(false);
91+
await navigator.clipboard.writeText(code);
92+
setCopied(true);
93+
setTimeout(() => setCopied(false), 1200);
94+
} catch (error) {
95+
Sentry.captureException(error);
96+
setCopied(false);
97+
}
98+
}
99+
100+
return (
101+
<HighlightBlockContainer>
102+
<CodeLinesContainer ref={codeRef}>{children}</CodeLinesContainer>
103+
<ClipBoardContainer onClick={copyCodeOnClick}>
104+
{showCopyButton && !copied && (
105+
<Clipboard size={16} opacity={0.15} stroke={'white'} />
106+
)}
107+
{showCopyButton && copied && <Check size={16} stroke={'green'} />}
108+
</ClipBoardContainer>
109+
</HighlightBlockContainer>
110+
);
111+
}
112+
113+
const HighlightBlockContainer = styled('div')`
114+
display: flex;
115+
flex-direction: row;
116+
justify-content: space-between;
117+
align-items: stretch;
118+
float: left;
119+
min-width: 100%;
120+
box-sizing: border-box;
121+
background-color: rgba(239, 239, 239, 0.06);
122+
position: relative;
123+
124+
border-left: 4px solid var(--brandPink);
125+
126+
:hover svg {
127+
opacity: 1;
128+
}
129+
svg {
130+
transition: all 150ms linear;
131+
}
132+
`;
133+
134+
const CodeLinesContainer = styled('div')`
135+
padding: 8px 0;
136+
width: calc(100% - 48px);
137+
`;
138+
139+
const ClipBoardContainer = styled('div')`
140+
position: absolute;
141+
right: 0;
142+
top: 0;
143+
bottom: 0;
144+
width: 48px;
145+
display: flex;
146+
justify-content: center;
147+
align-items: center;
148+
cursor: pointer;
149+
`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {makeHighlightBlocks} from './codeHighlights';

0 commit comments

Comments
 (0)