Spaces:
Paused
Paused
| /** | |
| * Copy functionality for RadExtract output | |
| * Modular, testable, and maintainable | |
| */ | |
| /** | |
| * Initialize the copy button with event listener | |
| */ | |
| export function initCopyButton() { | |
| const btn = document.getElementById('copy-output'); | |
| if (!btn) return; | |
| // Add accessibility attributes | |
| btn.setAttribute('aria-label', 'Copy findings to clipboard'); | |
| btn.addEventListener('click', async () => { | |
| const text = buildTextToCopy(); | |
| if (!text) return; | |
| const succeeded = await copyToClipboard(text); | |
| if (succeeded) flashSuccess(btn); | |
| }); | |
| // Initialize button state based on output availability | |
| updateCopyButtonState(); | |
| } | |
| /** | |
| * Build the text to copy based on current mode and output | |
| * @returns {string} Text to copy, or empty string if nothing to copy | |
| */ | |
| function buildTextToCopy() { | |
| // ① Raw-JSON mode | |
| if (document.getElementById('raw-toggle')?.checked) { | |
| const rawOutput = document.getElementById('raw-output'); | |
| const json = rawOutput?._jsonData; | |
| return json ? JSON.stringify(json, null, 2) : ''; | |
| } | |
| // ② Pre-computed plain text (preferred path) | |
| const outputEl = document.getElementById('output-text'); | |
| if (outputEl?.dataset.copy) { | |
| return outputEl.dataset.copy; | |
| } | |
| // ③ Fallback: parse DOM structure (legacy support) | |
| return parseDOMStructure(outputEl) || outputEl?.textContent || ''; | |
| } | |
| /** | |
| * Parse DOM structure to extract formatted text (fallback method) | |
| * @param {HTMLElement} container - Output container element | |
| * @returns {string} Formatted text | |
| */ | |
| function parseDOMStructure(container) { | |
| if (!container || !container.children.length) return ''; | |
| const sections = []; | |
| // Get all section headers and content | |
| const sectionHeaders = container.querySelectorAll('.section-header'); | |
| sectionHeaders.forEach((header) => { | |
| sections.push(header.textContent); | |
| let nextElement = header.nextElementSibling; | |
| while (nextElement && !nextElement.classList.contains('section-header')) { | |
| if (nextElement.classList.contains('primary-label')) { | |
| sections.push('\n' + nextElement.textContent); | |
| } else if (nextElement.classList.contains('finding-list')) { | |
| nextElement.querySelectorAll('li').forEach((li) => { | |
| sections.push('• ' + li.textContent.trim()); | |
| }); | |
| } else if (nextElement.classList.contains('single-finding')) { | |
| sections.push('- ' + nextElement.textContent.trim()); | |
| } else if (nextElement.textContent.trim()) { | |
| sections.push(nextElement.textContent.trim()); | |
| } | |
| nextElement = nextElement.nextElementSibling; | |
| } | |
| sections.push(''); // Add blank line after each section | |
| }); | |
| // Handle prefix content (like examination type) | |
| const allContent = container.children; | |
| if ( | |
| allContent.length > 0 && | |
| !allContent[0].classList.contains('section-header') | |
| ) { | |
| const prefixContent = []; | |
| for (let i = 0; i < allContent.length; i++) { | |
| if (allContent[i].classList.contains('section-header')) break; | |
| if (allContent[i].textContent.trim()) { | |
| prefixContent.push(allContent[i].textContent.trim()); | |
| } | |
| } | |
| if (prefixContent.length > 0) { | |
| return prefixContent.join('\n') + '\n\n' + sections.join('\n'); | |
| } | |
| } | |
| return sections | |
| .join('\n') | |
| .replace(/\n{3,}/g, '\n\n') | |
| .trim(); | |
| } | |
| /** | |
| * Copy text to clipboard with fallback for older browsers | |
| * @param {string} text - Text to copy | |
| * @returns {Promise<boolean>} Success status | |
| */ | |
| async function copyToClipboard(text) { | |
| // Check if clipboard API is available and secure context | |
| if (navigator.clipboard && window.isSecureContext) { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| return true; | |
| } catch (err) { | |
| console.warn('Clipboard API failed, trying fallback:', err); | |
| return legacyCopy(text); | |
| } | |
| } else { | |
| // Use fallback for older browsers or insecure contexts | |
| return legacyCopy(text); | |
| } | |
| } | |
| /** | |
| * Legacy clipboard copy using execCommand | |
| * @param {string} text - Text to copy | |
| * @returns {boolean} Success status | |
| */ | |
| function legacyCopy(text) { | |
| const ta = Object.assign(document.createElement('textarea'), { | |
| value: text, | |
| style: 'position:fixed;left:-9999px', | |
| }); | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| let ok = false; | |
| try { | |
| ok = document.execCommand('copy'); | |
| } catch (err) { | |
| console.error('Legacy copy failed:', err); | |
| } | |
| document.body.removeChild(ta); | |
| return ok; | |
| } | |
| /** | |
| * Show success feedback on button | |
| * @param {HTMLElement} button - Copy button element | |
| */ | |
| function flashSuccess(button) { | |
| button.classList.add('copied'); | |
| button.setAttribute('title', 'Copied!'); | |
| setTimeout(() => { | |
| button.classList.remove('copied'); | |
| button.setAttribute('title', 'Copy output to clipboard'); | |
| }, 2000); | |
| } | |
| /** | |
| * Update copy button enabled/disabled state based on output availability | |
| */ | |
| export function updateCopyButtonState() { | |
| const btn = document.getElementById('copy-output'); | |
| if (!btn) return; | |
| const outputText = document.getElementById('output-text'); | |
| const hasOutput = outputText && outputText.textContent.trim().length > 0; | |
| btn.disabled = !hasOutput; | |
| } | |