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
137 changes: 137 additions & 0 deletions docs/assets/js/copy-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Copy button functionality for code snippets
* Adds a copy button to all highlighted code blocks
*/

(function() {
'use strict';

// SVG icon for copy button
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>`;

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>`;

/**
* Add copy button to a code block
*/
function addCopyButton(codeBlock) {
// Skip if button already added
if (codeBlock.parentElement.querySelector('.copy-button')) {
return;
}

// Get the text content from the code block
const codeText = codeBlock.textContent;

// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.className = 'copy-button-container';

// Create button
const button = document.createElement('button');
button.className = 'copy-button';
button.title = 'Copy code';
button.innerHTML = copyIcon;
button.type = 'button';

// Add click handler
button.addEventListener('click', function(e) {
e.preventDefault();

// Copy to clipboard
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(codeText).then(() => {
// Show success state
showCopySuccess(button);
}).catch(() => {
// Fallback: try using execCommand
fallbackCopy(codeText, button);
});
} else {
// Fallback for older browsers
fallbackCopy(codeText, button);
}
});

buttonContainer.appendChild(button);
codeBlock.parentElement.insertBefore(buttonContainer, codeBlock);
}

/**
* Fallback copy method for older browsers
*/
function fallbackCopy(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);

try {
textarea.select();
document.execCommand('copy');
showCopySuccess(button);
} catch (err) {
console.error('Failed to copy text:', err);
} finally {
document.body.removeChild(textarea);
}
}

/**
* Show copy success feedback
*/
function showCopySuccess(button) {
const originalHTML = button.innerHTML;
button.classList.add('copied');
button.innerHTML = checkIcon;
button.title = 'Copied!';

// Reset after 2 seconds
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
button.title = 'Copy code';
}, 2000);
}

/**
* Initialize copy buttons when DOM is ready
*/
function initCopyButtons() {
// Target code blocks within .highlight divs (Hugo/Chroma output)
const codeBlocks = document.querySelectorAll('.highlight > pre > code');

codeBlocks.forEach(codeBlock => {
addCopyButton(codeBlock);
});
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCopyButtons);
} else {
initCopyButtons();
}

// Also support dynamic content (if needed in future)
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
const codeBlocks = node.querySelectorAll?.('.highlight > pre > code') || [];
codeBlocks.forEach(addCopyButton);
}
});
}
});
});

observer.observe(document.body, {
childList: true,
subtree: true
});
}
})();
55 changes: 55 additions & 0 deletions docs/assets/scss/_copy-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copy button styles for code blocks

.copy-button-container {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 10;
}

.copy-button {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0.375rem;
cursor: pointer;
color: currentColor;
transition: all 0.2s ease-in-out;
font-size: 0.875rem;

&:hover {
background-color: rgba(0, 0, 0, 0.15);
border-color: rgba(0, 0, 0, 0.3);
}

&:active {
transform: scale(0.95);
}

&.copied {
background-color: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.3);
color: #22c55e;
}

svg {
width: 1rem;
height: 1rem;
}
}

// Ensure highlight blocks have position context for absolute positioning
.highlight {
position: relative;
}

// Add space at top of code block to prevent button overlap
.highlight > pre > code {
display: block;
margin-top: 2rem;
}
3 changes: 3 additions & 0 deletions docs/assets/scss/_styles_project.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Import copy button styles
@import 'copy-button';

.td-content {
img {
display: block;
Expand Down
6 changes: 6 additions & 0 deletions docs/layouts/partials/hooks/body-end.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{/* Docsy hook: body-end */}}
{{/* This hook is called at the end of the body, after all content */}}

{{/* Load copy button script for code snippets */}}
{{ $jsCopyButton := resources.Get "js/copy-button.js" }}
<script defer src="{{ $jsCopyButton.RelPermalink }}"></script>
Loading