diff --git a/examples/react/src/examples/Edges/index.tsx b/examples/react/src/examples/Edges/index.tsx
index d6870621..bbba30fb 100644
--- a/examples/react/src/examples/Edges/index.tsx
+++ b/examples/react/src/examples/Edges/index.tsx
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
custom2: CustomEdge2,
};
+const defaultEdgeOptions = {
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ color: 'red',
+ width: 20,
+ height: 20,
+ },
+};
+
const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -197,6 +206,7 @@ const EdgesFlow = () => {
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log}
+ defaultEdgeOptions={defaultEdgeOptions}
>
diff --git a/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte b/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte
index 809ae82c..e767e4ac 100644
--- a/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte
+++ b/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte
@@ -34,7 +34,7 @@
// See of connection landed inside the flow pane
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-flow__pane');
- if (targetIsPane) {
+ if (targetIsPane && 'clientX' in event && 'clientY' in event) {
const id = getId();
const position = {
x: event.clientX,
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index f47f7ef4..6931ea5b 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -6,7 +6,10 @@
- selection box is not interrupted by selectionKey being let go
- fix `OnNodeDrag` type
-- refactor(handles): do not use fallback handle if an id is being used #3409
+- do not use fallback handle if a specific id is being used
+- fix `defaultEdgeOptions` markers not being applied
+- fix `getNodesBounds` and add second param for passing options
+- fix `expandParent` for child nodes
## 12.0.0-next.7
diff --git a/packages/react/package.json b/packages/react/package.json
index a7eb71c3..097cba0d 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
- "version": "12.0.0-next.7",
+ "version": "12.0.0-next.8",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",
diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx
index 34a66fa1..cd3f64ae 100644
--- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx
+++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx
@@ -25,7 +25,8 @@ const selector = (s: ReactFlowState) => {
return {
viewBB,
- boundingRect: s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, s.nodeOrigin), viewBB) : viewBB,
+ boundingRect:
+ s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB,
rfId: s.rfId,
nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom,
diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
index a681d6f3..0ecc8185 100644
--- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
+++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
@@ -73,7 +73,7 @@ export function NodeToolbar({
return null;
}
- const nodeRect: Rect = getNodesBounds(nodes, nodeOrigin);
+ const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
const wrapperStyle: CSSProperties = {
diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx
index 8ba01979..9ded77b8 100644
--- a/packages/react/src/components/NodesSelection/index.tsx
+++ b/packages/react/src/components/NodesSelection/index.tsx
@@ -22,7 +22,7 @@ export type NodesSelectionProps = {
const selector = (s: ReactFlowState) => {
const selectedNodes = s.nodes.filter((n) => n.selected);
- const { width, height, x, y } = getNodesBounds(selectedNodes, s.nodeOrigin);
+ const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
return {
width,
diff --git a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx
index a4696e5b..5b6194c5 100644
--- a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx
+++ b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx
@@ -1,9 +1,8 @@
-import { memo, useCallback } from 'react';
+import { memo, useMemo } from 'react';
import { type MarkerProps, createMarkerIds } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import { useMarkerSymbol } from './MarkerSymbols';
-import type { ReactFlowState } from '../../types';
type MarkerDefinitionsProps = {
defaultColor: string;
@@ -43,23 +42,23 @@ const Marker = ({
);
};
-const markerSelector =
- ({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) =>
- (s: ReactFlowState) => {
- const markers = createMarkerIds(s.edges, { id: rfId, defaultColor });
-
- return markers;
- };
-
-const markersEqual = (a: MarkerProps[], b: MarkerProps[]) =>
- // the id includes all marker options, so we just need to look at that part of the marker
- !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id));
-
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
- const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual);
+ const edges = useStore((s) => s.edges);
+ const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
+
+ const markers = useMemo(() => {
+ const markers = createMarkerIds(edges, {
+ id: rfId,
+ defaultColor,
+ defaultMarkerStart: defaultEdgeOptions?.markerStart,
+ defaultMarkerEnd: defaultEdgeOptions?.markerEnd,
+ });
+
+ return markers;
+ }, [edges, defaultEdgeOptions, rfId, defaultColor]);
if (!markers.length) {
return null;
diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts
index 3544f31f..9b458e87 100644
--- a/packages/react/src/hooks/useConnection.ts
+++ b/packages/react/src/hooks/useConnection.ts
@@ -10,18 +10,24 @@ const selector = (s: ReactFlowStore) => ({
position: s.connectionStartHandle ? s.connectionPosition : null,
});
+type UseConnectionResult = {
+ /** The start handle where the user interaction started or null */
+ startHandle: ReactFlowStore['connectionStartHandle'];
+ /** The target handle that's inside the connection radius or null */
+ endHandle: ReactFlowStore['connectionEndHandle'];
+ /** The current connection status 'valid', 'invalid' or null*/
+ status: ReactFlowStore['connectionStatus'];
+ /** The current connection position or null */
+ position: ReactFlowStore['connectionPosition'] | null;
+};
+
/**
* Hook for accessing the ongoing connection.
*
* @public
- * @returns ongoing connection: startHandle, endHandle, status, position
+ * @returns ongoing connection
*/
-export function useConnection(): {
- startHandle: ReactFlowStore['connectionStartHandle'];
- endHandle: ReactFlowStore['connectionEndHandle'];
- status: ReactFlowStore['connectionStatus'];
- position: ReactFlowStore['connectionPosition'] | null;
-} {
+export function useConnection(): UseConnectionResult {
const ongoingConnection = useStore(selector, shallow);
return ongoingConnection;
diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts
index 7232cce4..4f62615e 100644
--- a/packages/react/src/hooks/useHandleConnections.ts
+++ b/packages/react/src/hooks/useHandleConnections.ts
@@ -13,7 +13,7 @@ type useHandleConnectionsParams = {
};
/**
- * Hook to check if a is connected to another and get the connections.
+ * Hook to check if a is connected to another and get the connections.
*
* @public
* @param param.type - handle type 'source' or 'target'
diff --git a/packages/react/src/hooks/useOnSelectionChange.ts b/packages/react/src/hooks/useOnSelectionChange.ts
index 53c48891..8e08191b 100644
--- a/packages/react/src/hooks/useOnSelectionChange.ts
+++ b/packages/react/src/hooks/useOnSelectionChange.ts
@@ -11,7 +11,7 @@ export type UseOnSelectionChangeOptions = {
* Hook for registering an onSelectionChange handler.
*
* @public
- * @params params.onChange - The handler to register
+ * @param params.onChange - The handler to register
*/
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();
diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts
index 39320459..0e27d933 100644
--- a/packages/react/src/hooks/useViewportHelper.ts
+++ b/packages/react/src/hooks/useViewportHelper.ts
@@ -86,31 +86,31 @@ const useViewportHelper = (): ViewportHelperFunctions => {
panZoom?.setViewport(viewport, { duration: options?.duration });
},
- screenToFlowPosition: (position: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
+ screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
const { transform, snapGrid, domNode } = store.getState();
if (!domNode) {
- return position;
+ return clientPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = {
- x: position.x - domX,
- y: position.y - domY,
+ x: clientPosition.x - domX,
+ y: clientPosition.y - domY,
};
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid);
},
- flowToScreenPosition: (position: XYPosition) => {
+ flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState();
if (!domNode) {
- return position;
+ return flowPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
- const rendererPosition = rendererPointToPoint(position, transform);
+ const rendererPosition = rendererPointToPoint(flowPosition, transform);
return {
x: rendererPosition.x + domX,
diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts
index 109899d6..aace0bb4 100644
--- a/packages/react/src/store/initialState.ts
+++ b/packages/react/src/store/initialState.ts
@@ -38,7 +38,8 @@ const getInitialState = ({
if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
- const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
+ // @todo users nodeOrigin should be used here
+ const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
transform = [x, y, zoom];
}
diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts
index 53d95d68..4f3f127a 100644
--- a/packages/react/src/types/general.ts
+++ b/packages/react/src/types/general.ts
@@ -47,17 +47,91 @@ export type OnInit =
) => void;
export type ViewportHelperFunctions = {
+ /**
+ * Zooms viewport in by 1.2.
+ *
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
zoomIn: ZoomInOut;
+ /**
+ * Zooms viewport out by 1 / 1.2.
+ *
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
zoomOut: ZoomInOut;
+ /**
+ * Sets the current zoom level.
+ *
+ * @param zoomLevel - the zoom level to set
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
zoomTo: ZoomTo;
+ /**
+ * Returns the current zoom level.
+ *
+ * @returns current zoom as a number
+ */
getZoom: GetZoom;
+ /**
+ * Sets the current viewport.
+ *
+ * @param viewport - the viewport to set
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
setViewport: SetViewport;
+ /**
+ * Returns the current viewport.
+ *
+ * @returns Viewport
+ */
getViewport: GetViewport;
+ /**
+ * Fits the view.
+ *
+ * @param options.padding - optional padding
+ * @param options.includeHiddenNodes - optional includeHiddenNodes
+ * @param options.minZoom - optional minZoom
+ * @param options.maxZoom - optional maxZoom
+ * @param options.duration - optional duration. If set, a transition will be applied
+ * @param options.nodes - optional nodes to fit the view to
+ */
fitView: FitView;
+ /**
+ * Sets the center of the view to the given position.
+ *
+ * @param x - x position
+ * @param y - y position
+ * @param options.zoom - optional zoom
+ */
setCenter: SetCenter;
+ /**
+ * Fits the view to the given bounds .
+ *
+ * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
+ * @param options.padding - optional padding
+ */
fitBounds: FitBounds;
- screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
- flowToScreenPosition: (position: XYPosition) => XYPosition;
+ /**
+ * Converts a screen / client position to a flow position.
+ *
+ * @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
+ * @param options.snapToGrid - if true, the converted position will be snapped to the grid
+ * @returns position as { x: number, y: number }
+ *
+ * @example
+ * const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY })
+ */
+ screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
+ /**
+ * Converts a flow position to a screen / client position.
+ *
+ * @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
+ * @returns position as { x: number, y: number }
+ *
+ * @example
+ * const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
+ */
+ flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
viewportInitialized: boolean;
};
diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts
index 6b37fd14..d34c23dc 100644
--- a/packages/react/src/types/instance.ts
+++ b/packages/react/src/types/instance.ts
@@ -59,19 +59,112 @@ export namespace Instance {
}
export type ReactFlowInstance = {
+ /**
+ * Returns nodes.
+ *
+ * @returns nodes array
+ */
getNodes: Instance.GetNodes;
+ /**
+ * Sets nodes.
+ *
+ * @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes
+ */
setNodes: Instance.SetNodes;
+ /**
+ * Adds nodes.
+ *
+ * @param payload - the nodes to add
+ */
addNodes: Instance.AddNodes;
+ /**
+ * Returns a node by id.
+ *
+ * @param id - the node id
+ * @returns the node or undefined if no node was found
+ */
getNode: Instance.GetNode;
+ /**
+ * Returns edges.
+ *
+ * @returns edges array
+ */
getEdges: Instance.GetEdges;
+ /**
+ * Sets edges.
+ *
+ * @param payload - the edges to set or a function that receives the current edges and returns the new edges
+ */
setEdges: Instance.SetEdges;
+ /**
+ * Adds edges.
+ *
+ * @param payload - the edges to add
+ */
addEdges: Instance.AddEdges;
+ /**
+ * Returns an edge by id.
+ *
+ * @param id - the edge id
+ * @returns the edge or undefined if no edge was found
+ */
getEdge: Instance.GetEdge;
+ /**
+ * Returns the nodes, edges and the viewport as a JSON object.
+ *
+ * @returns the nodes, edges and the viewport as a JSON object
+ */
toObject: Instance.ToObject;
+ /**
+ * Deletes nodes and edges.
+ *
+ * @param params.nodes - optional nodes array to delete
+ * @param params.edges - optional edges array to delete
+ *
+ * @returns a promise that resolves with the deleted nodes and edges
+ */
deleteElements: Instance.DeleteElements;
+ /**
+ * Returns all nodes that intersect with the given node or rect.
+ *
+ * @param node - the node or rect to check for intersections
+ * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
+ * @param nodes - optional nodes array to check for intersections
+ *
+ * @returns an array of intersecting nodes
+ */
getIntersectingNodes: Instance.GetIntersectingNodes;
+ /**
+ * Checks if the given node or rect intersects with the passed rect.
+ *
+ * @param node - the node or rect to check for intersections
+ * @param area - the rect to check for intersections
+ * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react
+ *
+ * @returns true if the node or rect intersects with the given area
+ */
isNodeIntersecting: Instance.IsNodeIntersecting;
+ /**
+ * Updates a node.
+ *
+ * @param id - id of the node to update
+ * @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update
+ * @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged
+ *
+ * @example
+ * updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
+ */
updateNode: Instance.UpdateNode;
+ /**
+ * Updates the data attribute of a node.
+ *
+ * @param id - id of the node to update
+ * @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
+ * @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
+ *
+ * @example
+ * updateNodeData('node-1', { label: 'A new label' });
+ */
updateNodeData: Instance.UpdateNodeData;
viewportInitialized: boolean;
} & Omit;
diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts
index 6bcddabd..310b784c 100644
--- a/packages/react/src/utils/changes.ts
+++ b/packages/react/src/utils/changes.ts
@@ -6,10 +6,7 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) {
for (const [index, item] of updatedElements.entries()) {
if (item.id === updateItem.parentNode) {
const parent = { ...item };
-
- if (!parent.computed) {
- parent.computed = {};
- }
+ parent.computed ??= {};
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
@@ -106,7 +103,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElement = { ...element };
for (const change of changes) {
- applyChange(change, updatedElement, elements);
+ applyChange(change, updatedElement, updatedElements);
}
updatedElements.push(updatedElement);
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 6ca5483e..b534df65 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -11,7 +11,9 @@
- selection box is not interrupted by selectionKey being let go
- Edge label has a default background and is clickable
-- refactor(handles): do not use fallback handle if an id is being used #3409
+- do not use fallback handle if a specific id is being used
+- use correct id for `` data-id attribute
+- fix `getNodesBounds` and add second param for passing options
## 0.0.34
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 5095441e..ac688280 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
- "version": "0.0.34",
+ "version": "0.0.35",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",
diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte
index 87b6c9d2..f00ccc44 100644
--- a/packages/svelte/src/lib/components/Handle/Handle.svelte
+++ b/packages/svelte/src/lib/components/Handle/Handle.svelte
@@ -134,7 +134,7 @@ The Handle component is the part of a node that can be used to connect nodes.
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
- data-id="{flowId}-{nodeId}-{id || null}-{type}"
+ data-id="{$flowId}-{nodeId}-{id || null}-{type}"
class={cc([
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts
index b6bb941e..c035a52a 100644
--- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts
+++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts
@@ -24,32 +24,136 @@ import { isNode } from '$lib/utils';
* Hook for accessing the ReactFlow instance.
*
* @public
+ *
* @returns helper functions
*/
export function useSvelteFlow(): {
+ /**
+ * Zooms viewport in by 1.2.
+ *
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
zoomIn: ZoomInOut;
+ /**
+ * Zooms viewport out by 1 / 1.2.
+ *
+ * @param options.duration - optional duration. If set, a transition will be applied
+ */
zoomOut: ZoomInOut;
+ /**
+ * Returns a node by id.
+ *
+ * @param id - the node id
+ * @returns the node or undefined if no node was found
+ */
getNode: (id: string) => Node | undefined;
+ /**
+ * Returns nodes.
+ *
+ * @returns nodes array
+ */
getNodes: (ids?: string[]) => Node[];
+ /**
+ * Returns an edge by id.
+ *
+ * @param id - the edge id
+ * @returns the edge or undefined if no edge was found
+ */
getEdge: (id: string) => Edge | undefined;
+ /**
+ * Returns edges.
+ *
+ * @returns edges array
+ */
getEdges: (ids?: string[]) => Edge[];
+ /**
+ * Sets the current zoom level.
+ *
+ * @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;
+ /**
+ * Returns the current zoom level.
+ *
+ * @returns current zoom as a number
+ */
getZoom: () => number;
+ /**
+ * Sets the center of the view to the given position.
+ *
+ * @param x - x position
+ * @param y - y position
+ * @param options.zoom - optional zoom
+ */
setCenter: (x: number, y: number, options?: SetCenterOptions) => void;
+ /**
+ * 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;
+ /**
+ * Returns the current viewport.
+ *
+ * @returns Viewport
+ */
getViewport: () => Viewport;
+ /**
+ * Fits the view.
+ *
+ * @param options.padding - optional padding
+ * @param options.includeHiddenNodes - optional includeHiddenNodes
+ * @param options.minZoom - optional minZoom
+ * @param options.maxZoom - optional maxZoom
+ * @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;
+ /**
+ * Returns all nodes that intersect with the given node or rect.
+ *
+ * @param node - the node or rect to check for intersections
+ * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
+ * @param nodes - optional nodes array to check for intersections
+ *
+ * @returns an array of intersecting nodes
+ */
getIntersectingNodes: (
nodeOrRect: Node | { id: Node['id'] } | Rect,
partially?: boolean,
nodesToIntersect?: Node[]
) => Node[];
+ /**
+ * Checks if the given node or rect intersects with the passed rect.
+ *
+ * @param node - the node or rect to check for intersections
+ * @param area - the rect to check for intersections
+ * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react
+ *
+ * @returns true if the node or rect intersects with the given area
+ */
isNodeIntersecting: (
nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect,
partially?: boolean
) => boolean;
+ /**
+ * Fits the view to the given bounds .
+ *
+ * @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;
+ /**
+ * Deletes nodes and edges.
+ *
+ * @param params.nodes - optional nodes array to delete
+ * @param params.edges - optional edges array to delete
+ *
+ * @returns a promise that resolves with the deleted nodes and edges
+ */
deleteElements: ({
nodes,
edges
@@ -57,19 +161,66 @@ export function useSvelteFlow(): {
nodes?: (Node | { id: Node['id'] })[];
edges?: (Edge | { id: Edge['id'] })[];
}) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
- screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
- flowToScreenPosition: (position: XYPosition) => XYPosition;
+ /**
+ * Converts a screen / client position to a flow position.
+ *
+ * @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
+ * @param options.snapToGrid - if true, the converted position will be snapped to the grid
+ * @returns position as { x: number, y: number }
+ *
+ * @example
+ * const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY })
+ */
+ screenToFlowPosition: (
+ clientPosition: XYPosition,
+ options?: { snapToGrid: boolean }
+ ) => XYPosition;
+ /**
+ * Converts a flow position to a screen / client position.
+ *
+ * @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
+ * @returns position as { x: number, y: number }
+ *
+ * @example
+ * const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
+ */
+ flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
viewport: Writable;
+ /**
+ * Updates a node.
+ *
+ * @param id - id of the node to update
+ * @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update
+ * @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged
+ *
+ * @example
+ * updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
+ */
updateNode: (
id: string,
nodeUpdate: Partial | ((node: Node) => Partial),
options?: { replace: boolean }
) => void;
+ /**
+ * Updates the data attribute of a node.
+ *
+ * @param id - id of the node to update
+ * @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
+ * @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
+ *
+ * @example
+ * updateNodeData('node-1', { label: 'A new label' });
+ */
updateNodeData: (
id: string,
dataUpdate: object | ((node: Node) => object),
options?: { replace: boolean }
) => void;
+ /**
+ * Returns the nodes, edges and the viewport as a JSON object.
+ *
+ * @returns the nodes, edges and the viewport as a JSON object
+ */
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
} {
const {
@@ -263,6 +414,11 @@ export function useSvelteFlow(): {
_snapGrid || [1, 1]
);
},
+ /**
+ *
+ * @param position
+ * @returns
+ */
flowToScreenPosition: (position: XYPosition) => {
const _domNode = get(domNode);
@@ -279,6 +435,7 @@ export function useSvelteFlow(): {
y: rendererPosition.y + domY
};
},
+
toObject: () => {
return {
nodes: get(nodes).map((node) => ({
diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte
index fab6ce2d..ca6dba1f 100644
--- a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte
+++ b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte
@@ -58,7 +58,7 @@
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
};
} else if (toolbarNodes.length > 1) {
- nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin);
+ nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin });
}
if (nodeRect) {
diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts
index fedaa364..6c18c877 100644
--- a/packages/svelte/src/lib/store/initial-store.ts
+++ b/packages/svelte/src/lib/store/initial-store.ts
@@ -92,7 +92,8 @@ export const getInitialStore = ({
if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
- const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
+ // @todo users nodeOrigin should be used here
+ const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
}
diff --git a/packages/system/package.json b/packages/system/package.json
index 4eecdb57..e4fc6875 100644
--- a/packages/system/package.json
+++ b/packages/system/package.json
@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
- "version": "0.0.15",
+ "version": "0.0.16",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",
diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts
index 9dd23a1c..6407d1a1 100644
--- a/packages/system/src/utils/graph.ts
+++ b/packages/system/src/utils/graph.ts
@@ -102,11 +102,13 @@ export const getIncomers = {
+): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) {
return {
- x: 0,
- y: 0,
+ position: {
+ x: 0,
+ y: 0,
+ },
positionAbsolute: {
x: 0,
y: 0,
@@ -123,7 +125,7 @@ export const getNodePositionWithOrigin = (
};
return {
- ...position,
+ position,
positionAbsolute: node.computed?.positionAbsolute
? {
x: node.computed.positionAbsolute.x - offsetX,
@@ -133,27 +135,35 @@ export const getNodePositionWithOrigin = (
};
};
+export type GetNodesBoundsParams = {
+ nodeOrigin?: NodeOrigin;
+ useRelativePosition?: boolean;
+};
+
/**
* Determines a bounding box that contains all given nodes in an array
* @public
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
* @param nodes - Nodes to calculate the bounds for
- * @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
+ * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
+ * @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes
*/
-export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0]): Rect => {
+export const getNodesBounds = (
+ nodes: NodeBase[],
+ params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
+): Rect => {
if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}
const box = nodes.reduce(
(currBox, node) => {
- const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
+ const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
return getBoundsOfBoxes(
currBox,
rectToBox({
- x,
- y,
+ ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
width: node.computed?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 0,
})
@@ -239,7 +249,7 @@ export function fitView, Options exte
});
if (filteredNodes.length > 0) {
- const bounds = getNodesBounds(filteredNodes, nodeOrigin);
+ const bounds = getNodesBounds(filteredNodes, { nodeOrigin });
const viewport = getViewportForBounds(
bounds,
diff --git a/packages/system/src/utils/marker.ts b/packages/system/src/utils/marker.ts
index 0a191276..834f17ef 100644
--- a/packages/system/src/utils/marker.ts
+++ b/packages/system/src/utils/marker.ts
@@ -19,21 +19,32 @@ export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | nu
export function createMarkerIds(
edges: EdgeBase[],
- { id, defaultColor }: { id?: string | null; defaultColor?: string }
+ {
+ id,
+ defaultColor,
+ defaultMarkerStart,
+ defaultMarkerEnd,
+ }: {
+ id?: string | null;
+ defaultColor?: string;
+ defaultMarkerStart?: EdgeMarkerType;
+ defaultMarkerEnd?: EdgeMarkerType;
+ }
) {
- const ids: string[] = [];
+ const ids = new Set();
return edges
.reduce((markers, edge) => {
- [edge.markerStart, edge.markerEnd].forEach((marker) => {
+ [edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, id);
- if (!ids.includes(markerId)) {
+ if (!ids.has(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
- ids.push(markerId);
+ ids.add(markerId);
}
}
});
+
return markers;
}, [])
.sort((a, b) => a.id.localeCompare(b.id));
diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts
index 8efb84c2..51cb1329 100644
--- a/packages/system/src/utils/store.ts
+++ b/packages/system/src/utils/store.ts
@@ -136,7 +136,7 @@ function calculateXYZPosition(
}
const parentNode = nodeLookup.get(node.parentNode)!;
- const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
+ const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition(
parentNode,
diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts
index b2e8af9f..ffd19f37 100644
--- a/packages/system/src/xydrag/XYDrag.ts
+++ b/packages/system/src/xydrag/XYDrag.ts
@@ -120,7 +120,7 @@ export function XYDrag voi
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.length > 1 && nodeExtent) {
- const rect = getNodesBounds(dragItems as unknown as NodeBase[], nodeOrigin);
+ const rect = getNodesBounds(dragItems as unknown as NodeBase[], { nodeOrigin });
nodesBox = rectToBox(rect);
}
diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts
index 239e6356..4ddb5313 100644
--- a/packages/system/src/xyhandle/XYHandle.ts
+++ b/packages/system/src/xyhandle/XYHandle.ts
@@ -260,9 +260,10 @@ function isValidHandle(
}: IsValidParams
) {
const isTarget = fromType === 'target';
- const handleDomNode = doc.querySelector(
- `.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
- );
+ const handleDomNode = handle
+ ? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
+ : null;
+
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,