fix(editor): fit the view after loaded nodes are measured

This commit is contained in:
Julien Calixte
2026-06-20 10:07:59 +02:00
parent 5e50b2dfd4
commit 6815b81701

View File

@@ -25,7 +25,7 @@ import {
VueFlow, VueFlow,
type XYPosition, type XYPosition,
} from "@vue-flow/core" } from "@vue-flow/core"
import { computed, nextTick, onBeforeUnmount, onMounted, useTemplateRef } from "vue" import { computed, onBeforeUnmount, onMounted, useTemplateRef } from "vue"
import { useAutosave } from "@/composables/useAutosave" import { useAutosave } from "@/composables/useAutosave"
import { parseModel, serializeModel } from "@/model/io" import { parseModel, serializeModel } from "@/model/io"
import { project } from "@/model/projection" import { project } from "@/model/projection"
@@ -57,6 +57,7 @@ const {
onConnectStart, onConnectStart,
onConnectEnd, onConnectEnd,
onError, onError,
onNodesInitialized,
getSelectedNodes, getSelectedNodes,
getSelectedEdges, getSelectedEdges,
viewport, viewport,
@@ -68,6 +69,18 @@ const {
// view once a restored model has re-projected so it lands framed. // view once a restored model has re-projected so it lands framed.
useAutosave({ onRestore: () => fitView({ padding: 0.2 }) }) useAutosave({ onRestore: () => fitView({ padding: 0.2 }) })
// Fit the view after a *load* (sample/import), once the freshly projected nodes
// have been measured. Vue Flow measures node dimensions a frame after they mount,
// so calling fitView straight after setModel fits to 0×0 boxes and the model
// lands jammed in the top-left; onNodesInitialized fires post-measure, so framing
// is correct. A one-shot flag keeps it scoped to loads (not every re-init).
let fitAfterInit = false
onNodesInitialized(() => {
if (!fitAfterInit) return
fitAfterInit = false
fitView({ padding: 0.2 })
})
onNodeDragStart(() => store.beginInteraction()) onNodeDragStart(() => store.beginInteraction())
onNodeDragStop(({ nodes: dragged }) => { onNodeDragStop(({ nodes: dragged }) => {
for (const node of dragged) store.moveNode(node.id, node.position) for (const node of dragged) store.moveNode(node.id, node.position)
@@ -175,15 +188,14 @@ function onDragOver(event: DragEvent): void {
* stay frictionless on the empty starting canvas. Fit the view once the * stay frictionless on the empty starting canvas. Fit the view once the
* projection has re-derived so the loaded model lands framed. * projection has re-derived so the loaded model lands framed.
*/ */
async function loadSample(sample: Sample): Promise<void> { function loadSample(sample: Sample): void {
if (store.nodeCount > 0 && !window.confirm(`Replace the current model with “${sample.title}”?`)) { if (store.nodeCount > 0 && !window.confirm(`Replace the current model with “${sample.title}”?`)) {
return return
} }
fitAfterInit = true
store.setModel(sample.build()) store.setModel(sample.build())
// Close the DaisyUI dropdown (it stays open while the trigger keeps focus). // Close the DaisyUI dropdown (it stays open while the trigger keeps focus).
;(document.activeElement as HTMLElement | null)?.blur() ;(document.activeElement as HTMLElement | null)?.blur()
await nextTick()
fitView({ padding: 0.2 })
} }
const fileInput = useTemplateRef<HTMLInputElement>("fileInput") const fileInput = useTemplateRef<HTMLInputElement>("fileInput")
@@ -229,9 +241,8 @@ async function onImportFile(event: Event): Promise<void> {
) { ) {
return return
} }
fitAfterInit = true
store.setModel(result.model) store.setModel(result.model)
await nextTick()
fitView({ padding: 0.2 })
} }
function isTextEntry(target: EventTarget | null): boolean { function isTextEntry(target: EventTarget | null): boolean {