main
js 59 lines 1.9 KB
Raw
1 import { Controller } from "@hotwired/stimulus"
2
3 // Copies text to the clipboard and flashes a "Copied" confirmation.
4 //
5 // Source of the text, in priority order:
6 // 1. data-clipboard-text-value on the controller element
7 // 2. the textContent of the element matched by the "source" target
8 //
9 // <div data-controller="clipboard" data-clipboard-text-value="git clone …">
10 // <button data-action="clipboard#copy">Copy</button>
11 // </div>
12 export default class extends Controller {
13 static targets = ["button", "source"]
14 static values = { text: String, label: { type: String, default: "Copied" } }
15
16 copy(event) {
17 event.preventDefault()
18 let text = ""
19 if (this.hasTextValue && this.textValue) {
20 text = this.textValue
21 } else if (this.sourceTargets.length) {
22 // Multiple sources (e.g. one per code line) join with newlines so a whole
23 // file copies back as the original text.
24 text = this.sourceTargets.map((el) => el.textContent).join("\n")
25 }
26 if (!text) return
27
28 this.#write(text).then(() => this.#flash(event.currentTarget))
29 }
30
31 async #write(text) {
32 try {
33 await navigator.clipboard.writeText(text)
34 } catch {
35 // Fallback for non-secure contexts where the async clipboard API is absent.
36 const ta = document.createElement("textarea")
37 ta.value = text
38 ta.style.position = "fixed"
39 ta.style.opacity = "0"
40 document.body.appendChild(ta)
41 ta.select()
42 try { document.execCommand("copy") } catch { /* no-op */ }
43 ta.remove()
44 }
45 }
46
47 #flash(button) {
48 if (!button) return
49 const original = button.dataset.originalLabel ?? button.textContent
50 button.dataset.originalLabel = original
51 button.textContent = this.labelValue
52 button.classList.add("copied")
53 clearTimeout(this._timer)
54 this._timer = setTimeout(() => {
55 button.textContent = original
56 button.classList.remove("copied")
57 }, 1500)
58 }
59 }