1import { Controller } from "@hotwired/stimulus" 2 3// Copy-to-clipboard for a code block or a draft, the way every assistant does 4// it: a button on the block, and the label confirms rather than a toast. 5// 6// Two sources, because the two blocks want different things copied: 7// 8// * a draft carries the RAW markdown in `textValue`. Copying the rendering 9// would drop the list markers, and the bullets are the part someone 10// pasting into an email wants kept. 11// * a code block has no value set, so it copies the <pre> exactly as it 12// stands â byte-for-byte what's on screen, which for code is the only 13// acceptable answer. 14export default class extends Controller { 15 static targets = ["button"] 16 static values = { text: String } 17 18 async copy() { 19 const text = this.hasTextValue ? this.textValue : this.element.querySelector("pre")?.innerText 20 if (!text) return 21 22 try { 23 await navigator.clipboard.writeText(text) 24 this.confirm("Copied") 25 } catch { 26 // navigator.clipboard needs a secure context and permission, and neither 27 // is guaranteed. The old selection-based route works where it doesn't, 28 // and failing silently on a button someone just pressed is worse than 29 // either. 30 this.confirm(this.fallback(text) ? "Copied" : "Press âC") 31 } 32 } 33 34 fallback(text) { 35 const scratch = document.createElement("textarea") 36 scratch.value = text 37 scratch.setAttribute("readonly", "") 38 // Off-screen rather than hidden: a display:none textarea can't be selected. 39 scratch.style.cssText = "position:fixed;top:-9999px;opacity:0" 40 document.body.append(scratch) 41 scratch.select() 42 43 let copied = false 44 try { 45 copied = document.execCommand("copy") 46 } catch { 47 copied = false 48 } 49 scratch.remove() 50 return copied 51 } 52 53 confirm(label) { 54 if (!this.hasButtonTarget) return 55 56 const button = this.buttonTarget 57 button.textContent = label 58 button.dataset.copied = "true" 59 60 clearTimeout(this.resetTimer) 61 this.resetTimer = setTimeout(() => { 62 button.textContent = "Copy" 63 delete button.dataset.copied 64 }, 1600) 65 } 66 67 disconnect() { 68 clearTimeout(this.resetTimer) 69 } 70}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.