|
| 1 | +/** |
| 2 | + * A copy button on every code block in a post. |
| 3 | + * |
| 4 | + * 87 of the 104 posts carry code — connection strings, compose fragments, SQL — |
| 5 | + * and the whole point of publishing them is that someone runs them. Selecting a |
| 6 | + * multi-line block by hand is the one part of that the page controlled and was |
| 7 | + * not helping with. |
| 8 | + * |
| 9 | + * The button is injected at runtime rather than rendered into the markup: the |
| 10 | + * alternative is a `data-copy` attribute holding a second copy of every block, |
| 11 | + * which would roughly double the weight of a code-heavy page to save a few |
| 12 | + * lines of script. The text is read from the DOM at click time, so it is by |
| 13 | + * definition the code the reader is looking at. |
| 14 | + * |
| 15 | + * With JavaScript off the blocks render exactly as before — this adds an |
| 16 | + * affordance, it does not carry the content. |
| 17 | + */ |
| 18 | +import { write } from './clipboard'; |
| 19 | + |
| 20 | +const IDLE = 'copy'; |
| 21 | +const RESET_MS = 1600; |
| 22 | + |
| 23 | +for (const pre of document.querySelectorAll<HTMLPreElement>('.post__body pre')) { |
| 24 | + // Shiki nests <code> inside <pre>; either way textContent is the source. |
| 25 | + const source = pre.textContent ?? ''; |
| 26 | + if (!source.trim()) continue; |
| 27 | + |
| 28 | + const btn = document.createElement('button'); |
| 29 | + btn.type = 'button'; |
| 30 | + btn.className = 'codecopy'; |
| 31 | + btn.textContent = IDLE; |
| 32 | + btn.setAttribute('aria-label', 'Copy this code block'); |
| 33 | + |
| 34 | + // The button is positioned against the <pre>; a wrapper would change the |
| 35 | + // prose flow and the margins the stylesheet sets on sibling blocks. |
| 36 | + pre.classList.add('has-codecopy'); |
| 37 | + pre.append(btn); |
| 38 | + |
| 39 | + let timer: number | undefined; |
| 40 | + btn.addEventListener('click', async () => { |
| 41 | + let ok = true; |
| 42 | + try { |
| 43 | + await write(source); |
| 44 | + } catch { |
| 45 | + ok = false; |
| 46 | + } |
| 47 | + btn.textContent = ok ? 'copied' : 'press ⌘C'; |
| 48 | + btn.setAttribute('aria-label', ok ? 'Copied to clipboard' : 'Copy failed — select the code and press Command C'); |
| 49 | + btn.classList.toggle('is-copied', ok); |
| 50 | + btn.classList.toggle('is-copyfailed', !ok); |
| 51 | + |
| 52 | + window.clearTimeout(timer); |
| 53 | + timer = window.setTimeout(() => { |
| 54 | + btn.textContent = IDLE; |
| 55 | + btn.setAttribute('aria-label', 'Copy this code block'); |
| 56 | + btn.classList.remove('is-copied', 'is-copyfailed'); |
| 57 | + }, RESET_MS); |
| 58 | + }); |
| 59 | +} |
0 commit comments