Skip to content

Commit d3d7de2

Browse files
committed
Support code-copying
1 parent 97a74f1 commit d3d7de2

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

lib/rdoc/generator/template/aliki/css/rdoc.css

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,95 @@ pre {
381381
font-size: var(--font-size-sm);
382382
line-height: var(--line-height-normal);
383383
margin: var(--space-4) 0;
384+
position: relative;
385+
}
386+
387+
/* Code block wrapper for copy button */
388+
.code-block-wrapper {
389+
position: relative;
390+
margin: var(--space-4) 0;
391+
}
392+
393+
.code-block-wrapper pre {
394+
margin: 0;
395+
}
396+
397+
/* Copy button styling */
398+
.copy-code-button {
399+
position: absolute;
400+
top: var(--space-2);
401+
right: var(--space-2);
402+
padding: var(--space-2);
403+
background: var(--color-background-secondary);
404+
border: 1px solid var(--color-border-default);
405+
border-radius: var(--radius-sm);
406+
cursor: pointer;
407+
opacity: 0.6;
408+
transition: opacity var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
409+
display: flex;
410+
align-items: center;
411+
justify-content: center;
412+
width: 2rem;
413+
height: 2rem;
414+
z-index: 10;
415+
}
416+
417+
.copy-code-button:hover,
418+
.copy-code-button:focus {
419+
opacity: 1;
420+
background: var(--color-background-tertiary);
421+
border-color: var(--color-border-emphasis);
422+
}
423+
424+
.copy-code-button:active {
425+
transform: scale(0.95);
426+
}
427+
428+
.copy-code-button svg {
429+
width: 1rem;
430+
height: 1rem;
431+
fill: none;
432+
stroke: currentColor;
433+
stroke-width: 2;
434+
stroke-linecap: round;
435+
stroke-linejoin: round;
436+
color: var(--color-text-secondary);
437+
transition: color var(--transition-fast);
438+
}
439+
440+
.copy-code-button:hover svg {
441+
color: var(--color-text-primary);
442+
}
443+
444+
/* Copied state - subtle green checkmark */
445+
.copy-code-button.copied {
446+
background: rgba(34, 197, 94, 0.1);
447+
border-color: var(--color-green-500);
448+
opacity: 1;
449+
}
450+
451+
.copy-code-button.copied svg {
452+
color: var(--color-green-600);
453+
}
454+
455+
[data-theme="dark"] .copy-code-button.copied {
456+
background: rgba(34, 197, 94, 0.15);
457+
border-color: var(--color-green-400);
458+
}
459+
460+
[data-theme="dark"] .copy-code-button.copied svg {
461+
color: var(--color-green-400);
462+
}
463+
464+
/* Mobile adjustments */
465+
@media (hover: none) {
466+
.copy-code-button {
467+
opacity: 0.7;
468+
}
469+
470+
.copy-code-button:active {
471+
opacity: 1;
472+
}
384473
}
385474

386475
pre code {

lib/rdoc/generator/template/aliki/js/aliki.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,104 @@ function hookSearchModal() {
361361
}
362362
}
363363

364+
/* ===== Code Block Copy Functionality ===== */
365+
366+
function createCopyButton() {
367+
var button = document.createElement('button');
368+
button.className = 'copy-code-button';
369+
button.type = 'button';
370+
button.setAttribute('aria-label', 'Copy code to clipboard');
371+
button.setAttribute('title', 'Copy code');
372+
373+
// Create clipboard icon SVG
374+
var clipboardIcon = `
375+
<svg viewBox="0 0 24 24">
376+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
377+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
378+
</svg>
379+
`;
380+
381+
// Create checkmark icon SVG (for copied state)
382+
var checkIcon = `
383+
<svg viewBox="0 0 24 24">
384+
<polyline points="20 6 9 17 4 12"></polyline>
385+
</svg>
386+
`;
387+
388+
button.innerHTML = clipboardIcon;
389+
button.dataset.clipboardIcon = clipboardIcon;
390+
button.dataset.checkIcon = checkIcon;
391+
392+
return button;
393+
}
394+
395+
function wrapCodeBlocksWithCopyButton() {
396+
// Copy buttons are generated dynamically rather than statically in rhtml templates because:
397+
// - Code blocks are generated by RDoc's markup formatter (RDoc::Markup::ToHtml),
398+
// not directly in rhtml templates
399+
// - Modifying the formatter would require extending RDoc's core internals
400+
401+
// Find all pre elements that are not already wrapped
402+
var preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');
403+
404+
preElements.forEach(function(pre) {
405+
// Skip if already wrapped
406+
if (pre.parentElement.classList.contains('code-block-wrapper')) {
407+
return;
408+
}
409+
410+
// Create wrapper
411+
var wrapper = document.createElement('div');
412+
wrapper.className = 'code-block-wrapper';
413+
414+
// Insert wrapper before pre
415+
pre.parentNode.insertBefore(wrapper, pre);
416+
417+
// Move pre into wrapper
418+
wrapper.appendChild(pre);
419+
420+
// Create and add copy button
421+
var copyButton = createCopyButton();
422+
wrapper.appendChild(copyButton);
423+
424+
// Add click handler
425+
copyButton.addEventListener('click', function() {
426+
copyCodeToClipboard(pre, copyButton);
427+
});
428+
});
429+
}
430+
431+
function copyCodeToClipboard(preElement, button) {
432+
var code = preElement.textContent;
433+
434+
// Use the Clipboard API (supported by all modern browsers)
435+
if (navigator.clipboard && navigator.clipboard.writeText) {
436+
navigator.clipboard.writeText(code).then(function() {
437+
showCopySuccess(button);
438+
}).catch(function() {
439+
alert('Failed to copy code.');
440+
});
441+
} else {
442+
alert('Failed to copy code.');
443+
}
444+
}
445+
446+
function showCopySuccess(button) {
447+
// Change icon to checkmark
448+
button.innerHTML = button.dataset.checkIcon;
449+
button.classList.add('copied');
450+
button.setAttribute('aria-label', 'Copied!');
451+
button.setAttribute('title', 'Copied!');
452+
453+
// Revert back after 2 seconds
454+
setTimeout(function() {
455+
button.innerHTML = button.dataset.clipboardIcon;
456+
button.classList.remove('copied');
457+
button.setAttribute('aria-label', 'Copy code to clipboard');
458+
button.setAttribute('title', 'Copy code');
459+
}, 2000);
460+
}
461+
364462
/* ===== Initialization ===== */
365463

366464
document.addEventListener('DOMContentLoaded', function() {
@@ -371,4 +469,5 @@ document.addEventListener('DOMContentLoaded', function() {
371469
generateToc();
372470
hookTocActiveHighlighting();
373471
hookSearchModal();
472+
wrapCodeBlocksWithCopyButton();
374473
});

0 commit comments

Comments
 (0)