Skip to content

Commit f24f45a

Browse files
ShubhamOulkarcarlosstenzelbjohansebasCopilot
authored
feat: add copy code btn (#1841)
* test: copy code btn * add copy btn svg * add copy code btn * fix: keyboard a11y and improve design * show "copied !" text on the copy btn * remove space in copied text * Remove text shift * handle failed copy code * Minimize delay * remove outline on code blocks * refactor copycode.js * Remove border width Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com> * Convert timerId into a Number() Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Carlos Stenzel <carlosstenzel@hotmail.com> Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 021c4d6 commit f24f45a

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

_includes/head.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<script data-cfasync="false" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
4646
<script data-cfasync="false" src="/js/app.js"></script>
4747
<script data-cfasync="false" defer src="/js/menu.js"></script>
48+
<script data-cfasync="false" defer src="/js/copycode.js"></script>
4849
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
4950

5051
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Express Blog" />

css/style.css

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,132 @@ code {
344344
pre {
345345
padding: 16px;
346346
border-radius: 3px;
347-
border: 1px solid #ddd;
347+
border: 1px solid var(--border);
348348
background-color: var(--code-bg);
349+
/* keyboard focus offset improve visibility */
350+
&:focus {
351+
border-color: var(--hover-border);
352+
}
349353
}
350354

351355
pre code {
352356
padding: 0;
353357
}
354358

359+
pre:has(code) {
360+
position: relative;
361+
362+
&:is(:hover, :focus) {
363+
button {
364+
display: flex;
365+
}
366+
}
367+
/* focus copy btn by keyboard */
368+
&:focus-within button {
369+
display: flex;
370+
}
371+
}
372+
373+
pre:has(code) button {
374+
position: absolute;
375+
top: 5px;
376+
right: 5px;
377+
border: none;
378+
z-index: 100;
379+
display: none;
380+
cursor: pointer;
381+
background-color: inherit;
382+
padding: 2px;
383+
border-radius: 5px;
384+
385+
&::after {
386+
content: "";
387+
background-color: var(--card-fg);
388+
mask-image: url("../images/copy-btn.svg");
389+
mask-size: 1.5rem;
390+
mask-repeat: no-repeat;
391+
width: 1.5rem;
392+
height: 1.5rem;
393+
}
394+
395+
&:is(:hover, :focus) {
396+
background-color: var(--hover-bg);
397+
outline: 2px solid var(--hover-border);
398+
}
399+
400+
@media all and (max-width: 370px) {
401+
padding: 1px;
402+
403+
&::after {
404+
mask-size: 1rem;
405+
width: 1rem;
406+
height: 1rem;
407+
}
408+
}
409+
}
410+
411+
pre:has(code) button.copied {
412+
outline-color: var(--supported-fg);
413+
414+
&::after {
415+
background-color: var(--supported-fg);
416+
}
417+
418+
&::before {
419+
font-size: 0.85rem;
420+
position: absolute;
421+
left: -58px;
422+
content: "copied!";
423+
424+
width: fit-content;
425+
height: fit-content;
426+
padding: 4px;
427+
border-radius: 2px;
428+
color: var(--card-fg);
429+
background-color: var(--card-bg);
430+
outline: 1px solid var(--supported-fg);
431+
}
432+
433+
@media all and (max-width: 400px) {
434+
&::before {
435+
left: -50px;
436+
font-size: 0.7rem;
437+
padding: 3px;
438+
}
439+
}
440+
}
441+
442+
pre:has(code) button.failed {
443+
outline-color: var(--eol-fg);
444+
445+
&::after {
446+
background-color: var(--eol-fg);
447+
}
448+
449+
&::before {
450+
font-size: 0.85rem;
451+
position: absolute;
452+
left: -58px;
453+
content: "failed!";
454+
455+
width: fit-content;
456+
height: fit-content;
457+
padding: 4px;
458+
border-radius: 2px;
459+
color: var(--card-fg);
460+
background-color: var(--card-bg);
461+
outline: 1px solid var(--eol-fg);
462+
}
463+
464+
@media all and (max-width: 400px) {
465+
&::before {
466+
left: -50px;
467+
font-size: 0.7rem;
468+
padding: 3px;
469+
}
470+
}
471+
}
472+
355473
/* top button */
356474

357475
.scroll #top {

images/copy-btn.svg

Lines changed: 1 addition & 0 deletions
Loading

js/copycode.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const codeBlocks = document.querySelectorAll("pre:has(code)");
2+
3+
codeBlocks.forEach((block) => {
4+
// Only add button if browser supports Clipboard API
5+
if (!navigator.clipboard) return;
6+
7+
const button = createCopyButton();
8+
block.appendChild(button);
9+
block.setAttribute("tabindex", 0); // Add keyboard a11y for <pre></pre>
10+
11+
button.addEventListener("click", async () => {
12+
await copyCode(block, button);
13+
});
14+
});
15+
16+
function createCopyButton() {
17+
const button = document.createElement("button");
18+
setButtonAttributes(button, {
19+
type: "button", // button doesn't act as a submit button
20+
title: "copy code",
21+
"aria-label": "click to copy code",
22+
});
23+
return button;
24+
}
25+
26+
function setButtonAttributes(button, attributes) {
27+
for (const [key, value] of Object.entries(attributes)) {
28+
button.setAttribute(key, value);
29+
}
30+
}
31+
32+
async function copyCode(block, button) {
33+
const code = block.querySelector("code");
34+
const text = code.innerText;
35+
36+
try {
37+
await navigator.clipboard.writeText(text);
38+
updateButtonState(button, "copied", "code is copied!");
39+
} catch {
40+
updateButtonState(button, "failed", "failed!");
41+
}
42+
}
43+
44+
function updateButtonState(button, statusClass, ariaLabel) {
45+
button.setAttribute("aria-live", "polite");
46+
button.setAttribute("aria-label", ariaLabel);
47+
button.classList.add(statusClass);
48+
49+
// Clear any existing timer
50+
if (button.dataset.timerId) clearTimeout(Number(button.dataset.timerId));
51+
const timer = setTimeout(() => {
52+
button.classList.remove(statusClass);
53+
button.setAttribute("aria-label", "click to copy code");
54+
}, 1000);
55+
56+
button.dataset.timerId = timer;
57+
}

0 commit comments

Comments
 (0)