liaoch's picture
rendering for mobile view
b6c7e17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid Live Renderer</title>
<style>
body { font-family: sans-serif; margin: 1em; background-color: #f4f4f4; color: #333; }
.container { max-width: 1200px; margin: auto; background: #fff; padding: 1em; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); display: flex; gap: 2em;}
.input-area { flex: 1; }
.preview-area { flex: 1; border-left: 1px solid #eee; padding-left: 2em; }
/* Mobile responsive styles */
@media (max-width: 768px) {
body { margin: 0.5em; }
.container {
flex-direction: column;
padding: 0.5em;
gap: 1em;
}
.input-area {
width: 100%;
}
.preview-area {
width: 100%;
border-left: none;
padding-left: 0;
border-top: 1px solid #eee;
padding-top: 1em;
}
h1 { font-size: 1.5em; }
textarea {
width: 95%;
min-height: 200px;
}
.options {
flex-direction: column;
align-items: stretch;
}
.options > div {
width: 100%;
}
select {
width: 100%;
margin-top: 0.5em;
margin-right: 0;
}
.preview-controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.preview-controls button {
padding: 0.5em;
font-size: 1rem;
min-width: 40px;
margin: 0.2em;
}
#preview-box {
min-height: 200px;
}
}
@media (max-width: 480px) {
.container {
padding: 0.3em;
}
textarea {
width: 93%;
min-height: 150px;
}
.preview-controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.preview-controls button {
flex: 1 1 30%;
margin: 0.2em;
}
}
h1 { color: #555; text-align: center; margin-bottom: 1em; width: 100%;}
label { display: block; margin-top: 1em; font-weight: bold; }
textarea { width: 95%; min-height: 300px; margin-top: 0.5em; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; font-size: 1rem; }
select, button { padding: 0.8em 1.2em; margin-top: 0.5em; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; cursor: pointer; margin-right: 0.5em; }
button.primary { background-color: #5cb85c; color: white; border-color: #4cae4c; font-weight: bold; }
button.primary:hover { background-color: #4cae4c; }
button.secondary { background-color: #5bc0de; color: white; border-color: #46b8da; }
button.secondary:hover { background-color: #31b0d5; }
.options { display: flex; gap: 1em; align-items: center; margin-top: 1em; flex-wrap: wrap;}
.flash { padding: 1em; margin-bottom: 1em; border-radius: 5px; border: 1px solid transparent; }
.flash.error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
.flash.warning { background-color: #fff3cd; color: #856404; border-color: #ffeeba; }
.flash.info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb; }
#preview-box { margin-top: 1em; padding: 1em; border: 1px solid #ccc; border-radius: 4px; min-height: 300px; text-align: center; background-color: #fdfdfd; overflow: auto; display: flex; justify-content: center; align-items: center;}
#preview-box img, #preview-box svg { display: block; /* Prevent extra space below */ }
#preview-content-wrapper { /* Inner wrapper for transformations */
width: 100%;
height: 100%;
transition: transform 0.2s ease-out; /* Smooth transitions */
transform-origin: center center; /* Zoom from center */
cursor: grab; /* Indicate pannable */
}
#preview-content-wrapper:active {
cursor: grabbing;
}
#preview-status { margin-top: 0.5em; font-style: italic; color: #777; min-height: 1.2em;}
.preview-controls { text-align: center; margin-bottom: 0.5em; }
.preview-controls button { padding: 0.3em 0.6em; font-size: 0.9rem; min-width: 30px; }
</style>
</head>
</head>
<body>
<div class="container">
<div class="input-area">
<h1>Mermaid Input</h1>
<p style="font-size: 0.9em; color: #666; margin-top: -0.5em; margin-bottom: 1.5em;">Enter Mermaid code below. The preview on the right updates automatically.</p>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Main form for final download -->
<form id="download-form" action="{{ url_for('render_mermaid') }}" method="post">
<label for="mermaid_code">Mermaid Code:</label>
<textarea id="mermaid_code" name="mermaid_code" required placeholder="e.g., graph TD; A-->B;">{{ default_code }}</textarea>
<div class="options">
<div>
<label for="output_format">Format:</label>
<select id="output_format" name="output_format">
<option value="png">PNG</option>
<option value="svg" selected>SVG</option> <!-- Default to SVG for better preview -->
<option value="pdf">PDF</option>
</select>
</div>
<div>
<label for="theme">Theme:</label>
<select id="theme" name="theme">
<option value="default" selected>Default</option>
<option value="forest">Forest</option>
<option value="dark">Dark</option>
<option value="neutral">Neutral</option>
</select>
</div>
</div>
<br><br>
<!-- Download Button (submits the form) -->
<button type="submit" class="primary">Download Diagram</button>
<!-- Preview button removed, preview updates automatically -->
</form>
</div>
<div class="preview-area">
<h1>Live Preview</h1>
<p style="font-size: 0.9em; color: #666; margin-top: -0.5em; margin-bottom: 0.5em;">Use controls to zoom/pan, or click & drag the preview.</p>
<div id="preview-status">Enter code to see preview.</div>
<div class="preview-controls">
<button id="zoom-in">+</button>
<button id="zoom-out">-</button>
<button id="pan-up"></button>
<button id="pan-down"></button>
<button id="pan-left"></button>
<button id="pan-right"></button>
<button id="reset-view">Reset</button>
</div>
<div id="preview-box">
<div id="preview-content-wrapper">
<!-- Preview will be loaded here by JavaScript -->
<p style="color: #aaa;">Preview Area</p>
</div>
</div>
</div>
</div>
<script>
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const previewBox = document.getElementById('preview-box');
const previewContentWrapper = document.getElementById('preview-content-wrapper');
const previewStatus = document.getElementById('preview-status');
const mermaidCodeInput = document.getElementById('mermaid_code');
const outputFormatSelect = document.getElementById('output_format'); // Still needed for download format
const themeSelect = document.getElementById('theme');
// Function to fetch and update preview
const updatePreview = async () => {
const mermaidCode = mermaidCodeInput.value;
// Preview will always use SVG for best results and simplicity, download format is separate
const previewFormat = 'svg';
const theme = themeSelect.value;
if (!mermaidCode.trim()) {
// Don't show error for empty input, just clear preview
previewStatus.textContent = 'Enter code to see preview.';
previewStatus.style.color = '#777';
previewContentWrapper.innerHTML = '<p style="color: #aaa;">Preview Area</p>';
resetTransform(); // Reset view when input is empty
return;
}
previewStatus.textContent = 'Generating preview...';
previewStatus.style.color = '#777';
previewContentWrapper.innerHTML = '<p style="color: #aaa;">Loading...</p>'; // Show loading indicator in the wrapper
try {
const response = await fetch("{{ url_for('preview_mermaid') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mermaid_code: mermaidCode,
output_format: previewFormat, // Always request SVG for preview
theme: theme,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
resetTransform(); // Reset view before loading new content
previewContentWrapper.innerHTML = ''; // Clear previous preview/loading message
if (result.format === 'png') {
const img = document.createElement('img');
img.src = `data:image/png;base64,${result.data}`;
img.alt = 'Mermaid Diagram Preview';
// Prevent dragging the image itself, we'll handle panning
img.style.pointerEvents = 'none';
previewContentWrapper.appendChild(img);
} else if (result.format === 'svg') {
// Directly insert SVG markup into the wrapper
previewContentWrapper.innerHTML = result.data;
const svgElement = previewContentWrapper.querySelector('svg');
if (svgElement) {
// Make SVG take up space correctly and prevent internal pointer events interfering
svgElement.style.maxWidth = '100%';
svgElement.style.height = 'auto';
svgElement.style.display = 'block';
svgElement.style.pointerEvents = 'none';
}
}
previewStatus.textContent = `Preview updated (SVG).`; // Preview is always SVG now
previewStatus.style.color = 'green';
} catch (error) {
console.error('Error fetching preview:', error);
previewStatus.textContent = `Error: ${error.message}`;
previewStatus.style.color = 'red';
previewContentWrapper.innerHTML = '<p style="color: red;">Failed to load preview.</p>';
}
};
// --- Zoom and Pan Logic ---
let scale = 1;
let translateX = 0;
let translateY = 0;
const zoomStep = 0.1;
const panStep = 30; // pixels
function applyTransform() {
previewContentWrapper.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
function resetTransform() {
scale = 1;
translateX = 0;
translateY = 0;
applyTransform();
}
// Button Event Listeners
document.getElementById('zoom-in').addEventListener('click', () => {
scale += zoomStep;
applyTransform();
});
document.getElementById('zoom-out').addEventListener('click', () => {
scale = Math.max(0.1, scale - zoomStep); // Prevent zooming out too much
applyTransform();
});
document.getElementById('pan-up').addEventListener('click', () => {
translateY -= panStep;
applyTransform();
});
document.getElementById('pan-down').addEventListener('click', () => {
translateY += panStep;
applyTransform();
});
document.getElementById('pan-left').addEventListener('click', () => {
translateX -= panStep;
applyTransform();
});
document.getElementById('pan-right').addEventListener('click', () => {
translateX += panStep;
applyTransform();
});
document.getElementById('reset-view').addEventListener('click', resetTransform);
// --- Drag to Pan Logic ---
let isDragging = false;
let startX, startY;
let initialTranslateX, initialTranslateY;
previewBox.addEventListener('mousedown', (e) => {
// Only start drag if clicking directly on the preview box (not buttons etc.)
if (e.target === previewBox || e.target === previewContentWrapper) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialTranslateX = translateX;
initialTranslateY = translateY;
previewContentWrapper.style.transition = 'none'; // Disable transition during drag
previewContentWrapper.style.cursor = 'grabbing'; // Change cursor
previewBox.style.userSelect = 'none'; // Prevent text selection during drag
}
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const currentX = e.clientX;
const currentY = e.clientY;
translateX = initialTranslateX + (currentX - startX);
translateY = initialTranslateY + (currentY - startY);
applyTransform();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
previewContentWrapper.style.transition = 'transform 0.2s ease-out'; // Re-enable transition
previewContentWrapper.style.cursor = 'grab'; // Restore cursor
previewBox.style.userSelect = ''; // Re-enable text selection
}
});
// Prevent dragging state from sticking if mouse leaves the window
document.addEventListener('mouseleave', () => {
if (isDragging) {
isDragging = false;
previewContentWrapper.style.transition = 'transform 0.2s ease-out';
previewContentWrapper.style.cursor = 'grab';
previewBox.style.userSelect = '';
}
});
// Debounced version of the updatePreview function
const debouncedUpdatePreview = debounce(updatePreview, 750); // 750ms delay
// Event listener for textarea input
mermaidCodeInput.addEventListener('input', debouncedUpdatePreview);
// Also update preview if theme changes
themeSelect.addEventListener('change', debouncedUpdatePreview);
// Initial preview load on page ready
document.addEventListener('DOMContentLoaded', () => {
if (mermaidCodeInput.value.trim()) {
previewStatus.textContent = 'Rendering initial example...';
updatePreview(); // Render the default code on load
} else {
previewStatus.textContent = 'Enter code to see preview.';
}
});
</script>
</body>
</html>