Compare commits
No commits in common. "d3-renderer" and "main" have entirely different histories.
d3-rendere
...
main
3 changed files with 83 additions and 1505 deletions
431
INSTALL.md
431
INSTALL.md
|
|
@ -1,431 +0,0 @@
|
|||
# 📋 Installation Guide
|
||||
|
||||
Complete installation instructions for the Interactive Mermaid Editor across different environments.
|
||||
|
||||
## 🎯 Quick Start (30 seconds)
|
||||
|
||||
```bash
|
||||
# Clone and run
|
||||
git clone git@bitbucket.org:zlalani/mermaid-edit.git
|
||||
cd mermaid-edit
|
||||
php -S localhost:8000
|
||||
# Open http://localhost:8000
|
||||
```
|
||||
|
||||
## 💻 Local Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Web Server** (PHP built-in, Python, Node.js, or traditional server)
|
||||
- **Modern Browser** (Chrome 88+, Firefox 85+, Safari 14+, Edge 88+)
|
||||
- **Git** (for cloning repository)
|
||||
|
||||
### Method 1: PHP Built-in Server (Recommended)
|
||||
|
||||
**Requirements:** PHP 7.4 or newer
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone git@bitbucket.org:zlalani/mermaid-edit.git
|
||||
cd mermaid-edit
|
||||
|
||||
# 2. Start PHP server
|
||||
php -S localhost:8000
|
||||
|
||||
# 3. Open browser
|
||||
open http://localhost:8000
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- ✅ Zero configuration
|
||||
- ✅ PHP features work out of the box
|
||||
- ✅ Fast setup
|
||||
|
||||
### Method 2: Python HTTP Server
|
||||
|
||||
**Requirements:** Python 3.x or Python 2.7
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone git@bitbucket.org:zlalani/mermaid-edit.git
|
||||
cd mermaid-edit
|
||||
|
||||
# 2. Start Python server
|
||||
# Python 3:
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Python 2:
|
||||
python -m SimpleHTTPServer 8000
|
||||
|
||||
# 3. Open browser
|
||||
open http://localhost:8000
|
||||
```
|
||||
|
||||
**Note:** PHP features will not work with Python server.
|
||||
|
||||
### Method 3: Node.js HTTP Server
|
||||
|
||||
**Requirements:** Node.js and npm
|
||||
|
||||
```bash
|
||||
# 1. Install http-server globally (one time)
|
||||
npm install -g http-server
|
||||
|
||||
# 2. Clone repository
|
||||
git clone git@bitbucket.org:zlalani/mermaid-edit.git
|
||||
cd mermaid-edit
|
||||
|
||||
# 3. Start Node server
|
||||
http-server -p 8000
|
||||
|
||||
# 4. Open browser
|
||||
open http://localhost:8000
|
||||
```
|
||||
|
||||
**Alternative:** Using npx (no global install)
|
||||
```bash
|
||||
npx http-server -p 8000
|
||||
```
|
||||
|
||||
### Method 4: Docker (Advanced)
|
||||
|
||||
Create `Dockerfile`:
|
||||
```dockerfile
|
||||
FROM php:8.1-apache
|
||||
COPY . /var/www/html/
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
Create `docker-compose.yml`:
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
mermaid-editor:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
open http://localhost:8000
|
||||
```
|
||||
|
||||
## 🌐 Web Server Deployment
|
||||
|
||||
### Apache Setup
|
||||
|
||||
1. **Upload files** to web root (e.g., `/var/www/html/mermaid-edit/`)
|
||||
|
||||
2. **Set permissions**:
|
||||
```bash
|
||||
# Set directory permissions
|
||||
find /var/www/html/mermaid-edit -type d -exec chmod 755 {} \;
|
||||
|
||||
# Set file permissions
|
||||
find /var/www/html/mermaid-edit -type f -exec chmod 644 {} \;
|
||||
```
|
||||
|
||||
3. **Configure Apache** (optional `.htaccess`):
|
||||
```apache
|
||||
# .htaccess
|
||||
RewriteEngine On
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Security headers
|
||||
Header always set X-Frame-Options DENY
|
||||
Header always set X-Content-Type-Options nosniff
|
||||
```
|
||||
|
||||
4. **Access**: `http://yourdomain.com/mermaid-edit/`
|
||||
|
||||
### Nginx Setup
|
||||
|
||||
1. **Upload files** to web root
|
||||
|
||||
2. **Configure Nginx**:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name yourdomain.com;
|
||||
root /var/www/html/mermaid-edit;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Restart Nginx**: `sudo systemctl restart nginx`
|
||||
|
||||
### Shared Hosting (cPanel/Plesk)
|
||||
|
||||
1. **Upload via FTP/File Manager**:
|
||||
- Extract files to `public_html/mermaid-edit/`
|
||||
|
||||
2. **Set permissions** via cPanel File Manager:
|
||||
- Directories: 755
|
||||
- Files: 644
|
||||
|
||||
3. **Access**: `http://yourdomain.com/mermaid-edit/`
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Edit `config.php`:
|
||||
```php
|
||||
<?php
|
||||
// Site settings
|
||||
define('SITE_TITLE', 'My Interactive Mermaid Editor');
|
||||
define('DEBUG_MODE', false);
|
||||
|
||||
// Optional: Custom settings
|
||||
define('MAX_FILE_SIZE', '10MB');
|
||||
define('ALLOWED_EXTENSIONS', 'mmd,json');
|
||||
?>
|
||||
```
|
||||
|
||||
### Environment Variables (Advanced)
|
||||
|
||||
Create `.env` file:
|
||||
```env
|
||||
SITE_TITLE="Interactive Mermaid Editor"
|
||||
DEBUG_MODE=false
|
||||
MAX_UPLOAD_SIZE=10485760
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
#### Change Default Colors
|
||||
Edit CSS variables in `index.php`:
|
||||
```css
|
||||
:root {
|
||||
--primary-btn-color: #your-color;
|
||||
--background-color: #your-background;
|
||||
/* ... other variables ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### Modify Canvas Size
|
||||
Edit D3 renderer initialization:
|
||||
```javascript
|
||||
this.width = 1200; // Change from 800
|
||||
this.height = 800; // Change from 600
|
||||
```
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
### Production Deployment
|
||||
|
||||
1. **Disable Debug Mode**:
|
||||
```php
|
||||
define('DEBUG_MODE', false);
|
||||
```
|
||||
|
||||
2. **Set Secure Headers**:
|
||||
```apache
|
||||
Header always set X-Frame-Options DENY
|
||||
Header always set X-Content-Type-Options nosniff
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
```
|
||||
|
||||
3. **File Permissions**:
|
||||
```bash
|
||||
chmod 755 directories
|
||||
chmod 644 files
|
||||
chmod 600 config.php # Restrict config access
|
||||
```
|
||||
|
||||
4. **Hide Sensitive Files**:
|
||||
```apache
|
||||
# .htaccess
|
||||
<Files "config.php">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</Files>
|
||||
```
|
||||
|
||||
### HTTPS Setup
|
||||
|
||||
1. **Obtain SSL Certificate** (Let's Encrypt recommended)
|
||||
2. **Configure HTTPS redirect**
|
||||
3. **Update any hardcoded HTTP URLs**
|
||||
|
||||
## 🧪 Testing Installation
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
1. **Basic Access**:
|
||||
- [ ] Page loads without errors
|
||||
- [ ] Default Mermaid diagram renders
|
||||
- [ ] Dark mode toggle works
|
||||
|
||||
2. **D3 Features**:
|
||||
- [ ] "Render with D3" button works
|
||||
- [ ] Nodes are draggable
|
||||
- [ ] Property panel appears on click
|
||||
- [ ] Positions persist after page refresh
|
||||
|
||||
3. **Export/Import**:
|
||||
- [ ] Export .mmd file works
|
||||
- [ ] Export project (.json) works
|
||||
- [ ] Import project restores layout
|
||||
- [ ] PNG export functions
|
||||
|
||||
4. **Browser Console**:
|
||||
- [ ] No JavaScript errors
|
||||
- [ ] Position save/load messages appear
|
||||
|
||||
### Common Issues & Fixes
|
||||
|
||||
**"File not found" errors:**
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la index.php
|
||||
# Should show: -rw-r--r--
|
||||
```
|
||||
|
||||
**PHP errors:**
|
||||
```bash
|
||||
# Check PHP error log
|
||||
tail -f /var/log/apache2/error.log
|
||||
# or
|
||||
tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
**JavaScript not working:**
|
||||
- Check browser console (F12)
|
||||
- Verify CDN resources load (Mermaid.js, D3.js)
|
||||
- Test with different browser
|
||||
|
||||
**Positions not saving:**
|
||||
- Check localStorage is enabled
|
||||
- Verify browser supports modern JavaScript
|
||||
- Check for browser extensions blocking storage
|
||||
|
||||
## 🚀 Performance Optimization
|
||||
|
||||
### For Large Diagrams
|
||||
|
||||
1. **Increase PHP Limits**:
|
||||
```ini
|
||||
; php.ini
|
||||
memory_limit = 256M
|
||||
max_execution_time = 300
|
||||
max_input_vars = 3000
|
||||
```
|
||||
|
||||
2. **Browser Optimization**:
|
||||
- Use Chrome for best D3 performance
|
||||
- Close unnecessary browser tabs
|
||||
- Disable browser extensions during editing
|
||||
|
||||
### Caching (Production)
|
||||
|
||||
1. **Enable Browser Caching**:
|
||||
```apache
|
||||
# .htaccess
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
2. **Enable Gzip Compression**:
|
||||
```apache
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Installation Issues
|
||||
|
||||
**Problem: Permission denied**
|
||||
```bash
|
||||
# Fix permissions
|
||||
sudo chown -R www-data:www-data /var/www/html/mermaid-edit
|
||||
sudo chmod -R 755 /var/www/html/mermaid-edit
|
||||
```
|
||||
|
||||
**Problem: PHP not working**
|
||||
```bash
|
||||
# Check PHP version
|
||||
php --version
|
||||
|
||||
# Install PHP if missing (Ubuntu/Debian)
|
||||
sudo apt update
|
||||
sudo apt install php php-cli
|
||||
|
||||
# Install PHP (macOS with Homebrew)
|
||||
brew install php
|
||||
```
|
||||
|
||||
**Problem: Port already in use**
|
||||
```bash
|
||||
# Find process using port
|
||||
lsof -i :8000
|
||||
|
||||
# Kill process
|
||||
kill -9 [PID]
|
||||
|
||||
# Or use different port
|
||||
php -S localhost:8080
|
||||
```
|
||||
|
||||
### Runtime Issues
|
||||
|
||||
**Problem: Blank page**
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify all CDN resources load
|
||||
- Check PHP error logs
|
||||
|
||||
**Problem: Drag and drop not working**
|
||||
- Ensure you're using "Render with D3"
|
||||
- Check browser supports modern JavaScript
|
||||
- Try different browser
|
||||
|
||||
**Problem: Import/Export not working**
|
||||
- Check browser localStorage is enabled
|
||||
- Verify file permissions for uploads
|
||||
- Check browser security settings
|
||||
|
||||
## 📞 Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. **Check browser console** (F12 → Console)
|
||||
2. **Review server error logs**
|
||||
3. **Test in different browser**
|
||||
4. **Create GitHub issue** with:
|
||||
- Browser version
|
||||
- Server environment
|
||||
- Error messages
|
||||
- Steps to reproduce
|
||||
|
||||
## ✅ Success!
|
||||
|
||||
Once installed, you should see:
|
||||
- 🎨 Interactive Mermaid Editor interface
|
||||
- 🖱️ Draggable nodes in D3 mode
|
||||
- 🎨 Property panel for editing
|
||||
- 💾 Export/import functionality
|
||||
|
||||
**Happy diagram editing!** 🚀
|
||||
328
README.md
328
README.md
|
|
@ -1,280 +1,110 @@
|
|||
# 🎨 Interactive Mermaid Editor
|
||||
# Mermaid Diagram Tool
|
||||
|
||||
A powerful visual editor for Mermaid diagrams with **persistent positioning**, **real-time editing**, and **bidirectional synchronization** between visual elements and code.
|
||||
An interactive web-based tool for creating, editing, and exporting Mermaid diagrams.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
### 🎯 **Dual Rendering Modes**
|
||||
- **Mermaid Renderer**: Standard Mermaid.js rendering with automatic layout
|
||||
- **D3 Renderer**: Interactive D3.js rendering with **persistent positioning**
|
||||
### Core Features
|
||||
- **Live Diagram Rendering**: See your Mermaid diagram code visualized in real-time
|
||||
- **Dark/Light Mode**: Toggle between dark and light UI themes while keeping diagrams in high-contrast mode
|
||||
- **Export Options**: Download diagrams as high-resolution PNG files or as .mmd source files
|
||||
- **Import Feature**: Upload and edit existing .mmd diagram files
|
||||
- **Responsive Design**: Works on desktop and mobile devices with adaptive layout
|
||||
|
||||
### 🖱️ **Visual Editing**
|
||||
- **Drag & Drop**: Move nodes with mouse - positions stay permanently
|
||||
- **Click to Edit**: Select any node or edge to edit properties
|
||||
- **Real-time Updates**: Changes appear instantly in the diagram
|
||||
- **Shape Support**: Rectangles, circles, diamonds with live transformation
|
||||
### Technical Highlights
|
||||
- **Local Storage**: Remembers your UI theme preference between sessions
|
||||
- **High-Resolution Export**: PNG exports are generated at 4x resolution for high-quality images
|
||||
- **Syntax Highlighting**: Improved readability for diagram code (via Mermaid's built-in rendering)
|
||||
- **Client-Side Processing**: No server communication needed for diagram rendering or file operations
|
||||
|
||||
### 🎨 **Styling & Customization**
|
||||
- **Node Properties**: Text, colors, shapes, sizes
|
||||
- **Edge Properties**: Labels, colors, line styles, widths
|
||||
- **Color Picker**: Full color customization with live preview
|
||||
- **Multiple Shapes**: Rectangle `[text]`, Circle `(text)`, Diamond `{text}`
|
||||
## Getting Started
|
||||
|
||||
### 🔄 **Bidirectional Sync**
|
||||
- **Visual → Code**: Edit visually, Mermaid code updates automatically
|
||||
- **Code → Visual**: Paste Mermaid code, renders with all features
|
||||
- **Syntax Preservation**: Maintains proper Mermaid syntax compatibility
|
||||
### Installation
|
||||
1. Clone this repository to your local environment
|
||||
2. No build process required - open `index.php` in a PHP-enabled web server
|
||||
3. Alternatively, deploy the files to any PHP web hosting environment
|
||||
|
||||
### 💾 **Project Management**
|
||||
- **Export Projects**: Save complete layouts with positioning data
|
||||
- **Import Projects**: Restore exact layouts from saved files
|
||||
- **Position Persistence**: Node positions survive page refreshes
|
||||
- **Mermaid Export**: Standard `.mmd` file export for compatibility
|
||||
### Prerequisites
|
||||
- PHP 7.0 or higher
|
||||
- Modern web browser (Chrome, Firefox, Safari, Edge)
|
||||
- No special server requirements beyond basic PHP support
|
||||
|
||||
### 🎪 **Advanced Features**
|
||||
- **Subgraph Support**: Group nodes for better organization
|
||||
- **Dark Mode**: Toggle between light and dark themes
|
||||
- **PNG Export**: High-resolution image export
|
||||
- **Layout Memory**: Automatic position saving via localStorage
|
||||
## Usage Guide
|
||||
|
||||
## 🚀 Quick Start
|
||||
### Creating a Diagram
|
||||
1. Enter Mermaid syntax in the "Input Mermaid Diagram Code" textarea
|
||||
2. Click "Render Diagram" to visualize your diagram in the output area
|
||||
3. Edit and re-render as needed to refine your diagram
|
||||
|
||||
### Method 1: Local Web Server (Recommended)
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone git@bitbucket.org:zlalani/mermaid-edit.git
|
||||
cd mermaid-edit
|
||||
```
|
||||
|
||||
2. **Start local server** (choose one):
|
||||
|
||||
**PHP Built-in Server:**
|
||||
```bash
|
||||
php -S localhost:8000
|
||||
```
|
||||
|
||||
**Python HTTP Server:**
|
||||
```bash
|
||||
# Python 3
|
||||
python -m http.server 8000
|
||||
# Python 2
|
||||
python -m SimpleHTTPServer 8000
|
||||
```
|
||||
|
||||
**Node.js HTTP Server:**
|
||||
```bash
|
||||
npx http-server -p 8000
|
||||
```
|
||||
|
||||
3. **Open in browser**
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
### Method 2: Web Server Deployment
|
||||
|
||||
1. **Upload files** to your web server
|
||||
2. **Configure PHP** (if using PHP features)
|
||||
3. **Set permissions** (755 for directories, 644 for files)
|
||||
4. **Access via domain/IP**
|
||||
|
||||
## 📖 Usage Guide
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Create a diagram** by typing Mermaid syntax in the left panel
|
||||
2. **Choose renderer**:
|
||||
- Click **"Render Diagram"** for standard Mermaid
|
||||
- Click **"Render with D3"** for interactive editing
|
||||
3. **Start editing** - click nodes/edges to customize
|
||||
|
||||
### Interactive Editing Workflow
|
||||
|
||||
#### Basic Editing
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> B{Decision}
|
||||
B -->|Yes| C[Action]
|
||||
B -->|No| D[End]
|
||||
### Example Diagram Syntax
|
||||
```
|
||||
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]
|
||||
```
|
||||
|
||||
1. **Click "Render with D3"**
|
||||
2. **Drag nodes** to desired positions
|
||||
3. **Click nodes** to edit text, colors, shapes
|
||||
4. **Click edges** to edit labels, styles, colors
|
||||
5. **Positions save automatically**
|
||||
### Importing a Diagram
|
||||
1. Click "Import .mmd" button
|
||||
2. Select a .mmd file from your local computer
|
||||
3. The diagram code will be loaded and automatically rendered
|
||||
|
||||
#### Project Management
|
||||
- **Export Project**: Saves everything (code + positions + styling)
|
||||
- **Import Project**: Restores exact layout from saved file
|
||||
- **Export .mmd**: Standard Mermaid file for other tools
|
||||
- **Export PNG**: High-resolution image
|
||||
### Exporting Your Work
|
||||
- **PNG Export**: Click "Download as PNG" to save the rendered diagram as a high-resolution image
|
||||
- **Source Code Export**: Click "Download as .mmd" to save your Mermaid code for later use
|
||||
|
||||
### Supported Mermaid Syntax
|
||||
### Using Dark Mode
|
||||
- Click the sun/moon icon in the top-right corner to toggle between dark and light modes
|
||||
- Your preference is automatically saved for future visits
|
||||
- The diagram area always remains in light mode for optimal readability and export quality
|
||||
|
||||
#### Node Types
|
||||
```mermaid
|
||||
A[Rectangle] # Rectangle shape
|
||||
B(Rounded) # Circle/rounded shape
|
||||
C{Diamond} # Diamond shape
|
||||
D((Circle)) # Perfect circle
|
||||
```
|
||||
## Diagram Types Supported
|
||||
|
||||
#### Connection Types
|
||||
```mermaid
|
||||
A --> B # Arrow
|
||||
A --- B # Line
|
||||
A -.-> B # Dotted arrow
|
||||
A ==> B # Thick arrow
|
||||
A -->|Label| B # Labeled connection
|
||||
```
|
||||
This tool supports all Mermaid diagram types, including:
|
||||
|
||||
#### Styling
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Node] --> B[Another]
|
||||
|
||||
style A fill:#f96,color:#fff
|
||||
style B fill:#bbf,color:#000
|
||||
linkStyle 0 stroke:#f66,stroke-width:3px
|
||||
```
|
||||
- Flowcharts
|
||||
- Sequence diagrams
|
||||
- Class diagrams
|
||||
- State diagrams
|
||||
- Entity Relationship diagrams
|
||||
- User Journey diagrams
|
||||
- Gantt charts
|
||||
- Pie charts
|
||||
- Requirement diagrams
|
||||
|
||||
## 🔧 Configuration
|
||||
## Customization
|
||||
|
||||
### Basic Configuration (config.php)
|
||||
```php
|
||||
<?php
|
||||
define('SITE_TITLE', 'Interactive Mermaid Editor');
|
||||
define('DEBUG_MODE', false);
|
||||
?>
|
||||
```
|
||||
The tool can be customized by modifying the following files:
|
||||
|
||||
### Customization Options
|
||||
- **Canvas Size**: Modify `width` and `height` in D3 renderer
|
||||
- **Color Defaults**: Update default colors in CSS variables
|
||||
- **Theme Settings**: Customize light/dark mode colors
|
||||
- `config.php`: Change site title and other configuration settings
|
||||
- `index.php`: Modify UI layout, styling, and JavaScript functionality
|
||||
- `css/style.css`: Additional CSS styling (if needed)
|
||||
|
||||
## 🌐 Browser Compatibility
|
||||
|
||||
| Browser | Version | Status |
|
||||
|---------|---------|--------|
|
||||
| Chrome | 88+ | ✅ Full Support |
|
||||
| Firefox | 85+ | ✅ Full Support |
|
||||
| Safari | 14+ | ✅ Full Support |
|
||||
| Edge | 88+ | ✅ Full Support |
|
||||
|
||||
**Requirements:**
|
||||
- Modern browser with ES6+ support
|
||||
- JavaScript enabled
|
||||
- LocalStorage support (for position persistence)
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
mermaid-edit/
|
||||
├── index.php # Main application
|
||||
├── config.php # Configuration
|
||||
├── css/
|
||||
│ └── style.css # Styles (legacy)
|
||||
├── js/ # JavaScript (legacy)
|
||||
├── OLD/ # Previous versions
|
||||
├── README.md # This file
|
||||
└── .git/ # Git repository
|
||||
```
|
||||
|
||||
## 🔄 Export/Import Format
|
||||
|
||||
### Project Export (.json)
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"exportedAt": "2024-01-15T10:30:00.000Z",
|
||||
"mermaidCode": "flowchart TD\n A[Start] --> B[End]",
|
||||
"d3Data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "A",
|
||||
"text": "Start",
|
||||
"shape": "rect",
|
||||
"position": {"x": 240, "y": 100, "fx": 240, "fy": 100},
|
||||
"style": {"fill": "#ffc406", "color": "#000000"}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "A",
|
||||
"target": "B",
|
||||
"label": "",
|
||||
"type": "-->",
|
||||
"style": {"stroke": "#000", "strokeWidth": 2}
|
||||
}
|
||||
]
|
||||
},
|
||||
"renderSettings": {
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"renderer": "D3"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "My Diagram",
|
||||
"description": "Interactive Mermaid project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Diagram Not Rendering**: Check your Mermaid syntax for errors, which will display in the output area
|
||||
2. **Export Not Working**: Ensure JavaScript is enabled in your browser
|
||||
3. **UI Issues**: Try clearing browser cache or using incognito/private mode
|
||||
|
||||
**Q: Positions don't save**
|
||||
- A: Use "Render with D3" for persistent positioning
|
||||
- A: Check browser localStorage is enabled
|
||||
## Credits and License
|
||||
|
||||
**Q: Import doesn't work**
|
||||
- A: Ensure JSON file format is valid
|
||||
- A: Check browser console for error messages
|
||||
- Built using [Mermaid.js](https://mermaid.js.org/)
|
||||
- Licensed under MIT license
|
||||
|
||||
**Q: Shapes don't match Mermaid syntax**
|
||||
- A: Verify correct syntax: `[rect]`, `(round)`, `{diamond}`
|
||||
- A: Check console for parsing errors
|
||||
## Future Development Plans
|
||||
|
||||
**Q: Performance issues**
|
||||
- A: Large diagrams may be slow in D3 mode
|
||||
- A: Use standard Mermaid mode for complex diagrams
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. **Fork** the repository
|
||||
2. **Create feature branch** (`git checkout -b feature/amazing-feature`)
|
||||
3. **Commit changes** (`git commit -m 'Add amazing feature'`)
|
||||
4. **Push to branch** (`git push origin feature/amazing-feature`)
|
||||
5. **Open Pull Request**
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **Mermaid.js** - Original diagram rendering
|
||||
- **D3.js** - Interactive visualizations
|
||||
- **Contributors** - All developers who helped improve this tool
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Issues**: [Create GitHub Issue](https://github.com/your-repo/issues)
|
||||
- **Documentation**: This README and inline code comments
|
||||
- **Community**: Share your diagrams and get help
|
||||
Potential future enhancements:
|
||||
- Diagram template library
|
||||
- Custom theme builder
|
||||
- Multiple diagram tabs
|
||||
- Collaborative editing features
|
||||
- Integration with version control systems
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for better diagram editing**
|
||||
|
||||
*Interactive Mermaid Editor - Where visual meets code*
|
||||
*This tool is designed to simplify the creation and sharing of Mermaid diagrams for documentation, presentations, and educational purposes.*
|
||||
829
index.php
829
index.php
|
|
@ -219,7 +219,6 @@ require_once 'config.php';
|
|||
}
|
||||
</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>
|
||||
|
|
@ -253,14 +252,9 @@ require_once 'config.php';
|
|||
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">
|
||||
|
|
@ -335,52 +329,6 @@ require_once 'config.php';
|
|||
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');
|
||||
|
|
@ -580,15 +528,12 @@ require_once 'config.php';
|
|||
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 };
|
||||
|
|
@ -678,11 +623,10 @@ require_once 'config.php';
|
|||
}
|
||||
|
||||
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';
|
||||
if (nodeString.includes('(') && nodeString.includes(')')) return 'round';
|
||||
if (nodeString.includes('{') && nodeString.includes('}')) return 'diamond';
|
||||
if (nodeString.includes('((') && nodeString.includes('))')) return 'circle';
|
||||
return 'rect';
|
||||
}
|
||||
|
||||
|
|
@ -1684,773 +1628,8 @@ require_once 'config.php';
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Initialize the editor
|
||||
const mermaidEditor = new MermaidEditor();
|
||||
const d3Renderer = new D3Renderer();
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue