mermaid-edit/index.php
DJP c4c3e02b41 Initial commit: Interactive Mermaid Editor
Features implemented:
- Visual node/edge editing with property panels
- Drag and drop node positioning
- Color customization for nodes and edges
- Text editing with real-time updates
- Bidirectional Mermaid code synchronization
- Subgraph/grouping support for layout control
- Export functionality to JSON with position data
- Layout lock system (experimental)
- Edge label editing and styling
- Comprehensive Mermaid syntax parser

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

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

1727 lines
No EOL
76 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>
</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="exportMmdBtn">Download as .mmd</button>
<button id="importMmdBtn">Import .mmd</button>
<input type="file" id="fileInput" accept=".mmd" 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);
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';
} else if (parenLabel) {
text = parenLabel;
shape = 'round';
} else if (braceLabel) {
text = braceLabel;
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) {
if (nodeString.includes('[') && nodeString.includes(']')) return 'rect';
if (nodeString.includes('(') && nodeString.includes(')')) return 'round';
if (nodeString.includes('{') && nodeString.includes('}')) return 'diamond';
if (nodeString.includes('((') && nodeString.includes('))')) return 'circle';
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`);
}
}
// Initialize the editor
const mermaidEditor = new MermaidEditor();
// 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>