1import { Controller } from "@hotwired/stimulus" 2import ClipboardJS from "clipboard" 3import { autoPlacement, computePosition, flip, offset, arrow } from "@floating-ui/dom" 4 5export default class extends Controller { 6 static values = { 7 hideTooltipAfter: 1500, 8 tooltipPlacement: "top", 9 tooltipOffset: 6, 10 errorMessage: { type: String, default: "Failed!" }, 11 successMessage: { type: String, default: "Copied!" } 12 } 13 14 connect() { 15 this.clipboard = new ClipboardJS(this.element) 16 this.clipboard.on("success", (e) => this.handleEvent(this.successMessageValue)) 17 this.clipboard.on("error", (e) => this.handleEvent(this.errorMessageValue)) 18 } 19 20 disconnect() { 21 this.clipboard?.destroy() 22 this.clearTooltip() 23 } 24 25 async copy(event) { 26 event.preventDefault() 27 28 const text = this.element.dataset.clipboardText 29 if (!text) { 30 this.handleEvent(this.errorMessageValue) 31 return 32 } 33 34 try { 35 await navigator.clipboard.writeText(text) 36 this.handleEvent(this.successMessageValue) 37 } catch (_error) { 38 try { 39 const input = document.createElement("textarea") 40 input.value = text 41 input.setAttribute("readonly", "") 42 input.style.position = "absolute" 43 input.style.left = "-9999px" 44 document.body.appendChild(input) 45 input.select() 46 document.execCommand("copy") 47 input.remove() 48 this.handleEvent(this.successMessageValue) 49 } catch (_fallbackError) { 50 this.handleEvent(this.errorMessageValue) 51 } 52 } 53 } 54 55 handleEvent(message) { 56 this.clearTooltip() 57 this.createTooltipElements(message) 58 this.updatePosition() 59 60 this.timeout = setTimeout(() => { 61 this.clearTooltip() 62 }, this.hideTooltipAfterValue) 63 } 64 65 clearTooltip() { 66 this.tooltip?.remove() 67 clearTimeout(this.timeout) 68 } 69 70 createTooltipElements(message) { 71 this.tooltip = document.createElement("div") 72 this.tooltip.dataset.tooltipTarget = "content" 73 this.tooltip.classList.add("tooltip", "open") 74 this.tooltip.style.position = "fixed" 75 this.tooltip.textContent = message 76 77 this.arrow = document.createElement("div") 78 this.arrow.classList.add("arrow") 79 80 this.tooltip.appendChild(this.arrow) 81 this.tooltipContainer().appendChild(this.tooltip) 82 } 83 84 updatePosition() { 85 computePosition(this.element, this.tooltip, { 86 placement: this.tooltipPlacementValue, 87 middleware: [ 88 flip(), 89 offset(this.tooltipOffsetValue), 90 arrow({element: this.arrow}), 91 ] 92 }).then(({x, y, placement, middlewareData}) => { 93 Object.assign(this.tooltip.style, { 94 left: `${x}px`, 95 top: `${y}px` 96 }); 97 98 const side = placement.split("-")[0]; 99 100 const staticSide = { 101 top: "bottom", 102 right: "left", 103 bottom: "top", 104 left: "right" 105 }[side]; 106 107 if (middlewareData.arrow) { 108 const { x, y } = middlewareData.arrow; 109 Object.assign(this.arrow.style, { 110 left: x != null ? `${x}px` : "", 111 top: y != null ? `${y}px` : "", 112 // Ensure the static side gets unset when 113 // flipping to other placements' axes. 114 right: "", 115 bottom: "", 116 [staticSide]: `${-8 / 2}px`, 117 }); 118 } 119 }) 120 } 121 122 tooltipContainer() { 123 return this.element.closest("dialog[open]") || document.body 124 } 125}
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.