mermaid-edit/index.php
DJP 89c8c78331 Fix UX: Import Project now available immediately
UX IMPROVEMENTS:
🔧 Import Project button always visible:
   - No longer hidden until after D3 rendering
   - Available immediately when page loads
   - Logical workflow: Import → Render (not Render → Import)

📥 Better import workflow:
   1. Page loads → Import Project button visible
   2. Click Import Project → Select .json file
   3. Project imports → Automatically renders with D3
   4. D3 buttons appear → Ready for editing

 Smart auto-rendering:
   - Import automatically switches to D3 mode
   - Preserves exact positioning from imported project
   - Shows D3-specific buttons (Export, Clear Positions)
   - Updated success message explains D3 auto-rendering

🎯 Now supports proper workflow:
   - Start fresh → Import existing project → Continue editing
   - No need to render first just to access import
   - Intuitive user experience

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 09:48:19 -04:00

2548 lines
No EOL
112 KiB
PHP

<?php
require_once 'config.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_TITLE; ?></title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-btn-color: #333;
--primary-btn-hover-color: #555;
--background-color: #fff;
--text-color: #333;
--border-color: #ccc;
--header-footer-bg: #333;
--header-footer-text: #fff;
--textarea-bg: #fff;
--textarea-text: #333;
--output-bg: #fff;
}
body {
font-family: 'Montserrat', sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
header {
background-color: var(--header-footer-bg);
color: var(--header-footer-text);
text-align: center;
padding: 0.5rem;
}
main {
flex-grow: 1;
padding: 2rem;
display: flex;
flex-direction: column;
}
.container {
display: flex;
gap: 2rem;
flex-grow: 1;
}
.input-area, .output-area {
flex: 1;
display: flex;
flex-direction: column;
}
textarea {
width: 100%;
height: 300px;
margin-bottom: 1rem;
font-family: 'Montserrat', sans-serif;
background-color: var(--textarea-bg);
color: var(--textarea-text);
border: 1px solid var(--border-color);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
#mermaidOutput {
border: 1px solid var(--border-color);
padding: 1rem;
flex-grow: 1;
overflow: auto;
background-color: #fff; /* Always keep diagram background white */
color: #333; /* Always keep diagram text dark */
transition: border-color 0.3s ease;
position: relative;
}
/* Interactive editing styles */
.mermaid-node {
cursor: pointer;
}
.mermaid-node:hover {
filter: brightness(1.1);
}
.mermaid-node.selected {
stroke: #ff6b6b !important;
stroke-width: 3px !important;
filter: drop-shadow(0 0 5px rgba(255, 107, 107, 0.5));
}
.mermaid-edge {
cursor: pointer;
}
.mermaid-edge:hover {
stroke-width: 3px !important;
}
.mermaid-edge.selected {
stroke: #ff6b6b !important;
stroke-width: 3px !important;
}
.property-panel {
position: absolute;
top: 10px;
right: 10px;
width: 250px;
background: white;
border: 2px solid #ff6b6b;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
display: none;
z-index: 9999;
max-height: 400px;
overflow-y: auto;
}
.property-panel.visible {
display: block !important;
}
.property-panel h3 {
margin: 0 0 10px 0;
font-size: 14px;
color: #333;
}
.property-panel label {
display: block;
margin-bottom: 5px;
font-size: 12px;
color: #666;
}
.property-panel input, .property-panel select {
width: 100%;
padding: 5px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
}
.property-panel button {
width: 100%;
padding: 5px;
margin-top: 5px;
font-size: 12px;
}
.button-container {
margin-top: 1rem;
}
button {
padding: 0.5rem 1rem;
background-color: var(--primary-btn-color);
color: #fff;
border: none;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
transition: background-color 0.3s ease;
}
button:hover {
background-color: var(--primary-btn-hover-color);
}
footer {
background-color: var(--header-footer-bg);
color: var(--header-footer-text);
text-align: center;
padding: 1rem;
}
/* Dark Mode Toggle Button */
.dark-mode-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-btn-color);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
color: #fff;
}
/* Dark Mode Styles */
.dark-mode {
--background-color: #1e1e1e;
--text-color: #f5f5f5;
--border-color: #444;
--header-footer-bg: #000;
--header-footer-text: #f5f5f5;
--textarea-bg: #333;
--textarea-text: #f5f5f5;
--output-bg: #333;
--primary-btn-color: #555;
--primary-btn-hover-color: #777;
}
/* Special styling for the output area heading in dark mode */
.dark-mode .output-area h2 {
background-color: #333;
padding: 8px;
border-radius: 4px;
margin-bottom: 12px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<header>
<h1><?php echo SITE_TITLE; ?></h1>
</header>
<!-- Dark Mode Toggle Button -->
<button id="darkModeToggle" class="dark-mode-toggle" title="Toggle Dark Mode">
<span id="lightModeIcon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<!-- Sun icon path -->
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg>
</span>
<span id="darkModeIcon" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<!-- Moon icon path -->
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
</svg>
</span>
</button>
<main>
<div class="container">
<div class="input-area">
<h2>Input Mermaid Diagram Code</h2>
<textarea id="mermaidInput">graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Android]
C -->|Two| E[Nuka Cola]
C -->|Three| F[Cold fusion generator]</textarea>
<div class="button-container">
<button id="renderBtn">Render Diagram</button>
<button id="renderD3Btn">Render with D3</button>
<button id="clearPositionsBtn" style="display: none;">Reset Positions</button>
<button id="exportMmdBtn">Download as .mmd</button>
<button id="exportProjectBtn" style="display: none;">Export Project</button>
<button id="importMmdBtn">Import .mmd</button>
<button id="importProjectBtn">Import Project</button>
<input type="file" id="fileInput" accept=".mmd" style="display: none;">
<input type="file" id="projectFileInput" accept=".json" style="display: none;">
</div>
</div>
<div class="output-area">
<h2>Rendered Diagram</h2>
<div style="position: relative;">
<div id="mermaidOutput"></div>
<div class="property-panel" id="propertyPanel">
<h3>Element Properties</h3>
<div id="propertyContent">
<p>Select an element to edit its properties</p>
</div>
</div>
</div>
<div class="button-container">
<button id="exportBtn">Download as PNG</button>
<button id="toggleEditMode">Enable Edit Mode</button>
<button id="syncCodeBtn" style="display: none;">Sync Code</button>
<button id="testPanelBtn" style="display: none;">Test Panel</button>
<button id="addSubgraphBtn" style="display: none;">Add Group</button>
<button id="exportJsonBtn" style="display: none;">Export Layout</button>
<button id="lockLayoutBtn" style="display: none;">Lock Layout</button>
</div>
</div>
</div>
</main>
<footer>
<p>&copy; <?php echo date('Y'); ?> <?php echo SITE_TITLE; ?></p>
</footer>
<script>
// Initial configuration will be set based on dark mode preference
let initialConfig = {
startOnLoad: false,
theme: 'base',
themeVariables: {
primaryColor: '#ffc406',
primaryTextColor: '#000000',
primaryBorderColor: '#000000',
lineColor: '#000000',
secondaryColor: '#B8B9B9',
tertiaryColor: '#B8B9B9'
},
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
},
securityLevel: 'loose',
fontFamily: 'Montserrat, sans-serif',
fontSize: 14
};
// Always keep the Mermaid diagram in light mode for consistency
// Dark mode only applies to the UI, not the diagram itself
// Initialize with the appropriate theme
mermaid.initialize(initialConfig);
function renderMermaidDiagram() {
const input = document.getElementById('mermaidInput').value;
const output = document.getElementById('mermaidOutput');
output.innerHTML = ''; // Clear previous content
mermaid.render('mermaid-svg', input).then(result => {
output.innerHTML = result.svg;
}).catch(error => {
output.innerHTML = `<p style="color: red;">Error rendering diagram: ${error.message}</p>`;
});
}
// Initial render
document.addEventListener('DOMContentLoaded', renderMermaidDiagram);
document.getElementById('renderBtn').addEventListener('click', renderMermaidDiagram);
// Add D3 render button
document.getElementById('renderD3Btn').addEventListener('click', function() {
const mermaidCode = document.getElementById('mermaidInput').value;
d3Renderer.render(mermaidCode);
document.getElementById('clearPositionsBtn').style.display = 'inline-block';
document.getElementById('exportProjectBtn').style.display = 'inline-block';
});
// Add clear positions button
document.getElementById('clearPositionsBtn').addEventListener('click', function() {
localStorage.removeItem('d3_node_positions');
console.log('Cleared saved positions');
alert('Positions reset! Next render will use automatic layout.');
});
// Add export project button
document.getElementById('exportProjectBtn').addEventListener('click', function() {
d3Renderer.exportProject();
});
// Add import project button
document.getElementById('importProjectBtn').addEventListener('click', function() {
document.getElementById('projectFileInput').click();
});
// Handle project file import
document.getElementById('projectFileInput').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const projectData = JSON.parse(e.target.result);
d3Renderer.importProject(projectData);
} catch (error) {
console.error('Error parsing project file:', error);
alert('Error reading project file: ' + error.message);
}
};
reader.readAsText(file);
// Reset file input
event.target.value = '';
});
document.getElementById('exportBtn').addEventListener('click', function() {
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) {
alert('No diagram to export. Please render a diagram first.');
return;
}
const svgData = new XMLSerializer().serializeToString(svg);
// Create a high-resolution canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const scale = 4; // Increase this for higher resolution
canvas.width = svg.viewBox.baseVal.width * scale;
canvas.height = svg.viewBox.baseVal.height * scale;
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const pngFile = canvas.toDataURL('image/png');
const downloadLink = document.createElement('a');
downloadLink.download = 'mermaid_diagram_high_res.png';
downloadLink.href = pngFile;
downloadLink.click();
};
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
});
// Export as .mmd file
document.getElementById('exportMmdBtn').addEventListener('click', function() {
const mermaidCode = document.getElementById('mermaidInput').value;
if (!mermaidCode.trim()) {
alert('No diagram code to export.');
return;
}
// Create a blob with the mermaid code
const blob = new Blob([mermaidCode], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
// Create download link
const downloadLink = document.createElement('a');
downloadLink.download = 'mermaid_diagram.mmd';
downloadLink.href = url;
downloadLink.click();
// Clean up
URL.revokeObjectURL(url);
});
// Import .mmd file
document.getElementById('importMmdBtn').addEventListener('click', function() {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const contents = e.target.result;
document.getElementById('mermaidInput').value = contents;
renderMermaidDiagram();
};
reader.readAsText(file);
// Reset file input
event.target.value = '';
});
// Dark mode toggle functionality
document.addEventListener('DOMContentLoaded', function() {
// Check for saved dark mode preference
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
if (darkModeEnabled) {
document.body.classList.add('dark-mode');
document.getElementById('lightModeIcon').style.display = 'none';
document.getElementById('darkModeIcon').style.display = 'block';
// Update Mermaid theme for dark mode
updateMermaidTheme(true);
}
// Toggle dark mode when button is clicked
document.getElementById('darkModeToggle').addEventListener('click', function() {
document.body.classList.toggle('dark-mode');
const isDarkMode = document.body.classList.contains('dark-mode');
// Save preference and toggle icons
if (isDarkMode) {
localStorage.setItem('darkMode', 'enabled');
document.getElementById('lightModeIcon').style.display = 'none';
document.getElementById('darkModeIcon').style.display = 'block';
} else {
localStorage.setItem('darkMode', 'disabled');
document.getElementById('lightModeIcon').style.display = 'block';
document.getElementById('darkModeIcon').style.display = 'none';
}
// Update Mermaid theme based on dark mode
updateMermaidTheme(isDarkMode);
// Re-render the diagram with new theme
renderMermaidDiagram();
});
});
// Function to update Mermaid theme based on dark mode
function updateMermaidTheme(isDarkMode) {
// Always use light theme for diagrams regardless of site theme
// This ensures exported diagrams look good and consistent
mermaid.initialize({
startOnLoad: false,
theme: 'base',
themeVariables: {
primaryColor: '#ffc406',
primaryTextColor: '#000000',
primaryBorderColor: '#000000',
lineColor: '#000000',
secondaryColor: '#B8B9B9',
tertiaryColor: '#B8B9B9'
},
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
},
securityLevel: 'loose',
fontFamily: 'Montserrat, sans-serif',
fontSize: 14
});
}
// Mermaid Parser and Code Generator
class MermaidParser {
constructor() {
this.nodes = new Map();
this.edges = [];
this.diagramType = 'flowchart';
this.direction = 'TD';
}
parseCode(mermaidCode) {
this.nodes.clear();
this.edges = [];
const lines = mermaidCode.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('//'));
if (lines.length === 0) return;
// Parse first line to get diagram type and direction
const firstLine = lines[0];
if (firstLine.includes('graph') || firstLine.includes('flowchart')) {
this.diagramType = firstLine.includes('flowchart') ? 'flowchart' : 'graph';
const match = firstLine.match(/(TD|TB|BT|RL|LR|DT)/);
this.direction = match ? match[1] : 'TD';
}
// Parse nodes and edges
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
this.parseLine(line);
}
return {
nodes: this.nodes,
edges: this.edges,
diagramType: this.diagramType,
direction: this.direction
};
}
parseLine(line) {
console.log('Parsing line:', line);
// Updated pattern to handle complex Mermaid syntax:
// A[Christmas] -->|Get money| B(Go shopping)
// Node format: ID + optional [label] or (label) or {label}
const nodeWithLabel = '([A-Za-z0-9_]+)(?:\\[([^\\]]+)\\]|\\(([^\\)]+)\\)|\\{([^\\}]+)\\})?';
const connectionTypes = '(-->|---|--\\||-.->|==>\\||[=-]{2,}[>|]?)';
const edgeLabel = '(?:\\|([^|]+)\\|)?';
// Full pattern: NodeA -->|label| NodeB
const connectionPattern = new RegExp(
`${nodeWithLabel}\\s*${connectionTypes}${edgeLabel}\\s*${nodeWithLabel}`
);
console.log('Using pattern:', connectionPattern);
// Helper function to extract node info from regex groups
const extractNodeInfo = (nodeId, bracketLabel, parenLabel, braceLabel) => {
let text = nodeId; // Default to ID
let shape = 'rect'; // Default shape
if (bracketLabel) {
text = bracketLabel;
shape = 'rect';
console.log(`Node ${nodeId}: [${text}] → shape: rect`);
} else if (parenLabel) {
text = parenLabel;
shape = 'round';
console.log(`Node ${nodeId}: (${text}) → shape: round`);
} else if (braceLabel) {
text = braceLabel;
shape = 'diamond';
console.log(`Node ${nodeId}: {${text}} → shape: diamond`);
}
return { id: nodeId, text, shape };
};
// Parse connections
const connectionMatch = line.match(connectionPattern);
if (connectionMatch) {
console.log('Connection match groups:', connectionMatch);
// Extract groups from the complex regex
const fromNodeId = connectionMatch[1];
const fromBracketLabel = connectionMatch[2];
const fromParenLabel = connectionMatch[3];
const fromBraceLabel = connectionMatch[4];
const connectionType = connectionMatch[5];
const edgeLabel = connectionMatch[6] || '';
const toNodeId = connectionMatch[7];
const toBracketLabel = connectionMatch[8];
const toParenLabel = connectionMatch[9];
const toBraceLabel = connectionMatch[10];
console.log('Parsed connection:', {
from: fromNodeId,
to: toNodeId,
type: connectionType,
edgeLabel
});
// Extract node information
const fromNodeInfo = extractNodeInfo(fromNodeId, fromBracketLabel, fromParenLabel, fromBraceLabel);
const toNodeInfo = extractNodeInfo(toNodeId, toBracketLabel, toParenLabel, toBraceLabel);
console.log('From node info:', fromNodeInfo);
console.log('To node info:', toNodeInfo);
// Add/update nodes
if (!this.nodes.has(fromNodeId)) {
this.nodes.set(fromNodeId, {
...fromNodeInfo,
style: { fill: '#ffc406', stroke: '#000000', color: '#000000' },
position: { x: 0, y: 0 }
});
console.log('Added fromNode:', fromNodeId);
} else {
// Update existing node with new text if available
const existingNode = this.nodes.get(fromNodeId);
if (fromNodeInfo.text !== fromNodeId) {
existingNode.text = fromNodeInfo.text;
existingNode.shape = fromNodeInfo.shape;
}
}
if (!this.nodes.has(toNodeId)) {
this.nodes.set(toNodeId, {
...toNodeInfo,
style: { fill: '#ffc406', stroke: '#000000', color: '#000000' },
position: { x: 0, y: 0 }
});
console.log('Added toNode:', toNodeId);
} else {
// Update existing node with new text if available
const existingNode = this.nodes.get(toNodeId);
if (toNodeInfo.text !== toNodeId) {
existingNode.text = toNodeInfo.text;
existingNode.shape = toNodeInfo.shape;
}
}
const edge = {
from: fromNodeId,
to: toNodeId,
type: connectionType,
label: edgeLabel,
style: {
stroke: '#000000',
strokeWidth: 2,
strokeDasharray: 'none'
}
};
this.edges.push(edge);
console.log('Added edge:', edge);
} else {
console.log('No connection pattern matched for line:', line);
}
}
determineNodeShape(nodeString) {
console.log('Determining shape for:', nodeString);
if (nodeString.includes('((') && nodeString.includes('))')) return 'circle';
if (nodeString.includes('{') && nodeString.includes('}')) return 'diamond';
if (nodeString.includes('(') && nodeString.includes(')')) return 'round';
if (nodeString.includes('[') && nodeString.includes(']')) return 'rect';
return 'rect';
}
generateCode(nodes, edges, diagramType = 'flowchart', direction = 'TD', subgraphs = new Map()) {
let code = `${diagramType} ${direction}\n`;
const nodesInSubgraphs = new Set();
// Generate subgraphs first
for (const [subgraphName, subgraph] of subgraphs) {
code += `\n subgraph ${subgraphName}\n`;
// Add nodes that belong to this subgraph
for (const nodeId of subgraph.nodes) {
if (nodes.has(nodeId)) {
const node = nodes.get(nodeId);
const shapeStart = this.getShapeStart(node.shape);
const shapeEnd = this.getShapeEnd(node.shape);
code += ` ${nodeId}${shapeStart}${node.text}${shapeEnd}\n`;
nodesInSubgraphs.add(nodeId);
}
}
code += ` end\n`;
}
// Generate remaining nodes (not in subgraphs)
for (const [nodeId, node] of nodes) {
if (!nodesInSubgraphs.has(nodeId)) {
const shapeStart = this.getShapeStart(node.shape);
const shapeEnd = this.getShapeEnd(node.shape);
code += ` ${nodeId}${shapeStart}${node.text}${shapeEnd}\n`;
}
}
// Generate edges
if (edges.length > 0) {
code += '\n %% Connections\n';
edges.forEach(edge => {
const label = edge.label ? `|${edge.label}|` : '';
code += ` ${edge.from} ${edge.type}${label} ${edge.to}\n`;
});
}
// Generate styling section if nodes have custom colors
let hasCustomStyles = false;
let styleCode = '';
for (const [nodeId, node] of nodes) {
if (node.style.fill !== '#ffc406' || node.style.color !== '#000000') {
if (!hasCustomStyles) {
styleCode += '\n %% Styling\n';
hasCustomStyles = true;
}
styleCode += ` style ${nodeId} fill:${node.style.fill},color:${node.style.color}\n`;
}
}
// Add edge styling if any edges have custom styles
edges.forEach((edge, index) => {
if (edge.style.stroke !== '#000000' || edge.style.strokeWidth !== 2) {
if (!hasCustomStyles) {
styleCode += '\n %% Styling\n';
hasCustomStyles = true;
}
styleCode += ` linkStyle ${index} stroke:${edge.style.stroke},stroke-width:${edge.style.strokeWidth}\n`;
}
});
code += styleCode;
return code;
}
getShapeStart(shape) {
switch (shape) {
case 'round': return '(';
case 'diamond': return '{';
case 'circle': return '((';
default: return '[';
}
}
getShapeEnd(shape) {
switch (shape) {
case 'round': return ')';
case 'diamond': return '}';
case 'circle': return '))';
default: return ']';
}
}
}
// Interactive Editing System
class MermaidEditor {
constructor() {
this.isEditMode = false;
this.selectedElement = null;
this.diagramData = { nodes: new Map(), edges: [] };
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.parser = new MermaidParser();
this.nodePositions = new Map();
this.subgraphs = new Map(); // Store subgraph information
this.isLayoutLocked = false;
this.savedPositions = new Map(); // Store manually adjusted positions
}
toggleEditMode() {
this.isEditMode = !this.isEditMode;
const toggleBtn = document.getElementById('toggleEditMode');
const syncBtn = document.getElementById('syncCodeBtn');
if (this.isEditMode) {
toggleBtn.textContent = 'Disable Edit Mode';
toggleBtn.style.backgroundColor = '#ff6b6b';
syncBtn.style.display = 'inline-block';
document.getElementById('testPanelBtn').style.display = 'inline-block';
document.getElementById('addSubgraphBtn').style.display = 'inline-block';
document.getElementById('exportJsonBtn').style.display = 'inline-block';
document.getElementById('lockLayoutBtn').style.display = 'inline-block';
this.parseCurrentCode();
this.enableInteractivity();
} else {
toggleBtn.textContent = 'Enable Edit Mode';
toggleBtn.style.backgroundColor = '';
syncBtn.style.display = 'none';
document.getElementById('testPanelBtn').style.display = 'none';
document.getElementById('addSubgraphBtn').style.display = 'none';
document.getElementById('exportJsonBtn').style.display = 'none';
document.getElementById('lockLayoutBtn').style.display = 'none';
this.disableInteractivity();
this.hidePropertyPanel();
}
}
parseCurrentCode() {
const mermaidCode = document.getElementById('mermaidInput').value;
console.log('Parsing current code:', mermaidCode);
const parsedData = this.parser.parseCode(mermaidCode);
console.log('Parsed data:', parsedData);
if (parsedData) {
this.diagramData = parsedData;
console.log('Set diagram data:', this.diagramData);
this.syncVisualPositions();
} else {
console.log('Failed to parse Mermaid code');
}
}
syncVisualPositions() {
// Capture current visual positions of nodes
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) return;
const nodes = svg.querySelectorAll('.node');
console.log('Syncing positions for', nodes.length, 'nodes');
nodes.forEach(nodeElement => {
const nodeId = this.extractNodeId(nodeElement);
if (nodeId && this.diagramData.nodes.has(nodeId)) {
const transform = nodeElement.getAttribute('transform');
if (transform) {
const match = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
if (match) {
const node = this.diagramData.nodes.get(nodeId);
node.position = {
x: parseFloat(match[1]),
y: parseFloat(match[2])
};
console.log(`Synced ${nodeId} position to (${node.position.x}, ${node.position.y})`);
}
}
}
});
console.log('Final node positions:',
Array.from(this.diagramData.nodes.entries()).map(([id, node]) =>
({ id, position: node.position }))
);
}
extractNodeId(nodeElement) {
// Debug the node element structure
console.log('Extracting ID from node:', nodeElement);
console.log('Node classes:', nodeElement.classList);
console.log('Node id attribute:', nodeElement.getAttribute('id'));
// Try multiple methods to extract node ID
const id = nodeElement.getAttribute('id');
if (id) {
// Clean up Mermaid-generated IDs
const patterns = [
/flowchart-([A-Za-z0-9_]+)-\d+/, // flowchart-A-123
/([A-Za-z0-9_]+)-\d+$/, // A-123
/flowchart-([A-Za-z0-9_]+)$/, // flowchart-A
];
for (const pattern of patterns) {
const match = id.match(pattern);
if (match) {
console.log('Extracted node ID:', match[1]);
return match[1];
}
}
// Fallback: use the ID as-is or extract last part
const fallback = id.split('-').pop();
console.log('Fallback node ID:', fallback);
return fallback;
}
// Try to find ID from class names
const classList = Array.from(nodeElement.classList);
for (const className of classList) {
if (className.startsWith('node-')) {
const nodeId = className.replace('node-', '');
console.log('Found node ID from class:', nodeId);
return nodeId;
}
}
console.log('Could not extract node ID');
return null;
}
enableInteractivity() {
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) return;
// Add event listeners to all nodes and edges
this.addEventListenersToElements(svg);
}
disableInteractivity() {
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) return;
// Remove event listeners and classes
svg.querySelectorAll('.mermaid-node, .mermaid-edge').forEach(el => {
el.classList.remove('mermaid-node', 'mermaid-edge', 'selected');
});
this.selectedElement = null;
}
addEventListenersToElements(svg) {
console.log('Adding event listeners to SVG:', svg);
// Debug: Log the SVG structure
console.log('SVG innerHTML preview:', svg.innerHTML.substring(0, 500));
// Find all flowchart nodes and edges
const nodes = svg.querySelectorAll('.node');
// Try different edge selectors (more comprehensive)
let edges = svg.querySelectorAll('.edgePath');
if (edges.length === 0) edges = svg.querySelectorAll('.edge');
if (edges.length === 0) edges = svg.querySelectorAll('.flowchart-link');
if (edges.length === 0) edges = svg.querySelectorAll('path[marker-end]');
// If still no edges, try to find any clickable path elements
if (edges.length === 0) {
const allPaths = svg.querySelectorAll('path');
edges = Array.from(allPaths).filter(path => {
const d = path.getAttribute('d');
return d && d.includes('L') && !path.classList.contains('arrowMarkerPath');
});
}
console.log('Found nodes:', nodes.length);
console.log('Found edges:', edges.length);
// Debug: Show all path elements to find edge selector
const allPaths = svg.querySelectorAll('path');
console.log('All path elements:', allPaths.length);
if (allPaths.length > 0) {
console.log('First path element:', allPaths[0]);
console.log('First path classes:', allPaths[0].classList);
}
// Debug first node structure
if (nodes.length > 0) {
console.log('First node structure:', nodes[0]);
console.log('First node HTML:', nodes[0].outerHTML);
}
nodes.forEach(node => {
node.classList.add('mermaid-node');
node.addEventListener('click', (e) => this.selectElement(e, 'node'));
node.addEventListener('mousedown', (e) => this.startDrag(e, node));
});
edges.forEach(edge => {
edge.classList.add('mermaid-edge');
edge.addEventListener('click', (e) => this.selectElement(e, 'edge'));
});
// Add global mouse events for dragging
svg.addEventListener('mousemove', (e) => this.drag(e));
svg.addEventListener('mouseup', () => this.endDrag());
}
selectElement(event, type) {
if (!this.isEditMode) {
console.log('Not in edit mode, ignoring click');
return;
}
event.stopPropagation();
console.log('Element selected:', type, event.currentTarget);
// Remove previous selection
if (this.selectedElement) {
this.selectedElement.classList.remove('selected');
}
// Select new element
this.selectedElement = event.currentTarget;
this.selectedElement.classList.add('selected');
console.log('Element now selected, showing property panel');
// Show property panel
this.showPropertyPanel(type, this.selectedElement);
}
showPropertyPanel(type, element) {
const panel = document.getElementById('propertyPanel');
const content = document.getElementById('propertyContent');
console.log('Showing property panel for:', type, element);
if (type === 'node') {
// Look for text in foreignObject (Mermaid v10+ structure)
const foreignObjects = element.querySelectorAll('foreignObject');
const textElements = element.querySelectorAll('text');
const nodeLabelElements = element.querySelectorAll('.nodeLabel');
console.log('Found foreignObject elements:', foreignObjects);
console.log('Found text elements:', textElements);
console.log('Found nodeLabel elements:', nodeLabelElements);
let currentText = '';
// Try different ways to extract text
if (nodeLabelElements.length > 0) {
// Method 1: Extract from .nodeLabel elements
nodeLabelElements.forEach(labelEl => {
const text = labelEl.textContent.trim();
if (text) {
currentText += (currentText ? ' ' : '') + text;
}
});
} else if (foreignObjects.length > 0) {
// Method 2: Extract from foreignObject
foreignObjects.forEach(fo => {
const text = fo.textContent.trim();
if (text) {
currentText += (currentText ? ' ' : '') + text;
}
});
} else if (textElements.length > 0) {
// Method 3: Traditional SVG text elements
textElements.forEach(textEl => {
const text = textEl.textContent.trim();
if (text && !text.match(/^flowchart-/)) {
currentText += (currentText ? ' ' : '') + text;
}
});
}
console.log('Extracted text:', currentText);
const nodeId = this.extractNodeId(element);
const shapeElement = element.querySelector('rect, circle, polygon, path');
// Get current colors from styles or data model
let currentFill = '#ffc406';
let currentTextColor = '#000000';
if (nodeId && this.diagramData.nodes.has(nodeId)) {
const node = this.diagramData.nodes.get(nodeId);
currentFill = node.style.fill || currentFill;
currentTextColor = node.style.color || currentTextColor;
// Use text from data model if available
if (node.text && !currentText) {
currentText = node.text;
}
}
content.innerHTML = `
<label>Node ID:</label>
<input type="text" id="nodeId" value="${nodeId || 'unknown'}" readonly>
<label>Node Text:</label>
<input type="text" id="nodeText" value="${currentText}">
<label>Node Color:</label>
<input type="color" id="nodeColor" value="${currentFill}">
<label>Text Color:</label>
<input type="color" id="textColor" value="${currentTextColor}">
<button onclick="mermaidEditor.updateNodeProperties()">Update Node</button>
<button onclick="mermaidEditor.deleteElement()">Delete Node</button>
<hr>
<label>Grouping:</label>
<input type="text" id="subgraphName" placeholder="Group name (optional)">
<button onclick="mermaidEditor.addToSubgraph('${nodeId}')">Add to Group</button>
`;
} else if (type === 'edge') {
const pathElement = element.querySelector('path');
let currentStroke = '#000000';
let currentWidth = 2;
let currentStyle = 'solid';
let currentLabel = '';
// Try to find edge in data model to get current label
const edgeIndex = Array.from(element.parentElement.children).indexOf(element);
if (this.diagramData.edges[edgeIndex]) {
currentLabel = this.diagramData.edges[edgeIndex].label || '';
}
if (pathElement) {
currentStroke = pathElement.style.stroke || currentStroke;
currentWidth = parseInt(pathElement.style.strokeWidth) || currentWidth;
const dashArray = pathElement.style.strokeDasharray;
if (dashArray === '5,5') currentStyle = 'dashed';
else if (dashArray === '2,2') currentStyle = 'dotted';
}
content.innerHTML = `
<label>Edge Label:</label>
<input type="text" id="edgeLabel" value="${currentLabel}">
<label>Edge Style:</label>
<select id="edgeStyle">
<option value="solid" ${currentStyle === 'solid' ? 'selected' : ''}>Solid</option>
<option value="dashed" ${currentStyle === 'dashed' ? 'selected' : ''}>Dashed</option>
<option value="dotted" ${currentStyle === 'dotted' ? 'selected' : ''}>Dotted</option>
</select>
<label>Edge Color:</label>
<input type="color" id="edgeColor" value="${currentStroke}">
<label>Edge Width:</label>
<input type="range" id="edgeWidth" min="1" max="8" value="${currentWidth}">
<span id="widthValue">${currentWidth}px</span>
<button onclick="mermaidEditor.updateEdgeProperties()">Update Edge</button>
<button onclick="mermaidEditor.deleteElement()">Delete Edge</button>
`;
// Add live width display
setTimeout(() => {
const widthSlider = document.getElementById('edgeWidth');
const widthDisplay = document.getElementById('widthValue');
if (widthSlider && widthDisplay) {
widthSlider.addEventListener('input', () => {
widthDisplay.textContent = widthSlider.value + 'px';
});
}
}, 100);
}
panel.classList.add('visible');
panel.style.display = 'block';
console.log('Property panel should be visible now');
}
hidePropertyPanel() {
const panel = document.getElementById('propertyPanel');
panel.classList.remove('visible');
panel.style.display = 'none';
}
startDrag(event, element) {
if (!this.isEditMode) return;
event.preventDefault();
event.stopPropagation();
this.isDragging = true;
this.selectedElement = element;
// Get current transform values
const transform = element.getAttribute('transform') || 'translate(0, 0)';
const match = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
const currentX = match ? parseFloat(match[1]) : 0;
const currentY = match ? parseFloat(match[2]) : 0;
const svgRect = document.querySelector('#mermaidOutput svg').getBoundingClientRect();
// Calculate offset from current transform position
this.dragOffset.x = event.clientX - svgRect.left - currentX;
this.dragOffset.y = event.clientY - svgRect.top - currentY;
element.style.cursor = 'grabbing';
console.log('Started dragging node:', this.extractNodeId(element));
}
drag(event) {
if (!this.isDragging || !this.selectedElement) return;
event.preventDefault();
const svg = document.querySelector('#mermaidOutput svg');
const svgRect = svg.getBoundingClientRect();
// Calculate new position
const newX = event.clientX - svgRect.left - this.dragOffset.x;
const newY = event.clientY - svgRect.top - this.dragOffset.y;
// Apply transform
this.selectedElement.style.transform = `translate(${newX}px, ${newY}px)`;
// Update position in data model
const nodeId = this.extractNodeId(this.selectedElement);
if (nodeId && this.diagramData.nodes.has(nodeId)) {
const node = this.diagramData.nodes.get(nodeId);
node.position = { x: newX, y: newY };
}
}
endDrag() {
if (this.isDragging) {
this.isDragging = false;
if (this.selectedElement) {
this.selectedElement.style.cursor = 'pointer';
// Trigger code sync when dragging ends
const nodeId = this.extractNodeId(this.selectedElement);
if (nodeId) {
console.log(`Node ${nodeId} moved to new position`);
// Note: We don't update code for position changes since
// Mermaid handles layout automatically
}
}
}
}
updateNodeProperties() {
if (!this.selectedElement) return;
const nodeText = document.getElementById('nodeText').value;
const nodeColor = document.getElementById('nodeColor').value;
const textColor = document.getElementById('textColor').value;
const nodeId = this.extractNodeId(this.selectedElement);
console.log('Updating node properties:', { nodeId, nodeText, nodeColor, textColor });
// Update text in foreignObject (Mermaid v10+ structure)
const foreignObjects = this.selectedElement.querySelectorAll('foreignObject');
const textElements = this.selectedElement.querySelectorAll('text');
const nodeLabelElements = this.selectedElement.querySelectorAll('.nodeLabel');
const paragraphElements = this.selectedElement.querySelectorAll('p');
console.log('Found for text update - foreignObjects:', foreignObjects.length, 'textElements:', textElements.length, 'nodeLabels:', nodeLabelElements.length, 'paragraphs:', paragraphElements.length);
let textUpdated = false;
// Method 1: Update paragraph elements (most likely in Mermaid v10+)
if (paragraphElements.length > 0) {
paragraphElements.forEach(pEl => {
pEl.textContent = nodeText;
pEl.style.color = textColor;
});
textUpdated = true;
console.log('Updated paragraph elements');
}
// Method 2: Update .nodeLabel elements
if (nodeLabelElements.length > 0 && !textUpdated) {
nodeLabelElements.forEach(labelEl => {
labelEl.textContent = nodeText;
labelEl.style.color = textColor;
});
textUpdated = true;
console.log('Updated nodeLabel elements');
}
// Method 3: Update traditional SVG text elements
if (textElements.length > 0 && !textUpdated) {
textElements.forEach(textEl => {
textEl.textContent = nodeText;
textEl.style.fill = textColor;
});
textUpdated = true;
console.log('Updated text elements');
}
if (!textUpdated) {
console.log('No text elements found to update');
}
// Update node background/shape color
const shapeElements = this.selectedElement.querySelectorAll('rect, circle, polygon, path');
console.log('Updating shape elements:', shapeElements);
shapeElements.forEach(shapeEl => {
shapeEl.style.fill = nodeColor;
shapeEl.style.stroke = nodeColor; // Also update border color
});
// Update data model
if (nodeId && this.diagramData.nodes.has(nodeId)) {
const node = this.diagramData.nodes.get(nodeId);
node.text = nodeText;
node.style.fill = nodeColor;
node.style.color = textColor;
console.log('Updated data model for node:', nodeId, node);
}
// Update the Mermaid code
this.updateCodeFromData();
}
updateEdgeProperties() {
if (!this.selectedElement) return;
const edgeLabel = document.getElementById('edgeLabel').value;
const edgeColor = document.getElementById('edgeColor').value;
const edgeWidth = document.getElementById('edgeWidth').value;
const edgeStyle = document.getElementById('edgeStyle').value;
console.log('Updating edge properties:', { edgeLabel, edgeColor, edgeWidth, edgeStyle });
// Update visual appearance
const pathElement = this.selectedElement.querySelector('path');
if (pathElement) {
pathElement.style.stroke = edgeColor;
pathElement.style.strokeWidth = edgeWidth + 'px';
switch(edgeStyle) {
case 'dashed':
pathElement.style.strokeDasharray = '5,5';
break;
case 'dotted':
pathElement.style.strokeDasharray = '2,2';
break;
default:
pathElement.style.strokeDasharray = 'none';
}
}
// Update data model - find edge by position in DOM
const edgeIndex = Array.from(this.selectedElement.parentElement.children).indexOf(this.selectedElement);
const pathElements = document.querySelectorAll('#mermaidOutput svg path[marker-end]');
const actualEdgeIndex = Array.from(pathElements).indexOf(pathElement);
console.log('Edge indices - DOM:', edgeIndex, 'Path:', actualEdgeIndex);
console.log('Available edges:', this.diagramData.edges);
if (actualEdgeIndex >= 0 && actualEdgeIndex < this.diagramData.edges.length) {
const edge = this.diagramData.edges[actualEdgeIndex];
console.log('Updating edge:', edge);
edge.label = edgeLabel;
edge.style.stroke = edgeColor;
edge.style.strokeWidth = parseInt(edgeWidth);
edge.style.strokeDasharray = edgeStyle === 'solid' ? 'none' :
edgeStyle === 'dashed' ? '5,5' : '2,2';
console.log('Updated edge:', edge);
}
// Update the Mermaid code
this.updateCodeFromData();
}
deleteElement() {
if (!this.selectedElement) return;
const nodeId = this.extractNodeId(this.selectedElement);
// Remove from data model
if (nodeId) {
// Remove node
this.diagramData.nodes.delete(nodeId);
// Remove edges connected to this node
this.diagramData.edges = this.diagramData.edges.filter(edge =>
edge.from !== nodeId && edge.to !== nodeId
);
} else {
// Handle edge deletion
const edgeId = this.extractEdgeId(this.selectedElement);
this.diagramData.edges = this.diagramData.edges.filter(edge =>
edge.id !== edgeId
);
}
// Remove from DOM
this.selectedElement.remove();
this.selectedElement = null;
this.hidePropertyPanel();
// Update the Mermaid code
this.updateCodeFromData();
}
extractEdgeId(edgeElement) {
// Try to extract edge identifier from classes or attributes
const classes = edgeElement.className.baseVal || edgeElement.className;
const match = classes.match(/edge-(\w+)-(\w+)/);
return match ? `${match[1]}-${match[2]}` : null;
}
updateCodeFromData() {
if (!this.diagramData || !this.diagramData.nodes) return;
console.log('Updating code from data model:');
console.log('Nodes:', this.diagramData.nodes);
console.log('Edges:', this.diagramData.edges);
console.log('Diagram type:', this.diagramData.diagramType);
console.log('Direction:', this.diagramData.direction);
// Generate new Mermaid code from current data model
const newCode = this.parser.generateCode(
this.diagramData.nodes,
this.diagramData.edges,
this.diagramData.diagramType,
this.diagramData.direction,
this.subgraphs
);
console.log('Generated new code:');
console.log(newCode);
// Update textarea
const textarea = document.getElementById('mermaidInput');
const oldCode = textarea.value;
textarea.value = newCode;
console.log('Old code:');
console.log(oldCode);
console.log('New code:');
console.log(newCode);
console.log('Code synchronized with visual changes');
}
drag(event) {
if (!this.isDragging || !this.selectedElement) return;
event.preventDefault();
const svg = document.querySelector('#mermaidOutput svg');
const svgRect = svg.getBoundingClientRect();
// Calculate new position relative to SVG
const newX = event.clientX - svgRect.left - this.dragOffset.x;
const newY = event.clientY - svgRect.top - this.dragOffset.y;
// Update the transform attribute directly
this.selectedElement.setAttribute('transform', `translate(${newX}, ${newY})`);
// Update connected edges
const nodeId = this.extractNodeId(this.selectedElement);
if (nodeId) {
this.updateConnectedEdges(nodeId, newX, newY);
// Update position in data model
if (this.diagramData.nodes.has(nodeId)) {
const node = this.diagramData.nodes.get(nodeId);
node.position = { x: newX, y: newY };
}
}
}
updateConnectedEdges(nodeId, nodeX, nodeY) {
// Find all edges connected to this node
const connectedEdges = this.diagramData.edges.filter(edge =>
edge.from === nodeId || edge.to === nodeId
);
console.log(`Updating ${connectedEdges.length} edges connected to node ${nodeId}`);
connectedEdges.forEach((edge, index) => {
// Find the corresponding path element in the SVG
const edgeIndex = this.diagramData.edges.indexOf(edge);
const pathElements = document.querySelectorAll('#mermaidOutput svg path[marker-end]');
if (edgeIndex < pathElements.length) {
const pathElement = pathElements[edgeIndex];
this.updateEdgePath(pathElement, edge, nodeId, nodeX, nodeY);
}
});
}
updateEdgePath(pathElement, edge, movedNodeId, nodeX, nodeY) {
// Get positions of both nodes
const fromNode = this.diagramData.nodes.get(edge.from);
const toNode = this.diagramData.nodes.get(edge.to);
if (!fromNode || !toNode) return;
// Use updated position for the moved node, stored position for the other
const fromPos = edge.from === movedNodeId ?
{ x: nodeX, y: nodeY } : fromNode.position || { x: 0, y: 0 };
const toPos = edge.to === movedNodeId ?
{ x: nodeX, y: nodeY } : toNode.position || { x: 0, y: 0 };
// If we don't have the other node's position, try to get it from the DOM
if (edge.from !== movedNodeId && (!fromNode.position || (fromNode.position.x === 0 && fromNode.position.y === 0))) {
const fromElement = document.getElementById(`flowchart-${edge.from}-${document.querySelectorAll(`[id^="flowchart-${edge.from}-"]`).length - 1}`);
if (fromElement) {
const transform = fromElement.getAttribute('transform');
const match = transform && transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
if (match) {
fromPos.x = parseFloat(match[1]);
fromPos.y = parseFloat(match[2]);
}
}
}
if (edge.to !== movedNodeId && (!toNode.position || (toNode.position.x === 0 && toNode.position.y === 0))) {
const toElement = document.getElementById(`flowchart-${edge.to}-${document.querySelectorAll(`[id^="flowchart-${edge.to}-"]`).length - 1}`);
if (toElement) {
const transform = toElement.getAttribute('transform');
const match = transform && transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
if (match) {
toPos.x = parseFloat(match[1]);
toPos.y = parseFloat(match[2]);
}
}
}
// Create a simple straight line path (Mermaid uses more complex curves, but this is a start)
const pathData = `M ${fromPos.x} ${fromPos.y} L ${toPos.x} ${toPos.y}`;
pathElement.setAttribute('d', pathData);
console.log(`Updated path for edge ${edge.from} -> ${edge.to}:`, pathData);
}
addToSubgraph(nodeId) {
const subgraphNameInput = document.getElementById('subgraphName');
let subgraphName = subgraphNameInput ? subgraphNameInput.value.trim() : '';
if (!subgraphName) {
subgraphName = `group_${this.subgraphs.size + 1}`;
}
console.log(`Adding node ${nodeId} to subgraph ${subgraphName}`);
// Create or update subgraph
if (!this.subgraphs.has(subgraphName)) {
this.subgraphs.set(subgraphName, {
name: subgraphName,
nodes: new Set(),
direction: 'TD'
});
}
this.subgraphs.get(subgraphName).nodes.add(nodeId);
// Update the Mermaid code with subgraphs
this.updateCodeFromData();
alert(`Added node ${nodeId} to group "${subgraphName}"`);
}
// B) Export functionality for position-aware formats
exportLayoutData() {
const layoutData = {
nodes: [],
edges: [],
subgraphs: [],
metadata: {
diagramType: this.diagramData.diagramType,
direction: this.diagramData.direction,
exportedAt: new Date().toISOString()
}
};
// Export nodes with positions
for (const [nodeId, node] of this.diagramData.nodes) {
layoutData.nodes.push({
id: nodeId,
text: node.text,
shape: node.shape,
position: node.position || { x: 0, y: 0 },
style: node.style
});
}
// Export edges
layoutData.edges = this.diagramData.edges.map(edge => ({
from: edge.from,
to: edge.to,
label: edge.label,
type: edge.type,
style: edge.style
}));
// Export subgraphs
for (const [subgraphName, subgraph] of this.subgraphs) {
layoutData.subgraphs.push({
name: subgraphName,
nodes: Array.from(subgraph.nodes),
direction: subgraph.direction
});
}
return layoutData;
}
downloadLayoutData() {
const layoutData = this.exportLayoutData();
const blob = new Blob([JSON.stringify(layoutData, null, 2)],
{ type: 'application/json' });
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.download = 'mermaid_layout_export.json';
downloadLink.href = url;
downloadLink.click();
URL.revokeObjectURL(url);
console.log('Layout data exported:', layoutData);
}
toggleLayoutLock() {
this.isLayoutLocked = !this.isLayoutLocked;
const lockBtn = document.getElementById('lockLayoutBtn');
if (this.isLayoutLocked) {
lockBtn.textContent = 'Unlock Layout';
lockBtn.style.backgroundColor = '#ff6b6b';
// Save current positions
this.saveCurrentPositions();
alert('Layout locked! Manual positions will be preserved when syncing/rendering.');
} else {
lockBtn.textContent = 'Lock Layout';
lockBtn.style.backgroundColor = '';
alert('Layout unlocked. Mermaid will use automatic positioning.');
}
}
saveCurrentPositions() {
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) return;
const nodes = svg.querySelectorAll('.node');
console.log('Saving positions for', nodes.length, 'nodes');
nodes.forEach(nodeElement => {
const nodeId = this.extractNodeId(nodeElement);
if (nodeId) {
const transform = nodeElement.getAttribute('transform');
if (transform) {
const match = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
if (match) {
const position = {
x: parseFloat(match[1]),
y: parseFloat(match[2])
};
this.savedPositions.set(nodeId, position);
console.log(`Saved position for ${nodeId}:`, position);
}
}
}
});
console.log('All saved positions:', Array.from(this.savedPositions.entries()));
}
restoreSavedPositions() {
if (!this.isLayoutLocked || this.savedPositions.size === 0) {
console.log('Not restoring positions - layout not locked or no saved positions');
return;
}
console.log('Restoring saved positions...');
const svg = document.querySelector('#mermaidOutput svg');
if (!svg) return;
const nodes = svg.querySelectorAll('.node');
let restoredCount = 0;
nodes.forEach(nodeElement => {
const nodeId = this.extractNodeId(nodeElement);
if (nodeId && this.savedPositions.has(nodeId)) {
const savedPos = this.savedPositions.get(nodeId);
nodeElement.setAttribute('transform', `translate(${savedPos.x}, ${savedPos.y})`);
// Also update connected edges
this.updateConnectedEdges(nodeId, savedPos.x, savedPos.y);
restoredCount++;
console.log(`Restored position for ${nodeId} to (${savedPos.x}, ${savedPos.y})`);
}
});
console.log(`Restored positions for ${restoredCount} nodes`);
}
}
// D3.js Renderer - Hybrid approach using existing Mermaid parser
class D3Renderer {
constructor() {
this.svg = null;
this.width = 800;
this.height = 600;
this.parser = new MermaidParser();
this.simulation = null;
this.nodes = [];
this.edges = [];
}
render(mermaidCode) {
console.log('D3 Rendering Mermaid code:', mermaidCode);
// Parse using existing Mermaid parser
const parsedData = this.parser.parseCode(mermaidCode);
if (!parsedData) {
console.error('Failed to parse Mermaid code');
return;
}
console.log('Parsed data for D3:', parsedData);
// Clear previous render
d3.select('#mermaidOutput').selectAll('*').remove();
// Create SVG
this.svg = d3.select('#mermaidOutput')
.append('svg')
.attr('width', this.width)
.attr('height', this.height)
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
.style('border', '1px solid #ccc');
// Create arrow marker
this.svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// Convert parsed data to D3 format
this.prepareD3Data(parsedData);
// Create force simulation
this.createForceSimulation();
// Draw edges
this.drawEdges();
// Draw nodes
this.drawNodes();
console.log('D3 render complete');
}
prepareD3Data(parsedData) {
// Load saved positions from localStorage
const savedPositions = this.loadSavedPositions();
// Convert nodes
this.nodes = Array.from(parsedData.nodes.values()).map(node => {
const savedPos = savedPositions.get(node.id);
return {
id: node.id,
text: node.text,
shape: node.shape,
style: node.style,
x: savedPos?.x || node.position?.x || (Math.random() * (this.width - 200)) + 100,
y: savedPos?.y || node.position?.y || (Math.random() * (this.height - 200)) + 100,
fx: savedPos?.fx || null, // fixed x (for dragging)
fy: savedPos?.fy || null // fixed y (for dragging)
};
});
// Convert edges
this.edges = parsedData.edges.map(edge => ({
source: edge.from,
target: edge.to,
label: edge.label,
type: edge.type,
style: edge.style
}));
console.log('D3 nodes with positions:', this.nodes);
console.log('D3 edges:', this.edges);
}
loadSavedPositions() {
const savedPositions = new Map();
try {
const positionsData = localStorage.getItem('d3_node_positions');
if (positionsData) {
const positions = JSON.parse(positionsData);
Object.keys(positions).forEach(nodeId => {
savedPositions.set(nodeId, positions[nodeId]);
});
console.log('Loaded saved positions:', savedPositions);
}
} catch (error) {
console.error('Error loading saved positions:', error);
}
return savedPositions;
}
saveCurrentPositions() {
const positions = {};
this.nodes.forEach(node => {
positions[node.id] = {
x: node.x,
y: node.y,
fx: node.fx,
fy: node.fy
};
});
try {
localStorage.setItem('d3_node_positions', JSON.stringify(positions));
console.log('Saved node positions to localStorage:', positions);
} catch (error) {
console.error('Error saving positions:', error);
}
}
createForceSimulation() {
this.simulation = d3.forceSimulation(this.nodes)
.force('link', d3.forceLink(this.edges).id(d => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
.force('collision', d3.forceCollide().radius(60));
}
drawEdges() {
const linkContainer = this.svg.append('g').attr('class', 'links');
this.linkElements = linkContainer.selectAll('line')
.data(this.edges)
.enter().append('line')
.attr('stroke', d => d.style.stroke || '#000')
.attr('stroke-width', d => d.style.strokeWidth || 2)
.attr('marker-end', 'url(#arrowhead)')
.style('cursor', 'pointer')
.on('click', (event, d) => {
console.log('Edge clicked:', d);
this.selectEdge(event.currentTarget, d);
});
// Add edge labels
this.labelElements = linkContainer.selectAll('text')
.data(this.edges.filter(d => d.label))
.enter().append('text')
.text(d => d.label)
.attr('font-size', 12)
.attr('fill', '#333')
.attr('text-anchor', 'middle')
.style('pointer-events', 'none');
}
drawNodes() {
const nodeContainer = this.svg.append('g').attr('class', 'nodes');
const nodeGroups = nodeContainer.selectAll('g')
.data(this.nodes)
.enter().append('g')
.attr('class', 'node-group')
.style('cursor', 'pointer')
.call(this.createDragBehavior());
// Add node shapes
nodeGroups.each((d, i, nodes) => {
const group = d3.select(nodes[i]);
this.addNodeShape(group, d);
});
// Add node text
nodeGroups.append('text')
.text(d => d.text)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-family', 'Montserrat, sans-serif')
.attr('font-size', 14)
.attr('fill', d => d.style.color || '#000')
.style('pointer-events', 'none');
// Click handler for nodes
nodeGroups.on('click', (event, d) => {
console.log('Node clicked:', d);
this.selectNode(event.currentTarget, d);
});
this.nodeElements = nodeGroups;
// Update positions on simulation tick
this.simulation.on('tick', () => {
this.linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
this.labelElements
.attr('x', d => (d.source.x + d.target.x) / 2)
.attr('y', d => (d.source.y + d.target.y) / 2);
this.nodeElements
.attr('transform', d => `translate(${d.x}, ${d.y})`);
});
}
addNodeShape(group, d) {
const width = Math.max(80, d.text.length * 8);
const height = 40;
switch (d.shape) {
case 'round':
group.append('ellipse')
.attr('rx', width / 2)
.attr('ry', height / 2)
.attr('fill', d.style.fill || '#ffc406')
.attr('stroke', d.style.stroke || '#000')
.attr('stroke-width', 2);
break;
case 'diamond':
group.append('polygon')
.attr('points', `0,${-height/2} ${width/2},0 0,${height/2} ${-width/2},0`)
.attr('fill', d.style.fill || '#ffc406')
.attr('stroke', d.style.stroke || '#000')
.attr('stroke-width', 2);
break;
default: // rectangle
group.append('rect')
.attr('x', -width / 2)
.attr('y', -height / 2)
.attr('width', width)
.attr('height', height)
.attr('fill', d.style.fill || '#ffc406')
.attr('stroke', d.style.stroke || '#000')
.attr('stroke-width', 2)
.attr('rx', 5);
break;
}
}
createDragBehavior() {
return d3.drag()
.on('start', (event, d) => {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
console.log('Drag start:', d.id);
})
.on('drag', (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on('end', (event, d) => {
if (!event.active) this.simulation.alphaTarget(0);
// Keep nodes fixed at dragged position
// d.fx = null; d.fy = null; // Uncomment to allow nodes to float freely
console.log(`Node ${d.id} positioned at (${d.fx}, ${d.fy})`);
// Auto-save positions when dragging ends
this.saveCurrentPositions();
});
}
selectNode(element, nodeData) {
// Clear previous selections
this.clearSelections();
// Highlight selected node
d3.select(element).select('rect, ellipse, polygon')
.attr('stroke', '#ff6b6b')
.attr('stroke-width', 3)
.style('filter', 'drop-shadow(0 0 5px rgba(255, 107, 107, 0.5))');
this.selectedElement = element;
this.selectedData = nodeData;
// Show property panel using existing MermaidEditor method
this.showNodePropertyPanel(nodeData);
}
selectEdge(element, edgeData) {
// Clear previous selections
this.clearSelections();
// Highlight selected edge
d3.select(element)
.attr('stroke', '#ff6b6b')
.attr('stroke-width', 3);
this.selectedElement = element;
this.selectedData = edgeData;
// Show property panel using existing MermaidEditor method
this.showEdgePropertyPanel(edgeData);
}
clearSelections() {
// Clear node selections
this.svg.selectAll('.node-group rect, .node-group ellipse, .node-group polygon')
.attr('stroke', '#000')
.attr('stroke-width', 2)
.style('filter', 'none');
// Clear edge selections
this.svg.selectAll('.links line')
.attr('stroke', d => d.style.stroke || '#000')
.attr('stroke-width', d => d.style.strokeWidth || 2);
this.selectedElement = null;
this.selectedData = null;
}
showNodePropertyPanel(nodeData) {
const panel = document.getElementById('propertyPanel');
const content = document.getElementById('propertyContent');
content.innerHTML = `
<label>Node ID:</label>
<input type="text" id="d3NodeId" value="${nodeData.id}" readonly>
<label>Node Text:</label>
<input type="text" id="d3NodeText" value="${nodeData.text}">
<label>Node Shape:</label>
<select id="d3NodeShape">
<option value="rect" ${nodeData.shape === 'rect' ? 'selected' : ''}>Rectangle</option>
<option value="round" ${nodeData.shape === 'round' ? 'selected' : ''}>Circle</option>
<option value="diamond" ${nodeData.shape === 'diamond' ? 'selected' : ''}>Diamond</option>
</select>
<label>Node Color:</label>
<input type="color" id="d3NodeColor" value="${nodeData.style.fill || '#ffc406'}">
<label>Text Color:</label>
<input type="color" id="d3TextColor" value="${nodeData.style.color || '#000000'}">
<button onclick="d3Renderer.updateNodeProperties()">Update Node</button>
<button onclick="d3Renderer.deleteNode()">Delete Node</button>
`;
panel.classList.add('visible');
panel.style.display = 'block';
}
showEdgePropertyPanel(edgeData) {
const panel = document.getElementById('propertyPanel');
const content = document.getElementById('propertyContent');
content.innerHTML = `
<label>From:</label>
<input type="text" value="${edgeData.source.id || edgeData.source}" readonly>
<label>To:</label>
<input type="text" value="${edgeData.target.id || edgeData.target}" readonly>
<label>Edge Label:</label>
<input type="text" id="d3EdgeLabel" value="${edgeData.label || ''}">
<label>Edge Type:</label>
<select id="d3EdgeType">
<option value="-->" ${edgeData.type === '-->' ? 'selected' : ''}>Arrow (-->)</option>
<option value="---" ${edgeData.type === '---' ? 'selected' : ''}>Line (---)</option>
<option value="-..->" ${edgeData.type === '-..->' ? 'selected' : ''}>Dotted (-.->)</option>
</select>
<label>Edge Color:</label>
<input type="color" id="d3EdgeColor" value="${edgeData.style.stroke || '#000000'}">
<label>Edge Width:</label>
<input type="range" id="d3EdgeWidth" min="1" max="8" value="${edgeData.style.strokeWidth || 2}">
<span id="d3WidthValue">${edgeData.style.strokeWidth || 2}px</span>
<button onclick="d3Renderer.updateEdgeProperties()">Update Edge</button>
<button onclick="d3Renderer.deleteEdge()">Delete Edge</button>
`;
// Add live width display
setTimeout(() => {
const widthSlider = document.getElementById('d3EdgeWidth');
const widthDisplay = document.getElementById('d3WidthValue');
if (widthSlider && widthDisplay) {
widthSlider.addEventListener('input', () => {
widthDisplay.textContent = widthSlider.value + 'px';
});
}
}, 100);
panel.classList.add('visible');
panel.style.display = 'block';
}
updateNodeProperties() {
if (!this.selectedData) return;
const nodeText = document.getElementById('d3NodeText').value;
const nodeShape = document.getElementById('d3NodeShape').value;
const nodeColor = document.getElementById('d3NodeColor').value;
const textColor = document.getElementById('d3TextColor').value;
console.log('Updating D3 node properties:', {
id: this.selectedData.id,
text: nodeText,
shape: nodeShape,
color: nodeColor,
textColor: textColor
});
// Update the data
this.selectedData.text = nodeText;
this.selectedData.shape = nodeShape;
this.selectedData.style.fill = nodeColor;
this.selectedData.style.color = textColor;
// Update the visual elements
const selectedGroup = d3.select(this.selectedElement);
// Update text
selectedGroup.select('text')
.text(nodeText)
.attr('fill', textColor);
// Update shape and color
selectedGroup.select('rect, ellipse, polygon')
.attr('fill', nodeColor);
// If shape changed, redraw the node
if (nodeShape !== this.selectedData.shape) {
selectedGroup.select('rect, ellipse, polygon').remove();
this.addNodeShape(selectedGroup, this.selectedData);
}
console.log('Node updated successfully');
// Generate updated Mermaid code
this.generateMermaidCode();
}
updateEdgeProperties() {
if (!this.selectedData) return;
const edgeLabel = document.getElementById('d3EdgeLabel').value;
const edgeType = document.getElementById('d3EdgeType').value;
const edgeColor = document.getElementById('d3EdgeColor').value;
const edgeWidth = document.getElementById('d3EdgeWidth').value;
console.log('Updating D3 edge properties:', {
label: edgeLabel,
type: edgeType,
color: edgeColor,
width: edgeWidth
});
// Update the data
this.selectedData.label = edgeLabel;
this.selectedData.type = edgeType;
this.selectedData.style.stroke = edgeColor;
this.selectedData.style.strokeWidth = parseInt(edgeWidth);
// Update the visual elements
d3.select(this.selectedElement)
.attr('stroke', edgeColor)
.attr('stroke-width', edgeWidth);
// Update or add edge label
if (edgeLabel) {
// Find or create label element
let labelElement = this.svg.select(`.links text[data-edge="${this.selectedData.source.id}-${this.selectedData.target.id}"]`);
if (labelElement.empty()) {
labelElement = this.svg.select('.links').append('text')
.attr('data-edge', `${this.selectedData.source.id}-${this.selectedData.target.id}`)
.attr('font-size', 12)
.attr('fill', '#333')
.attr('text-anchor', 'middle')
.style('pointer-events', 'none');
}
labelElement.text(edgeLabel);
}
console.log('Edge updated successfully');
// Generate updated Mermaid code
this.generateMermaidCode();
}
deleteNode() {
if (!this.selectedData) return;
const nodeId = this.selectedData.id;
console.log('Deleting node:', nodeId);
// Remove from data arrays
this.nodes = this.nodes.filter(n => n.id !== nodeId);
this.edges = this.edges.filter(e =>
(e.source.id || e.source) !== nodeId &&
(e.target.id || e.target) !== nodeId
);
// Remove visual elements
d3.select(this.selectedElement).remove();
// Remove connected edges
this.svg.selectAll('.links line')
.filter(d => (d.source.id || d.source) === nodeId || (d.target.id || d.target) === nodeId)
.remove();
// Remove connected edge labels
this.svg.selectAll('.links text')
.filter(d => (d.source.id || d.source) === nodeId || (d.target.id || d.target) === nodeId)
.remove();
// Hide property panel
document.getElementById('propertyPanel').style.display = 'none';
// Update simulation
this.simulation.nodes(this.nodes);
this.simulation.force('link').links(this.edges);
this.simulation.alpha(1).restart();
// Generate updated Mermaid code
this.generateMermaidCode();
}
deleteEdge() {
if (!this.selectedData) return;
console.log('Deleting edge:', this.selectedData);
// Remove from data array
this.edges = this.edges.filter(e => e !== this.selectedData);
// Remove visual elements
d3.select(this.selectedElement).remove();
// Remove label if exists
this.svg.selectAll('.links text')
.filter(d => d === this.selectedData)
.remove();
// Hide property panel
document.getElementById('propertyPanel').style.display = 'none';
// Update simulation
this.simulation.force('link').links(this.edges);
this.simulation.alpha(1).restart();
// Generate updated Mermaid code
this.generateMermaidCode();
}
generateMermaidCode() {
console.log('Generating Mermaid code from D3 data...');
// Save current positions before generating code
this.saveCurrentPositions();
// Convert D3 data back to parser format
const nodes = new Map();
this.nodes.forEach(node => {
nodes.set(node.id, {
id: node.id,
text: node.text,
shape: node.shape,
style: node.style,
position: { x: node.fx || node.x, y: node.fy || node.y }
});
});
const edges = this.edges.map(edge => ({
from: edge.source.id || edge.source,
to: edge.target.id || edge.target,
type: edge.type,
label: edge.label || '',
style: edge.style
}));
// Use existing parser to generate code
const newCode = this.parser.generateCode(
nodes,
edges,
'flowchart',
'TD',
new Map() // No subgraphs for now
);
// Update textarea
document.getElementById('mermaidInput').value = newCode;
console.log('Generated Mermaid code:', newCode);
console.log('Positions preserved in localStorage');
}
// Export complete project (Mermaid code + positions + styling)
exportProject() {
console.log('Exporting complete project...');
// Make sure positions are saved
this.saveCurrentPositions();
const projectData = {
version: "1.0",
exportedAt: new Date().toISOString(),
mermaidCode: document.getElementById('mermaidInput').value,
d3Data: {
nodes: this.nodes.map(node => ({
id: node.id,
text: node.text,
shape: node.shape,
style: node.style,
position: {
x: node.x,
y: node.y,
fx: node.fx,
fy: node.fy
}
})),
edges: this.edges.map(edge => ({
source: edge.source.id || edge.source,
target: edge.target.id || edge.target,
label: edge.label,
type: edge.type,
style: edge.style
}))
},
renderSettings: {
width: this.width,
height: this.height,
renderer: 'D3'
},
metadata: {
title: `Mermaid Project ${new Date().toLocaleDateString()}`,
description: "Exported from Interactive Mermaid Editor with D3 positioning"
}
};
const blob = new Blob([JSON.stringify(projectData, null, 2)],
{ type: 'application/json' });
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.download = `mermaid-project-${new Date().toISOString().slice(0,10)}.json`;
downloadLink.href = url;
downloadLink.click();
URL.revokeObjectURL(url);
console.log('Project exported:', projectData);
return projectData;
}
// Import complete project and restore exact layout
importProject(projectData) {
console.log('Importing project:', projectData);
try {
// Validate project data
if (!projectData.mermaidCode || !projectData.d3Data) {
throw new Error('Invalid project file format');
}
// Restore Mermaid code
document.getElementById('mermaidInput').value = projectData.mermaidCode;
// Restore D3 data
this.nodes = projectData.d3Data.nodes.map(node => ({
id: node.id,
text: node.text,
shape: node.shape,
style: node.style,
x: node.position.x,
y: node.position.y,
fx: node.position.fx,
fy: node.position.fy
}));
this.edges = projectData.d3Data.edges.map(edge => ({
source: edge.source,
target: edge.target,
label: edge.label,
type: edge.type,
style: edge.style
}));
// Save positions to localStorage
const positions = {};
this.nodes.forEach(node => {
positions[node.id] = {
x: node.x,
y: node.y,
fx: node.fx,
fy: node.fy
};
});
localStorage.setItem('d3_node_positions', JSON.stringify(positions));
// Render with restored data
this.renderFromImportedData();
// Show D3-specific buttons since we're now in D3 mode
document.getElementById('clearPositionsBtn').style.display = 'inline-block';
document.getElementById('exportProjectBtn').style.display = 'inline-block';
console.log('Project imported successfully');
alert(`Project imported successfully!\nTitle: ${projectData.metadata?.title}\nExported: ${new Date(projectData.exportedAt).toLocaleString()}\n\nThe diagram has been rendered with D3 to preserve exact positioning.`);
} catch (error) {
console.error('Error importing project:', error);
alert(`Error importing project: ${error.message}`);
}
}
// Special render method for imported data (skips parsing)
renderFromImportedData() {
console.log('Rendering from imported data...');
// Clear previous render
d3.select('#mermaidOutput').selectAll('*').remove();
// Create SVG
this.svg = d3.select('#mermaidOutput')
.append('svg')
.attr('width', this.width)
.attr('height', this.height)
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
.style('border', '1px solid #ccc');
// Create arrow marker
this.svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// Create force simulation with imported positions
this.simulation = d3.forceSimulation(this.nodes)
.force('link', d3.forceLink(this.edges).id(d => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
.force('collision', d3.forceCollide().radius(60))
.alpha(0); // Start with low energy to preserve imported positions
// Draw edges and nodes
this.drawEdges();
this.drawNodes();
console.log('Imported data rendered successfully');
}
}
// Initialize both renderers
const mermaidEditor = new MermaidEditor();
const d3Renderer = new D3Renderer();
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('Page loaded, checking for property panel...');
const panel = document.getElementById('propertyPanel');
const content = document.getElementById('propertyContent');
console.log('Property panel element:', panel);
console.log('Property content element:', content);
if (panel) {
console.log('Panel found! Ready for use.');
// Reset to original content
const content = document.getElementById('propertyContent');
if (content) {
content.innerHTML = '<p>Select an element to edit its properties</p>';
}
} else {
console.log('ERROR: Property panel not found!');
}
// Add event listener for edit mode toggle
document.getElementById('toggleEditMode').addEventListener('click', () => {
mermaidEditor.toggleEditMode();
});
// Add event listener for sync code button
document.getElementById('syncCodeBtn').addEventListener('click', () => {
mermaidEditor.updateCodeFromData();
alert('Code synchronized with visual changes!');
});
// Add test button for debugging property panel
document.getElementById('testPanelBtn').addEventListener('click', () => {
console.log('Test button clicked');
const panel = document.getElementById('propertyPanel');
const content = document.getElementById('propertyContent');
console.log('Panel found:', panel);
console.log('Content found:', content);
if (panel && content) {
content.innerHTML = `
<label>Test Panel:</label>
<input type="text" value="Panel is working!">
<button onclick="alert('Panel button works!')">Test Button</button>
`;
panel.classList.add('visible');
panel.style.display = 'block';
console.log('Test panel should be visible now');
} else {
console.error('Panel or content element not found!');
alert('Panel elements not found - check console');
}
});
// Add subgraph functionality
document.getElementById('addSubgraphBtn').addEventListener('click', () => {
const groupName = prompt('Enter group name:');
if (groupName) {
alert(`Group "${groupName}" created. Select nodes and use "Add to Group" to add them to this group.`);
}
});
// Add export functionality
document.getElementById('exportJsonBtn').addEventListener('click', () => {
mermaidEditor.downloadLayoutData();
});
// Add layout lock functionality
document.getElementById('lockLayoutBtn').addEventListener('click', () => {
mermaidEditor.toggleLayoutLock();
});
});
// Override the render function to enable interactivity after rendering
const originalRender = renderMermaidDiagram;
renderMermaidDiagram = function() {
originalRender();
// Re-enable interactivity if edit mode is on
setTimeout(() => {
if (mermaidEditor.isEditMode) {
mermaidEditor.enableInteractivity();
// Restore saved positions if layout is locked
if (mermaidEditor.isLayoutLocked) {
mermaidEditor.restoreSavedPositions();
}
}
}, 100);
};
</script>
</body>
</html>