Merge pull request #4421 from xyflow/refactor/promised-panzoom

Refactor/promised panzoom
This commit is contained in:
Moritz Klack
2024-07-08 15:51:54 +02:00
committed by GitHub
17 changed files with 284 additions and 114 deletions
@@ -96,9 +96,13 @@ const UseZoomPanHelperFlow = () => {
); );
const onNodeClick = useCallback( const onNodeClick = useCallback(
(_: MouseEvent, node: Node) => { async (_: MouseEvent, node: Node) => {
console.log('set center start');
const { x, y } = node.position; const { x, y } = node.position;
setCenter(x, y, { zoom: 1, duration: 1200 }); await setCenter(x, y, { zoom: 1, duration: 1200 });
console.log('set center success');
}, },
[setCenter] [setCenter]
); );
@@ -172,7 +176,15 @@ const UseZoomPanHelperFlow = () => {
<Panel position="top-right"> <Panel position="top-right">
<button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button> <button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button>
<button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button> <button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button> <button
onClick={async () => {
console.log('fit view start');
await fitView({ duration: 1200, padding: 0.3 });
console.log('fit view success');
}}
>
fitView
</button>
<button onClick={onAddNode}>add node</button> <button onClick={onAddNode}>add node</button>
<button onClick={onResetNodes}>reset nodes</button> <button onClick={onResetNodes}>reset nodes</button>
<button onClick={logNodes}>useNodes</button> <button onClick={logNodes}>useNodes</button>
+4
View File
@@ -1,5 +1,9 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.27
- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update
## 12.0.0-next.26 ## 12.0.0-next.26
- add `autoPanSpeed` prop - add `autoPanSpeed` prop
+54 -24
View File
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { import {
pointToRendererPoint, pointToRendererPoint,
getViewportForBounds, getViewportForBounds,
getFitViewNodes,
fitView, fitView,
type XYPosition, type XYPosition,
rendererPointToPoint, rendererPointToPoint,
@@ -21,17 +22,33 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return useMemo<ViewportHelperFunctions>(() => { return useMemo<ViewportHelperFunctions>(() => {
return { return {
zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }), zoomIn: (options) => {
zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }), const { panZoom } = store.getState();
zoomTo: (zoomLevel, options) => store.getState().panZoom?.scaleTo(zoomLevel, { duration: options?.duration }),
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], getZoom: () => store.getState().transform[2],
setViewport: (viewport, options) => { setViewport: async (viewport, options) => {
const { const {
transform: [tX, tY, tZoom], transform: [tX, tY, tZoom],
panZoom, panZoom,
} = store.getState(); } = store.getState();
panZoom?.setViewport( if (!panZoom) {
return Promise.resolve(false);
}
await panZoom.setViewport(
{ {
x: viewport.x ?? tX, x: viewport.x ?? tX,
y: viewport.y ?? tY, y: viewport.y ?? tY,
@@ -39,6 +56,8 @@ const useViewportHelper = (): ViewportHelperFunctions => {
}, },
{ duration: options?.duration } { duration: options?.duration }
); );
return Promise.resolve(true);
}, },
getViewport: () => { getViewport: () => {
const [x, y, zoom] = store.getState().transform; const [x, y, zoom] = store.getState().transform;
@@ -46,30 +65,33 @@ const useViewportHelper = (): ViewportHelperFunctions => {
}, },
fitView: (options) => { fitView: (options) => {
const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState(); const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState();
const fitViewNodes = getFitViewNodes(nodeLookup, options);
if (!panZoom) { return panZoom
return false; ? fitView(
} {
nodes: fitViewNodes,
return fitView( width,
{ height,
nodeLookup, minZoom,
width, maxZoom,
height, panZoom,
minZoom, },
maxZoom, options
panZoom, )
}, : Promise.resolve(false);
options
);
}, },
setCenter: (x, y, options) => { setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState(); const { width, height, maxZoom, panZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
const centerX = width / 2 - x * nextZoom; const centerX = width / 2 - x * nextZoom;
const centerY = height / 2 - y * nextZoom; const centerY = height / 2 - y * nextZoom;
panZoom?.setViewport( if (!panZoom) {
return Promise.resolve(false);
}
await panZoom.setViewport(
{ {
x: centerX, x: centerX,
y: centerY, y: centerY,
@@ -77,12 +99,20 @@ const useViewportHelper = (): ViewportHelperFunctions => {
}, },
{ duration: options?.duration } { duration: options?.duration }
); );
return Promise.resolve(true);
}, },
fitBounds: (bounds, options) => { fitBounds: async (bounds, options) => {
const { width, height, minZoom, maxZoom, panZoom } = store.getState(); const { width, height, minZoom, maxZoom, panZoom } = store.getState();
const viewport = getViewportForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1); 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 }) => { screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
const { transform, snapGrid, domNode } = store.getState(); const { transform, snapGrid, domNode } = store.getState();
+36 -6
View File
@@ -1,6 +1,7 @@
import { createWithEqualityFn } from 'zustand/traditional'; import { createWithEqualityFn } from 'zustand/traditional';
import { import {
clampPosition, clampPosition,
getFitViewNodes,
fitView as fitViewSystem, fitView as fitViewSystem,
adoptUserNodes, adoptUserNodes,
updateAbsolutePositions, updateAbsolutePositions,
@@ -79,7 +80,6 @@ const createStore = ({
updateNodeInternals: (updates) => { updateNodeInternals: (updates) => {
const { const {
triggerNodeChanges, triggerNodeChanges,
fitView,
nodeLookup, nodeLookup,
parentLookup, parentLookup,
fitViewOnInit, fitViewOnInit,
@@ -88,6 +88,7 @@ const createStore = ({
domNode, domNode,
nodeOrigin, nodeOrigin,
debug, debug,
fitViewSync,
} = get(); } = get();
const { changes, updatedInternals } = updateNodeInternalsSystem( const { changes, updatedInternals } = updateNodeInternalsSystem(
@@ -106,8 +107,9 @@ const createStore = ({
// we call fitView once initially after all dimensions are set // we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone; let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) { if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitView({ nextFitViewDone = fitViewSync({
...fitViewOnInitOptions, ...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes, nodes: fitViewOnInitOptions?.nodes,
}); });
@@ -287,20 +289,23 @@ const createStore = ({
nodeExtent, nodeExtent,
}); });
}, },
panBy: (delta): boolean => { panBy: (delta): Promise<boolean> => {
const { transform, width, height, panZoom, translateExtent } = get(); const { transform, width, height, panZoom, translateExtent } = get();
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (options?: FitViewOptions): boolean => { fitView: (options?: FitViewOptions): Promise<boolean> => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) { if (!panZoom) {
return false; return Promise.resolve(false);
} }
const fitViewNodes = getFitViewNodes(nodeLookup, options);
return fitViewSystem( return fitViewSystem(
{ {
nodeLookup, nodes: fitViewNodes,
width, width,
height, height,
panZoom, panZoom,
@@ -310,6 +315,31 @@ const createStore = ({
options options
); );
}, },
// we can't call an asnychronous function in updateNodeInternals
// for that we created this sync version of fitView
fitViewSync: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) {
return false;
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
panZoom,
minZoom,
maxZoom,
},
options
);
return fitViewNodes.size > 0;
},
cancelConnection: () => { cancelConnection: () => {
set({ set({
connection: { ...initialConnection }, connection: { ...initialConnection },
+1 -1
View File
@@ -65,7 +65,7 @@ export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>; export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>; export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
export type FitView = (fitViewOptions?: FitViewOptions) => boolean; export type FitView = (fitViewOptions?: FitViewOptions) => Promise<boolean>;
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ( export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType> reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void; ) => void;
+2 -1
View File
@@ -168,7 +168,8 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void; triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void; triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy; panBy: PanBy;
fitView: (options?: FitViewOptions) => boolean; fitView: (options?: FitViewOptions) => Promise<boolean>;
fitViewSync: (options?: FitViewOptions) => boolean;
}; };
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore< export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
+4
View File
@@ -1,5 +1,9 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.9
- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update
## 0.1.8 ## 0.1.8
- `useConnection` returns `ConnectionState` with `toNode` and `toHandle` - `useConnection` returns `ConnectionState` with `toNode` and `toHandle`
+40 -15
View File
@@ -80,7 +80,7 @@ export function useSvelteFlow(): {
* @param zoomLevel - the zoom level to set * @param zoomLevel - the zoom level to set
* @param options.duration - optional duration. If set, a transition will be applied * @param options.duration - optional duration. If set, a transition will be applied
*/ */
setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/** /**
* Returns the current zoom level. * Returns the current zoom level.
* *
@@ -94,14 +94,14 @@ export function useSvelteFlow(): {
* @param y - y position * @param y - y position
* @param options.zoom - optional zoom * @param options.zoom - optional zoom
*/ */
setCenter: (x: number, y: number, options?: SetCenterOptions) => void; setCenter: (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
/** /**
* Sets the current viewport. * Sets the current viewport.
* *
* @param viewport - the viewport to set * @param viewport - the viewport to set
* @param options.duration - optional duration. If set, a transition will be applied * @param options.duration - optional duration. If set, a transition will be applied
*/ */
setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/** /**
* Returns the current viewport. * 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.duration - optional duration. If set, a transition will be applied
* @param options.nodes - optional nodes to fit the view to * @param options.nodes - optional nodes to fit the view to
*/ */
fitView: (options?: FitViewOptions) => void; fitView: (options?: FitViewOptions) => Promise<boolean>;
/** /**
* Returns all nodes that intersect with the given node or rect. * 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 bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding * @param options.padding - optional padding
*/ */
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; fitBounds: (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
/** /**
* Deletes nodes and edges. * Deletes nodes and edges.
* *
@@ -298,26 +298,41 @@ export function useSvelteFlow(): {
getEdge: (id) => get(edgeLookup).get(id), getEdge: (id) => get(edgeLookup).get(id),
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)), getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)),
setZoom: (zoomLevel, options) => { 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, getZoom: () => get(viewport).zoom,
setViewport: (vieport, options) => { setViewport: async (nextViewport, options) => {
const currentViewport = get(viewport); 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, x: nextViewport.x ?? currentViewport.x,
y: vieport.y ?? currentViewport.y, y: nextViewport.y ?? currentViewport.y,
zoom: vieport.zoom ?? currentViewport.zoom zoom: nextViewport.zoom ?? currentViewport.zoom
}, },
{ duration: options?.duration } { duration: options?.duration }
); );
return Promise.resolve(true);
}, },
getViewport: () => get(viewport), getViewport: () => get(viewport),
setCenter: (x, y, options) => { setCenter: async (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom); 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, x: get(width) / 2 - x * nextZoom,
y: get(height) / 2 - y * nextZoom, y: get(height) / 2 - y * nextZoom,
@@ -325,9 +340,17 @@ export function useSvelteFlow(): {
}, },
{ duration: options?.duration } { duration: options?.duration }
); );
return Promise.resolve(true);
}, },
fitView, 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( const viewport = getViewportForBounds(
bounds, bounds,
get(width), get(width),
@@ -337,7 +360,9 @@ export function useSvelteFlow(): {
options?.padding ?? 0.1 options?.padding ?? 0.1
); );
get(panZoom)?.setViewport(viewport, { duration: options?.duration }); await currentPanZoom.setViewport(viewport, { duration: options?.duration });
return Promise.resolve(true);
}, },
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: Node | { id: Node['id'] } | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
+37 -9
View File
@@ -18,7 +18,8 @@ import {
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
type ConnectionState, type ConnectionState,
type NodeOrigin type NodeOrigin,
getFitViewNodes
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -106,7 +107,7 @@ export function createStore({
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) {
const fitViewOptions = get(store.fitViewOptions); const fitViewOptions = get(store.fitViewOptions);
const fitViewOnInitDone = fitView({ const fitViewOnInitDone = fitViewSync({
...fitViewOptions, ...fitViewOptions,
nodes: fitViewOptions?.nodes nodes: fitViewOptions?.nodes
}); });
@@ -150,12 +151,14 @@ export function createStore({
const panZoom = get(store.panZoom); const panZoom = get(store.panZoom);
if (!panZoom) { if (!panZoom) {
return false; return Promise.resolve(false);
} }
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options);
return fitViewSystem( return fitViewSystem(
{ {
nodeLookup: get(store.nodeLookup), nodes: fitViewNodes,
width: get(store.width), width: get(store.width),
height: get(store.height), height: get(store.height),
minZoom: get(store.minZoom), minZoom: get(store.minZoom),
@@ -166,20 +169,45 @@ export function createStore({
); );
} }
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { function fitViewSync(options?: FitViewOptions) {
const panZoom = get(store.panZoom); const panZoom = get(store.panZoom);
if (panZoom) { if (!panZoom) {
panZoom.scaleBy(factor, options); return false;
} }
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options);
fitViewSystem(
{
nodes: fitViewNodes,
width: get(store.width),
height: get(store.height),
minZoom: get(store.minZoom),
maxZoom: get(store.maxZoom),
panZoom
},
options
);
return fitViewNodes.size > 0;
}
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
const panZoom = get(store.panZoom);
if (!panZoom) {
return Promise.resolve(false);
}
return panZoom.scaleBy(factor, options);
} }
function zoomIn(options?: ViewportHelperFunctionOptions) { function zoomIn(options?: ViewportHelperFunctionOptions) {
zoomBy(1.2, options); return zoomBy(1.2, options);
} }
function zoomOut(options?: ViewportHelperFunctionOptions) { function zoomOut(options?: ViewportHelperFunctionOptions) {
zoomBy(1 / 1.2, options); return zoomBy(1 / 1.2, options);
} }
function setMinZoom(minZoom: number) { function setMinZoom(minZoom: number) {
+4 -4
View File
@@ -20,19 +20,19 @@ export type SvelteFlowStoreActions = {
setNodeTypes: (nodeTypes: NodeTypes) => void; setNodeTypes: (nodeTypes: NodeTypes) => void;
setEdgeTypes: (edgeTypes: EdgeTypes) => void; setEdgeTypes: (edgeTypes: EdgeTypes) => void;
addEdge: (edge: Edge | Connection) => void; addEdge: (edge: Edge | Connection) => void;
zoomIn: (options?: ViewportHelperFunctionOptions) => void; zoomIn: (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
zoomOut: (options?: ViewportHelperFunctionOptions) => void; zoomOut: (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
setMinZoom: (minZoom: number) => void; setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void; setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (extent: CoordinateExtent) => void; setTranslateExtent: (extent: CoordinateExtent) => void;
fitView: (options?: FitViewOptions) => boolean; fitView: (options?: FitViewOptions) => Promise<boolean>;
updateNodePositions: UpdateNodePositions; updateNodePositions: UpdateNodePositions;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void; updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void; unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void;
addSelectedNodes: (ids: string[]) => void; addSelectedNodes: (ids: string[]) => void;
addSelectedEdges: (ids: string[]) => void; addSelectedEdges: (ids: string[]) => void;
handleNodeSelection: (id: string) => void; handleNodeSelection: (id: string) => void;
panBy: (delta: XYPosition) => boolean; panBy: (delta: XYPosition) => Promise<boolean>;
updateConnection: UpdateConnection; updateConnection: UpdateConnection;
cancelConnection: () => void; cancelConnection: () => void;
reset(): void; reset(): void;
+7 -7
View File
@@ -18,13 +18,13 @@ export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport)
export type OnMoveStart = OnMove; export type OnMoveStart = OnMove;
export type OnMoveEnd = OnMove; export type OnMoveEnd = OnMove;
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => void; export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
export type GetZoom = () => number; export type GetZoom = () => number;
export type GetViewport = () => Viewport; export type GetViewport = () => Viewport;
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void; export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void; export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
export type Connection = { export type Connection = {
source: string; source: string;
@@ -55,7 +55,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase<NodeType extends NodeBase> = { export type FitViewParamsBase<NodeType extends NodeBase> = {
nodeLookup: Map<string, InternalNodeBase<NodeType>>; nodes: Map<string, InternalNodeBase<NodeType>>;
width: number; width: number;
height: number; height: number;
panZoom: PanZoomInstance; panZoom: PanZoomInstance;
@@ -130,7 +130,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void; export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void; export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean; export type PanBy = (delta: XYPosition) => Promise<boolean>;
export const initialConnection: NoConnection = { export const initialConnection: NoConnection = {
inProgress: false, inProgress: false,
+4 -4
View File
@@ -45,15 +45,15 @@ export type PanZoomInstance = {
update: (params: PanZoomUpdateOptions) => void; update: (params: PanZoomUpdateOptions) => void;
destroy: () => void; destroy: () => void;
getViewport: () => Viewport; getViewport: () => Viewport;
setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => ZoomTransform | undefined; setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => Promise<ZoomTransform | undefined>;
setViewportConstrained: ( setViewportConstrained: (
viewport: Viewport, viewport: Viewport,
extent: CoordinateExtent, extent: CoordinateExtent,
translateExtent: CoordinateExtent translateExtent: CoordinateExtent
) => ZoomTransform | undefined; ) => Promise<ZoomTransform | undefined>;
setScaleExtent: (scaleExtent: [number, number]) => void; setScaleExtent: (scaleExtent: [number, number]) => void;
setTranslateExtent: (translateExtent: CoordinateExtent) => void; setTranslateExtent: (translateExtent: CoordinateExtent) => void;
scaleTo: (scale: number, options?: PanZoomTransformOptions) => void; scaleTo: (scale: number, options?: PanZoomTransformOptions) => Promise<boolean>;
scaleBy: (factor: number, options?: PanZoomTransformOptions) => void; scaleBy: (factor: number, options?: PanZoomTransformOptions) => Promise<boolean>;
syncViewport: (viewport: Viewport) => void; syncViewport: (viewport: Viewport) => void;
}; };
+28 -21
View File
@@ -233,39 +233,46 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target)); return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
}; };
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>( export function getFitViewNodes<
{ nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params, Params extends NodeLookup<InternalNodeBase<NodeBase>>,
options?: Options Options extends FitViewOptionsBase<NodeBase>
) { >(nodeLookup: Params, options?: Pick<Options, 'nodes' | 'includeHiddenNodes'>) {
const filteredNodes: Map<string, InternalNodeBase> = new Map(); const fitViewNodes: NodeLookup = new Map();
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
nodeLookup.forEach((n) => { nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) { if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) {
filteredNodes.set(n.id, n); fitViewNodes.set(n.id, n);
} }
}); });
if (filteredNodes.size > 0) { return fitViewNodes;
const bounds = getInternalNodesBounds(filteredNodes); }
const viewport = getViewportForBounds( export async function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
bounds, { nodes, width, height, panZoom, minZoom, maxZoom }: Params,
width, options?: Omit<Options, 'nodes' | 'includeHiddenNodes'>
height, ): Promise<boolean> {
options?.minZoom ?? minZoom, if (nodes.size === 0) {
options?.maxZoom ?? maxZoom, return Promise.resolve(false);
options?.padding ?? 0.1
);
panZoom.setViewport(viewport, { duration: options?.duration });
return true;
} }
return false; const bounds = getInternalNodesBounds(nodes);
const viewport = getViewportForBounds(
bounds,
width,
height,
options?.minZoom ?? minZoom,
options?.maxZoom ?? maxZoom,
options?.padding ?? 0.1
);
await panZoom.setViewport(viewport, { duration: options?.duration });
return Promise.resolve(true);
} }
/** /**
+5 -5
View File
@@ -325,7 +325,7 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
return { changes, updatedInternals }; return { changes, updatedInternals };
} }
export function panBy({ export async function panBy({
delta, delta,
panZoom, panZoom,
transform, transform,
@@ -339,12 +339,12 @@ export function panBy({
translateExtent: CoordinateExtent; translateExtent: CoordinateExtent;
width: number; width: number;
height: number; height: number;
}) { }): Promise<boolean> {
if (!panZoom || (!delta.x && !delta.y)) { 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, x: transform[0] + delta.x,
y: transform[1] + delta.y, y: transform[1] + delta.y,
@@ -361,7 +361,7 @@ export function panBy({
!!nextViewport && !!nextViewport &&
(nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]); (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[]) { export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
+3 -2
View File
@@ -195,7 +195,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
} }
} }
function autoPan() { async function autoPan() {
if (!containerBounds) { if (!containerBounds) {
return; return;
} }
@@ -208,10 +208,11 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2]; lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2];
lastPos.y = (lastPos.y ?? 0) - yMovement / 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); updateNodes(lastPos as XYPosition, null);
} }
} }
autoPanId = requestAnimationFrame(autoPan); autoPanId = requestAnimationFrame(autoPan);
} }
+31 -10
View File
@@ -74,8 +74,15 @@ export function XYPanZoom({
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) { function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration), transform); return new Promise<boolean>((resolve) => {
d3ZoomInstance?.transform(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
transform
);
});
} }
return Promise.resolve(false);
} }
// public functions // public functions
@@ -181,27 +188,27 @@ export function XYPanZoom({
d3ZoomInstance.on('zoom', null); d3ZoomInstance.on('zoom', null);
} }
function setViewportConstrained( async function setViewportConstrained(
viewport: Viewport, viewport: Viewport,
extent: CoordinateExtent, extent: CoordinateExtent,
translateExtent: CoordinateExtent translateExtent: CoordinateExtent
): ZoomTransform | undefined { ): Promise<ZoomTransform | undefined> {
const nextTransform = viewportToTransform(viewport); const nextTransform = viewportToTransform(viewport);
const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent); const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent);
if (contrainedTransform) { 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); const nextTransform = viewportToTransform(viewport);
setTransform(nextTransform, options); await setTransform(nextTransform, options);
return nextTransform; return new Promise<ZoomTransform>((resolve) => resolve(nextTransform));
} }
function syncViewport(viewport: Viewport) { function syncViewport(viewport: Viewport) {
@@ -228,14 +235,28 @@ export function XYPanZoom({
function scaleTo(zoom: number, options?: PanZoomTransformOptions) { function scaleTo(zoom: number, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration), zoom); return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleTo(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
zoom
);
});
} }
return Promise.resolve(false);
} }
function scaleBy(factor: number, options?: PanZoomTransformOptions) { function scaleBy(factor: number, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration), factor); return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleBy(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
factor
);
});
} }
return Promise.resolve(false);
} }
function setScaleExtent(scaleExtent: [number, number]) { function setScaleExtent(scaleExtent: [number, number]) {
+9 -2
View File
@@ -21,8 +21,15 @@ export const isWrappedWithClass = (event: any, className: string | undefined) =>
export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) =>
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
export const getD3Transition = (selection: D3SelectionInstance, duration = 0) => export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => {
typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection; const hasDuration = typeof duration === 'number' && duration > 0;
if (!hasDuration) {
onEnd();
}
return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection;
};
export const wheelDelta = (event: any) => { export const wheelDelta = (event: any) => {
const factor = event.ctrlKey && isMacOs() ? 10 : 1; const factor = event.ctrlKey && isMacOs() ? 10 : 1;