| 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 | } |