mermaid.initialize({ startOnLoad: false, theme: 'dark' }); const editor = document.getElementById('editor'); const status = document.getElementById('status'); const previewArea = document.getElementById('previewArea'); const panLayer = document.getElementById('pan-layer'); const svgFrame = document.getElementById('svg-frame'); const overlay = document.getElementById('overlay'); const placeholder = document.getElementById('placeholder'); const STORAGE_KEY = 'mermaid-editor-content'; const DEFAULT = `flowchart TD A[Start] --> B{Decision} B -->|Yes| C[Do the thing] B -->|No| D[Skip it] C --> E[End] D --> E`; editor.value = localStorage.getItem(STORAGE_KEY) ?? DEFAULT; editor.addEventListener('input', () => { localStorage.setItem(STORAGE_KEY, editor.value); }); // ── Render ─────────────────────────────────────────────────────────────────── let lastSvg = null; async function render() { const code = editor.value.trim(); if (!code) return; try { const id = 'mermaid-' + Date.now(); const { svg } = await mermaid.render(id, code); lastSvg = svg; const { w, h } = parseSvgSize(svg); svgFrame.width = w; svgFrame.height = h; // Write SVG into sandboxed iframe — scripts inside cannot reach the parent page svgFrame.srcdoc = ` ${svg}`; svgFrame.style.display = 'block'; overlay.style.display = 'none'; setStatus('OK', '#4ade80'); requestAnimationFrame(resetView); } catch (err) { showError(err.message || String(err)); } } function parseSvgSize(svgString) { const doc = new DOMParser().parseFromString(svgString, 'image/svg+xml'); const svg = doc.querySelector('svg'); let w = parseFloat(svg?.getAttribute('width')); let h = parseFloat(svg?.getAttribute('height')); if (!w || !h) { const vb = svg?.getAttribute('viewBox')?.trim().split(/[\s,]+/); if (vb?.length === 4) { w = parseFloat(vb[2]); h = parseFloat(vb[3]); } } return { w: w || 800, h: h || 600 }; } function showError(msg) { const errEl = document.createElement('div'); errEl.className = 'error'; errEl.textContent = msg; overlay.innerHTML = ''; overlay.appendChild(errEl); overlay.style.display = 'flex'; setStatus('Error', '#f87171'); setTimeout(() => errEl.classList.add('fade-out'), 4000); setTimeout(() => { if (overlay.contains(errEl)) { overlay.innerHTML = ''; overlay.appendChild(placeholder); overlay.style.display = 'flex'; } }, 4600); } // ── Toolbar actions ────────────────────────────────────────────────────────── function clearEditor() { editor.value = ''; localStorage.removeItem(STORAGE_KEY); lastSvg = null; svgFrame.srcdoc = ''; svgFrame.style.display = 'none'; overlay.innerHTML = ''; overlay.appendChild(placeholder); overlay.style.display = 'flex'; } function downloadSvg() { if (!lastSvg) return; const blob = new Blob([lastSvg], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'diagram.svg'; a.click(); URL.revokeObjectURL(url); } async function copySvg() { if (!lastSvg) return; await navigator.clipboard.writeText(lastSvg); setStatus('Copied!', '#60a5fa'); } function setStatus(msg, color) { status.textContent = msg; status.style.color = color; setTimeout(() => { status.textContent = ''; }, 2000); } // ── Pan & zoom ─────────────────────────────────────────────────────────────── let vt = { x: 0, y: 0, scale: 1 }; let drag = null; function applyTransform() { panLayer.style.transform = `translate(${vt.x}px, ${vt.y}px) scale(${vt.scale})`; } function resetView() { const area = previewArea.getBoundingClientRect(); vt = { x: (area.width - svgFrame.offsetWidth) / 2, y: (area.height - svgFrame.offsetHeight) / 2, scale: 1, }; applyTransform(); } previewArea.addEventListener('wheel', e => { e.preventDefault(); const rect = previewArea.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const delta = e.deltaY < 0 ? 1.1 : 0.9; vt.x = mx - (mx - vt.x) * delta; vt.y = my - (my - vt.y) * delta; vt.scale = Math.min(10, Math.max(0.1, vt.scale * delta)); applyTransform(); }, { passive: false }); previewArea.addEventListener('mousedown', e => { drag = { startX: e.clientX - vt.x, startY: e.clientY - vt.y }; previewArea.classList.add('dragging'); }); window.addEventListener('mousemove', e => { if (!drag) return; vt.x = e.clientX - drag.startX; vt.y = e.clientY - drag.startY; applyTransform(); }); window.addEventListener('mouseup', () => { drag = null; previewArea.classList.remove('dragging'); }); // ── Keyboard shortcuts ─────────────────────────────────────────────────────── editor.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); render(); } if (e.key === 'Tab') { e.preventDefault(); const s = editor.selectionStart; editor.value = editor.value.slice(0, s) + ' ' + editor.value.slice(editor.selectionEnd); editor.selectionStart = editor.selectionEnd = s + 2; } }); // ── Init ───────────────────────────────────────────────────────────────────── render();