|
1 |
| -import {hugoEnvironment, i18n} from '@params'; |
2 |
| -console.debug(`Environment: ${hugoEnvironment}`); |
| 1 | +import { hugoEnvironment, i18n } from '@params'; |
3 | 2 |
|
| 3 | +// Constants |
| 4 | +const NOTIFICATION_DURATION = 2000; // milliseconds |
| 5 | +const DEBOUNCE_DELAY = 300; // milliseconds |
| 6 | + |
| 7 | +// Debug mode based on environment |
| 8 | +const isDebugMode = hugoEnvironment === 'development'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Debounce function to prevent rapid clicking |
| 12 | + * @param {Function} func - Function to debounce |
| 13 | + * @param {number} wait - Wait time in milliseconds |
| 14 | + * @returns {Function} Debounced function |
| 15 | + */ |
| 16 | +const debounce = (func, wait) => { |
| 17 | + let timeout; |
| 18 | + return function executedFunction(...args) { |
| 19 | + const later = () => { |
| 20 | + clearTimeout(timeout); |
| 21 | + func(...args); |
| 22 | + }; |
| 23 | + clearTimeout(timeout); |
| 24 | + timeout = setTimeout(later, wait); |
| 25 | + }; |
| 26 | +}; |
| 27 | + |
| 28 | +/** |
| 29 | + * Copies code to clipboard, excluding the copy button text |
| 30 | + * @param {HTMLElement} button - The copy button element |
| 31 | + * @param {HTMLElement} codeWrapper - The wrapper containing the code |
| 32 | + * @throws {Error} When clipboard operations fail |
| 33 | + */ |
4 | 34 | async function copyCodeToClipboard(button, codeWrapper) {
|
5 |
| - const codeToCopy = codeWrapper.textContent; |
| 35 | + if (!button || !(button instanceof HTMLElement)) { |
| 36 | + throw new Error('Invalid button element'); |
| 37 | + } |
| 38 | + if (!codeWrapper || !(codeWrapper instanceof HTMLElement)) { |
| 39 | + throw new Error('Invalid code wrapper element'); |
| 40 | + } |
| 41 | + |
| 42 | + // Clone the wrapper to avoid modifying the displayed content |
| 43 | + const tempWrapper = codeWrapper.cloneNode(true); |
| 44 | + |
| 45 | + // Remove the copy button from the cloned wrapper |
| 46 | + const copyButton = tempWrapper.querySelector('.copy-button'); |
| 47 | + if (copyButton) { |
| 48 | + copyButton.remove(); |
| 49 | + } |
| 50 | + |
| 51 | + const codeToCopy = tempWrapper.textContent?.trim() ?? ''; |
| 52 | + |
| 53 | + if (!codeToCopy) { |
| 54 | + throw new Error('No code content found to copy'); |
| 55 | + } |
| 56 | + |
6 | 57 | try {
|
7 |
| - if ('clipboard' in navigator) { |
8 |
| - // Note: Clipboard API requires HTTPS or localhost |
9 |
| - await navigator.clipboard.writeText(codeToCopy); |
10 |
| - } else { |
11 |
| - console.error('Failed to copy. Dead browser.') |
12 |
| - } |
13 |
| - } catch (_) { |
14 |
| - console.error('Failed to copy. Check permissions...') |
15 |
| - } finally { |
| 58 | + await navigator.clipboard.writeText(codeToCopy); |
16 | 59 | copiedNotification(button);
|
| 60 | + isDebugMode && console.debug('Code copied successfully'); |
| 61 | + } catch (err) { |
| 62 | + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; |
| 63 | + console.error('Failed to copy:', errorMessage); |
| 64 | + button.innerHTML = i18n['copyFailed'] || 'Failed'; |
| 65 | + setTimeout(() => { |
| 66 | + button.innerHTML = i18n['copy']; |
| 67 | + }, NOTIFICATION_DURATION); |
| 68 | + throw err; // Re-throw for potential error boundary handling |
17 | 69 | }
|
18 | 70 | }
|
19 | 71 |
|
| 72 | +/** |
| 73 | + * Updates button text to show copied notification |
| 74 | + * @param {HTMLElement} copyBtn - The copy button element |
| 75 | + */ |
20 | 76 | function copiedNotification(copyBtn) {
|
21 | 77 | copyBtn.innerHTML = i18n['copied'];
|
| 78 | + copyBtn.disabled = true; |
| 79 | + copyBtn.classList.add('copied'); |
| 80 | + |
22 | 81 | setTimeout(() => {
|
23 | 82 | copyBtn.innerHTML = i18n['copy'];
|
24 |
| - }, 2000); |
| 83 | + copyBtn.disabled = false; |
| 84 | + copyBtn.classList.remove('copied'); |
| 85 | + }, NOTIFICATION_DURATION); |
25 | 86 | }
|
26 | 87 |
|
27 |
| -// Code block copy button |
28 |
| -window.addEventListener("DOMContentLoaded", () => { |
29 |
| - document.querySelectorAll('pre > code').forEach((codeblock) => { |
30 |
| - const container = codeblock.parentNode.parentNode; |
| 88 | +/** |
| 89 | + * Creates a copy button element |
| 90 | + * @returns {HTMLButtonElement} The created button |
| 91 | + */ |
| 92 | +function createCopyButton() { |
| 93 | + const copyBtn = document.createElement('button'); |
| 94 | + copyBtn.classList.add('copy-button'); |
| 95 | + copyBtn.innerHTML = i18n['copy']; |
| 96 | + copyBtn.setAttribute('aria-label', i18n['copyLabel'] || 'Copy code to clipboard'); |
| 97 | + copyBtn.setAttribute('type', 'button'); // Explicit button type |
| 98 | + return copyBtn; |
| 99 | +} |
31 | 100 |
|
32 |
| - // Create copy button |
33 |
| - const copyBtn = document.createElement('button'); |
34 |
| - let classesToAdd = ['copy-button']; |
35 |
| - copyBtn.classList.add(...classesToAdd); |
36 |
| - copyBtn.innerHTML = i18n['copy']; |
| 101 | +/** |
| 102 | + * Gets the appropriate wrapper for a code block |
| 103 | + * @param {HTMLElement} codeblock - The code block element |
| 104 | + * @returns {HTMLElement} The wrapper element |
| 105 | + */ |
| 106 | +function getCodeWrapper(codeblock) { |
| 107 | + const container = codeblock.parentNode?.parentNode; |
| 108 | + if (!container) { |
| 109 | + throw new Error('Invalid code block structure'); |
| 110 | + } |
37 | 111 |
|
38 |
| - // There are 3 kinds of code block wrappers in Hugo, handle them all. |
39 |
| - let wrapper; |
40 |
| - if (container.classList.contains('highlight')) { |
41 |
| - // Parent when Hugo line numbers disabled |
42 |
| - wrapper = container; |
43 |
| - } else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName === 'TABLE') { |
44 |
| - // Parent when Hugo line numbers enabled |
45 |
| - wrapper = codeblock.parentNode.parentNode.parentNode.parentNode.parentNode; |
46 |
| - } else { |
47 |
| - // Parent when Hugo `highlight` class not applied to code block |
48 |
| - // Hugo only applies `highlight` class when a language is specified on the Markdown block |
49 |
| - // But we need the `highlight` style to be applied so that absolute button has relative block parent |
50 |
| - codeblock.parentElement.classList.add('highlight'); |
51 |
| - wrapper = codeblock.parentNode; |
52 |
| - } |
53 |
| - copyBtn.addEventListener("click", () => copyCodeToClipboard(copyBtn, wrapper)); |
54 |
| - wrapper.appendChild(copyBtn); |
55 |
| - }); |
56 |
| -}); |
| 112 | + if (container.classList.contains('highlight')) { |
| 113 | + return container; |
| 114 | + } |
| 115 | + |
| 116 | + const tableWrapper = container.closest('table'); |
| 117 | + if (tableWrapper) { |
| 118 | + return tableWrapper; |
| 119 | + } |
| 120 | + |
| 121 | + const preElement = codeblock.parentElement; |
| 122 | + if (preElement) { |
| 123 | + preElement.classList.add('highlight'); |
| 124 | + return preElement; |
| 125 | + } |
| 126 | + |
| 127 | + throw new Error('Could not determine code wrapper'); |
| 128 | +} |
| 129 | + |
| 130 | +/** |
| 131 | + * Initializes copy buttons for all code blocks |
| 132 | + */ |
| 133 | +function initializeCodeCopyButtons() { |
| 134 | + try { |
| 135 | + const codeBlocks = document.querySelectorAll('pre > code'); |
| 136 | + isDebugMode && console.debug(`Found ${codeBlocks.length} code blocks`); |
| 137 | + |
| 138 | + codeBlocks.forEach((codeblock, index) => { |
| 139 | + try { |
| 140 | + const wrapper = getCodeWrapper(codeblock); |
| 141 | + const copyBtn = createCopyButton(); |
| 142 | + |
| 143 | + // Use debounced version of copy function |
| 144 | + const debouncedCopy = debounce( |
| 145 | + () => copyCodeToClipboard(copyBtn, wrapper), |
| 146 | + DEBOUNCE_DELAY |
| 147 | + ); |
| 148 | + |
| 149 | + copyBtn.addEventListener('click', debouncedCopy); |
| 150 | + wrapper.appendChild(copyBtn); |
| 151 | + } catch (err) { |
| 152 | + console.error(`Failed to initialize copy button for code block ${index}:`, err); |
| 153 | + } |
| 154 | + }); |
| 155 | + } catch (err) { |
| 156 | + console.error('Failed to initialize code copy buttons:', err); |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +// Initialize when DOM is ready |
| 161 | +if (document.readyState === 'loading') { |
| 162 | + window.addEventListener('DOMContentLoaded', initializeCodeCopyButtons); |
| 163 | +} else { |
| 164 | + initializeCodeCopyButtons(); |
| 165 | +} |
0 commit comments