about summary refs log tree commit diff stats
path: root/html/merfolk/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/merfolk/app.js')
-rw-r--r--html/merfolk/app.js280
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