fix(diagram): fit the canvas after switching notes
Wait for a real canvas size, then refit once scroller auto-resize settles, so a newly opened diagram is not left in a corner of the previous viewport.
This commit is contained in:
1 parent
a4a10a32b9
commit
c6577b4fab
4 files changed
+64
-5
No files matched your search
@@ -33,6 +33,19 @@ describe("diagram editor keyboard workflow", () => {
|
||||
expect(source).not.toContain("visibleNodes.length !== graph.getNodes().length");
|
||||
});
|
||||
|
||||
test("fits a newly opened diagram after the canvas has size and scroller auto-resize settles", () => {
|
||||
const init = source.slice(source.indexOf("graph = new Graph"), source.indexOf("const updateHistory"));
|
||||
expect(init.indexOf("graphRef.current = graph")).toBeGreaterThan(-1);
|
||||
expect(init.indexOf("graphRef.current = graph")).toBeLessThan(init.indexOf("const settleLoadedViewport"));
|
||||
expect(init).toContain("diagramCanvasIsReady(canvasSurfaceRef.current)");
|
||||
expect(init).toContain("scroller?.disableAutoResize()");
|
||||
expect(init).toContain("new ResizeObserver");
|
||||
expect(init).toContain("scroller?.enableAutoResize()");
|
||||
expect(init).toContain("scroller?.updateScroller()");
|
||||
expect(init).toContain("SCROLLER_AUTORESIZE_SETTLE_MS");
|
||||
expect(init).toContain("loadFitObserver?.disconnect()");
|
||||
});
|
||||
|
||||
test("does not let scroller auto-fit flash a detached node while inserting", () => {
|
||||
expect(source).toContain("disableAutoResize()");
|
||||
expect(source).toContain("enableAutoResize()");
|
||||
|
||||
@@ -136,7 +136,7 @@ import {
|
||||
getDiagramLayoutViewport,
|
||||
type DiagramLayoutViewport,
|
||||
} from "@/lib/diagram-layout";
|
||||
import { applyDiagramScrollerFitOptions, isUsableDiagramBounds } from "@/lib/diagram-scroller-fit";
|
||||
import { applyDiagramScrollerFitOptions, diagramCanvasIsReady, isUsableDiagramBounds } from "@/lib/diagram-scroller-fit";
|
||||
import { resolveDiagramPalette, type DiagramAppearance } from "@/lib/diagram-theme";
|
||||
import { isLocalMemoId } from "@/lib/local-mirror";
|
||||
import { isBrowserOffline } from "@/lib/network-status";
|
||||
@@ -1611,6 +1611,7 @@ export const DiagramEditorPane = ({
|
||||
className: "edgeever-diagram-scroller",
|
||||
}));
|
||||
bindDiagramScrollerFit(graph);
|
||||
graphRef.current = graph;
|
||||
graph.use(new History({ enabled: !readOnly }));
|
||||
graph.use(new Export());
|
||||
graph.use(new Keyboard({
|
||||
@@ -1652,14 +1653,35 @@ export const DiagramEditorPane = ({
|
||||
applyGraphPalette(graph, documentTheme, document.kind, appearance, documentStructure);
|
||||
graph.on("scale", () => setZoomPercent(Math.round(graph.scale().sx * 100)));
|
||||
graph.cleanHistory();
|
||||
const scroller = getDiagramScroller(graph);
|
||||
scroller?.disableAutoResize();
|
||||
const settleLoadedViewport = () => {
|
||||
if (graphRef.current !== graph) return;
|
||||
if (graphRef.current !== graph) return false;
|
||||
if (!diagramCanvasIsReady(canvasSurfaceRef.current)) return false;
|
||||
ensureDiagramPaperContainsNodes(graph);
|
||||
fitDiagramContent(graph, document, containerRef.current);
|
||||
return true;
|
||||
};
|
||||
settleLoadedViewport();
|
||||
graph.once("render:done", settleLoadedViewport);
|
||||
requestAnimationFrame(settleLoadedViewport);
|
||||
const loadFitFrame = window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => settleLoadedViewport());
|
||||
});
|
||||
const canvasSurface = canvasSurfaceRef.current;
|
||||
let loadFitObserver: ResizeObserver | null = null;
|
||||
if (canvasSurface) {
|
||||
loadFitObserver = new ResizeObserver(() => {
|
||||
if (settleLoadedViewport()) loadFitObserver?.disconnect();
|
||||
});
|
||||
loadFitObserver.observe(canvasSurface);
|
||||
}
|
||||
const loadFitTimer = window.setTimeout(() => {
|
||||
if (graphRef.current !== graph) return;
|
||||
loadFitObserver?.disconnect();
|
||||
scroller?.enableAutoResize();
|
||||
scroller?.updateScroller();
|
||||
settleLoadedViewport();
|
||||
}, SCROLLER_AUTORESIZE_SETTLE_MS);
|
||||
|
||||
const updateHistory = () => setHistoryState({ undo: graph.canUndo(), redo: graph.canRedo() });
|
||||
const markDirty = () => {
|
||||
@@ -2015,13 +2037,15 @@ export const DiagramEditorPane = ({
|
||||
graphToDocument(graph, document.kind, themeRef.current, structureRef.current),
|
||||
));
|
||||
});
|
||||
graphRef.current = graph;
|
||||
return () => {
|
||||
containerRef.current?.removeEventListener("pointerdown", handleFlowPointerDown, true);
|
||||
window.removeEventListener("pointerup", handleFlowPointerUp, true);
|
||||
flowPointerDragRef.current = null;
|
||||
openFlowQuickCreateRef.current = () => undefined;
|
||||
nodeEditorRef.current = null;
|
||||
loadFitObserver?.disconnect();
|
||||
window.clearTimeout(loadFitTimer);
|
||||
window.cancelAnimationFrame(loadFitFrame);
|
||||
if (scrollerResumeTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollerResumeTimerRef.current);
|
||||
scrollerResumeTimerRef.current = null;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { applyDiagramScrollerFitOptions, isUsableDiagramBounds } from "./diagram-scroller-fit.ts";
|
||||
import {
|
||||
DIAGRAM_CANVAS_READY_MIN_SIZE,
|
||||
applyDiagramScrollerFitOptions,
|
||||
diagramCanvasIsReady,
|
||||
isUsableDiagramBounds,
|
||||
} from "./diagram-scroller-fit.ts";
|
||||
|
||||
describe("diagram scroller fit options", () => {
|
||||
test("uses every node box so left-of-origin mind-map branches stay on the paper", () => {
|
||||
@@ -24,4 +29,11 @@ describe("diagram scroller fit options", () => {
|
||||
expect(isUsableDiagramBounds({ x: Number.NaN, y: 0, width: 80, height: 36 })).toBe(false);
|
||||
expect(isUsableDiagramBounds({ x: -10, y: 0, width: 80, height: 36 })).toBe(true);
|
||||
});
|
||||
|
||||
test("waits for the canvas to have a real viewport before fitting a loaded diagram", () => {
|
||||
expect(diagramCanvasIsReady(null)).toBe(false);
|
||||
expect(diagramCanvasIsReady({ clientWidth: 0, clientHeight: 640 })).toBe(false);
|
||||
expect(diagramCanvasIsReady({ clientWidth: DIAGRAM_CANVAS_READY_MIN_SIZE - 1, clientHeight: 640 })).toBe(false);
|
||||
expect(diagramCanvasIsReady({ clientWidth: 960, clientHeight: 640 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,16 @@ export type DiagramFitBounds = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const DIAGRAM_CANVAS_READY_MIN_SIZE = 32;
|
||||
|
||||
export const diagramCanvasIsReady = (
|
||||
element: { clientWidth: number; clientHeight: number } | null | undefined,
|
||||
) => Boolean(
|
||||
element
|
||||
&& element.clientWidth >= DIAGRAM_CANVAS_READY_MIN_SIZE
|
||||
&& element.clientHeight >= DIAGRAM_CANVAS_READY_MIN_SIZE,
|
||||
);
|
||||
|
||||
const isFiniteNumber = (value: unknown): value is number =>
|
||||
typeof value === "number" && Number.isFinite(value);
|
||||
|
||||
|
||||
Reference in new issue
Block a user