main
ts 75 lines 2.78 KB
Raw
1 /**
2 * Svelte action that adds a copy button to all <pre> blocks inside an element.
3 * Wraps each <pre> in a container div so the button stays fixed even when
4 * the code scrolls horizontally.
5 * Usage: <article use:copyCode={pathname}>
6 */
7 export function copyCode(node: HTMLElement) {
8 const wrappers: HTMLDivElement[] = [];
9
10 function addButtons() {
11 removeButtons();
12
13 const pres = node.querySelectorAll('pre');
14 for (const pre of pres) {
15 // Skip if already wrapped
16 if (pre.parentElement?.hasAttribute('data-copy-wrapper')) continue;
17
18 // Wrap pre in a relative container
19 const wrapper = document.createElement('div');
20 wrapper.setAttribute('data-copy-wrapper', '');
21 wrapper.style.position = 'relative';
22 pre.parentNode?.insertBefore(wrapper, pre);
23 wrapper.appendChild(pre);
24
25 const btn = document.createElement('button');
26 btn.setAttribute('data-copy-btn', '');
27 btn.setAttribute('aria-label', 'Copy code');
28 btn.setAttribute('title', 'Copy');
29 btn.className =
30 'absolute top-2.5 right-2.5 z-10 inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-400 transition-colors hover:text-gray-200';
31 btn.innerHTML = `<svg class="h-[22px] w-[22px]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>`;
32
33 btn.addEventListener('click', async () => {
34 const code = pre.querySelector('code');
35 const text = code?.textContent ?? pre.textContent ?? '';
36 try {
37 await navigator.clipboard.writeText(text);
38 btn.innerHTML = `<svg class="h-[22px] w-[22px] text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>`;
39 btn.setAttribute('title', 'Copied!');
40 setTimeout(() => {
41 btn.innerHTML = `<svg class="h-[22px] w-[22px]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>`;
42 btn.setAttribute('title', 'Copy');
43 }, 2000);
44 } catch {
45 // Fallback: silent fail
46 }
47 });
48
49 wrapper.appendChild(btn);
50 wrappers.push(wrapper);
51 }
52 }
53
54 function removeButtons() {
55 for (const wrapper of wrappers) {
56 const pre = wrapper.querySelector('pre');
57 if (pre && wrapper.parentNode) {
58 wrapper.parentNode.insertBefore(pre, wrapper);
59 wrapper.remove();
60 }
61 }
62 wrappers.length = 0;
63 }
64
65 requestAnimationFrame(addButtons);
66
67 return {
68 update() {
69 requestAnimationFrame(addButtons);
70 },
71 destroy() {
72 removeButtons();
73 }
74 };
75 }