1053 lines
39 KiB
HTML
1053 lines
39 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Mermaid Diagram Viewer</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 30px;
|
||
text-align: center;
|
||
color: white;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header p {
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.content {
|
||
display: grid;
|
||
grid-template-columns: 400px 1fr;
|
||
gap: 30px;
|
||
padding: 30px;
|
||
}
|
||
|
||
.panel {
|
||
background: #f8f9fa;
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.panel h2 {
|
||
color: #667eea;
|
||
margin-bottom: 20px;
|
||
font-size: 1.5em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tab-btn {
|
||
flex: 1;
|
||
padding: 12px 20px;
|
||
border: none;
|
||
background: white;
|
||
color: #667eea;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 3px dashed #667eea;
|
||
border-radius: 15px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #764ba2;
|
||
background: #f0f4ff;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
border-color: #764ba2;
|
||
background: #e8ecff;
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 3em;
|
||
margin-bottom: 15px;
|
||
color: #667eea;
|
||
}
|
||
|
||
.upload-area p {
|
||
color: #666;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
#fileInput {
|
||
display: none;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 300px;
|
||
padding: 15px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 10px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.95em;
|
||
resize: vertical;
|
||
transition: border-color 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
padding: 15px;
|
||
margin-top: 15px;
|
||
border: none;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 10px;
|
||
font-size: 1.1em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
|
||
.btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
#diagramOutput {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
position: relative;
|
||
cursor: grab;
|
||
}
|
||
|
||
#diagramOutput.grabbing {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
#diagramOutput svg {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
#diagramWrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.zoom-controls {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
z-index: 100;
|
||
}
|
||
|
||
.zoom-btn {
|
||
min-width: 45px;
|
||
height: 45px;
|
||
padding: 0 12px;
|
||
border: none;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 10px;
|
||
font-size: 1.5em;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.zoom-btn:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
|
||
.zoom-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.zoom-level {
|
||
background: rgba(102, 126, 234, 0.9);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
font-size: 0.9em;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.placeholder {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.placeholder-icon {
|
||
font-size: 4em;
|
||
margin-bottom: 15px;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.error {
|
||
background: #fee;
|
||
border: 2px solid #fcc;
|
||
color: #c33;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.success {
|
||
background: #efe;
|
||
border: 2px solid #cfc;
|
||
color: #3c3;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
@media (max-width: 968px) {
|
||
.content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Fullscreen mode styles */
|
||
.fullscreen-mode {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
z-index: 9999;
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
margin: 0;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.fullscreen-mode #diagramOutput {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: calc(100vh - 40px);
|
||
border-radius: 0;
|
||
background: white;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.fullscreen-exit-hint {
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(102, 126, 234, 0.95);
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 25px;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
z-index: 10000;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||
animation: slideDown 0.3s ease;
|
||
pointer-events: none;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
transform: translateX(-50%) translateY(-20px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(-50%) translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎨 Mermaid Diagram Viewer</h1>
|
||
<p>Upload a file or paste your Mermaid code to visualize beautiful diagrams</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="panel">
|
||
<h2>📝 Input</h2>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="switchTab('paste')">Paste Code</button>
|
||
<button class="tab-btn" onclick="switchTab('upload')">Upload File</button>
|
||
</div>
|
||
|
||
<div id="pasteTab" class="tab-content active">
|
||
<textarea id="mermaidCode" placeholder="Paste your Mermaid diagram code here...
|
||
|
||
Example:
|
||
graph TD
|
||
A[Start] --> B{Decision}
|
||
B -->|Yes| C[Option 1]
|
||
B -->|No| D[Option 2]
|
||
C --> E[End]
|
||
D --> E"></textarea>
|
||
<button class="btn" onclick="renderFromTextarea()">🎨 Render Diagram</button>
|
||
</div>
|
||
|
||
<div id="uploadTab" class="tab-content">
|
||
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
|
||
<div class="upload-icon">📁</div>
|
||
<p><strong>Click to upload</strong> or drag and drop</p>
|
||
<p style="font-size: 0.9em; margin-top: 10px; opacity: 0.7;">Supports .mmd, .mermaid, .txt files</p>
|
||
</div>
|
||
<input type="file" id="fileInput" accept=".mmd,.mermaid,.txt" onchange="handleFile(event)">
|
||
</div>
|
||
|
||
<div id="message"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>🖼️ Preview</h2>
|
||
<div id="diagramOutput">
|
||
<div class="placeholder">
|
||
<div class="placeholder-icon">📊</div>
|
||
<p>Your diagram will appear here</p>
|
||
</div>
|
||
<div class="zoom-controls" id="zoomControls" style="display: none;">
|
||
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
||
<div class="zoom-level" id="zoomLevel">100%</div>
|
||
<button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
|
||
<button class="zoom-btn" onclick="resetZoom()" title="Reset Zoom" style="font-size: 1.2em;">⟲</button>
|
||
<button class="zoom-btn" onclick="fitToScreen()" title="Fit to Screen" style="font-size: 1.2em;">⛶</button>
|
||
<button class="zoom-btn" id="fullscreenBtn" onclick="toggleFullscreen()" title="Fullscreen Mode" style="font-size: 1.2em; background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);">📺</button>
|
||
<button class="zoom-btn" onclick="exportAsSVG()" title="Export as SVG (Best for Word/PDF - Vector Format)" style="font-size: 1em; background: linear-gradient(135deg, #28a745 0%, #20c997 100%);">SVG</button>
|
||
<button class="zoom-btn" onclick="exportAsPNG()" title="Export as PNG (Raster Format)" style="font-size: 1em;">PNG</button>
|
||
<button class="zoom-btn" onclick="exportAsJPEG()" title="Export as JPEG (Raster Format)" style="font-size: 1em;">JPG</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
mermaid.initialize({
|
||
startOnLoad: false,
|
||
theme: 'default',
|
||
securityLevel: 'loose',
|
||
themeVariables: {
|
||
fontFamily: 'Arial, Helvetica, Verdana, sans-serif',
|
||
fontSize: '16px'
|
||
},
|
||
flowchart: {
|
||
htmlLabels: true,
|
||
curve: 'basis'
|
||
}
|
||
});
|
||
|
||
let currentTab = 'paste';
|
||
let diagramCounter = 0;
|
||
let zoomScale = 1;
|
||
let panX = 0;
|
||
let panY = 0;
|
||
let isDragging = false;
|
||
let startX = 0;
|
||
let startY = 0;
|
||
let panAndZoomSetup = false;
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||
|
||
if (tab === 'paste') {
|
||
document.querySelector('.tab-btn:first-child').classList.add('active');
|
||
document.getElementById('pasteTab').classList.add('active');
|
||
} else {
|
||
document.querySelector('.tab-btn:last-child').classList.add('active');
|
||
document.getElementById('uploadTab').classList.add('active');
|
||
}
|
||
}
|
||
|
||
function showMessage(message, type) {
|
||
const messageDiv = document.getElementById('message');
|
||
messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
|
||
setTimeout(() => {
|
||
messageDiv.innerHTML = '';
|
||
}, 5000);
|
||
}
|
||
|
||
function renderDiagram(code) {
|
||
const output = document.getElementById('diagramOutput');
|
||
|
||
if (!code.trim()) {
|
||
showMessage('Please provide Mermaid code', 'error');
|
||
return;
|
||
}
|
||
|
||
diagramCounter++;
|
||
const graphId = 'mermaid-graph-' + diagramCounter;
|
||
|
||
// Create a wrapper for the diagram
|
||
const diagramWrapper = document.createElement('div');
|
||
diagramWrapper.id = 'diagramWrapper';
|
||
diagramWrapper.innerHTML = `<div id="${graphId}"></div>`;
|
||
|
||
// Clear output but keep zoom controls
|
||
const zoomControls = document.getElementById('zoomControls');
|
||
output.innerHTML = '';
|
||
output.appendChild(diagramWrapper);
|
||
output.appendChild(zoomControls);
|
||
|
||
try {
|
||
mermaid.render(graphId, code).then(result => {
|
||
diagramWrapper.innerHTML = result.svg;
|
||
|
||
// Wait a bit for DOM to update
|
||
setTimeout(() => {
|
||
resetZoom();
|
||
zoomControls.style.display = 'flex';
|
||
if (!panAndZoomSetup) {
|
||
setupPanAndZoom();
|
||
panAndZoomSetup = true;
|
||
}
|
||
showMessage('✅ Diagram rendered successfully!', 'success');
|
||
}, 100);
|
||
}).catch(error => {
|
||
diagramWrapper.innerHTML = '<div class="placeholder"><div class="placeholder-icon">❌</div><p>Failed to render diagram</p></div>';
|
||
zoomControls.style.display = 'none';
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
});
|
||
} catch (error) {
|
||
diagramWrapper.innerHTML = '<div class="placeholder"><div class="placeholder-icon">❌</div><p>Failed to render diagram</p></div>';
|
||
zoomControls.style.display = 'none';
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function updateTransform() {
|
||
const output = document.getElementById('diagramOutput');
|
||
const svg = output.querySelector('svg');
|
||
if (svg) {
|
||
svg.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomScale})`;
|
||
document.getElementById('zoomLevel').textContent = Math.round(zoomScale * 100) + '%';
|
||
}
|
||
}
|
||
|
||
function zoomIn() {
|
||
zoomScale = Math.min(zoomScale + 0.2, 10);
|
||
updateTransform();
|
||
}
|
||
|
||
function zoomOut() {
|
||
zoomScale = Math.max(zoomScale - 0.2, 0.2);
|
||
updateTransform();
|
||
}
|
||
|
||
function resetZoom() {
|
||
zoomScale = 1;
|
||
panX = 0;
|
||
panY = 0;
|
||
updateTransform();
|
||
}
|
||
|
||
function fitToScreen() {
|
||
const output = document.getElementById('diagramOutput');
|
||
if (!output) return;
|
||
|
||
const svg = output.querySelector('svg');
|
||
if (!svg) return;
|
||
|
||
try {
|
||
const outputRect = output.getBoundingClientRect();
|
||
const svgRect = svg.getBBox();
|
||
|
||
if (svgRect.width === 0 || svgRect.height === 0) return;
|
||
|
||
const scaleX = (outputRect.width - 40) / svgRect.width;
|
||
const scaleY = (outputRect.height - 40) / svgRect.height;
|
||
|
||
zoomScale = Math.min(scaleX, scaleY, 1);
|
||
panX = 0;
|
||
panY = 0;
|
||
updateTransform();
|
||
} catch (error) {
|
||
console.warn('Could not fit to screen:', error);
|
||
}
|
||
}
|
||
|
||
function exportDiagram(format) {
|
||
const output = document.getElementById('diagramOutput');
|
||
const svg = output.querySelector('svg');
|
||
|
||
if (!svg) {
|
||
showMessage('❌ No diagram to export', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Clone the SVG to avoid modifying the original
|
||
const clonedSvg = svg.cloneNode(true);
|
||
|
||
// Remove any transform applied by zoom/pan
|
||
clonedSvg.style.transform = '';
|
||
|
||
// Get SVG dimensions
|
||
const bbox = svg.getBBox();
|
||
const width = bbox.width || svg.clientWidth || 800;
|
||
const height = bbox.height || svg.clientHeight || 600;
|
||
|
||
// Add proper dimensions to the cloned SVG
|
||
clonedSvg.setAttribute('width', width);
|
||
clonedSvg.setAttribute('height', height);
|
||
|
||
// Set viewBox if not already set
|
||
if (!clonedSvg.getAttribute('viewBox')) {
|
||
clonedSvg.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${width} ${height}`);
|
||
}
|
||
|
||
// Ensure xmlns attribute is present for proper rendering
|
||
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||
|
||
// Serialize SVG to string
|
||
const serializer = new XMLSerializer();
|
||
let svgString = serializer.serializeToString(clonedSvg);
|
||
|
||
// Encode SVG as data URI
|
||
const svgDataUri = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
|
||
|
||
// Create an image element
|
||
const img = new Image();
|
||
|
||
img.onload = function() {
|
||
// Create canvas with proper dimensions
|
||
const canvas = document.createElement('canvas');
|
||
const scale = 3; // Higher scale for better quality in Word documents
|
||
canvas.width = width * scale;
|
||
canvas.height = height * scale;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// Enable high-quality rendering
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.imageSmoothingQuality = 'high';
|
||
|
||
// Scale context for high quality
|
||
ctx.scale(scale, scale);
|
||
|
||
// For JPEG, fill with white background
|
||
if (format === 'jpeg') {
|
||
ctx.fillStyle = 'white';
|
||
ctx.fillRect(0, 0, width, height);
|
||
}
|
||
|
||
// Draw image on canvas
|
||
ctx.drawImage(img, 0, 0, width, height);
|
||
|
||
// Convert canvas to data URL
|
||
try {
|
||
const dataUrl = canvas.toDataURL(`image/${format}`, 0.95);
|
||
|
||
// Create download link
|
||
const link = document.createElement('a');
|
||
link.download = `mermaid-diagram-${Date.now()}.${format}`;
|
||
link.href = dataUrl;
|
||
link.click();
|
||
|
||
showMessage(`✅ Diagram exported as ${format.toUpperCase()}`, 'success');
|
||
} catch (error) {
|
||
showMessage('❌ Error creating image: ' + error.message, 'error');
|
||
console.error('Canvas export error:', error);
|
||
}
|
||
};
|
||
|
||
img.onerror = function(error) {
|
||
showMessage('❌ Error loading SVG for export', 'error');
|
||
console.error('Image load error:', error);
|
||
};
|
||
|
||
// Set the source to the data URI
|
||
img.src = svgDataUri;
|
||
|
||
} catch (error) {
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
console.error('Export error:', error);
|
||
}
|
||
}
|
||
|
||
function exportAsPNG() {
|
||
exportDiagram('png');
|
||
}
|
||
|
||
function exportAsJPEG() {
|
||
exportDiagram('jpeg');
|
||
}
|
||
|
||
function toggleFullscreen() {
|
||
const panel = document.querySelector('.panel:last-child');
|
||
const output = document.getElementById('diagramOutput');
|
||
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
||
|
||
if (!output.querySelector('svg')) {
|
||
showMessage('❌ No diagram to display in fullscreen', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!panel.classList.contains('fullscreen-mode')) {
|
||
// Enter fullscreen
|
||
panel.classList.add('fullscreen-mode');
|
||
|
||
// Update button icon and title
|
||
fullscreenBtn.innerHTML = '✕';
|
||
fullscreenBtn.title = 'Exit Fullscreen (Press ESC)';
|
||
fullscreenBtn.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
|
||
|
||
// Show exit hint
|
||
const hint = document.createElement('div');
|
||
hint.className = 'fullscreen-exit-hint';
|
||
hint.textContent = '🖥️ Fullscreen Mode Active | Press ESC or click ✕ to exit';
|
||
hint.id = 'fullscreenHint';
|
||
document.body.appendChild(hint);
|
||
|
||
// Auto-hide hint after 4 seconds
|
||
setTimeout(() => {
|
||
const hintElement = document.getElementById('fullscreenHint');
|
||
if (hintElement) {
|
||
hintElement.style.transition = 'opacity 0.3s ease';
|
||
hintElement.style.opacity = '0';
|
||
setTimeout(() => hintElement.remove(), 300);
|
||
}
|
||
}, 4000);
|
||
|
||
// Fit to screen after a short delay for layout adjustment
|
||
setTimeout(() => {
|
||
fitToScreen();
|
||
}, 100);
|
||
|
||
showMessage('✅ Fullscreen mode activated - Press ESC to exit', 'success');
|
||
} else {
|
||
// Exit fullscreen
|
||
panel.classList.remove('fullscreen-mode');
|
||
|
||
// Update button icon and title back
|
||
fullscreenBtn.innerHTML = '📺';
|
||
fullscreenBtn.title = 'Fullscreen Mode';
|
||
fullscreenBtn.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%)';
|
||
|
||
// Remove hint if still visible
|
||
const hint = document.getElementById('fullscreenHint');
|
||
if (hint) hint.remove();
|
||
|
||
// Reset zoom after exiting fullscreen
|
||
setTimeout(() => {
|
||
fitToScreen();
|
||
}, 100);
|
||
|
||
showMessage('✅ Exited fullscreen mode', 'success');
|
||
}
|
||
}
|
||
|
||
// ESC key to exit fullscreen
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
const panel = document.querySelector('.panel:last-child');
|
||
if (panel && panel.classList.contains('fullscreen-mode')) {
|
||
toggleFullscreen();
|
||
}
|
||
}
|
||
});
|
||
|
||
function exportAsSVG() {
|
||
const output = document.getElementById('diagramOutput');
|
||
const svg = output.querySelector('svg');
|
||
|
||
if (!svg) {
|
||
showMessage('❌ No diagram to export', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Clone the SVG to avoid modifying the original
|
||
const clonedSvg = svg.cloneNode(true);
|
||
|
||
// Remove any transform applied by zoom/pan
|
||
clonedSvg.style.transform = '';
|
||
|
||
// Ensure xmlns attributes are present for proper rendering
|
||
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||
clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||
|
||
// Convert foreignObject elements to native SVG text (Word doesn't support foreignObject)
|
||
const foreignObjects = clonedSvg.querySelectorAll('foreignObject');
|
||
foreignObjects.forEach(fo => {
|
||
const foText = fo.textContent || '';
|
||
if (foText.trim()) {
|
||
const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||
|
||
// Handle multi-line text by splitting into tspan elements
|
||
const lines = foText.trim().split('\n').filter(line => line.trim());
|
||
|
||
// Copy position attributes
|
||
const x = fo.getAttribute('x') || '0';
|
||
const y = fo.getAttribute('y') || '0';
|
||
const width = fo.getAttribute('width') || '100';
|
||
const height = fo.getAttribute('height') || '20';
|
||
|
||
const centerX = parseFloat(x) + parseFloat(width)/2;
|
||
const centerY = parseFloat(y) + parseFloat(height)/2;
|
||
|
||
textElement.setAttribute('x', centerX);
|
||
textElement.setAttribute('y', centerY);
|
||
textElement.setAttribute('text-anchor', 'middle');
|
||
textElement.setAttribute('dominant-baseline', 'middle');
|
||
textElement.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
|
||
textElement.setAttribute('font-size', '14px');
|
||
textElement.setAttribute('fill', 'black');
|
||
textElement.setAttribute('stroke', 'none');
|
||
|
||
if (lines.length === 1) {
|
||
textElement.textContent = lines[0];
|
||
} else {
|
||
// Multi-line text
|
||
lines.forEach((line, index) => {
|
||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||
tspan.textContent = line;
|
||
tspan.setAttribute('x', centerX);
|
||
tspan.setAttribute('dy', index === 0 ? '0' : '1.2em');
|
||
textElement.appendChild(tspan);
|
||
});
|
||
}
|
||
|
||
// Replace foreignObject with text
|
||
fo.parentNode.replaceChild(textElement, fo);
|
||
} else {
|
||
fo.remove();
|
||
}
|
||
});
|
||
|
||
// ONLY process text elements - leave shapes and paths untouched
|
||
const allTextElements = clonedSvg.querySelectorAll('text');
|
||
allTextElements.forEach((textElement, index) => {
|
||
// Remove contentEditable
|
||
textElement.removeAttribute('contentEditable');
|
||
|
||
// Only add missing font attributes, don't modify existing ones
|
||
if (!textElement.hasAttribute('font-family')) {
|
||
textElement.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
|
||
}
|
||
if (!textElement.hasAttribute('font-size')) {
|
||
textElement.setAttribute('font-size', '14px');
|
||
}
|
||
if (!textElement.hasAttribute('fill')) {
|
||
textElement.setAttribute('fill', 'black');
|
||
}
|
||
|
||
// Only clean up pointer-events and user-select from style, keep everything else
|
||
const styleAttr = textElement.getAttribute('style');
|
||
if (styleAttr) {
|
||
const cleanedStyles = styleAttr.split(';')
|
||
.filter(s => {
|
||
const trimmed = s.trim();
|
||
return trimmed &&
|
||
!trimmed.startsWith('pointer-events') &&
|
||
!trimmed.startsWith('user-select') &&
|
||
!trimmed.startsWith('-webkit-user-select') &&
|
||
!trimmed.startsWith('-moz-user-select');
|
||
})
|
||
.join('; ');
|
||
|
||
if (cleanedStyles) {
|
||
textElement.setAttribute('style', cleanedStyles);
|
||
} else {
|
||
textElement.removeAttribute('style');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Process tspan elements - minimal changes
|
||
const allTspanElements = clonedSvg.querySelectorAll('tspan');
|
||
allTspanElements.forEach(tspan => {
|
||
tspan.removeAttribute('contentEditable');
|
||
|
||
// Only clean up pointer-events and user-select
|
||
const styleAttr = tspan.getAttribute('style');
|
||
if (styleAttr) {
|
||
const cleanedStyles = styleAttr.split(';')
|
||
.filter(s => {
|
||
const trimmed = s.trim();
|
||
return trimmed &&
|
||
!trimmed.startsWith('pointer-events') &&
|
||
!trimmed.startsWith('user-select') &&
|
||
!trimmed.startsWith('-webkit-user-select') &&
|
||
!trimmed.startsWith('-moz-user-select');
|
||
})
|
||
.join('; ');
|
||
|
||
if (cleanedStyles) {
|
||
tspan.setAttribute('style', cleanedStyles);
|
||
} else {
|
||
tspan.removeAttribute('style');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Get SVG dimensions
|
||
const bbox = svg.getBBox();
|
||
const width = bbox.width || svg.clientWidth || 800;
|
||
const height = bbox.height || svg.clientHeight || 600;
|
||
|
||
// Set proper dimensions
|
||
clonedSvg.setAttribute('width', width);
|
||
clonedSvg.setAttribute('height', height);
|
||
|
||
// Set viewBox if not already set
|
||
if (!clonedSvg.getAttribute('viewBox')) {
|
||
clonedSvg.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${width} ${height}`);
|
||
}
|
||
|
||
// Serialize SVG to string
|
||
const serializer = new XMLSerializer();
|
||
let svgString = serializer.serializeToString(clonedSvg);
|
||
|
||
// Add XML declaration for standalone SVG file
|
||
svgString = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + svgString;
|
||
|
||
// Create blob and download
|
||
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const link = document.createElement('a');
|
||
link.download = `mermaid-diagram-${Date.now()}.svg`;
|
||
link.href = url;
|
||
link.click();
|
||
|
||
// Cleanup
|
||
URL.revokeObjectURL(url);
|
||
|
||
showMessage('✅ SVG exported - Optimized for Word/PDF with full text compatibility', 'success');
|
||
} catch (error) {
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
console.error('SVG export error:', error);
|
||
}
|
||
}
|
||
|
||
function setupPanAndZoom() {
|
||
const output = document.getElementById('diagramOutput');
|
||
if (!output) return;
|
||
|
||
// Mouse wheel zoom
|
||
const handleWheel = (e) => {
|
||
e.preventDefault();
|
||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||
zoomScale = Math.max(0.2, Math.min(10, zoomScale + delta));
|
||
updateTransform();
|
||
};
|
||
output.addEventListener('wheel', handleWheel, { passive: false });
|
||
|
||
// Pan functionality
|
||
const handleMouseDown = (e) => {
|
||
if (e.target.closest('.zoom-controls')) return;
|
||
isDragging = true;
|
||
startX = e.clientX - panX;
|
||
startY = e.clientY - panY;
|
||
output.classList.add('grabbing');
|
||
};
|
||
output.addEventListener('mousedown', handleMouseDown);
|
||
|
||
const handleMouseMove = (e) => {
|
||
if (!isDragging) return;
|
||
panX = e.clientX - startX;
|
||
panY = e.clientY - startY;
|
||
updateTransform();
|
||
};
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
|
||
const handleMouseUp = () => {
|
||
isDragging = false;
|
||
output.classList.remove('grabbing');
|
||
};
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
|
||
// Touch support for mobile
|
||
let touchStartX = 0, touchStartY = 0;
|
||
let touchStartDistance = 0;
|
||
let initialScale = 1;
|
||
|
||
const handleTouchStart = (e) => {
|
||
if (e.touches.length === 1) {
|
||
touchStartX = e.touches[0].clientX - panX;
|
||
touchStartY = e.touches[0].clientY - panY;
|
||
} else if (e.touches.length === 2) {
|
||
touchStartDistance = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
initialScale = zoomScale;
|
||
}
|
||
};
|
||
output.addEventListener('touchstart', handleTouchStart);
|
||
|
||
const handleTouchMove = (e) => {
|
||
e.preventDefault();
|
||
if (e.touches.length === 1) {
|
||
panX = e.touches[0].clientX - touchStartX;
|
||
panY = e.touches[0].clientY - touchStartY;
|
||
updateTransform();
|
||
} else if (e.touches.length === 2) {
|
||
const distance = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
zoomScale = Math.max(0.2, Math.min(10, initialScale * (distance / touchStartDistance)));
|
||
updateTransform();
|
||
}
|
||
};
|
||
output.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||
}
|
||
|
||
function renderFromTextarea() {
|
||
const code = document.getElementById('mermaidCode').value;
|
||
renderDiagram(code);
|
||
}
|
||
|
||
function handleFile(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const content = e.target.result;
|
||
document.getElementById('mermaidCode').value = content;
|
||
renderDiagram(content);
|
||
showMessage('✅ File loaded: ' + file.name, 'success');
|
||
};
|
||
reader.onerror = function() {
|
||
showMessage('❌ Error reading file', 'error');
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(event) {
|
||
const content = event.target.result;
|
||
document.getElementById('mermaidCode').value = content;
|
||
renderDiagram(content);
|
||
showMessage('✅ File loaded: ' + file.name, 'success');
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
});
|
||
|
||
document.getElementById('mermaidCode').addEventListener('keydown', function(e) {
|
||
if (e.ctrlKey && e.key === 'Enter') {
|
||
renderFromTextarea();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |