Skip to content

Commit 8e9e187

Browse files
committed
feat: add optional copy button to the code block
1 parent 7e30fd2 commit 8e9e187

File tree

5 files changed

+84
-8
lines changed

5 files changed

+84
-8
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ import "./style.css";
7676
/>;
7777
```
7878

79+
## API
80+
| Property | Description | Type | Default |
81+
|-----------------|----------------------------------------------------------|-----------|---------------------------------------------|
82+
| block | Receives render code content from `NotionRenderer` | CodeBlock | - |
83+
| className | Additional class for Code | string | - |
84+
| defaultLanguage | Default programming language if not specified in `block` | string | typescript |
85+
| themes | Themes for rendering code | object | {light: "catppuccin-latte", dark: "dracula"} |
86+
| showCopy | Whether to show the copy button | boolean | true |
87+
7988
## Run the Example
8089

8190
1. Install dependencies `pnpm i`

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-notion-x-code-block",
3-
"version": "0.0.2",
3+
"version": "0.1.0",
44
"description": "",
55
"type": "module",
66
"module": "dist/index.js",
@@ -45,6 +45,7 @@
4545
"notion-utils": "^6.16.0",
4646
"prettier": "^3.2.5",
4747
"react": "^18.2.0",
48+
"react-icons": "^5.0.1",
4849
"react-notion-x": "^6.16.0",
4950
"rollup": "^4.14.0",
5051
"rollup-plugin-delete": "^2.0.0",

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/code.module.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
width: 100%;
1010
font-size: 85%;
1111
margin: 2px;
12+
position: relative;
1213
}
1314

1415
/*
@@ -23,3 +24,29 @@
2324
line-height: 1.4;
2425
color: var(--fg-color-3);
2526
}
27+
28+
.codeCopyButton {
29+
position: absolute;
30+
display: flex;
31+
right: 10px;
32+
top: 15px;
33+
align-items: center;
34+
justify-content: center;
35+
gap: 2px;
36+
padding: 5px;
37+
border: none;
38+
cursor: pointer;
39+
background-color: var(--bg-color);
40+
color: var(--fg-color);
41+
border-radius: 4px;
42+
opacity: 0;
43+
transition: opacity 0.1s linear;
44+
}
45+
46+
@media (prefers-reduced-motion) {
47+
transition: none;
48+
}
49+
50+
.codeBlock:hover .codeCopyButton {
51+
opacity: 1;
52+
}

src/code.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
11
import { getBlockTitle } from "notion-utils";
2-
import React, { useEffect, useState } from "react";
2+
import { IoMdCopy } from "react-icons/io";
3+
import React, { useCallback, useEffect, useRef, useState } from "react";
34
import { cs, useNotionContext, Text } from "react-notion-x";
45
import { codeToHtml } from "shiki";
5-
import { BundledTheme } from "shiki/themes";
6-
76
import styles from "./code.module.css";
87

98
import type { CodeBlock } from "notion-types";
9+
import type { BundledTheme } from "shiki/themes";
1010

1111
export const Code: React.FC<{
1212
block: CodeBlock;
1313
defaultLanguage?: string;
1414
className?: string;
15-
captionClassName?: string;
15+
showCopy?: boolean;
1616
themes?: {
1717
light: BundledTheme;
1818
dark: BundledTheme;
1919
};
2020
}> = ({
2121
block,
22+
className,
2223
defaultLanguage = "typescript",
2324
themes = {
2425
light: "catppuccin-latte",
2526
dark: "dracula"
2627
},
27-
className,
28-
captionClassName
28+
showCopy = true
2929
}) => {
3030
const { recordMap } = useNotionContext();
31+
const [isCopied, setIsCopied] = useState(false);
3132
const content = getBlockTitle(block, recordMap);
3233
const [code, setCode] = useState<string | undefined>(undefined);
34+
const timer = useRef<null | number>(null);
3335

3436
const language = (
3537
block.properties?.language?.[0]?.[0] || defaultLanguage
@@ -47,15 +49,38 @@ export const Code: React.FC<{
4749
content && renderCodeToHtml();
4850
}, [content, language, themes]);
4951

52+
const clickCopy = useCallback(() => {
53+
navigator.clipboard.writeText(content).then(() => {
54+
setIsCopied(true);
55+
if (timer.current) {
56+
clearTimeout(timer.current);
57+
timer.current = null;
58+
}
59+
timer.current = setTimeout(() => {
60+
setIsCopied(false);
61+
timer.current = null;
62+
}, 1000);
63+
});
64+
}, [content]);
65+
5066
return (
5167
<figure className={cs(styles.codeBlock, className)}>
68+
{showCopy ? (
69+
<button
70+
onClick={clickCopy}
71+
className={styles.codeCopyButton}
72+
>
73+
<IoMdCopy />
74+
{isCopied ? "Copied" : "Copy"}
75+
</button>
76+
) : null}
5277
{code == undefined ? (
5378
<pre>{content}</pre>
5479
) : (
5580
<div dangerouslySetInnerHTML={{ __html: code }} />
5681
)}
5782
{caption && (
58-
<figcaption className={cs(styles.codeCaption, captionClassName)}>
83+
<figcaption className={styles.codeCaption}>
5984
<Text value={caption} block={block} />
6085
</figcaption>
6186
)}

0 commit comments

Comments
 (0)