mirror of
https://github.com/expressjs/expressjs.com.git
synced 2026-02-22 03:51:33 +00:00
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>
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
<script data-cfasync="false" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
|
||||
<script data-cfasync="false" src="/js/app.js"></script>
|
||||
<script data-cfasync="false" defer src="/js/menu.js"></script>
|
||||
<script data-cfasync="false" defer src="/js/copycode.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
|
||||
|
||||
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Express Blog" />
|
||||
|
||||
120
css/style.css
120
css/style.css
@@ -344,14 +344,132 @@ code {
|
||||
pre {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--code-bg);
|
||||
/* keyboard focus offset improve visibility */
|
||||
&:focus {
|
||||
border-color: var(--hover-border);
|
||||
}
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre:has(code) {
|
||||
position: relative;
|
||||
|
||||
&:is(:hover, :focus) {
|
||||
button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
/* focus copy btn by keyboard */
|
||||
&:focus-within button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
pre:has(code) button {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
border: none;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
background-color: inherit;
|
||||
padding: 2px;
|
||||
border-radius: 5px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
background-color: var(--card-fg);
|
||||
mask-image: url("../images/copy-btn.svg");
|
||||
mask-size: 1.5rem;
|
||||
mask-repeat: no-repeat;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus) {
|
||||
background-color: var(--hover-bg);
|
||||
outline: 2px solid var(--hover-border);
|
||||
}
|
||||
|
||||
@media all and (max-width: 370px) {
|
||||
padding: 1px;
|
||||
|
||||
&::after {
|
||||
mask-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pre:has(code) button.copied {
|
||||
outline-color: var(--supported-fg);
|
||||
|
||||
&::after {
|
||||
background-color: var(--supported-fg);
|
||||
}
|
||||
|
||||
&::before {
|
||||
font-size: 0.85rem;
|
||||
position: absolute;
|
||||
left: -58px;
|
||||
content: "copied!";
|
||||
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
color: var(--card-fg);
|
||||
background-color: var(--card-bg);
|
||||
outline: 1px solid var(--supported-fg);
|
||||
}
|
||||
|
||||
@media all and (max-width: 400px) {
|
||||
&::before {
|
||||
left: -50px;
|
||||
font-size: 0.7rem;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pre:has(code) button.failed {
|
||||
outline-color: var(--eol-fg);
|
||||
|
||||
&::after {
|
||||
background-color: var(--eol-fg);
|
||||
}
|
||||
|
||||
&::before {
|
||||
font-size: 0.85rem;
|
||||
position: absolute;
|
||||
left: -58px;
|
||||
content: "failed!";
|
||||
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
color: var(--card-fg);
|
||||
background-color: var(--card-bg);
|
||||
outline: 1px solid var(--eol-fg);
|
||||
}
|
||||
|
||||
@media all and (max-width: 400px) {
|
||||
&::before {
|
||||
left: -50px;
|
||||
font-size: 0.7rem;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* top button */
|
||||
|
||||
.scroll #top {
|
||||
|
||||
1
images/copy-btn.svg
Normal file
1
images/copy-btn.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" class="CodeBox_icon__pCRVM"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"></path></svg>
|
||||
|
After Width: | Height: | Size: 717 B |
57
js/copycode.js
Normal file
57
js/copycode.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const codeBlocks = document.querySelectorAll("pre:has(code)");
|
||||
|
||||
codeBlocks.forEach((block) => {
|
||||
// Only add button if browser supports Clipboard API
|
||||
if (!navigator.clipboard) return;
|
||||
|
||||
const button = createCopyButton();
|
||||
block.appendChild(button);
|
||||
block.setAttribute("tabindex", 0); // Add keyboard a11y for <pre></pre>
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
await copyCode(block, button);
|
||||
});
|
||||
});
|
||||
|
||||
function createCopyButton() {
|
||||
const button = document.createElement("button");
|
||||
setButtonAttributes(button, {
|
||||
type: "button", // button doesn't act as a submit button
|
||||
title: "copy code",
|
||||
"aria-label": "click to copy code",
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function setButtonAttributes(button, attributes) {
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
button.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyCode(block, button) {
|
||||
const code = block.querySelector("code");
|
||||
const text = code.innerText;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
updateButtonState(button, "copied", "code is copied!");
|
||||
} catch {
|
||||
updateButtonState(button, "failed", "failed!");
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtonState(button, statusClass, ariaLabel) {
|
||||
button.setAttribute("aria-live", "polite");
|
||||
button.setAttribute("aria-label", ariaLabel);
|
||||
button.classList.add(statusClass);
|
||||
|
||||
// Clear any existing timer
|
||||
if (button.dataset.timerId) clearTimeout(Number(button.dataset.timerId));
|
||||
const timer = setTimeout(() => {
|
||||
button.classList.remove(statusClass);
|
||||
button.setAttribute("aria-label", "click to copy code");
|
||||
}, 1000);
|
||||
|
||||
button.dataset.timerId = timer;
|
||||
}
|
||||
Reference in New Issue
Block a user