PageSourceSearch

https://railsblocks.com/assets/controllers/clipboard_controller-b0…1f605d2a69e65188d9f8aedde952f33c97e08452.js

js railsblocks.com collected 2026-09-25 20:22:55 UTC 10,716 bytes, 281 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2import { computePosition, offset, flip, shift, arrow, autoUpdate } from "@floating-ui/dom";
3
4export default class extends Controller {
5  static values = {
6    successMessage: { type: String, default: "Copied!" }, // Success message to show when text is copied
7    errorMessage: { type: String, default: "Failed to copy!" }, // Error message to show when text is not copied
8    showTooltip: { type: Boolean, default: true }, // Whether to show the tooltip
9    tooltipPlacement: { type: String, default: "top" }, // Placement(s) of the tooltip, e.g., "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end"
10    tooltipOffset: { type: Number, default: 8 }, // Offset of the tooltip
11    tooltipDuration: { type: Number, default: 2000 }, // Duration of the tooltip
12  };
13
14  static targets = ["copyContent", "copiedContent"];
15
16  connect() {
17    this.boundCopyTextToClipboard = this._copyTextToClipboard.bind(this);
18    this.element.addEventListener("click", this.boundCopyTextToClipboard);
19
20    this.cleanupAutoUpdate = null;
21    this.tooltipElement = null;
22    this.arrowElement = null;
23    this.hideTooltipTimeout = null;
24    this.finalizeHideTooltipTimeout = null;
25    this.copiedStateTimeout = null;
26    this.intersectionObserver = null;
27  }
28
29  disconnect() {
30    // if (this.clipboard) {
31    //   this.clipboard.destroy();
32    // }
33    if (this.boundCopyTextToClipboard) {
34      this.element.removeEventListener("click", this.boundCopyTextToClipboard);
35    }
36    clearTimeout(this.copiedStateTimeout);
37    clearTimeout(this.hideTooltipTimeout);
38    clearTimeout(this.finalizeHideTooltipTimeout);
39    this._removeTooltipDOM();
39 // This also handles cleanupAutoUpdate
40  }
41
42  handleSuccess(e) {
43    if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
44      this.showCopiedState();
45    }
46    if (this.showTooltipValue) {
47      this._showFloatingTooltip(this.successMessageValue);
48    }
49    // if (e && typeof e.clearSelection === "function") {  Removed e.clearSelection()
50    //   e.clearSelection();
51    // }
52  }
53
54  handleError(e) {
55    if (this.showTooltipValue) {
56      this._showFloatingTooltip(this.errorMessageValue);
57    }
58  }
59
60  showCopiedState() {
61    if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
62      this.copyContentTarget.classList.add("hidden");
63      this.copiedContentTarget.classList.remove("hidden");
64      clearTimeout(this.copiedStateTimeout);
65      this.copiedStateTimeout = setTimeout(() => {
66        if (this.hasCopyContentTarget && this.hasCopiedContentTarget) {
67          // Check targets still exist
68          this.copyContentTarget.classList.remove("hidden");
69          this.copiedContentTarget.classList.add("hidden");
70        }
71      }, this.tooltipDurationValue);
72    }
73  }
74
75  async _copyTextToClipboard() {
76    const textToCopy = this.element.dataset.clipboardText || this.element.getAttribute("data-clipboard-text");
77    if (textToCopy === null || typeof textToCopy === "undefined") {
78      console.warn("No text to copy. Missing data-clipboard-text attribute on element:", this.element);
79      this.handleError({ message: "No text to copy specified." }); // Pass a mock error object or just the message
80      return;
81    }
82
83    try {
84      await navigator.clipboard.writeText(textToCopy);
85      this.handleSuccess(); // Removed 'e' argument as it's not provided by navigator.clipboard
86    } catch (err) {
87      console.error("Failed to copy text: ", err);
88      this.handleError(err); // Pass the actual error object
89    }
90  }
91
92  _createOrUpdateTooltipDOMSafer(message) {
93    if (!this.tooltipElement) {
94      this.tooltipElement = document.createElement("div");
95      this.tooltipElement.className =
96        "tooltip-content pointer-events-none shadow-sm border rounded-lg border-white/10 absolute bg-[#333333] text-white text-sm py-1 px-2 z-[1000] opacity-0 transition-opacity duration-150";
97      // Example: this.tooltipElement.style.maxWidth = `${this.maxWidthValue}px`; // If you add a maxWidthValue
98
99      const messageSpan = document.createElement("span");
100      messageSpan.setAttribute("data-tooltip-message-span", "");
101      this.tooltipElement.appendChild(messageSpan);
102
103      // Create arrow container with padding to prevent clipping at viewport edges
104      this.arrowContainer = document.createElement("div");
105      this.arrowContainer.className = "absolute z-[1000]";
106
107      this.arrowElement = document.createElement("div");
108      this.arrowElement.className = "tooltip-arrow-element bg-[#333333] w-2 h-2 border-white/10";
109      this.arrowElement.style.transform = "rotate(45deg)";
110
111      this.arrowContainer.appendChild(this.arrowElement);
112      this.tooltipElement.appendChild(this.arrowContainer);
113
114      this.tooltipElement.style.visibility = "hidden";
115    }
116
117    const messageSpan = this.tooltipElement.querySelector("[data-tooltip-message-span]");
118    if (messageSpan) {
119      messageSpan.textContent = message;
120    }
121  }
122
123  _ensureTooltipAppendTarget() {
124    if (!this.tooltipElement) return;
125    const appendTarget = this.element.closest("dialog[open]") || document.body;
126    if (this.tooltipElement.parentElement !== appendTarget) {
127      appendTarget.appendChild(this.tooltipElement);
128    }
129  }
130
131  _cleanupObservers() {
132    if (this.cleanupAutoUpdate) {
133      this.cleanupAutoUpdate();
134      this.cleanupAutoUpdate = null;
135    }
136    if (this.intersectionObserver) {
137      this.intersectionObserver.disconnect();
138      this.intersectionObserver = null;
139    }
140  }
141
142  _removeTooltipDOM() {
143    this._cleanupObservers();
144    if (this.tooltipElement && this.tooltipElement.parentElement) {
145      this.tooltipElement.remove();
146    }
147    this.tooltipElement = null;
148    this.arrowContainer = null;
149    this.arrowElement = null;
150  }
151
152  async _showFloatingTooltip(message) {
153    if (!this.showTooltipValue) return;
154
155    clearTimeout(this.hideTooltipTimeout);
156    clearTimeout(this.finalizeHideTooltipTimeout);
157    this._createOrUpdateTooltipDOMSafer(message);
158
159    if (!this.tooltipElement) return; // Should not happen if _createOrUpdateTooltipDOMSafer worked
160
161    this._ensureTooltipAppendTarget();
162
163    this.tooltipElement.style.visibility = "visible";
164
165    // Ensure autoUpdate is cleaned up if it was somehow active without a tooltipElement (defensive)
166    this._cleanupObservers();
167
168    const referenceElement = this.element;
169    this.cleanupAutoUpdate = autoUpdate(
170      referenceElement,
171      this.tooltipElement,
172      async () => {
173        if (!this.tooltipElement || !this.arrowContainer) {
174          this._cleanupObservers();
175          return;
176        }
177
178        this._ensureTooltipAppendTarget();
179
180        // Parse placement value to support multiple placements
181        const placements = this.tooltipPlacementValue.split(/[\s,]+/).filter(Boolean);
182        const primaryPlacement = placements[0] || "top";
183        const fallbackPlacements = placements.slice(1);
184
185        const middleware = [
186          offset(this.tooltipOffsetValue),
187          flip({
188            fallbackPlacements: fallbackPlacements.length > 0 ? fallbackPlacements : undefined,
189          }),
190          shift({ padding: 5 }),
191        ];
192        middleware.push(arrow({ element: this.arrowContainer, padding: 2 }));
193
194        const { x, y, placement, middlewareData } = await computePosition(referenceElement, this.tooltipElement, {
195          placement: primaryPlacement,
196          middleware: middleware,
197        });
198
199        Object.assign(this.tooltipElement.style, {
200          left: `${x}px`,
201          top: `${y}px`,
202        });
203
204        if (this.arrowContainer && this.arrowElement && middlewareData.arrow) {
205          const { x: arrowX, y: arrowY } = middlewareData.arrow;
206          const basePlacement = placement.split("-")[0];
207          const staticSide = { top: "bottom", right: "left", bottom: "top", left: "right" }[basePlacement];
208
209          // Apply appropriate padding based on placement direction
210          this.arrowContainer.classList.remove("px-1", "py-1");
211          if (basePlacement === "top" || basePlacement === "bottom") {
212            this.arrowContainer.classList.add("px-1"); // Horizontal padding for top/bottom
213          } else {
214            this.arrowContainer.classList.add("py-1"); // Vertical padding for left/right
215          }
216
217          // Position the arrow container
218          Object.assign(this.arrowContainer.style, {
219            left: arrowX != null ? `${arrowX}px` : "",
220            top: arrowY != null ? `${arrowY}px` : "",
221            right: "",
222            bottom: "",
223            [staticSide]: "-0.28rem", // Matches tooltip_controller.js arrow positioning
224          });
225
226          // Style the arrow element within the container
227          this.arrowElement.classList.remove("border-t", "border-r", "border-b", "border-l");
228          if (staticSide === "bottom") this.arrowElement.classList.add("border-b", "border-r");
229          else if (staticSide === "top") this.arrowElement.classList.add("border-t", "border-l");
230          else if (staticSide === "left") this.arrowElement.classList.add("border-b", "border-l");
231          else if (staticSide === "right") this.arrowElement.classList.add("border-t", "border-r");
232        }
233      },
234      { animationFrame: true },
235    );
236
237    // Setup intersection observer to hide tooltip when trigger element goes out of view
238    if (this.intersectionObserver) {
239      this.intersectionObserver.disconnect();
240    }
241    this.intersectionObserver = new IntersectionObserver(
242      (entries) => {
243        entries.forEach((entry) => {
244          if (!entry.isIntersecting) {
245            this._hideFloatingTooltip();
246          }
247        });
248      },
249      { threshold: 0 }, // Hide as soon as any part goes out of view
250    );
251    this.intersectionObserver.observe(this.element);
252
253    requestAnimationFrame(() => {
254      if (this.tooltipElement) {
255        this.tooltipElement.classList.remove("opacity-0");
256        this.tooltipElement.classList.add("opacity-100");
257      }
258    });
259
260    this.hideTooltipTimeout = setTimeout(() => {
261      this._hideFloatingTooltip();
262    }, this.tooltipDurationValue);
263  }
264
265  _hideFloatingTooltip() {
266    if (!this.tooltipElement || !this.tooltipElement.classList.contains("opacity-100")) {
267      return;
268    }
269
270    this.tooltipElement.classList.remove("opacity-100");
271    this.tooltipElement.classList.add("opacity-0");
272    this._cleanupObservers();
273
274    clearTimeout(this.finalizeHideTooltipTimeout);
275    this.finalizeHideTooltipTimeout = setTimeout(() => {
276      if (this.tooltipElement) {
277        this.tooltipElement.style.visibility = "hidden";
278      }
279    }, 150); // Transition duration (duration-150)
280  }
281};

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.