Skip to content
This repository was archived by the owner on Nov 25, 2024. It is now read-only.

Commit 5498d82

Browse files
authored
feat: Copy code button (#175)
* Push * Mobile appropriate, visible at all times * Allow for deprecated case * Minor fix * Fix * FIx positioning
1 parent 1462fbe commit 5498d82

File tree

8 files changed

+328
-141
lines changed

8 files changed

+328
-141
lines changed

.changeset/itchy-bikes-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/site-kit': minor
3+
---
4+
5+
Copy code button

packages/site-kit/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@
2323
"svelte-local-storage-store": "^0.5.0"
2424
},
2525
"devDependencies": {
26-
"@sveltejs/kit": "^1.21.0",
27-
"@sveltejs/package": "^2.1.0",
28-
"@types/marked": "^5.0.0",
29-
"@types/node": "^20.3.3",
26+
"@sveltejs/kit": "^1.22.3",
27+
"@sveltejs/package": "^2.2.0",
28+
"@types/marked": "^5.0.1",
29+
"@types/node": "^20.4.2",
3030
"@types/prettier": "^2.7.3",
3131
"flexsearch": "^0.7.31",
3232
"magic-string": "^0.30.1",
33-
"marked": "^5.1.0",
33+
"marked": "^5.1.1",
3434
"prettier": "^2.8.8",
3535
"shiki-twoslash": "^3.1.2",
36-
"svelte": "^4.0.4",
36+
"svelte": "^4.0.5",
3737
"typescript": "^5.1.6",
38-
"vite": "^4.3.9"
38+
"vite": "^4.4.4"
3939
},
4040
"publishConfig": {
4141
"access": "public"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import DocsCopyCodeButton from '../docs/DocsCopyCodeButton.svelte';
2+
import { page } from '$app/stores';
3+
4+
const map = new WeakMap();
5+
6+
/** @type {import('svelte/action').Action} */
7+
export const copy_code_descendants = (node) => {
8+
/** @type {NodeListOf<Element>} */
9+
let code_blocks;
10+
11+
function update() {
12+
code_blocks = node.querySelectorAll('.shiki');
13+
14+
// Add a button to each code block
15+
for (const block of code_blocks) {
16+
const parent_class = block.parentElement?.classList.toString() ?? '';
17+
18+
// Exclude the ts-block properties and stuff
19+
if (/ts-block/.test(parent_class)) continue;
20+
21+
const code = block.querySelector('code')?.innerText ?? '';
22+
if (!code) continue;
23+
24+
// This is to make sure that snippets with title get the button on their heading area
25+
const target = /code-block/.test(parent_class) ? block.parentElement : block;
26+
if (!target) continue;
27+
28+
map.set(
29+
block,
30+
new DocsCopyCodeButton({
31+
target: target,
32+
props: {
33+
code
34+
}
35+
})
36+
);
37+
}
38+
}
39+
40+
function destroy() {
41+
for (const block of code_blocks) {
42+
map.get(block)?.$destroy();
43+
map.delete(block);
44+
}
45+
}
46+
47+
// Page changed. Update again
48+
const unsubscribe = page.subscribe(update);
49+
50+
return {
51+
update,
52+
destroy() {
53+
destroy();
54+
unsubscribe();
55+
}
56+
};
57+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { click_outside } from './click-outside.js';
2+
export { copy_code_descendants } from './copy-code-descendants.js';
23
export { focus_outside } from './focus-outside.js';
34
export { focusable_children, trap } from './focus.js';
45
export { root_scroll } from './root-scroll.js';

packages/site-kit/src/lib/components/Icons.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,19 @@ Provides a list of svg icons that can be referenced through the `Icon` component
199199
d="M224 44h-64a43.86 43.86 0 0 0-32 13.85A43.86 43.86 0 0 0 96 44H32a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h64a20 20 0 0 1 20 20a12 12 0 0 0 24 0a20 20 0 0 1 20-20h64a20 20 0 0 0 20-20V64a20 20 0 0 0-20-20ZM96 188H36V68h60a20 20 0 0 1 20 20v104.81A43.79 43.79 0 0 0 96 188Zm124 0h-60a43.71 43.71 0 0 0-20 4.83V88a20 20 0 0 1 20-20h60Z"
200200
/>
201201
</symbol>
202+
203+
<symbol viewBox="0 0 24 24" id="copy-to-clipboard-empty">
204+
<path
205+
fill="currentColor"
206+
d="M5 22q-.825 0-1.413-.588T3 20V6h2v14h11v2H5Zm4-4q-.825 0-1.413-.588T7 16V4q0-.825.588-1.413T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.588 1.413T18 18H9Zm0-2h9V4H9v12Zm0 0V4v12Z"
207+
/>
208+
</symbol>
209+
210+
<symbol viewBox="0 0 24 24" id="copy-to-clipboard-filled">
211+
<path
212+
fill="currentColor"
213+
d="M5 22q-.825 0-1.413-.588T3 20V6h2v14h11v2H5Zm4-4q-.825 0-1.413-.588T7 16V4q0-.825.588-1.413T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.588 1.413T18 18H9Z"
214+
/>
215+
</symbol>
202216
</svg>
203217
</div>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script>
2+
import Icon from '$lib/components/Icon.svelte';
3+
import { cubicOut } from 'svelte/easing';
4+
import { fade } from 'svelte/transition';
5+
6+
/** @type {string} */
7+
export let code;
8+
9+
let copying = false;
10+
11+
function copy() {
12+
try {
13+
navigator.clipboard.writeText(code);
14+
} catch {
15+
/**
16+
* This is the fallback deprecated way of copying text to the clipboard. Only runs if it can't find the clipboard API.
17+
* Taken from https://github.com/ghostdevv/svelte-copy/blob/main/src/lib/copy.ts
18+
*/
19+
const element = document.createElement('input');
20+
21+
element.type = 'text';
22+
element.disabled = true;
23+
24+
element.style.cssText = `position: fixed;z-index: -100;pointer-events: none;opacity: 0;`;
25+
26+
element.value = code;
27+
28+
document.body.appendChild(element);
29+
30+
element.click();
31+
element.select();
32+
document.execCommand('copy');
33+
34+
document.body.removeChild(element);
35+
} finally {
36+
copying = true;
37+
38+
setTimeout(() => {
39+
copying = false;
40+
}, 1000);
41+
}
42+
}
43+
</script>
44+
45+
<button id="copy-to-clipboard-button" on:click={copy}>
46+
{#if copying}
47+
<span transition:fade={{ easing: cubicOut, duration: 400 }}>
48+
<Icon name="copy-to-clipboard-filled" />
49+
</span>
50+
{:else}
51+
<span transition:fade={{ easing: cubicOut, duration: 400 }}>
52+
<Icon name="copy-to-clipboard-empty" />
53+
</span>
54+
{/if}
55+
</button>
56+
57+
<style>
58+
:global(.code-block #copy-to-clipboard-button) {
59+
top: 5px;
60+
}
61+
62+
button {
63+
position: absolute;
64+
top: 1rem;
65+
right: 1rem;
66+
67+
display: grid;
68+
place-items: center;
69+
grid-template-columns: 1fr;
70+
grid-template-rows: 1fr;
71+
72+
opacity: 0.8;
73+
74+
height: 2.4rem;
75+
width: 2.4rem;
76+
77+
overflow: hidden;
78+
79+
transition: opacity 0.1s ease-in-out;
80+
}
81+
82+
button:hover {
83+
opacity: 1;
84+
}
85+
86+
span {
87+
grid-column: 1 / span 1;
88+
grid-row: 1 / span 1;
89+
}
90+
91+
button :global(svg) {
92+
stroke-width: 0 !important;
93+
}
94+
</style>

packages/site-kit/src/lib/styles/text.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
}
3434

3535
.text :where(pre) {
36+
position: relative;
3637
margin: 1em 0;
3738
width: 100%;
3839
padding: 1rem;
@@ -42,14 +43,17 @@
4243
color: var(--sk-code-base);
4344
border-radius: var(--sk-border-radius);
4445
font-size: var(--sk-text-s);
45-
overflow-x: auto;
46+
overflow-x: hidden;
4647
}
4748

4849
.text :where(pre code) {
50+
display: block;
4951
padding: 0;
5052
margin: 0;
5153
top: 0;
54+
width: 100%;
5255
background: transparent;
56+
overflow-x: auto;
5357
}
5458

5559
.text :where(p code) {

0 commit comments

Comments
 (0)