Files
mermaid-live-editor/src/lib/components/View.svelte
T
2025-03-16 17:32:14 -07:00

150 lines
4.9 KiB
Svelte

<script lang="ts">
import { PanZoomState } from '$/util/panZoom';
import type { State, ValidatedState } from '$lib/types';
import { recordRenderTime, shouldRefreshView } from '$lib/util/autoSync';
import { render as renderDiagram } from '$lib/util/mermaid';
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
import { logEvent, saveStatistics } from '$lib/util/stats';
import uniqueID from 'lodash-es/uniqueId';
import type { MermaidConfig } from 'mermaid';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
import { Svg2Roughjs } from 'svg2roughjs';
let {
panZoomState = new PanZoomState(),
shouldShowGrid = true
}: { panZoomState?: PanZoomState; shouldShowGrid?: boolean } = $props();
let code = '';
let config = '';
let container: HTMLDivElement | undefined = $state();
let rough: boolean;
let view: HTMLDivElement | undefined = $state();
let error = $state(false);
let manualUpdate = true;
// Set up panZoom state observer to update the store when pan/zoom changes
const setupPanZoomObserver = () => {
panZoomState.onPanZoomChange = (pan, zoom) => {
updateCodeStore({ pan, zoom });
logEvent('panZoom');
};
};
const handlePanZoom = (state: State, graphDiv: SVGSVGElement) => {
void Promise.resolve().then(() => {
panZoomState.updateElement(graphDiv, state);
});
};
const handleStateChange = async (state: ValidatedState) => {
const startTime = Date.now();
if (state.error !== undefined) {
error = true;
return;
}
error = false;
let diagramType: string | undefined;
try {
if (container) {
manualUpdate = true;
// Do not render if there is no change in Code/Config/PanZoom
if (code === state.code && config === state.mermaid && rough === state.rough) {
return;
}
if (!shouldRefreshView()) {
return;
}
code = state.code;
config = state.mermaid;
rough = state.rough;
const scroll = view?.parentElement?.scrollTop;
delete container.dataset.processed;
const viewID = uniqueID('graph-');
const {
svg,
bindFunctions,
diagramType: detectedDiagramType
} = await renderDiagram(JSON.parse(state.mermaid) as MermaidConfig, code, viewID);
diagramType = detectedDiagramType;
if (svg.length > 0) {
container.innerHTML = svg;
let graphDiv = document.querySelector<SVGSVGElement>(`#${viewID}`);
if (!graphDiv) {
throw new Error('graph-div not found');
}
if (state.rough) {
const svg2roughjs = new Svg2Roughjs('#container');
svg2roughjs.svg = graphDiv;
await svg2roughjs.sketch();
graphDiv.remove();
const sketch = document.querySelector<SVGSVGElement>('#container > svg');
if (!sketch) {
throw new Error('sketch not found');
}
const height = sketch.getAttribute('height');
const width = sketch.getAttribute('width');
sketch.setAttribute('id', 'graph-div');
sketch.setAttribute('height', '100%');
sketch.setAttribute('width', '100%');
sketch.setAttribute('viewBox', `0 0 ${width} ${height}`);
sketch.style.maxWidth = '100%';
graphDiv = sketch;
} else {
graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%';
if (bindFunctions) {
bindFunctions(graphDiv);
}
}
handlePanZoom(state, graphDiv);
}
if (view?.parentElement && scroll) {
view.parentElement.scrollTop = scroll;
}
error = false;
} else if (manualUpdate) {
manualUpdate = false;
}
} catch (error_) {
console.error('view fail', error_);
error = true;
}
const renderTime = Date.now() - startTime;
saveStatistics({ code, diagramType, isRough: state.rough, renderTime });
recordRenderTime(renderTime, () => {
$inputStateStore.updateDiagram = true;
});
};
onMount(() => {
setupPanZoomObserver();
// Queue state changes to avoid race condition
let pendingStateChange = Promise.resolve();
stateStore.subscribe((state) => {
pendingStateChange = pendingStateChange.then(() => handleStateChange(state).catch(() => {}));
});
});
</script>
<div
id="view"
bind:this={view}
class={[shouldShowGrid && `grid-bg-${$mode}`, error && 'opacity-50', 'h-full w-full']}>
<div id="container" bind:this={container} class="h-full overflow-auto"></div>
</div>
<style>
.grid-bg-light {
background-size: 30px 30px;
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 2px);
}
.grid-bg-dark {
background-size: 30px 30px;
background-image: radial-gradient(circle, #46464646 2px, #0000 2px);
}
</style>