copy.ts (1842B)
1 // Copy-to-clipboard for every [data-copy] button on the site — the per-tool 2 // pages and the catalog island share this one path. Delegated on `document` 3 // so rows rendered later (the catalog fetch, "Show more") need no re-wiring. 4 // The button copies the text of the nearest `[data-cmdbar] code`. 5 6 export function copyText(text: string, btn: HTMLElement): void { 7 const restore = btn.textContent; 8 const flash = (label: string) => { 9 btn.textContent = label; 10 btn.classList.toggle('is-copied', label === 'copied'); 11 setTimeout(() => { btn.textContent = restore; btn.classList.remove('is-copied'); }, 1400); 12 }; 13 const done = () => flash('copied'); 14 const failed = () => flash('failed'); 15 if (navigator.clipboard?.writeText) { 16 navigator.clipboard.writeText(text).then(done).catch(() => fallback(text, done, failed)); 17 } else { 18 fallback(text, done, failed); 19 } 20 } 21 22 function fallback(text: string, done: () => void, failed: () => void): void { 23 const ta = document.createElement('textarea'); 24 ta.value = text; 25 ta.setAttribute('readonly', ''); 26 ta.style.position = 'fixed'; 27 ta.style.opacity = '0'; 28 document.body.appendChild(ta); 29 ta.select(); 30 let ok = false; 31 try { ok = document.execCommand('copy'); } catch { ok = false; } 32 document.body.removeChild(ta); 33 if (ok) done(); else failed(); 34 } 35 36 function onClick(e: Event): void { 37 const btn = (e.target as HTMLElement | null)?.closest<HTMLElement>('[data-copy]'); 38 if (!btn) return; 39 const bar = btn.closest('[data-cmdbar]'); 40 const code = bar?.querySelector('code'); 41 copyText(code?.textContent || '', btn); 42 } 43 44 // Modules run once per JS context (the ClientRouter keeps it across 45 // navigations), so a module-level flag is the right double-bind guard. 46 let bound = false; 47 if (!bound) { 48 bound = true; 49 document.addEventListener('click', onClick); 50 }