Skip to content

Commit e8ab8d5

Browse files
Merge pull request #4 from copilot-dev-days/copilot/add-copy-buttons-to-code-blocks
Add copy-to-clipboard controls to workshop code blocks
2 parents aae903c + 51b1129 commit e8ab8d5

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

docs/step.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,60 @@
340340
margin: 1.5rem 0;
341341
}
342342

343+
.code-block-wrapper {
344+
position: relative;
345+
margin: 1.5rem 0;
346+
}
347+
348+
.code-block-wrapper pre {
349+
margin: 0;
350+
}
351+
352+
.copy-code-btn {
353+
position: absolute;
354+
top: 0.75rem;
355+
right: 0.75rem;
356+
z-index: 1;
357+
border: 1px solid var(--border-color);
358+
border: 1px solid color-mix(in srgb, var(--neon-cyan) 45%, var(--border-color));
359+
background: rgba(0, 245, 255, 0.12);
360+
background: color-mix(in srgb, var(--bg-dark) 78%, var(--neon-cyan));
361+
color: var(--text-primary);
362+
border-radius: 999px;
363+
padding: 0.35rem 0.75rem;
364+
font-size: 0.75rem;
365+
font-weight: 700;
366+
letter-spacing: 0.04em;
367+
text-transform: uppercase;
368+
cursor: pointer;
369+
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
370+
box-shadow: 0 0 0 1px rgba(0, 245, 255, 0.08);
371+
}
372+
373+
.copy-code-btn:hover,
374+
.copy-code-btn:focus-visible {
375+
background: rgba(0, 245, 255, 0.18);
376+
background: color-mix(in srgb, var(--neon-cyan) 24%, var(--bg-dark));
377+
border-color: var(--neon-cyan);
378+
color: var(--neon-cyan);
379+
transform: translateY(-1px);
380+
outline: none;
381+
}
382+
383+
.copy-code-btn[data-state="success"] {
384+
color: var(--success-green);
385+
border-color: var(--success-green);
386+
background: rgba(78, 201, 176, 0.14);
387+
background: color-mix(in srgb, var(--bg-dark) 70%, var(--success-green));
388+
}
389+
390+
.copy-code-btn[data-state="error"] {
391+
color: var(--warning-yellow);
392+
border-color: var(--warning-yellow);
393+
background: rgba(220, 220, 170, 0.14);
394+
background: color-mix(in srgb, var(--bg-dark) 72%, var(--warning-yellow));
395+
}
396+
343397
.markdown pre code {
344398
color: var(--text-primary);
345399
background: none;
@@ -454,3 +508,15 @@
454508
display: none;
455509
}
456510
}
511+
512+
@media (max-width: 640px) {
513+
.copy-code-btn {
514+
top: 0.5rem;
515+
right: 0.5rem;
516+
padding: 0.3rem 0.65rem;
517+
}
518+
519+
.markdown pre {
520+
padding-top: 3rem;
521+
}
522+
}

docs/step.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,92 @@
313313
});
314314
}
315315

316+
async function copyText(text) {
317+
if (navigator.clipboard?.writeText) {
318+
try {
319+
await navigator.clipboard.writeText(text);
320+
return;
321+
} catch (error) {
322+
// Fall back to document.execCommand when clipboard permissions are unavailable.
323+
}
324+
}
325+
326+
const textarea = document.createElement('textarea');
327+
textarea.value = text;
328+
textarea.setAttribute('readonly', '');
329+
textarea.style.position = 'absolute';
330+
textarea.style.left = '-9999px';
331+
document.body.appendChild(textarea);
332+
textarea.select();
333+
334+
try {
335+
document.execCommand('copy');
336+
} finally {
337+
textarea.remove();
338+
}
339+
}
340+
341+
const copyButtonResetTimers = new WeakMap();
342+
343+
function showCopyState(button, state) {
344+
const labels = {
345+
idle: 'Copy',
346+
success: 'Copied!',
347+
error: 'Try again'
348+
};
349+
350+
button.textContent = labels[state];
351+
button.dataset.state = state;
352+
353+
const existingTimer = copyButtonResetTimers.get(button);
354+
if (existingTimer) {
355+
clearTimeout(existingTimer);
356+
}
357+
358+
if (state !== 'idle') {
359+
const resetTimer = setTimeout(() => {
360+
button.textContent = labels.idle;
361+
button.dataset.state = 'idle';
362+
}, 2000);
363+
copyButtonResetTimers.set(button, resetTimer);
364+
}
365+
}
366+
367+
function enhanceCodeBlocks() {
368+
const blocks = document.querySelectorAll('#markdown-content pre');
369+
370+
blocks.forEach((pre) => {
371+
if (pre.parentElement?.classList.contains('code-block-wrapper')) {
372+
return;
373+
}
374+
375+
const code = pre.querySelector('code');
376+
const wrapper = document.createElement('div');
377+
const button = document.createElement('button');
378+
379+
wrapper.className = 'code-block-wrapper';
380+
button.className = 'copy-code-btn';
381+
button.type = 'button';
382+
button.textContent = 'Copy';
383+
button.setAttribute('aria-label', 'Copy code block to clipboard');
384+
385+
button.addEventListener('click', async () => {
386+
const codeText = code?.textContent ?? pre.textContent ?? '';
387+
388+
try {
389+
await copyText(codeText.trimEnd());
390+
showCopyState(button, 'success');
391+
} catch (error) {
392+
showCopyState(button, 'error');
393+
}
394+
});
395+
396+
pre.parentNode.insertBefore(wrapper, pre);
397+
wrapper.appendChild(button);
398+
wrapper.appendChild(pre);
399+
});
400+
}
401+
316402
// Process markdown - clean up navigation links and fix paths
317403
function processMarkdown(md) {
318404
// Remove the header navigation bar (we have our own sidebar)
@@ -373,6 +459,7 @@
373459

374460
document.getElementById('markdown-content').innerHTML = marked.parse(md);
375461
enableTaskListCheckboxes(step.id);
462+
enhanceCodeBlocks();
376463

377464
// If this is the completion page, add confetti!
378465
if (step.id === '05-complete') {

0 commit comments

Comments
 (0)