From 2503d32a88534bede9a5361f1bf7a26c9c2e2aef Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Jul 2024 13:18:07 +0200 Subject: [PATCH] feat(viewport-helpers): return promises --- examples/react/src/examples/Stress/index.tsx | 1 + packages/react/src/hooks/useViewportHelper.ts | 76 +++++++++++++------ packages/react/src/store/index.ts | 6 +- packages/react/src/types/general.ts | 2 +- packages/react/src/types/store.ts | 2 +- .../svelte/src/lib/hooks/useSvelteFlow.ts | 55 ++++++++++---- packages/svelte/src/lib/store/index.ts | 17 +++-- packages/svelte/src/lib/store/types.ts | 8 +- packages/system/src/types/general.ts | 12 +-- packages/system/src/types/panzoom.ts | 8 +- packages/system/src/utils/graph.ts | 10 +-- packages/system/src/utils/store.ts | 10 +-- packages/system/src/xydrag/XYDrag.ts | 4 +- packages/system/src/xypanzoom/XYPanZoom.ts | 14 ++-- packages/system/src/xypanzoom/utils.ts | 2 - 15 files changed, 140 insertions(+), 87 deletions(-) diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 9020ddd6..5fd6f9e9 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -224,6 +224,7 @@ const StressFlow = () => { onEdgesChange={onEdgeChange} minZoom={0.2} fitView + panOnDrag={false} > diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 245fafd7..2bfea817 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -21,17 +21,33 @@ const useViewportHelper = (): ViewportHelperFunctions => { return useMemo(() => { return { - zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }), - zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }), - zoomTo: (zoomLevel, options) => store.getState().panZoom?.scaleTo(zoomLevel, { duration: options?.duration }), + zoomIn: (options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleBy(1.2, { duration: options?.duration }) : Promise.resolve(false); + }, + zoomOut: (options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleBy(1 / 1.2, { duration: options?.duration }) : Promise.resolve(false); + }, + zoomTo: (zoomLevel, options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleTo(zoomLevel, { duration: options?.duration }) : Promise.resolve(false); + }, getZoom: () => store.getState().transform[2], - setViewport: (viewport, options) => { + setViewport: async (viewport, options) => { const { transform: [tX, tY, tZoom], panZoom, } = store.getState(); - panZoom?.setViewport( + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport( { x: viewport.x ?? tX, y: viewport.y ?? tY, @@ -39,6 +55,8 @@ const useViewportHelper = (): ViewportHelperFunctions => { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, getViewport: () => { const [x, y, zoom] = store.getState().transform; @@ -47,29 +65,31 @@ const useViewportHelper = (): ViewportHelperFunctions => { fitView: (options) => { const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState(); - if (!panZoom) { - return false; - } - - return fitView( - { - nodeLookup, - width, - height, - minZoom, - maxZoom, - panZoom, - }, - options - ); + return panZoom + ? fitView( + { + nodeLookup, + width, + height, + minZoom, + maxZoom, + panZoom, + }, + options + ) + : Promise.resolve(false); }, - setCenter: (x, y, options) => { + setCenter: async (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; const centerX = width / 2 - x * nextZoom; const centerY = height / 2 - y * nextZoom; - panZoom?.setViewport( + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport( { x: centerX, y: centerY, @@ -77,12 +97,20 @@ const useViewportHelper = (): ViewportHelperFunctions => { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, - fitBounds: (bounds, options) => { + fitBounds: async (bounds, options) => { const { width, height, minZoom, maxZoom, panZoom } = store.getState(); const viewport = getViewportForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1); - panZoom?.setViewport(viewport, { duration: options?.duration }); + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport(viewport, { duration: options?.duration }); + + return Promise.resolve(true); }, screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { const { transform, snapGrid, domNode } = store.getState(); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 57beb3f9..11245bb5 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -287,15 +287,15 @@ const createStore = ({ nodeExtent, }); }, - panBy: (delta): boolean => { + panBy: (delta): Promise => { const { transform, width, height, panZoom, translateExtent } = get(); return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, - fitView: (options?: FitViewOptions): boolean => { + fitView: (options?: FitViewOptions): Promise => { const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); if (!panZoom) { - return false; + return Promise.resolve(false); } return fitViewSystem( diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 2f518dca..48e5b2fe 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -65,7 +65,7 @@ export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; export type FitViewParams = FitViewParamsBase; export type FitViewOptions = FitViewOptionsBase; -export type FitView = (fitViewOptions?: FitViewOptions) => boolean; +export type FitView = (fitViewOptions?: FitViewOptions) => Promise; export type OnInit = ( reactFlowInstance: ReactFlowInstance ) => void; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 2e86ade0..30da6e64 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -168,7 +168,7 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => Promise; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index be40af64..f46c1757 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -80,7 +80,7 @@ export function useSvelteFlow(): { * @param zoomLevel - the zoom level to set * @param options.duration - optional duration. If set, a transition will be applied */ - setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; + setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise; /** * Returns the current zoom level. * @@ -94,14 +94,14 @@ export function useSvelteFlow(): { * @param y - y position * @param options.zoom - optional zoom */ - setCenter: (x: number, y: number, options?: SetCenterOptions) => void; + setCenter: (x: number, y: number, options?: SetCenterOptions) => Promise; /** * Sets the current viewport. * * @param viewport - the viewport to set * @param options.duration - optional duration. If set, a transition will be applied */ - setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; + setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise; /** * Returns the current viewport. * @@ -118,7 +118,7 @@ export function useSvelteFlow(): { * @param options.duration - optional duration. If set, a transition will be applied * @param options.nodes - optional nodes to fit the view to */ - fitView: (options?: FitViewOptions) => void; + fitView: (options?: FitViewOptions) => Promise; /** * Returns all nodes that intersect with the given node or rect. * @@ -153,7 +153,7 @@ export function useSvelteFlow(): { * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to * @param options.padding - optional padding */ - fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; + fitBounds: (bounds: Rect, options?: FitBoundsOptions) => Promise; /** * Deletes nodes and edges. * @@ -298,26 +298,41 @@ export function useSvelteFlow(): { getEdge: (id) => get(edgeLookup).get(id), getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)), setZoom: (zoomLevel, options) => { - get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration }); + const currentPanZoom = get(panZoom); + return currentPanZoom + ? currentPanZoom.scaleTo(zoomLevel, { duration: options?.duration }) + : Promise.resolve(false); }, getZoom: () => get(viewport).zoom, - setViewport: (vieport, options) => { + setViewport: async (nextViewport, options) => { const currentViewport = get(viewport); + const currentPanZoom = get(panZoom); - get(panZoom)?.setViewport( + if (!currentPanZoom) { + return Promise.resolve(false); + } + + await currentPanZoom.setViewport( { - x: vieport.x ?? currentViewport.x, - y: vieport.y ?? currentViewport.y, - zoom: vieport.zoom ?? currentViewport.zoom + x: nextViewport.x ?? currentViewport.x, + y: nextViewport.y ?? currentViewport.y, + zoom: nextViewport.zoom ?? currentViewport.zoom }, { duration: options?.duration } ); + + return Promise.resolve(true); }, getViewport: () => get(viewport), - setCenter: (x, y, options) => { + setCenter: async (x, y, options) => { const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom); + const currentPanZoom = get(panZoom); - get(panZoom)?.setViewport( + if (!currentPanZoom) { + return Promise.resolve(false); + } + + await currentPanZoom.setViewport( { x: get(width) / 2 - x * nextZoom, y: get(height) / 2 - y * nextZoom, @@ -325,9 +340,17 @@ export function useSvelteFlow(): { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, fitView, - fitBounds: (bounds: Rect, options?: FitBoundsOptions) => { + fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => { + const currentPanZoom = get(panZoom); + + if (!currentPanZoom) { + return Promise.resolve(false); + } + const viewport = getViewportForBounds( bounds, get(width), @@ -337,7 +360,9 @@ export function useSvelteFlow(): { options?.padding ?? 0.1 ); - get(panZoom)?.setViewport(viewport, { duration: options?.duration }); + await currentPanZoom.setViewport(viewport, { duration: options?.duration }); + + return Promise.resolve(true); }, getIntersectingNodes: ( nodeOrRect: Node | { id: Node['id'] } | Rect, diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index e1eea73f..e52fc8a8 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -90,7 +90,7 @@ export function createStore({ store.nodes.update((nds) => nds); }; - function updateNodeInternals(updates: Map) { + async function updateNodeInternals(updates: Map) { const nodeLookup = get(store.nodeLookup); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -106,7 +106,7 @@ export function createStore({ if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { const fitViewOptions = get(store.fitViewOptions); - const fitViewOnInitDone = fitView({ + const fitViewOnInitDone = await fitView({ ...fitViewOptions, nodes: fitViewOptions?.nodes }); @@ -150,7 +150,7 @@ export function createStore({ const panZoom = get(store.panZoom); if (!panZoom) { - return false; + return Promise.resolve(false); } return fitViewSystem( @@ -168,18 +168,19 @@ export function createStore({ function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { const panZoom = get(store.panZoom); - - if (panZoom) { - panZoom.scaleBy(factor, options); + if (!panZoom) { + return Promise.resolve(false); } + + return panZoom.scaleBy(factor, options); } function zoomIn(options?: ViewportHelperFunctionOptions) { - zoomBy(1.2, options); + return zoomBy(1.2, options); } function zoomOut(options?: ViewportHelperFunctionOptions) { - zoomBy(1 / 1.2, options); + return zoomBy(1 / 1.2, options); } function setMinZoom(minZoom: number) { diff --git a/packages/svelte/src/lib/store/types.ts b/packages/svelte/src/lib/store/types.ts index d87ca907..0d8c01bf 100644 --- a/packages/svelte/src/lib/store/types.ts +++ b/packages/svelte/src/lib/store/types.ts @@ -20,19 +20,19 @@ export type SvelteFlowStoreActions = { setNodeTypes: (nodeTypes: NodeTypes) => void; setEdgeTypes: (edgeTypes: EdgeTypes) => void; addEdge: (edge: Edge | Connection) => void; - zoomIn: (options?: ViewportHelperFunctionOptions) => void; - zoomOut: (options?: ViewportHelperFunctionOptions) => void; + zoomIn: (options?: ViewportHelperFunctionOptions) => Promise; + zoomOut: (options?: ViewportHelperFunctionOptions) => Promise; setMinZoom: (minZoom: number) => void; setMaxZoom: (maxZoom: number) => void; setTranslateExtent: (extent: CoordinateExtent) => void; - fitView: (options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => Promise; updateNodePositions: UpdateNodePositions; updateNodeInternals: (updates: Map) => void; unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void; addSelectedNodes: (ids: string[]) => void; addSelectedEdges: (ids: string[]) => void; handleNodeSelection: (id: string) => void; - panBy: (delta: XYPosition) => boolean; + panBy: (delta: XYPosition) => Promise; updateConnection: UpdateConnection; cancelConnection: () => void; reset(): void; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 7e44be9b..f0a0a504 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -18,13 +18,13 @@ export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) export type OnMoveStart = OnMove; export type OnMoveEnd = OnMove; -export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => void; -export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; +export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise; +export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise; export type GetZoom = () => number; export type GetViewport = () => Viewport; -export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; -export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void; -export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void; +export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise; +export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise; +export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise; export type Connection = { source: string; @@ -130,7 +130,7 @@ export type SelectionRect = Rect & { export type OnError = (id: string, message: string) => void; export type UpdateNodePositions = (dragItems: Map, dragging?: boolean) => void; -export type PanBy = (delta: XYPosition) => boolean; +export type PanBy = (delta: XYPosition) => Promise; export const initialConnection: NoConnection = { inProgress: false, diff --git a/packages/system/src/types/panzoom.ts b/packages/system/src/types/panzoom.ts index 81654bdb..4a126a98 100644 --- a/packages/system/src/types/panzoom.ts +++ b/packages/system/src/types/panzoom.ts @@ -45,15 +45,15 @@ export type PanZoomInstance = { update: (params: PanZoomUpdateOptions) => void; destroy: () => void; getViewport: () => Viewport; - setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => ZoomTransform | undefined; + setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => Promise; setViewportConstrained: ( viewport: Viewport, extent: CoordinateExtent, translateExtent: CoordinateExtent - ) => ZoomTransform | undefined; + ) => Promise; setScaleExtent: (scaleExtent: [number, number]) => void; setTranslateExtent: (translateExtent: CoordinateExtent) => void; - scaleTo: (scale: number, options?: PanZoomTransformOptions) => void; - scaleBy: (factor: number, options?: PanZoomTransformOptions) => void; + scaleTo: (scale: number, options?: PanZoomTransformOptions) => Promise; + scaleBy: (factor: number, options?: PanZoomTransformOptions) => Promise; syncViewport: (viewport: Viewport) => void; }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index d09fe1ef..8d98fa40 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -233,10 +233,10 @@ export const getConnectedEdges = nodeIds.has(edge.source) || nodeIds.has(edge.target)); }; -export function fitView, Options extends FitViewOptionsBase>( +export async function fitView, Options extends FitViewOptionsBase>( { nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params, options?: Options -) { +): Promise { const filteredNodes: Map = new Map(); const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; @@ -260,12 +260,12 @@ export function fitView, Options exte options?.padding ?? 0.1 ); - panZoom.setViewport(viewport, { duration: options?.duration }); + await panZoom.setViewport(viewport, { duration: options?.duration }); - return true; + return Promise.resolve(true); } - return false; + return Promise.resolve(false); } /** diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 9c4904c7..5a977cc0 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -325,7 +325,7 @@ export function updateNodeInternals( return { changes, updatedInternals }; } -export function panBy({ +export async function panBy({ delta, panZoom, transform, @@ -339,12 +339,12 @@ export function panBy({ translateExtent: CoordinateExtent; width: number; height: number; -}) { +}): Promise { if (!panZoom || (!delta.x && !delta.y)) { - return false; + return Promise.resolve(false); } - const nextViewport = panZoom.setViewportConstrained( + const nextViewport = await panZoom.setViewportConstrained( { x: transform[0] + delta.x, y: transform[1] + delta.y, @@ -361,7 +361,7 @@ export function panBy({ !!nextViewport && (nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]); - return transformChanged; + return Promise.resolve(transformChanged); } export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 675aef3a..27022760 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -195,7 +195,7 @@ export function XYDrag voi } } - function autoPan() { + async function autoPan() { if (!containerBounds) { return; } @@ -208,7 +208,7 @@ export function XYDrag voi lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2]; lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2]; - if (panBy({ x: xMovement, y: yMovement })) { + if (await panBy({ x: xMovement, y: yMovement })) { updateNodes(lastPos as XYPosition, null); } } diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index a1402dee..2db56b10 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -188,27 +188,27 @@ export function XYPanZoom({ d3ZoomInstance.on('zoom', null); } - function setViewportConstrained( + async function setViewportConstrained( viewport: Viewport, extent: CoordinateExtent, translateExtent: CoordinateExtent - ): ZoomTransform | undefined { + ): Promise { const nextTransform = viewportToTransform(viewport); const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent); if (contrainedTransform) { - setTransform(contrainedTransform); + await setTransform(contrainedTransform); } - return contrainedTransform; + return new Promise((resolve) => resolve(contrainedTransform)); } - function setViewport(viewport: Viewport, options?: PanZoomTransformOptions) { + async function setViewport(viewport: Viewport, options?: PanZoomTransformOptions) { const nextTransform = viewportToTransform(viewport); - setTransform(nextTransform, options); + await setTransform(nextTransform, options); - return nextTransform; + return new Promise((resolve) => resolve(nextTransform)); } function syncViewport(viewport: Viewport) { diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index 55969f3e..5a3e687f 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -21,8 +21,6 @@ export const isWrappedWithClass = (event: any, className: string | undefined) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); -export const getD3Transition = (selection: D3SelectionInstance, duration = 0) => - typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection; export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration).on('end', onEnd) : selection;