mirror of
https://github.com/gcushen/hugo-academic.git
synced 2025-07-23 18:10:52 +02:00
fix: code copy button
Fixes when copying a code block, the copy button was also copied, leading to copied text containing the word 'copy'. Fix #3160
This commit is contained in:
parent
1dbdd068d6
commit
ec0e30baa4
1 changed files with 150 additions and 41 deletions
|
@ -1,56 +1,165 @@
|
||||||
import {hugoEnvironment, i18n} from '@params';
|
import { hugoEnvironment, i18n } from '@params';
|
||||||
console.debug(`Environment: ${hugoEnvironment}`);
|
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const NOTIFICATION_DURATION = 2000; // milliseconds
|
||||||
|
const DEBOUNCE_DELAY = 300; // milliseconds
|
||||||
|
|
||||||
|
// Debug mode based on environment
|
||||||
|
const isDebugMode = hugoEnvironment === 'development';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function to prevent rapid clicking
|
||||||
|
* @param {Function} func - Function to debounce
|
||||||
|
* @param {number} wait - Wait time in milliseconds
|
||||||
|
* @returns {Function} Debounced function
|
||||||
|
*/
|
||||||
|
const debounce = (func, wait) => {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies code to clipboard, excluding the copy button text
|
||||||
|
* @param {HTMLElement} button - The copy button element
|
||||||
|
* @param {HTMLElement} codeWrapper - The wrapper containing the code
|
||||||
|
* @throws {Error} When clipboard operations fail
|
||||||
|
*/
|
||||||
async function copyCodeToClipboard(button, codeWrapper) {
|
async function copyCodeToClipboard(button, codeWrapper) {
|
||||||
const codeToCopy = codeWrapper.textContent;
|
if (!button || !(button instanceof HTMLElement)) {
|
||||||
|
throw new Error('Invalid button element');
|
||||||
|
}
|
||||||
|
if (!codeWrapper || !(codeWrapper instanceof HTMLElement)) {
|
||||||
|
throw new Error('Invalid code wrapper element');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the wrapper to avoid modifying the displayed content
|
||||||
|
const tempWrapper = codeWrapper.cloneNode(true);
|
||||||
|
|
||||||
|
// Remove the copy button from the cloned wrapper
|
||||||
|
const copyButton = tempWrapper.querySelector('.copy-button');
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeToCopy = tempWrapper.textContent?.trim() ?? '';
|
||||||
|
|
||||||
|
if (!codeToCopy) {
|
||||||
|
throw new Error('No code content found to copy');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ('clipboard' in navigator) {
|
await navigator.clipboard.writeText(codeToCopy);
|
||||||
// Note: Clipboard API requires HTTPS or localhost
|
|
||||||
await navigator.clipboard.writeText(codeToCopy);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to copy. Dead browser.')
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
console.error('Failed to copy. Check permissions...')
|
|
||||||
} finally {
|
|
||||||
copiedNotification(button);
|
copiedNotification(button);
|
||||||
|
isDebugMode && console.debug('Code copied successfully');
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
console.error('Failed to copy:', errorMessage);
|
||||||
|
button.innerHTML = i18n['copyFailed'] || 'Failed';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = i18n['copy'];
|
||||||
|
}, NOTIFICATION_DURATION);
|
||||||
|
throw err; // Re-throw for potential error boundary handling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates button text to show copied notification
|
||||||
|
* @param {HTMLElement} copyBtn - The copy button element
|
||||||
|
*/
|
||||||
function copiedNotification(copyBtn) {
|
function copiedNotification(copyBtn) {
|
||||||
copyBtn.innerHTML = i18n['copied'];
|
copyBtn.innerHTML = i18n['copied'];
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
copyBtn.classList.add('copied');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyBtn.innerHTML = i18n['copy'];
|
copyBtn.innerHTML = i18n['copy'];
|
||||||
}, 2000);
|
copyBtn.disabled = false;
|
||||||
|
copyBtn.classList.remove('copied');
|
||||||
|
}, NOTIFICATION_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code block copy button
|
/**
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
* Creates a copy button element
|
||||||
document.querySelectorAll('pre > code').forEach((codeblock) => {
|
* @returns {HTMLButtonElement} The created button
|
||||||
const container = codeblock.parentNode.parentNode;
|
*/
|
||||||
|
function createCopyButton() {
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.classList.add('copy-button');
|
||||||
|
copyBtn.innerHTML = i18n['copy'];
|
||||||
|
copyBtn.setAttribute('aria-label', i18n['copyLabel'] || 'Copy code to clipboard');
|
||||||
|
copyBtn.setAttribute('type', 'button'); // Explicit button type
|
||||||
|
return copyBtn;
|
||||||
|
}
|
||||||
|
|
||||||
// Create copy button
|
/**
|
||||||
const copyBtn = document.createElement('button');
|
* Gets the appropriate wrapper for a code block
|
||||||
let classesToAdd = ['copy-button'];
|
* @param {HTMLElement} codeblock - The code block element
|
||||||
copyBtn.classList.add(...classesToAdd);
|
* @returns {HTMLElement} The wrapper element
|
||||||
copyBtn.innerHTML = i18n['copy'];
|
*/
|
||||||
|
function getCodeWrapper(codeblock) {
|
||||||
|
const container = codeblock.parentNode?.parentNode;
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Invalid code block structure');
|
||||||
|
}
|
||||||
|
|
||||||
// There are 3 kinds of code block wrappers in Hugo, handle them all.
|
if (container.classList.contains('highlight')) {
|
||||||
let wrapper;
|
return container;
|
||||||
if (container.classList.contains('highlight')) {
|
}
|
||||||
// Parent when Hugo line numbers disabled
|
|
||||||
wrapper = container;
|
const tableWrapper = container.closest('table');
|
||||||
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName === 'TABLE') {
|
if (tableWrapper) {
|
||||||
// Parent when Hugo line numbers enabled
|
return tableWrapper;
|
||||||
wrapper = codeblock.parentNode.parentNode.parentNode.parentNode.parentNode;
|
}
|
||||||
} else {
|
|
||||||
// Parent when Hugo `highlight` class not applied to code block
|
const preElement = codeblock.parentElement;
|
||||||
// Hugo only applies `highlight` class when a language is specified on the Markdown block
|
if (preElement) {
|
||||||
// But we need the `highlight` style to be applied so that absolute button has relative block parent
|
preElement.classList.add('highlight');
|
||||||
codeblock.parentElement.classList.add('highlight');
|
return preElement;
|
||||||
wrapper = codeblock.parentNode;
|
}
|
||||||
}
|
|
||||||
copyBtn.addEventListener("click", () => copyCodeToClipboard(copyBtn, wrapper));
|
throw new Error('Could not determine code wrapper');
|
||||||
wrapper.appendChild(copyBtn);
|
}
|
||||||
});
|
|
||||||
});
|
/**
|
||||||
|
* Initializes copy buttons for all code blocks
|
||||||
|
*/
|
||||||
|
function initializeCodeCopyButtons() {
|
||||||
|
try {
|
||||||
|
const codeBlocks = document.querySelectorAll('pre > code');
|
||||||
|
isDebugMode && console.debug(`Found ${codeBlocks.length} code blocks`);
|
||||||
|
|
||||||
|
codeBlocks.forEach((codeblock, index) => {
|
||||||
|
try {
|
||||||
|
const wrapper = getCodeWrapper(codeblock);
|
||||||
|
const copyBtn = createCopyButton();
|
||||||
|
|
||||||
|
// Use debounced version of copy function
|
||||||
|
const debouncedCopy = debounce(
|
||||||
|
() => copyCodeToClipboard(copyBtn, wrapper),
|
||||||
|
DEBOUNCE_DELAY
|
||||||
|
);
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', debouncedCopy);
|
||||||
|
wrapper.appendChild(copyBtn);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to initialize copy button for code block ${index}:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to initialize code copy buttons:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
window.addEventListener('DOMContentLoaded', initializeCodeCopyButtons);
|
||||||
|
} else {
|
||||||
|
initializeCodeCopyButtons();
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue