Skip to main content

max / quasi

3.0 KB · 74 lines History Blame Raw
1 // Putting a described value on the clipboard.
2 //
3 // `c3e145e0`. An `Act` can say that pressing it copies a value, and this is the
4 // browser's half. It replaces seven `window.<name>` globals across 14 sites on
5 // the MNW server, six of which scraped the text back off the DOM at press time
6 // and one of which read an element by id.
7 //
8 // It reads one attribute the node emitter writes and nothing else:
9 // `data-copies` carries the value. An ordinary escaped attribute, so nothing
10 // here is a program built out of app text -- the same rule that makes this a
11 // script rather than an emitted _hyperscript program. A licence key reading
12 // `"; alert(1); "` is a value in an attribute here and could not be one in a
13 // program.
14 //
15 // What it does NOT do is say "Copied!". Every shipped site relabelled its own
16 // button and reverted, six at 1500ms and one at 2000ms, and that is a temporary
17 // label rather than a copy: it belongs to `makeover-timing` and to mnw-server
18 // `033c722f`. Splitting them is deliberate -- a host with no notion of a
19 // reverting label still needs to be told the act copies something.
20 (() => {
21 "use strict";
22
23 /** The value to put on the clipboard. */
24 const VALUE = "data-copies";
25
26 /**
27 * Write `text` to the clipboard, preferring the async API.
28 *
29 * `navigator.clipboard` is unavailable on an insecure origin and can be
30 * refused by permissions policy, and neither is an error worth surfacing to
31 * a reader who pressed a copy button. The fallback is the old selection
32 * dance, which works in both cases and is why it is still here.
33 */
34 const write = async (text) => {
35 try {
36 if (navigator.clipboard?.writeText) {
37 await navigator.clipboard.writeText(text);
38 return;
39 }
40 } catch {
41 // Fall through: refused, or no permission. The fallback below does
42 // not ask for one.
43 }
44
45 const carrier = document.createElement("textarea");
46 carrier.value = text;
47 // Off-screen rather than hidden: a `display: none` element cannot be
48 // selected, and selecting is the whole mechanism here.
49 carrier.setAttribute("readonly", "");
50 carrier.style.position = "fixed";
51 carrier.style.top = "-9999px";
52 document.body.appendChild(carrier);
53 carrier.select();
54 try {
55 document.execCommand("copy");
56 } catch {
57 // Nothing left to try. The reader sees no change, which is the
58 // same outcome as a page that never described the copy at all.
59 }
60 carrier.remove();
61 };
62
63 // One delegated listener rather than one per control, for `FILL_JS`'
64 // reason: these controls arrive in swaps, so binding per element would mean
65 // rebinding on every swap.
66 document.addEventListener("click", (event) => {
67 const control = event.target?.closest?.(`[${VALUE}]`);
68 if (!control) {
69 return;
70 }
71 void write(control.getAttribute(VALUE) ?? "");
72 });
73 })();
74