|
8 | 8 | * </span></code></pre> |
9 | 9 | * |
10 | 10 | * Line numbers render from CSS counters with zero JS. This script only adds the |
11 | | - * client-side interactions: the whole-block permalink button, gutter |
12 | | - * click/shift-click/drag selection, and hash restore. |
| 11 | + * client-side interactions: the permalink button (a real anchor that copies the |
| 12 | + * block/selection URL to the clipboard), gutter click/shift-click/drag |
| 13 | + * selection, and hash restore. |
13 | 14 | * |
14 | 15 | * Hash formats: #c-1a2b3c4d whole block |
15 | 16 | * #c-1a2b3c4d-L5 single line |
|
30 | 31 | '<svg viewBox="0 0 640 512" fill="currentColor" aria-hidden="true">' + |
31 | 32 | '<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/></svg>'; |
32 | 33 |
|
| 34 | + const CHECK_SVG = |
| 35 | + '<svg viewBox="0 0 448 512" fill="currentColor" aria-hidden="true">' + |
| 36 | + '<path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>'; |
| 37 | + |
33 | 38 | let anchor = null; // { pre, line } from the last plain gutter click |
34 | 39 | let drag = null; // { pre, wrap, start } while a gutter drag is in progress |
35 | 40 | let suppressClick = false; // swallow the click generated by a finished drag |
36 | 41 |
|
37 | 42 | // ---- setup ------------------------------------------------------------ |
38 | 43 |
|
| 44 | + // The permalink is a real <a href="#c-…"> so the browser's own affordances |
| 45 | + // work: right-click → Copy Link, middle/ctrl-click → new tab (#17). A plain |
| 46 | + // left click sets the hash (as before) AND copies the URL to the clipboard, |
| 47 | + // confirmed by flashing the icon to a checkmark — mirroring the copy-code |
| 48 | + // button that sits next to it. |
39 | 49 | function addPermalinks() { |
40 | 50 | document.querySelectorAll('article pre[id^="c-"]').forEach(function (pre) { |
41 | | - const btn = document.createElement("button"); |
42 | | - btn.className = "block-link"; |
43 | | - btn.setAttribute("aria-label", "Link to this code block"); |
44 | | - btn.title = "Link to this code block"; |
45 | | - btn.innerHTML = LINK_SVG; |
46 | | - btn.addEventListener("click", function () { |
47 | | - setHash("#" + pre.id); |
| 51 | + const link = document.createElement("a"); |
| 52 | + link.className = "block-link"; |
| 53 | + link.setAttribute("aria-label", "Copy link to this code block"); |
| 54 | + link.title = "Copy link to this code block"; |
| 55 | + link.href = "#" + pre.id; |
| 56 | + link.innerHTML = LINK_SVG; |
| 57 | + link.addEventListener("click", function (e) { |
| 58 | + if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.button !== 0) { |
| 59 | + return; // modified click: native anchor behavior |
| 60 | + } |
| 61 | + e.preventDefault(); |
| 62 | + // An active line selection in this block is what the user means to |
| 63 | + // share — keep it; otherwise (re)target the whole block. |
| 64 | + const m = HASH_RE.exec(location.hash); |
| 65 | + if (!(m && m[1] === pre.id && m[2])) setHash("#" + pre.id); |
| 66 | + copyText(location.href, link); |
48 | 67 | }); |
49 | | - pre.appendChild(btn); |
| 68 | + pre.appendChild(link); |
50 | 69 | }); |
51 | 70 | } |
52 | 71 |
|
| 72 | + // Copy `text` to the clipboard and flash `link`'s icon to a checkmark. |
| 73 | + // navigator.clipboard requires a secure context (https or localhost); on |
| 74 | + // plain-http or file:// views fall back to a transient textarea + |
| 75 | + // execCommand. If both fail the click has still set the hash, which is the |
| 76 | + // pre-copy behavior. |
| 77 | + function copyText(text, link) { |
| 78 | + let copied; |
| 79 | + if (navigator.clipboard && window.isSecureContext) { |
| 80 | + copied = navigator.clipboard.writeText(text); |
| 81 | + } else { |
| 82 | + copied = new Promise(function (resolve, reject) { |
| 83 | + const ta = document.createElement("textarea"); |
| 84 | + ta.value = text; |
| 85 | + ta.style.position = "fixed"; |
| 86 | + ta.style.opacity = "0"; |
| 87 | + document.body.appendChild(ta); |
| 88 | + ta.select(); |
| 89 | + try { |
| 90 | + document.execCommand("copy") ? resolve() : reject(new Error("execCommand")); |
| 91 | + } catch (err) { |
| 92 | + reject(err); |
| 93 | + } finally { |
| 94 | + ta.remove(); |
| 95 | + } |
| 96 | + }); |
| 97 | + } |
| 98 | + copied.then(function () { |
| 99 | + link.innerHTML = CHECK_SVG; |
| 100 | + link.classList.add("copied"); |
| 101 | + clearTimeout(link._copiedTimer); |
| 102 | + link._copiedTimer = setTimeout(function () { |
| 103 | + link.innerHTML = LINK_SVG; |
| 104 | + link.classList.remove("copied"); |
| 105 | + }, 1500); |
| 106 | + }).catch(function () {}); |
| 107 | + } |
| 108 | + |
53 | 109 | function armGutters() { |
54 | 110 | document.querySelectorAll("article pre .code-lines").forEach(function (wrap) { |
55 | 111 | wrap.addEventListener("mousedown", onGutterMouseDown); |
|
0 commit comments