Merge branch 'next' into refactor/infer-node-types

This commit is contained in:
moklick
2024-01-26 09:35:59 +01:00
28 changed files with 439 additions and 73 deletions
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
custom2: CustomEdge2, custom2: CustomEdge2,
}; };
const defaultEdgeOptions = {
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'red',
width: 20,
height: 20,
},
};
const EdgesFlow = () => { const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -197,6 +206,7 @@ const EdgesFlow = () => {
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onDelete={console.log} onDelete={console.log}
defaultEdgeOptions={defaultEdgeOptions}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
@@ -34,7 +34,7 @@
// See of connection landed inside the flow pane // See of connection landed inside the flow pane
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-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 id = getId();
const position = { const position = {
x: event.clientX, x: event.clientX,
+4 -1
View File
@@ -6,7 +6,10 @@
- selection box is not interrupted by selectionKey being let go - selection box is not interrupted by selectionKey being let go
- fix `OnNodeDrag` type - 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 ## 12.0.0-next.7
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "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.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -25,7 +25,8 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, 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, rfId: s.rfId,
nodeOrigin: s.nodeOrigin, nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
@@ -73,7 +73,7 @@ export function NodeToolbar({
return null; 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 zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
@@ -22,7 +22,7 @@ export type NodesSelectionProps<NodeType> = {
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const selectedNodes = s.nodes.filter((n) => n.selected); 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 { return {
width, width,
@@ -1,9 +1,8 @@
import { memo, useCallback } from 'react'; import { memo, useMemo } from 'react';
import { type MarkerProps, createMarkerIds } from '@xyflow/system'; import { type MarkerProps, createMarkerIds } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useMarkerSymbol } from './MarkerSymbols'; import { useMarkerSymbol } from './MarkerSymbols';
import type { ReactFlowState } from '../../types';
type MarkerDefinitionsProps = { type MarkerDefinitionsProps = {
defaultColor: string; 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 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 // 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 // that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { 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) { if (!markers.length) {
return null; return null;
+13 -7
View File
@@ -10,18 +10,24 @@ const selector = (s: ReactFlowStore) => ({
position: s.connectionStartHandle ? s.connectionPosition : null, 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. * Hook for accessing the ongoing connection.
* *
* @public * @public
* @returns ongoing connection: startHandle, endHandle, status, position * @returns ongoing connection
*/ */
export function useConnection(): { export function useConnection(): UseConnectionResult {
startHandle: ReactFlowStore['connectionStartHandle'];
endHandle: ReactFlowStore['connectionEndHandle'];
status: ReactFlowStore['connectionStatus'];
position: ReactFlowStore['connectionPosition'] | null;
} {
const ongoingConnection = useStore(selector, shallow); const ongoingConnection = useStore(selector, shallow);
return ongoingConnection; return ongoingConnection;
@@ -13,7 +13,7 @@ type useHandleConnectionsParams = {
}; };
/** /**
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections. * Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
* *
* @public * @public
* @param param.type - handle type 'source' or 'target' * @param param.type - handle type 'source' or 'target'
@@ -11,7 +11,7 @@ export type UseOnSelectionChangeOptions = {
* Hook for registering an onSelectionChange handler. * Hook for registering an onSelectionChange handler.
* *
* @public * @public
* @params params.onChange - The handler to register * @param params.onChange - The handler to register
*/ */
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi(); const store = useStoreApi();
@@ -86,31 +86,31 @@ const useViewportHelper = (): ViewportHelperFunctions => {
panZoom?.setViewport(viewport, { duration: options?.duration }); 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(); const { transform, snapGrid, domNode } = store.getState();
if (!domNode) { if (!domNode) {
return position; return clientPosition;
} }
const { x: domX, y: domY } = domNode.getBoundingClientRect(); const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = { const correctedPosition = {
x: position.x - domX, x: clientPosition.x - domX,
y: position.y - domY, y: clientPosition.y - domY,
}; };
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid);
}, },
flowToScreenPosition: (position: XYPosition) => { flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState(); const { transform, domNode } = store.getState();
if (!domNode) { if (!domNode) {
return position; return flowPosition;
} }
const { x: domX, y: domY } = domNode.getBoundingClientRect(); const { x: domX, y: domY } = domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, transform); const rendererPosition = rendererPointToPoint(flowPosition, transform);
return { return {
x: rendererPosition.x + domX, x: rendererPosition.x + domX,
+2 -1
View File
@@ -38,7 +38,8 @@ const getInitialState = ({
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.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); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
transform = [x, y, zoom]; transform = [x, y, zoom];
} }
+76 -2
View File
@@ -47,17 +47,91 @@ export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> =
) => void; ) => void;
export type ViewportHelperFunctions = { export type ViewportHelperFunctions = {
/**
* Zooms viewport in by 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
/**
* Zooms viewport out by 1 / 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomOut: ZoomInOut; 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; zoomTo: ZoomTo;
/**
* Returns the current zoom level.
*
* @returns current zoom as a number
*/
getZoom: GetZoom; 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; setViewport: SetViewport;
/**
* Returns the current viewport.
*
* @returns Viewport
*/
getViewport: GetViewport; 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; 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; 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; 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; viewportInitialized: boolean;
}; };
+93
View File
@@ -59,19 +59,112 @@ export namespace Instance {
} }
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/**
* Returns nodes.
*
* @returns nodes array
*/
getNodes: Instance.GetNodes<NodeType>; getNodes: Instance.GetNodes<NodeType>;
/**
* Sets nodes.
*
* @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes
*/
setNodes: Instance.SetNodes<NodeType>; setNodes: Instance.SetNodes<NodeType>;
/**
* Adds nodes.
*
* @param payload - the nodes to add
*/
addNodes: Instance.AddNodes<NodeType>; addNodes: Instance.AddNodes<NodeType>;
/**
* Returns a node by id.
*
* @param id - the node id
* @returns the node or undefined if no node was found
*/
getNode: Instance.GetNode<NodeType>; getNode: Instance.GetNode<NodeType>;
/**
* Returns edges.
*
* @returns edges array
*/
getEdges: Instance.GetEdges<EdgeType>; getEdges: Instance.GetEdges<EdgeType>;
/**
* Sets edges.
*
* @param payload - the edges to set or a function that receives the current edges and returns the new edges
*/
setEdges: Instance.SetEdges<EdgeType>; setEdges: Instance.SetEdges<EdgeType>;
/**
* Adds edges.
*
* @param payload - the edges to add
*/
addEdges: Instance.AddEdges<EdgeType>; addEdges: Instance.AddEdges<EdgeType>;
/**
* Returns an edge by id.
*
* @param id - the edge id
* @returns the edge or undefined if no edge was found
*/
getEdge: Instance.GetEdge<EdgeType>; getEdge: Instance.GetEdge<EdgeType>;
/**
* 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<NodeType, EdgeType>; toObject: Instance.ToObject<NodeType, EdgeType>;
/**
* 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; 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<NodeType>; getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
/**
* 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<NodeType>; isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
/**
* 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<NodeType>; updateNode: Instance.UpdateNode<NodeType>;
/**
* 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<NodeType>; updateNodeData: Instance.UpdateNodeData<NodeType>;
viewportInitialized: boolean; viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>; } & Omit<ViewportHelperFunctions, 'initialized'>;
+2 -5
View File
@@ -6,10 +6,7 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) {
for (const [index, item] of updatedElements.entries()) { for (const [index, item] of updatedElements.entries()) {
if (item.id === updateItem.parentNode) { if (item.id === updateItem.parentNode) {
const parent = { ...item }; const parent = { ...item };
parent.computed ??= {};
if (!parent.computed) {
parent.computed = {};
}
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width; const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height; 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 }; const updatedElement = { ...element };
for (const change of changes) { for (const change of changes) {
applyChange(change, updatedElement, elements); applyChange(change, updatedElement, updatedElements);
} }
updatedElements.push(updatedElement); updatedElements.push(updatedElement);
+3 -1
View File
@@ -11,7 +11,9 @@
- selection box is not interrupted by selectionKey being let go - selection box is not interrupted by selectionKey being let go
- Edge label has a default background and is clickable - 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 `<Handle />` data-id attribute
- fix `getNodesBounds` and add second param for passing options
## 0.0.34 ## 0.0.34
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "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.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "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-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
data-id="{flowId}-{nodeId}-{id || null}-{type}" data-id="{$flowId}-{nodeId}-{id || null}-{type}"
class={cc([ class={cc([
'svelte-flow__handle', 'svelte-flow__handle',
`svelte-flow__handle-${position}`, `svelte-flow__handle-${position}`,
+159 -2
View File
@@ -24,32 +24,136 @@ import { isNode } from '$lib/utils';
* Hook for accessing the ReactFlow instance. * Hook for accessing the ReactFlow instance.
* *
* @public * @public
*
* @returns helper functions * @returns helper functions
*/ */
export function useSvelteFlow(): { export function useSvelteFlow(): {
/**
* Zooms viewport in by 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
/**
* Zooms viewport out by 1 / 1.2.
*
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomOut: ZoomInOut; 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; getNode: (id: string) => Node | undefined;
/**
* Returns nodes.
*
* @returns nodes array
*/
getNodes: (ids?: string[]) => Node[]; 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; getEdge: (id: string) => Edge | undefined;
/**
* Returns edges.
*
* @returns edges array
*/
getEdges: (ids?: string[]) => Edge[]; 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; setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void;
/**
* Returns the current zoom level.
*
* @returns current zoom as a number
*/
getZoom: () => 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; 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; setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void;
/**
* Returns the current viewport.
*
* @returns Viewport
*/
getViewport: () => 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; 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: ( getIntersectingNodes: (
nodeOrRect: Node | { id: Node['id'] } | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
partially?: boolean, partially?: boolean,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => 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: ( isNodeIntersecting: (
nodeOrRect: Node | { id: Node['id'] } | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => 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; 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: ({ deleteElements: ({
nodes, nodes,
edges edges
@@ -57,19 +161,66 @@ export function useSvelteFlow(): {
nodes?: (Node | { id: Node['id'] })[]; nodes?: (Node | { id: Node['id'] })[];
edges?: (Edge | { id: Edge['id'] })[]; edges?: (Edge | { id: Edge['id'] })[];
}) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>; }) => 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<Viewport>; viewport: Writable<Viewport>;
/**
* 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: ( updateNode: (
id: string, id: string,
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>), nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => 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: ( updateNodeData: (
id: string, id: string,
dataUpdate: object | ((node: Node) => object), dataUpdate: object | ((node: Node) => object),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => 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 }; toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
} { } {
const { const {
@@ -263,6 +414,11 @@ export function useSvelteFlow(): {
_snapGrid || [1, 1] _snapGrid || [1, 1]
); );
}, },
/**
*
* @param position
* @returns
*/
flowToScreenPosition: (position: XYPosition) => { flowToScreenPosition: (position: XYPosition) => {
const _domNode = get(domNode); const _domNode = get(domNode);
@@ -279,6 +435,7 @@ export function useSvelteFlow(): {
y: rendererPosition.y + domY y: rendererPosition.y + domY
}; };
}, },
toObject: () => { toObject: () => {
return { return {
nodes: get(nodes).map((node) => ({ nodes: get(nodes).map((node) => ({
@@ -58,7 +58,7 @@
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0 height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
}; };
} else if (toolbarNodes.length > 1) { } else if (toolbarNodes.length > 1) {
nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin); nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin });
} }
if (nodeRect) { if (nodeRect) {
@@ -92,7 +92,8 @@ export const getInitialStore = ({
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.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); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.15", "version": "0.0.16",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+20 -10
View File
@@ -102,11 +102,13 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (
node: NodeBase | undefined, node: NodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): XYPosition & { positionAbsolute: XYPosition } => { ): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) { if (!node) {
return { return {
x: 0, position: {
y: 0, x: 0,
y: 0,
},
positionAbsolute: { positionAbsolute: {
x: 0, x: 0,
y: 0, y: 0,
@@ -123,7 +125,7 @@ export const getNodePositionWithOrigin = (
}; };
return { return {
...position, position,
positionAbsolute: node.computed?.positionAbsolute positionAbsolute: node.computed?.positionAbsolute
? { ? {
x: node.computed.positionAbsolute.x - offsetX, 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 * Determines a bounding box that contains all given nodes in an array
* @public * @public
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport. * @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 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 * @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) { if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 }; return { x: 0, y: 0, width: 0, height: 0 };
} }
const box = nodes.reduce( const box = nodes.reduce(
(currBox, node) => { (currBox, node) => {
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
return getBoundsOfBoxes( return getBoundsOfBoxes(
currBox, currBox,
rectToBox({ rectToBox({
x, ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
y,
width: node.computed?.width ?? node.width ?? 0, width: node.computed?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 0, height: node.computed?.height ?? node.height ?? 0,
}) })
@@ -239,7 +249,7 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
}); });
if (filteredNodes.length > 0) { if (filteredNodes.length > 0) {
const bounds = getNodesBounds(filteredNodes, nodeOrigin); const bounds = getNodesBounds(filteredNodes, { nodeOrigin });
const viewport = getViewportForBounds( const viewport = getViewportForBounds(
bounds, bounds,
+16 -5
View File
@@ -19,21 +19,32 @@ export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | nu
export function createMarkerIds( export function createMarkerIds(
edges: EdgeBase[], 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<string>();
return edges return edges
.reduce<MarkerProps[]>((markers, edge) => { .reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => { [edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') { if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, id); const markerId = getMarkerId(marker, id);
if (!ids.includes(markerId)) { if (!ids.has(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker }); markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId); ids.add(markerId);
} }
} }
}); });
return markers; return markers;
}, []) }, [])
.sort((a, b) => a.id.localeCompare(b.id)); .sort((a, b) => a.id.localeCompare(b.id));
+1 -1
View File
@@ -136,7 +136,7 @@ function calculateXYZPosition<NodeType extends NodeBase>(
} }
const parentNode = nodeLookup.get(node.parentNode)!; const parentNode = nodeLookup.get(node.parentNode)!;
const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition( return calculateXYZPosition(
parentNode, parentNode,
+1 -1
View File
@@ -120,7 +120,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.length > 1 && nodeExtent) { 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); nodesBox = rectToBox(rect);
} }
+4 -3
View File
@@ -260,9 +260,10 @@ function isValidHandle(
}: IsValidParams }: IsValidParams
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector( const handleDomNode = handle
`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]` ? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
); : null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y); const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle, // we always want to prioritize the handle below the mouse cursor over the closest distance handle,