1import { Controller } from "@hotwired/stimulus"; 2 3// Copies the invite link, and always says something back. 4// 5// Copying from a web page is less reliable than it looks. navigator.clipboard 6// can sit unresolved behind a permission prompt, and is absent entirely 7// outside a secure context; execCommand("copy") is deprecated and only works 8// inside a real user gesture. So this tries both, and â whichever way it goes 9// â leaves the link selected and tells the person what happened. A button that 10// silently does nothing is worse than one that says "press âC". 11export default class extends Controller { 12 static targets = ["source", "button"]; 13 14 copy(event) { 15 event.preventDefault(); 16 17 // Selecting is the fallback and the visible sign that the button did 18 // something. The copy has to happen while the field is still writable and 19 // still selected â putting readonly back first is what stopped 20 // execCommand working at all. 21 const copied = this.selectAndCopy(); 22 23 // Answer now, from what is already known. navigator.clipboard's promise 24 // can hang unresolved behind a permission prompt â never resolving, never 25 // rejecting â so nothing that depends on it may gate the feedback. The 26 // text is selected either way, which is what makes "Press âC" honest. 27 this.say(copied ? "Copied" : this.manualHint()); 28 29 // Fire and forget: if it does resolve, upgrade the message. 30 if (!copied && navigator.clipboard) { 31 navigator.clipboard 32 .writeText(this.sourceTarget.value) 33 .then(() => this.say("Copied")) 34 .catch(() => {}); 35 } 36 } 37 38 // Leaves the text selected either way, so âC works if this did not. 39 selectAndCopy() { 40 const input = this.sourceTarget; 41 42 input.removeAttribute("readonly"); 43 input.focus(); 44 input.select(); 45 input.setSelectionRange(0, input.value.length); 46 47 let copied = false; 48 try { 49 copied = document.execCommand("copy"); 50 } catch { 51 copied = false; 52 } 53 54 input.setAttribute("readonly", "readonly"); 55 return copied; 56 } 57 58 manualHint() { 59 const mac = navigator.platform.toUpperCase().includes("MAC"); 60 return mac ? "Press âC" : "Press Ctrl+C"; 61 } 62 63 say(message) { 64 if (!this.hasButtonTarget) return; 65 66 const button = this.buttonTarget; 67 button.dataset.originalLabel ||= button.textContent.trim(); 68 button.textContent = message; 69 70 clearTimeout(this.resetTimer); 71 this.resetTimer = setTimeout(() => { 72 button.textContent = button.dataset.originalLabel; 73 }, 2500); 74 } 75 76 disconnect() { 77 clearTimeout(this.resetTimer); 78 } 79}
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.