PageSourceSearch

https://cdn.showable.co/assets/controllers/clipboard_controller-c987dcc8.js

js showable.co collected 2026-10-08 20:21:30 UTC 3,395 bytes, 125 lines download raw bytes

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.