Skip to content

Commit 5771f3e

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

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,123 @@ 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+
// Find all pre elements that are not already wrapped
397+
var preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');
398+
399+
preElements.forEach(function(pre) {
400+
// Skip if already wrapped
401+
if (pre.parentElement.classList.contains('code-block-wrapper')) {
402+
return;
403+
}
404+
405+
// Create wrapper
406+
var wrapper = document.createElement('div');
407+
wrapper.className = 'code-block-wrapper';
408+
409+
// Insert wrapper before pre
410+
pre.parentNode.insertBefore(wrapper, pre);
411+
412+
// Move pre into wrapper
413+
wrapper.appendChild(pre);
414+
415+
// Create and add copy button
416+
var copyButton = createCopyButton();
417+
wrapper.appendChild(copyButton);
418+
419+
// Add click handler
420+
copyButton.addEventListener('click', function() {
421+
copyCodeToClipboard(pre, copyButton);
422+
});
423+
});
424+
}
425+
426+
function copyCodeToClipboard(preElement, button) {
427+
var code = preElement.textContent;
428+
429+
// Use modern clipboard API if available
430+
if (navigator.clipboard && navigator.clipboard.writeText) {
431+
navigator.clipboard.writeText(code).then(function() {
432+
showCopySuccess(button);
433+
}).catch(function(err) {
434+
// Fallback to old method
435+
fallbackCopyToClipboard(code, button);
436+
});
437+
} else {
438+
// Fallback for older browsers
439+
fallbackCopyToClipboard(code, button);
440+
}
441+
}
442+
443+
function fallbackCopyToClipboard(text, button) {
444+
var textArea = document.createElement('textarea');
445+
textArea.value = text;
446+
textArea.style.position = 'fixed';
447+
textArea.style.left = '-999999px';
448+
textArea.style.top = '-999999px';
449+
document.body.appendChild(textArea);
450+
textArea.focus();
451+
textArea.select();
452+
453+
try {
454+
var successful = document.execCommand('copy');
455+
if (successful) {
456+
showCopySuccess(button);
457+
}
458+
} catch (err) {
459+
console.error('Failed to copy code:', err);
460+
}
461+
462+
document.body.removeChild(textArea);
463+
}
464+
465+
function showCopySuccess(button) {
466+
// Change icon to checkmark
467+
button.innerHTML = button.dataset.checkIcon;
468+
button.classList.add('copied');
469+
button.setAttribute('aria-label', 'Copied!');
470+
button.setAttribute('title', 'Copied!');
471+
472+
// Revert back after 2 seconds
473+
setTimeout(function() {
474+
button.innerHTML = button.dataset.clipboardIcon;
475+
button.classList.remove('copied');
476+
button.setAttribute('aria-label', 'Copy code to clipboard');
477+
button.setAttribute('title', 'Copy code');
478+
}, 2000);
479+
}
480+
364481
/* ===== Initialization ===== */
365482

366483
document.addEventListener('DOMContentLoaded', function() {
@@ -371,4 +488,5 @@ document.addEventListener('DOMContentLoaded', function() {
371488
generateToc();
372489
hookTocActiveHighlighting();
373490
hookSearchModal();
491+
wrapCodeBlocksWithCopyButton();
374492
});

0 commit comments

Comments
 (0)