/** * Code Copy Button Component * Tractatus Framework - Phase 3: Interactive Documentation * * Adds "Copy" buttons to all code blocks for easy copying * Shows success feedback on copy */ class CodeCopyButtons { constructor() { this.buttonClass = 'code-copy-btn'; this.successClass = 'code-copy-success'; this.init(); } init() { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.addCopyButtons()); } else { this.addCopyButtons(); } console.log('[CodeCopyButtons] Initialized'); } addCopyButtons() { // Find all code blocks (pre elements) const codeBlocks = document.querySelectorAll('pre'); console.log(`[CodeCopyButtons] Found ${codeBlocks.length} code blocks`); codeBlocks.forEach((pre, index) => { // Skip if already has a copy button if (pre.querySelector(`.${this.buttonClass}`)) { return; } // Make pre relative positioned for absolute button pre.style.position = 'relative'; // Create copy button const button = this.createCopyButton(pre, index); // Add button to pre element pre.appendChild(button); }); } createCopyButton(pre, index) { const button = document.createElement('button'); button.className = `${this.buttonClass} absolute top-2 right-2 px-3 py-1 text-xs font-medium rounded transition-all duration-200`; button.style.cssText = ` background: rgba(255, 255, 255, 0.1); color: #e5e7eb; border: 1px solid rgba(255, 255, 255, 0.2); `; button.textContent = 'Copy'; button.setAttribute('aria-label', 'Copy code to clipboard'); button.setAttribute('data-code-index', index); // Add hover styles via class button.addEventListener('mouseenter', () => { button.style.background = 'rgba(255, 255, 255, 0.2)'; }); button.addEventListener('mouseleave', () => { if (!button.classList.contains(this.successClass)) { button.style.background = 'rgba(255, 255, 255, 0.1)'; } }); // Add click handler button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.copyCode(pre, button); }); return button; } async copyCode(pre, button) { // Get code content (find code element inside pre) const codeElement = pre.querySelector('code'); const code = codeElement ? codeElement.textContent : pre.textContent; try { // Use Clipboard API await navigator.clipboard.writeText(code); // Show success feedback this.showSuccess(button); console.log('[CodeCopyButtons] Code copied to clipboard'); } catch (err) { console.error('[CodeCopyButtons] Failed to copy code:', err); // Fallback: try using execCommand this.fallbackCopy(code, button); } } fallbackCopy(text, button) { // Create temporary textarea const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); try { textarea.select(); const successful = document.execCommand('copy'); if (successful) { this.showSuccess(button); console.log('[CodeCopyButtons] Code copied using fallback method'); } else { this.showError(button); } } catch (err) { console.error('[CodeCopyButtons] Fallback copy failed:', err); this.showError(button); } finally { document.body.removeChild(textarea); } } showSuccess(button) { button.classList.add(this.successClass); button.textContent = '✓ Copied!'; button.style.background = 'rgba(16, 185, 129, 0.3)'; // Green button.style.borderColor = 'rgba(16, 185, 129, 0.5)'; button.style.color = '#d1fae5'; // Reset after 2 seconds setTimeout(() => { button.classList.remove(this.successClass); button.textContent = 'Copy'; button.style.background = 'rgba(255, 255, 255, 0.1)'; button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; button.style.color = '#e5e7eb'; }, 2000); } showError(button) { button.textContent = '✗ Failed'; button.style.background = 'rgba(239, 68, 68, 0.3)'; // Red button.style.borderColor = 'rgba(239, 68, 68, 0.5)'; // Reset after 2 seconds setTimeout(() => { button.textContent = 'Copy'; button.style.background = 'rgba(255, 255, 255, 0.1)'; button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; }, 2000); } // Public method to refresh buttons (useful for dynamically loaded content) refresh() { this.addCopyButtons(); } } // Auto-initialize when script loads if (typeof window !== 'undefined') { window.codeCopyButtons = new CodeCopyButtons(); // Listen for custom event from document viewer for dynamic content document.addEventListener('documentLoaded', () => { console.log('[CodeCopyButtons] Document loaded, refreshing buttons'); window.codeCopyButtons.refresh(); }); } // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = CodeCopyButtons; }