Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions sass/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,102 @@ pre,code {
background-color: initial;
}

.code-copy-wrapper {
position: relative;
}

.code-copy-button {
position: absolute;
top: 0.65rem;
right: 0.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: 1px solid var(--bs-border-color);
border-radius: 999px;
background-color: rgba(var(--bs-body-bg-rgb), 0.92);
color: var(--bs-secondary-color);
box-shadow: 0 0.15rem 0.5rem rgba(0, 0, 0, 0.12);
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(-0.15rem);
transition:
opacity 0.18s ease,
transform 0.18s ease,
border-color 0.18s ease;
}

.code-copy-wrapper:hover .code-copy-button,
.code-copy-wrapper:focus-within .code-copy-button {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0);
}

.code-copy-button:hover,
.code-copy-button:focus-visible {
border-color: #fff;
}

.code-copy-button:focus-visible {
outline: 2px solid var(--bs-code-color);
outline-offset: 2px;
}

.code-copy-button__icon {
display: inline-flex;
align-items: center;
justify-content: center;
transition: filter 0.18s ease, color 0.18s ease;
}

.code-copy-button:hover .code-copy-button__icon,
.code-copy-button:focus-visible .code-copy-button__icon {
filter: brightness(1.12);
color: #fff;
}

.code-copy-button__icon svg {
width: 0.95rem;
height: 0.95rem;
}

.code-copy-button__status {
display: none;
font-size: 0.95rem;
line-height: 1;
font-weight: 700;
}

.code-copy-button[data-copy-state="idle"] .code-copy-button__icon,
.code-copy-button[data-copy-state="working"] .code-copy-button__icon {
display: inline-flex;
}

.code-copy-button[data-copy-state="success"] .code-copy-button__icon,
.code-copy-button[data-copy-state="error"] .code-copy-button__icon {
display: none;
}

.code-copy-button[data-copy-state="success"] .code-copy-button__status--success,
.code-copy-button[data-copy-state="error"] .code-copy-button__status--error {
display: inline-flex;
}

.code-copy-button[data-copy-state="success"] {
color: var(--bs-secondary-color);
}

.code-copy-button[data-copy-state="error"] {
color: var(--bs-danger-text-emphasis);
border-color: currentColor;
}

.eiptable .title {
width: 67%;
}
Expand Down
99 changes: 99 additions & 0 deletions static/assets/scripts/copy-code.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const SUCCESS_RESET_MS = 2000;
const FAILURE_RESET_MS = 2500;
const COPY_ICON = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
`;

const setCopyState = (button, state) => {
button.dataset.copyState = state;
};

const resetCopyStateLater = (button, delayMs) => {
if (button._resetTimer) {
window.clearTimeout(button._resetTimer);
}

button._resetTimer = window.setTimeout(() => {
setCopyState(button, "idle");
button._resetTimer = null;
}, delayMs);
};

const copyText = async (text) => {
if (!navigator.clipboard?.writeText) {
throw new Error("Clipboard API is unavailable");
}

await navigator.clipboard.writeText(text);
};

const handleCopyClick = async (button, codeBlock) => {
setCopyState(button, "working");

try {
await copyText(codeBlock.textContent ?? "");
setCopyState(button, "success");
resetCopyStateLater(button, SUCCESS_RESET_MS);
} catch (error) {
console.error("Unable to copy code block contents", error);
setCopyState(button, "error");
resetCopyStateLater(button, FAILURE_RESET_MS);
}
};

const createCopyButton = (codeBlock) => {
const button = document.createElement("button");
button.type = "button";
button.className = "code-copy-button";
button.dataset.copyState = "idle";
button.setAttribute("aria-label", "Copy code to clipboard");
button.innerHTML = `
<span class="code-copy-button__icon" aria-hidden="true">${COPY_ICON}</span>
<span class="code-copy-button__status code-copy-button__status--success" aria-hidden="true">✓</span>
<span class="code-copy-button__status code-copy-button__status--error" aria-hidden="true">✕</span>
`;

button.addEventListener("click", () => {
void handleCopyClick(button, codeBlock);
});

return button;
};

const wrapCodeBlock = (pre) => {
const wrapper = document.createElement("div");
wrapper.className = "code-copy-wrapper";
pre.insertAdjacentElement("beforebegin", wrapper);
wrapper.appendChild(pre);
return wrapper;
};

const installCopyButtons = () => {
const codeBlocks = document.querySelectorAll("pre.giallo.z-code > code");

for (const codeBlock of codeBlocks) {
const pre = codeBlock.parentElement;
if (!pre) {
continue;
}

if (!pre.hasAttribute("tabindex")) {
pre.tabIndex = 0;
}

const wrapper = pre.parentElement?.classList.contains("code-copy-wrapper")
? pre.parentElement
: wrapCodeBlock(pre);

if (wrapper.querySelector(":scope > .code-copy-button")) {
continue;
}

wrapper.appendChild(createCopyButton(codeBlock));
}
};

document.addEventListener("DOMContentLoaded", installCopyButtons);
7 changes: 4 additions & 3 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@
integrity="sha384-/1zmJ1mBdfKIOnwPxpdG6yaRrxP6qu3eVYm0cz2nOx+AcL4d3AqEFrwcqGZVVroG"
crossorigin="anonymous"
></script>
<script defer async src="{{ get_url(path='/assets/scripts/search.mjs', trailing_slash=false) }}" type="module"></script>
</head>
<body>
<script defer async src="{{ get_url(path='/assets/scripts/search.mjs', trailing_slash=false) }}" type="module"></script>
<script defer src="{{ get_url(path='/assets/scripts/copy-code.mjs', trailing_slash=false) }}" type="module"></script>
</head>
<body>
{% block nav %}
<header class="site-header" role="banner">
<div class="wrapper">
Expand Down