RE_Documents/Mermaid_Selector.html

825 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
}
</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" onclick="exportAsPNG()" title="Export as PNG" style="font-size: 1em;">PNG</button>
<button class="zoom-btn" onclick="exportAsJPEG()" title="Export as JPEG" style="font-size: 1em;">JPG</button>
<button class="zoom-btn" onclick="exportAsSVG()" title="Export as SVG" style="font-size: 1em;">SVG</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 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');
// Add embedded style for standard fonts
const styleElement = document.createElementNS('http://www.w3.org/2000/svg', 'style');
styleElement.textContent = `
text {
font-family: Arial, Helvetica, Verdana, sans-serif !important;
font-size: 14px !important;
}
.nodeLabel, .edgeLabel {
font-family: Arial, Helvetica, Verdana, sans-serif !important;
}
`;
clonedSvg.insertBefore(styleElement, clonedSvg.firstChild);
// 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('✅ Diagram exported as SVG', '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>