PageSourceSearch

https://tefter.io/assets/controllers/clipboard_controller-9dbc316c.js

js tefter.io collected 2026-09-24 10:38:38 UTC 2,618 bytes, 79 lines download raw bytes

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.