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 // ClipboardJS copies through a hidden textarea appended to `container`. 16 // Inside an open modal <dialog> the rest of the document is inert, so the 17 // default body mount can't take the selection and the copy silently fails â 18 // mount the fake element inside the dialog instead. 19 this.clipboard = new ClipboardJS(this.element, { 20 container: this.element.closest("dialog") || document.body 21 }) 22 this.clipboard.on("success", (e) => this.handleEvent(this.successMessageValue)) 23 this.clipboard.on("error", (e) => this.handleEvent(this.errorMessageValue)) 24 } 25 26 disconnect() { 27 this.clearTooltip() 28 } 29 30 handleEvent(message) { 31 this.clearTooltip() 32 this.createTooltipElements(message) 33 this.updatePosition() 34 35 this.timeout = setTimeout(() => { 36 this.clearTooltip() 37 }, this.hideTooltipAfterValue) 38 } 39 40 clearTooltip() { 41 this.tooltip?.remove() 42 clearTimeout(this.timeout) 43 } 44 45 createTooltipElements(message) { 46 this.tooltip = document.createElement("div") 47 this.tooltip.dataset.tooltipTarget = "content" 48 this.tooltip.classList.add("tooltip", "open") 49 this.tooltip.textContent = message 50 51 this.arrow = document.createElement("div") 52 this.arrow.classList.add("arrow") 53 54 this.tooltip.appendChild(this.arrow) 55 // A body-mounted tooltip paints beneath an open modal <dialog>'s top 56 // layer, so from inside one it must mount there to be visible. 57 const host = this.element.closest("dialog[open]") || document.body 58 host.appendChild(this.tooltip) 59 } 60 61 updatePosition() { 62 computePosition(this.element, this.tooltip, { 63 placement: this.tooltipPlacementValue, 64 middleware: [ 65 flip(), 66 offset(this.tooltipOffsetValue), 67 arrow({element: this.arrow}), 68 ] 69 }).then(({x, y, placement, middlewareData}) => { 70 Object.assign(this.tooltip.style, { 71 left: `${x}px`, 72 top: `${y}px` 73 }); 74 75 const side = placement.split("-")[0]; 76 77 const staticSide = { 78 top: "bottom", 79 right: "left", 80 bottom: "top", 81 left: "right" 82 }[side]; 83 84 if (middlewareData.arrow) { 85 const { x, y } = middlewareData.arrow; 86 Object.assign(this.arrow.style, { 87 left: x != null ? `${x}px` : "", 88 top: y != null ? `${y}px` : "", 89 // Ensure the static side gets unset when 90 // flipping to other placements' axes. 91 right: "", 92 bottom: "", 93 [staticSide]: `${-8 / 2}px`, 94 }); 95 } 96 }) 97 } 98}
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.