Skip to content

Commit 820ba8d

Browse files
committed
feat: implement copy button for code snippets
1 parent 2d07528 commit 820ba8d

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed

docs/assets/js/copy-button.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Copy button functionality for code snippets
3+
* Adds a copy button to all highlighted code blocks
4+
*/
5+
6+
(function() {
7+
'use strict';
8+
9+
// SVG icon for copy button
10+
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>`;
11+
12+
const checkIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
13+
14+
/**
15+
* Add copy button to a code block
16+
*/
17+
function addCopyButton(codeBlock) {
18+
// Skip if button already added
19+
if (codeBlock.parentElement.querySelector('.copy-button')) {
20+
return;
21+
}
22+
23+
// Get the text content from the code block
24+
const codeText = codeBlock.textContent;
25+
26+
// Create button container
27+
const buttonContainer = document.createElement('div');
28+
buttonContainer.className = 'copy-button-container';
29+
30+
// Create button
31+
const button = document.createElement('button');
32+
button.className = 'copy-button';
33+
button.title = 'Copy code';
34+
button.innerHTML = copyIcon;
35+
button.type = 'button';
36+
37+
// Add click handler
38+
button.addEventListener('click', function(e) {
39+
e.preventDefault();
40+
41+
// Copy to clipboard
42+
if (navigator.clipboard && navigator.clipboard.writeText) {
43+
navigator.clipboard.writeText(codeText).then(() => {
44+
// Show success state
45+
showCopySuccess(button);
46+
}).catch(() => {
47+
// Fallback: try using execCommand
48+
fallbackCopy(codeText, button);
49+
});
50+
} else {
51+
// Fallback for older browsers
52+
fallbackCopy(codeText, button);
53+
}
54+
});
55+
56+
buttonContainer.appendChild(button);
57+
codeBlock.parentElement.insertBefore(buttonContainer, codeBlock);
58+
}
59+
60+
/**
61+
* Fallback copy method for older browsers
62+
*/
63+
function fallbackCopy(text, button) {
64+
const textarea = document.createElement('textarea');
65+
textarea.value = text;
66+
textarea.style.position = 'fixed';
67+
textarea.style.left = '-9999px';
68+
document.body.appendChild(textarea);
69+
70+
try {
71+
textarea.select();
72+
document.execCommand('copy');
73+
showCopySuccess(button);
74+
} catch (err) {
75+
console.error('Failed to copy text:', err);
76+
} finally {
77+
document.body.removeChild(textarea);
78+
}
79+
}
80+
81+
/**
82+
* Show copy success feedback
83+
*/
84+
function showCopySuccess(button) {
85+
const originalHTML = button.innerHTML;
86+
button.classList.add('copied');
87+
button.innerHTML = checkIcon;
88+
button.title = 'Copied!';
89+
90+
// Reset after 2 seconds
91+
setTimeout(() => {
92+
button.classList.remove('copied');
93+
button.innerHTML = originalHTML;
94+
button.title = 'Copy code';
95+
}, 2000);
96+
}
97+
98+
/**
99+
* Initialize copy buttons when DOM is ready
100+
*/
101+
function initCopyButtons() {
102+
// Target code blocks within .highlight divs (Hugo/Chroma output)
103+
const codeBlocks = document.querySelectorAll('.highlight > pre > code');
104+
105+
codeBlocks.forEach(codeBlock => {
106+
addCopyButton(codeBlock);
107+
});
108+
}
109+
110+
// Initialize when DOM is ready
111+
if (document.readyState === 'loading') {
112+
document.addEventListener('DOMContentLoaded', initCopyButtons);
113+
} else {
114+
initCopyButtons();
115+
}
116+
117+
// Also support dynamic content (if needed in future)
118+
if (window.MutationObserver) {
119+
const observer = new MutationObserver((mutations) => {
120+
mutations.forEach((mutation) => {
121+
if (mutation.addedNodes.length) {
122+
mutation.addedNodes.forEach((node) => {
123+
if (node.nodeType === 1) { // Element node
124+
const codeBlocks = node.querySelectorAll?.('.highlight > pre > code') || [];
125+
codeBlocks.forEach(addCopyButton);
126+
}
127+
});
128+
}
129+
});
130+
});
131+
132+
observer.observe(document.body, {
133+
childList: true,
134+
subtree: true
135+
});
136+
}
137+
})();

docs/assets/scss/_copy-button.scss

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copy button styles for code blocks
2+
3+
.copy-button-container {
4+
position: absolute;
5+
top: 0.5rem;
6+
right: 0.5rem;
7+
z-index: 10;
8+
}
9+
10+
.copy-button {
11+
display: flex;
12+
align-items: center;
13+
justify-content: center;
14+
width: 2rem;
15+
height: 2rem;
16+
padding: 0;
17+
background-color: rgba(0, 0, 0, 0.1);
18+
border: 1px solid rgba(0, 0, 0, 0.2);
19+
border-radius: 0.375rem;
20+
cursor: pointer;
21+
color: currentColor;
22+
transition: all 0.2s ease-in-out;
23+
font-size: 0.875rem;
24+
25+
&:hover {
26+
background-color: rgba(0, 0, 0, 0.15);
27+
border-color: rgba(0, 0, 0, 0.3);
28+
}
29+
30+
&:active {
31+
transform: scale(0.95);
32+
}
33+
34+
&.copied {
35+
background-color: rgba(34, 197, 94, 0.1);
36+
border-color: rgba(34, 197, 94, 0.3);
37+
color: #22c55e;
38+
}
39+
40+
svg {
41+
width: 1rem;
42+
height: 1rem;
43+
}
44+
}
45+
46+
// Ensure highlight blocks have position context for absolute positioning
47+
.highlight {
48+
position: relative;
49+
}

docs/assets/scss/_styles_project.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Import copy button styles
2+
@import 'copy-button';
3+
14
.td-content {
25
img {
36
display: block;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{/* Docsy hook: body-end */}}
2+
{{/* This hook is called at the end of the body, after all content */}}
3+
4+
{{/* Load copy button script for code snippets */}}
5+
{{ $jsCopyButton := resources.Get "js/copy-button.js" }}
6+
<script defer src="{{ $jsCopyButton.RelPermalink }}"></script>

0 commit comments

Comments
 (0)