From c28450b97d48229de43fdec8335d159aa2dac335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 25 May 2026 07:47:29 +0300 Subject: [PATCH] refactor: enhance mermaid fullscreen functionality with improved SVG handling and dynamic ID management --- resources/views/proposals/show.blade.php | 159 +++++++++++++++++------ 1 file changed, 120 insertions(+), 39 deletions(-) diff --git a/resources/views/proposals/show.blade.php b/resources/views/proposals/show.blade.php index fa1f116..9163b47 100644 --- a/resources/views/proposals/show.blade.php +++ b/resources/views/proposals/show.blade.php @@ -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();