refactor: enhance mermaid fullscreen functionality with improved SVG handling and dynamic ID management
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user