diff options
Diffstat (limited to 'html/merfolk/app.js')
-rw-r--r-- | html/merfolk/app.js | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/html/merfolk/app.js b/html/merfolk/app.js new file mode 100644 index 0000000..963f22d --- /dev/null +++ b/html/merfolk/app.js @@ -0,0 +1,280 @@ +/** + * Merfolk is a mermaid diagram editor and viewer. + * + * Dependencies: + * - mermaid.js + * - html2canvas + * - panzoom + * + */ + +// Configuration +mermaid.initialize({ + startOnLoad: true, + theme: 'default', + securityLevel: 'loose' +}); + +// Known DOM Elements +const input = document.getElementById('mermaid-input'); +const preview = document.getElementById('mermaid-preview'); +const errorMessage = document.getElementById('error-message'); +const exportBtn = document.getElementById('export-btn'); +const exportSvgBtn = document.getElementById('export-svg-btn'); +const resetBtn = document.getElementById('reset-btn'); + +// State +let panzoomInstance = null; + +/** + * Creates a debounced version of a function to prevent too many renders + * @param {Function} func - The function to debounce + * @param {number} wait - The number of milliseconds to delay + * @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); + }; +}; + +/** + * Renders a Mermaid diagram from the provided markup + * Handles initialization of panzoom functionality + * @param {string} text - The Mermaid diagram syntax + * @returns {Promise<void>} + */ +const renderMermaid = async (text) => { + try { + errorMessage.textContent = ''; + if (!text.trim()) { + preview.innerHTML = ''; + if (panzoomInstance) { + panzoomInstance.dispose(); + panzoomInstance = null; + } + return; + } + preview.innerHTML = `<div class="mermaid">${text}</div>`; + await mermaid.run(); + + // Initialize panzoom on the SVG + if (panzoomInstance) { + panzoomInstance.dispose(); + } + const mermaidSvg = preview.querySelector('svg'); + if (mermaidSvg) { + panzoomInstance = panzoom(mermaidSvg, { + maxZoom: 5, + minZoom: 0.2, + bounds: true, + boundsPadding: 0.1, + transformOrigin: { x: 0.5, y: 0.5 } + }); + } + } catch (error) { + errorMessage.textContent = `Error: ${error.message}`; + console.error('Mermaid rendering error:', error); + } +}; + +/** + * Resets the pan/zoom view to the default position and zoom level + */ +const handleReset = () => { + if (panzoomInstance) { + panzoomInstance.moveTo(0, 0); + panzoomInstance.zoomAbs(0, 0, 1); + } +}; + +/** + * Exports the current diagram as a PNG image + * Prompts user to select a desired scale before exporting + * @returns {Promise<void>} + */ +const handleExport = async () => { + if (!preview) return; + try { + // Reset view before capturing + handleReset(); + + // Small delay to ensure the reset is complete + await new Promise(resolve => setTimeout(resolve, 100)); + + // Prompt for scale selection + const scale = await new Promise((resolve) => { + const scaleOptions = { + 'Small (2x)': 2, + 'Medium (5x)': 5, + 'Large (10x)': 10 + }; + + const scaleSelect = document.createElement('select'); + scaleSelect.style.cssText = ` + padding: 8px; + font-size: 16px; + border: 2px solid #111; + background: white; + margin: 10px 0; + width: 200px; + `; + + Object.entries(scaleOptions).forEach(([label, value]) => { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + scaleSelect.appendChild(option); + }); + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 20px; + border: 3px solid #111; + z-index: 1000; + text-align: center; + `; + + const title = document.createElement('h3'); + title.textContent = 'Select Scale'; + title.style.cssText = ` + margin: 0 0 15px 0; + font-size: 18px; + font-weight: bold; + `; + + const button = document.createElement('button'); + button.textContent = 'Export'; + button.style.cssText = ` + padding: 8px 16px; + font-size: 16px; + border: 2px solid #111; + background: white; + cursor: pointer; + margin-top: 10px; + margin-right: 8px; + `; + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Cancel'; + cancelButton.style.cssText = button.style.cssText; + + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; + `; + + button.onclick = () => { + document.body.removeChild(overlay); + resolve(Number(scaleSelect.value)); + }; + + cancelButton.onclick = () => { + document.body.removeChild(overlay); + resolve(null); + }; + + dialog.appendChild(title); + dialog.appendChild(scaleSelect); + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = 'display: flex; justify-content: center; gap: 8px;'; + buttonContainer.appendChild(button); + buttonContainer.appendChild(cancelButton); + dialog.appendChild(buttonContainer); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + }); + + // Return early on cancel + if (scale === null) return; + + const canvas = await html2canvas(preview, { + backgroundColor: null, + scale, + logging: false, + useCORS: true, + allowTaint: true + }); + const link = document.createElement('a'); + link.download = 'mermaid-diagram.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); + } catch (err) { + errorMessage.textContent = 'Export failed.'; + console.error('Export error:', err); + } +}; + +/** + * Exports the current diagram as an SVG file + * Resets view before export to ensure complete diagram is captured + * @returns {Promise<void>} + */ +const handleExportSvg = async () => { + if (!preview) return; + try { + // Reset view before capturing + handleReset(); + + await new Promise(resolve => setTimeout(resolve, 100)); + + const svg = preview.querySelector('svg'); + if (!svg) { + throw new Error('No SVG found to export'); + } + + // Create a clone of the SVG to avoid modifying the original + const svgClone = svg.cloneNode(true); + + // Create a data URL from the SVG + const svgData = new XMLSerializer().serializeToString(svgClone); + const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); + const svgUrl = URL.createObjectURL(svgBlob); + + // Create and trigger download + const link = document.createElement('a'); + link.download = 'mermaid-diagram.svg'; + link.href = svgUrl; + link.click(); + + URL.revokeObjectURL(svgUrl); + } catch (err) { + errorMessage.textContent = 'SVG export failed.'; + console.error('SVG export error:', err); + } +}; + + +const debouncedRender = debounce(renderMermaid, 300); +input.addEventListener('input', (e) => { + debouncedRender(e.target.value); +}); +exportBtn.addEventListener('click', handleExport); +exportSvgBtn.addEventListener('click', handleExportSvg); +resetBtn.addEventListener('click', handleReset); + +// Initialize with example diagram +const exampleDiagram = `graph TD + A[Start] --> B{Is it?} + B -- Yes --> C[OK] + B -- No --> D[End]`; + +input.value = exampleDiagram; +renderMermaid(exampleDiagram); \ No newline at end of file |