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 {
|
.mermaid-fullscreen-canvas {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -954,6 +955,7 @@
|
|||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'mermaid-container no-print transition-all duration-300';
|
wrapper.className = 'mermaid-container no-print transition-all duration-300';
|
||||||
wrapper.id = 'mermaid-wrapper-' + index;
|
wrapper.id = 'mermaid-wrapper-' + index;
|
||||||
|
wrapper.dataset.mermaidSource = rawMermaid;
|
||||||
|
|
||||||
// Zoom Toolbar
|
// Zoom Toolbar
|
||||||
const toolbar = document.createElement('div');
|
const toolbar = document.createElement('div');
|
||||||
@@ -1104,61 +1106,136 @@
|
|||||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
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;
|
let fsActivePanZoom = null;
|
||||||
function mermaidFullscreen(index) {
|
let fsActiveIndex = null;
|
||||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
|
||||||
if (!canvas) return;
|
function uniquifySvgIds(svgRoot, prefix) {
|
||||||
const svg = canvas.querySelector('svg');
|
const idMap = {};
|
||||||
if (!svg) return;
|
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');
|
const fsCanvas = document.getElementById('mermaid-fs-canvas');
|
||||||
fsCanvas.innerHTML = '';
|
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.getElementById('mermaid-fs-overlay').classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
// Init pan-zoom on cloned SVG after layout is available
|
try {
|
||||||
setTimeout(() => {
|
const { svg, bindFunctions } = await mermaid.render(renderId, rawMermaid);
|
||||||
const svgClone = svg.cloneNode(true);
|
fsCanvas.innerHTML = svg;
|
||||||
svgClone.id = 'mermaid-fs-svg-' + index;
|
if (typeof bindFunctions === 'function') {
|
||||||
|
bindFunctions(fsCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
// Get actual pixel dimensions of the fullscreen canvas
|
const svgEl = fsCanvas.querySelector('svg');
|
||||||
const fsW = fsCanvas.clientWidth || fsCanvas.offsetWidth || 1000;
|
if (!svgEl) return;
|
||||||
const fsH = fsCanvas.clientHeight || fsCanvas.offsetHeight || 600;
|
|
||||||
|
|
||||||
// Set explicit dimensions on SVG for svg-pan-zoom
|
uniquifySvgIds(svgEl, renderId);
|
||||||
svgClone.setAttribute('width', fsW);
|
const svgId = renderId + '-svg';
|
||||||
svgClone.setAttribute('height', fsH);
|
svgEl.id = svgId;
|
||||||
svgClone.style.width = fsW + 'px';
|
|
||||||
svgClone.style.height = fsH + 'px';
|
|
||||||
|
|
||||||
fsCanvas.appendChild(svgClone);
|
// Wait for overlay layout before pan-zoom init
|
||||||
|
setTimeout(() => initMermaidFsPanZoom(index, svgId), 100);
|
||||||
try {
|
} catch (err) {
|
||||||
fsActivePanZoom = svgPanZoom('#mermaid-fs-svg-' + index, {
|
console.warn('fs mermaid render error', err);
|
||||||
zoomEnabled: true,
|
closeMermaidFullscreen();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMermaidFullscreen() {
|
function closeMermaidFullscreen() {
|
||||||
document.getElementById('mermaid-fs-overlay').classList.remove('active');
|
document.getElementById('mermaid-fs-overlay').classList.remove('active');
|
||||||
document.body.style.overflow = '';
|
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) {
|
function mermaidFsZoom(action) {
|
||||||
if (!fsActivePanZoom) return;
|
if (!fsActivePanZoom) return;
|
||||||
if (action === 'in') fsActivePanZoom.zoomIn();
|
if (action === 'in') fsActivePanZoom.zoomIn();
|
||||||
@@ -1167,6 +1244,10 @@
|
|||||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||||
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.mermaidFullscreen = mermaidFullscreen;
|
||||||
|
window.closeMermaidFullscreen = closeMermaidFullscreen;
|
||||||
|
window.mermaidFsZoom = mermaidFsZoom;
|
||||||
// Keyboard ESC closes fullscreen
|
// Keyboard ESC closes fullscreen
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape') closeMermaidFullscreen();
|
if (e.key === 'Escape') closeMermaidFullscreen();
|
||||||
|
|||||||
Reference in New Issue
Block a user