refactor: enhance mermaid fullscreen functionality with improved SVG handling and dynamic ID management

This commit is contained in:
Ümit Tunç
2026-05-25 07:47:29 +03:00
parent 40fbb750db
commit c28450b97d
+120 -39
View File
@@ -299,6 +299,7 @@
}
.mermaid-fullscreen-canvas {
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
cursor: grab;
@@ -954,6 +955,7 @@
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-container no-print transition-all duration-300';
wrapper.id = 'mermaid-wrapper-' + index;
wrapper.dataset.mermaidSource = rawMermaid;
// Zoom Toolbar
const toolbar = document.createElement('div');
@@ -1104,61 +1106,136 @@
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
}
// Fullscreen
window.mermaidZoomIn = mermaidZoomIn;
window.mermaidZoomOut = mermaidZoomOut;
window.mermaidZoomReset = mermaidZoomReset;
// Fullscreen — independent render; original diagram is never moved or destroyed
let fsActivePanZoom = null;
function mermaidFullscreen(index) {
const canvas = document.getElementById('mermaid-canvas-' + index);
if (!canvas) return;
const svg = canvas.querySelector('svg');
if (!svg) return;
let fsActiveIndex = null;
function uniquifySvgIds(svgRoot, prefix) {
const idMap = {};
svgRoot.querySelectorAll('[id]').forEach((el) => {
const oldId = el.id;
const newId = prefix + '-' + oldId;
idMap[oldId] = newId;
el.id = newId;
});
const urlAttrs = ['fill', 'stroke', 'marker-end', 'marker-start', 'marker-mid', 'clip-path', 'mask', 'filter', 'href', 'xlink:href'];
svgRoot.querySelectorAll('*').forEach((el) => {
urlAttrs.forEach((attr) => {
const val = el.getAttribute(attr);
if (!val || !val.includes('url(#')) return;
const updated = val.replace(/url\(#([^)]+)\)/g, (match, id) => (
idMap[id] ? 'url(#' + idMap[id] + ')' : match
));
if (updated !== val) el.setAttribute(attr, updated);
});
const style = el.getAttribute('style');
if (style && style.includes('url(#')) {
const updatedStyle = style.replace(/url\(#([^)]+)\)/g, (match, id) => (
idMap[id] ? 'url(#' + idMap[id] + ')' : match
));
if (updatedStyle !== style) el.setAttribute('style', updatedStyle);
}
});
}
function initMermaidFsPanZoom(index, svgId) {
const fsCanvas = document.getElementById('mermaid-fs-canvas');
const svg = document.getElementById(svgId);
if (!svg || !fsCanvas) return;
const fsW = fsCanvas.clientWidth || fsCanvas.offsetWidth || 1000;
const fsH = fsCanvas.clientHeight || fsCanvas.offsetHeight || 600;
svg.setAttribute('width', fsW);
svg.setAttribute('height', fsH);
svg.style.width = fsW + 'px';
svg.style.height = fsH + 'px';
try {
if (fsActivePanZoom) {
fsActivePanZoom.destroy();
fsActivePanZoom = null;
}
fsActivePanZoom = svgPanZoom('#' + svgId, {
zoomEnabled: true,
panEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.2,
maxZoom: 10,
zoomScaleSensitivity: 0.3,
mouseWheelZoomEnabled: true,
onZoom: (zoom) => {
const pctEl = document.getElementById('mermaid-fs-pct');
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
}
});
const pctEl = document.getElementById('mermaid-fs-pct');
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
} catch (e) {
console.warn('fs pan-zoom init error', e);
}
}
async function mermaidFullscreen(index) {
const wrapper = document.getElementById('mermaid-wrapper-' + index);
if (!wrapper) return;
const rawMermaid = wrapper.dataset.mermaidSource;
if (!rawMermaid) return;
const fsCanvas = document.getElementById('mermaid-fs-canvas');
fsCanvas.innerHTML = '';
// Show overlay first so fsCanvas has layout dimensions
fsActiveIndex = index;
const renderId = 'mermaid-fs-' + index + '-' + Date.now();
document.getElementById('mermaid-fs-overlay').classList.add('active');
document.body.style.overflow = 'hidden';
// Init pan-zoom on cloned SVG after layout is available
setTimeout(() => {
const svgClone = svg.cloneNode(true);
svgClone.id = 'mermaid-fs-svg-' + index;
try {
const { svg, bindFunctions } = await mermaid.render(renderId, rawMermaid);
fsCanvas.innerHTML = svg;
if (typeof bindFunctions === 'function') {
bindFunctions(fsCanvas);
}
// Get actual pixel dimensions of the fullscreen canvas
const fsW = fsCanvas.clientWidth || fsCanvas.offsetWidth || 1000;
const fsH = fsCanvas.clientHeight || fsCanvas.offsetHeight || 600;
const svgEl = fsCanvas.querySelector('svg');
if (!svgEl) return;
// Set explicit dimensions on SVG for svg-pan-zoom
svgClone.setAttribute('width', fsW);
svgClone.setAttribute('height', fsH);
svgClone.style.width = fsW + 'px';
svgClone.style.height = fsH + 'px';
uniquifySvgIds(svgEl, renderId);
const svgId = renderId + '-svg';
svgEl.id = svgId;
fsCanvas.appendChild(svgClone);
try {
fsActivePanZoom = svgPanZoom('#mermaid-fs-svg-' + index, {
zoomEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.2,
maxZoom: 10,
zoomScaleSensitivity: 0.3,
onZoom: (zoom) => {
const pctEl = document.getElementById('mermaid-fs-pct');
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
}
});
document.getElementById('mermaid-fs-pct').textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
} catch(e) { console.warn('fs pan-zoom init error', e); }
}, 150);
// Wait for overlay layout before pan-zoom init
setTimeout(() => initMermaidFsPanZoom(index, svgId), 100);
} catch (err) {
console.warn('fs mermaid render error', err);
closeMermaidFullscreen();
}
}
function closeMermaidFullscreen() {
document.getElementById('mermaid-fs-overlay').classList.remove('active');
document.body.style.overflow = '';
if (fsActivePanZoom) { try { fsActivePanZoom.destroy(); } catch(e) {} fsActivePanZoom = null; }
if (fsActivePanZoom) {
try { fsActivePanZoom.destroy(); } catch (e) {}
fsActivePanZoom = null;
}
const fsCanvas = document.getElementById('mermaid-fs-canvas');
if (fsCanvas) fsCanvas.innerHTML = '';
fsActiveIndex = null;
}
function mermaidFsZoom(action) {
if (!fsActivePanZoom) return;
if (action === 'in') fsActivePanZoom.zoomIn();
@@ -1167,6 +1244,10 @@
const pctEl = document.getElementById('mermaid-fs-pct');
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
}
window.mermaidFullscreen = mermaidFullscreen;
window.closeMermaidFullscreen = closeMermaidFullscreen;
window.mermaidFsZoom = mermaidFsZoom;
// Keyboard ESC closes fullscreen
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeMermaidFullscreen();