refactor(panzoom): return promises for viewport helpers

This commit is contained in:
moklick
2024-07-08 15:34:41 +02:00
parent 2503d32a88
commit e5b8236a60
11 changed files with 122 additions and 41 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@xyflow/system': major
---
Return Promise from transitionable panzoom actions if a transition is applied. Promise is resolved when transition ends.
@@ -224,7 +224,6 @@ const StressFlow = () => {
onEdgesChange={onEdgeChange}
minZoom={0.2}
fitView
panOnDrag={false}
>
<Controls />
<Background />
@@ -96,9 +96,13 @@ const UseZoomPanHelperFlow = () => {
);
const onNodeClick = useCallback(
(_: MouseEvent, node: Node) => {
async (_: MouseEvent, node: Node) => {
console.log('set center start');
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]
);
@@ -172,7 +176,15 @@ const UseZoomPanHelperFlow = () => {
<Panel position="top-right">
<button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</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={onResetNodes}>reset nodes</button>
<button onClick={logNodes}>useNodes</button>
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import {
pointToRendererPoint,
getViewportForBounds,
getFitViewNodes,
fitView,
type XYPosition,
rendererPointToPoint,
@@ -64,11 +65,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
},
fitView: (options) => {
const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState();
const fitViewNodes = getFitViewNodes(nodeLookup, options);
return panZoom
? fitView(
{
nodeLookup,
nodes: fitViewNodes,
width,
height,
minZoom,
+33 -3
View File
@@ -1,6 +1,7 @@
import { createWithEqualityFn } from 'zustand/traditional';
import {
clampPosition,
getFitViewNodes,
fitView as fitViewSystem,
adoptUserNodes,
updateAbsolutePositions,
@@ -79,7 +80,6 @@ const createStore = ({
updateNodeInternals: (updates) => {
const {
triggerNodeChanges,
fitView,
nodeLookup,
parentLookup,
fitViewOnInit,
@@ -88,6 +88,7 @@ const createStore = ({
domNode,
nodeOrigin,
debug,
fitViewSync,
} = get();
const { changes, updatedInternals } = updateNodeInternalsSystem(
@@ -106,8 +107,9 @@ const createStore = ({
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitView({
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
@@ -289,6 +291,7 @@ const createStore = ({
},
panBy: (delta): Promise<boolean> => {
const { transform, width, height, panZoom, translateExtent } = get();
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
},
fitView: (options?: FitViewOptions): Promise<boolean> => {
@@ -298,9 +301,11 @@ const createStore = ({
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
return fitViewSystem(
{
nodeLookup,
nodes: fitViewNodes,
width,
height,
panZoom,
@@ -310,6 +315,31 @@ const createStore = ({
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: () => {
set({
connection: { ...initialConnection },
+1
View File
@@ -169,6 +169,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy;
fitView: (options?: FitViewOptions) => Promise<boolean>;
fitViewSync: (options?: FitViewOptions) => boolean;
};
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
+31 -4
View File
@@ -18,7 +18,8 @@ import {
type CoordinateExtent,
type UpdateConnection,
type ConnectionState,
type NodeOrigin
type NodeOrigin,
getFitViewNodes
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -90,7 +91,7 @@ export function createStore({
store.nodes.update((nds) => nds);
};
async function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) {
function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) {
const nodeLookup = get(store.nodeLookup);
const { changes, updatedInternals } = updateNodeInternalsSystem(
updates,
@@ -106,7 +107,7 @@ export function createStore({
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) {
const fitViewOptions = get(store.fitViewOptions);
const fitViewOnInitDone = await fitView({
const fitViewOnInitDone = fitViewSync({
...fitViewOptions,
nodes: fitViewOptions?.nodes
});
@@ -153,9 +154,11 @@ export function createStore({
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options);
return fitViewSystem(
{
nodeLookup: get(store.nodeLookup),
nodes: fitViewNodes,
width: get(store.width),
height: get(store.height),
minZoom: get(store.minZoom),
@@ -166,6 +169,30 @@ export function createStore({
);
}
function fitViewSync(options?: FitViewOptions) {
const panZoom = get(store.panZoom);
if (!panZoom) {
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) {
+1 -1
View File
@@ -55,7 +55,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase<NodeType extends NodeBase> = {
nodeLookup: Map<string, InternalNodeBase<NodeType>>;
nodes: Map<string, InternalNodeBase<NodeType>>;
width: number;
height: number;
panZoom: PanZoomInstance;
+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));
};
export async function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params,
options?: Options
): Promise<boolean> {
const filteredNodes: Map<string, InternalNodeBase> = new Map();
export function getFitViewNodes<
Params extends NodeLookup<InternalNodeBase<NodeBase>>,
Options extends FitViewOptionsBase<NodeBase>
>(nodeLookup: Params, options?: Pick<Options, 'nodes' | 'includeHiddenNodes'>) {
const fitViewNodes: NodeLookup = new Map();
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) {
filteredNodes.set(n.id, n);
fitViewNodes.set(n.id, n);
}
});
if (filteredNodes.size > 0) {
const bounds = getInternalNodesBounds(filteredNodes);
return fitViewNodes;
}
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);
export async function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodes, width, height, panZoom, minZoom, maxZoom }: Params,
options?: Omit<Options, 'nodes' | 'includeHiddenNodes'>
): Promise<boolean> {
if (nodes.size === 0) {
return Promise.resolve(false);
}
return Promise.resolve(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);
}
/**
+1
View File
@@ -212,6 +212,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
updateNodes(lastPos as XYPosition, null);
}
}
autoPanId = requestAnimationFrame(autoPan);
}
+9 -2
View File
@@ -21,8 +21,15 @@ 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, onEnd = () => {}) =>
typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration).on('end', onEnd) : selection;
export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => {
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) => {
const factor = event.ctrlKey && isMacOs() ? 10 : 1;