diff --git a/examples/svelte/src/routes/examples/overview/+page.svelte b/examples/svelte/src/routes/examples/overview/+page.svelte index 44d54544..586af3b9 100644 --- a/examples/svelte/src/routes/examples/overview/+page.svelte +++ b/examples/svelte/src/routes/examples/overview/+page.svelte @@ -13,7 +13,8 @@ type Node, type Edge, ConnectionMode, - useSvelteFlow + useSvelteFlow, + ControlButton } from '@xyflow/svelte'; import CustomNode from './CustomNode.svelte'; @@ -187,7 +188,12 @@ connectionMode={ConnectionMode.Strict} attributionPosition={'top-center'} > - + + xy + console.log('control button')} + >log + diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 5828b7c4..2dd4e593 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,16 @@ # @xyflow/react +## 12.0.0-next.7 + +## Minor changes + +- pass Node/Edge types to changes thanks @FelipeEmos +- use position instead of positionAbsolute for `getNodesBounds` +- add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used +- infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine +- refactor handles: prefix with flow id for handling nested flows +- add comments for types like `ReactFlowProps` or `Node` for a better developer experience + ## 12.0.0-next.6 ### Minor changes diff --git a/packages/react/package.json b/packages/react/package.json index 7560e0dc..a7eb71c3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.6", + "version": "12.0.0-next.7", "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/Background/types.ts b/packages/react/src/additional-components/Background/types.ts index 5d21b23f..409bbe31 100644 --- a/packages/react/src/additional-components/Background/types.ts +++ b/packages/react/src/additional-components/Background/types.ts @@ -8,14 +8,27 @@ export enum BackgroundVariant { export type BackgroundProps = { id?: string; + /** Color of the pattern */ color?: string; + /** Color of the background */ bgColor?: string; + /** Class applied to the container */ className?: string; + /** Class applied to the pattern */ patternClassName?: string; + /** Gap between repetitions of the pattern */ gap?: number | [number, number]; + /** Size of a single pattern element */ size?: number; + /** Offset of the pattern */ offset?: number; + /** Line width of the Line pattern */ lineWidth?: number; + /** Variant of the pattern + * @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross + * 'lines', 'dots', 'cross' + */ variant?: BackgroundVariant; + /** Style applied to the container */ style?: CSSProperties; }; diff --git a/packages/react/src/additional-components/Controls/types.ts b/packages/react/src/additional-components/Controls/types.ts index e687b09b..e9015dfb 100644 --- a/packages/react/src/additional-components/Controls/types.ts +++ b/packages/react/src/additional-components/Controls/types.ts @@ -4,17 +4,31 @@ import type { PanelPosition } from '@xyflow/system'; import type { FitViewOptions } from '../../types'; export type ControlProps = { + /** Show button for zoom in/out */ showZoom?: boolean; + /** Show button for fit view */ showFitView?: boolean; + /** Show button for toggling interactivity */ showInteractive?: boolean; + /** Options being used when fit view button is clicked */ fitViewOptions?: FitViewOptions; + /** Callback when zoom in button is clicked */ onZoomIn?: () => void; + /** Callback when zoom out button is clicked */ onZoomOut?: () => void; + /** Callback when fit view button is clicked */ onFitView?: () => void; + /** Callback when interactivity is toggled */ onInteractiveChange?: (interactiveStatus: boolean) => void; + /** Position of the controls on the pane + * @example PanelPosition.TopLeft, PanelPosition.TopRight, + * PanelPosition.BottomLeft, PanelPosition.BottomRight + */ position?: PanelPosition; children?: ReactNode; + /** Style applied to container */ style?: React.CSSProperties; + /** ClassName applied to container */ className?: string; 'aria-label'?: string; }; diff --git a/packages/react/src/additional-components/MiniMap/types.ts b/packages/react/src/additional-components/MiniMap/types.ts index 3b24b08d..31e67428 100644 --- a/packages/react/src/additional-components/MiniMap/types.ts +++ b/packages/react/src/additional-components/MiniMap/types.ts @@ -7,23 +7,44 @@ import type { Node } from '../../types'; export type GetMiniMapNodeAttribute = (node: NodeType) => string; export type MiniMapProps = Omit, 'onClick'> & { + /** Color of nodes on minimap */ nodeColor?: string | GetMiniMapNodeAttribute; + /** Stroke color of nodes on minimap */ nodeStrokeColor?: string | GetMiniMapNodeAttribute; + /** ClassName applied to nodes on minimap */ nodeClassName?: string | GetMiniMapNodeAttribute; + /** Border radius of nodes on minimap */ nodeBorderRadius?: number; + /** Stroke width of nodes on minimap */ nodeStrokeWidth?: number; + /** Component used to render nodes on minimap */ nodeComponent?: ComponentType; + /** Color of mask representing viewport */ maskColor?: string; + /** Stroke color of mask representing viewport */ maskStrokeColor?: string; + /** Stroke width of mask representing viewport */ maskStrokeWidth?: number; + /** Position of minimap on pane + * @example PanelPosition.TopLeft, PanelPosition.TopRight, + * PanelPosition.BottomLeft, PanelPosition.BottomRight + */ position?: PanelPosition; + /** Callback caled when minimap is clicked*/ onClick?: (event: MouseEvent, position: XYPosition) => void; + /** Callback called when node on minimap is clicked */ onNodeClick?: (event: MouseEvent, node: NodeType) => void; + /** If true, viewport is pannable via mini map component */ pannable?: boolean; + /** If true, viewport is zoomable via mini map component */ zoomable?: boolean; + /** The aria-label attribute */ ariaLabel?: string | null; + /** Invert direction when panning the minimap viewport */ inversePan?: boolean; + /** Step size for zooming in/out on minimap */ zoomStep?: number; + /** Offset the viewport on the minmap, acts like a padding */ offsetScale?: number; }; diff --git a/packages/react/src/additional-components/NodeResizer/types.ts b/packages/react/src/additional-components/NodeResizer/types.ts index 3a414517..5335c80d 100644 --- a/packages/react/src/additional-components/NodeResizer/types.ts +++ b/packages/react/src/additional-components/NodeResizer/types.ts @@ -10,21 +10,39 @@ import type { } from '@xyflow/system'; export type NodeResizerProps = { + /** Id of the node it is resizing + * @remarks optional if used inside custom node + */ nodeId?: string; + /** Color of the resize handle */ color?: string; + /** ClassName applied to handle */ handleClassName?: string; + /** Style applied to handle */ handleStyle?: CSSProperties; + /** ClassName applied to line */ lineClassName?: string; + /** Style applied to line */ lineStyle?: CSSProperties; + /** Are the controls visible */ isVisible?: boolean; + /** Minimum width of node */ minWidth?: number; + /** Minimum height of node */ minHeight?: number; + /** Maximum width of node */ maxWidth?: number; + /** Maximum height of node */ maxHeight?: number; + /** Keep aspect ratio when resizing */ keepAspectRatio?: boolean; + /** Callback to determine if node should resize */ shouldResize?: ShouldResize; + /** Callback called when resizing starts */ onResizeStart?: OnResizeStart; + /** Callback called when resizing */ onResize?: OnResize; + /** Callback called when resizing ends */ onResizeEnd?: OnResizeEnd; }; @@ -42,7 +60,14 @@ export type ResizeControlProps = Pick< | 'onResize' | 'onResizeEnd' > & { + /** Position of the control + * @example ControlPosition.TopLeft, ControlPosition.TopRight, + * ControlPosition.BottomLeft, ControlPosition.BottomRight + */ position?: ControlPosition; + /** Variant of the control + * @example ResizeControlVariant.Handle, ResizeControlVariant.Line + */ variant?: ResizeControlVariant; className?: string; style?: CSSProperties; diff --git a/packages/react/src/additional-components/NodeToolbar/types.ts b/packages/react/src/additional-components/NodeToolbar/types.ts index f08859dc..4de679bd 100644 --- a/packages/react/src/additional-components/NodeToolbar/types.ts +++ b/packages/react/src/additional-components/NodeToolbar/types.ts @@ -2,9 +2,19 @@ import type { HTMLAttributes } from 'react'; import type { Position, Align } from '@xyflow/system'; export type NodeToolbarProps = HTMLAttributes & { + /** Id of the node, or array of ids the toolbar should be displayed at */ nodeId?: string | string[]; + /** If true, node toolbar is visible even if node is not selected */ isVisible?: boolean; + /** Position of the toolbar relative to the node + * @example Position.TopLeft, Position.TopRight, + * Position.BottomLeft, Position.BottomRight + */ position?: Position; + /** Offset the toolbar from the node */ offset?: number; + /** Align the toolbar relative to the node + * @example Align.Start, Align.Center, Align.End + */ align?: Align; }; diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 3e3f3bf7..db56017c 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -55,6 +55,7 @@ export function EdgeUpdateAnchors({ onConnectEnd, cancelConnection, nodes, + rfId: flowId, panBy, updateConnection, } = store.getState(); @@ -85,6 +86,7 @@ export function EdgeUpdateAnchors({ isTarget, edgeUpdaterType: handleType, lib, + flowId, cancelConnection, panBy, isValidConnection, diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 41bf492c..362027ab 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -12,6 +12,7 @@ import { XYHandle, getHostForElement, isMouseEvent, + addEdge, type HandleProps, type Connection, type HandleType, @@ -19,15 +20,14 @@ import { import { useStore, useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; -import { addEdge } from '../../utils/'; import { type ReactFlowState } from '../../types'; export type HandleComponentProps = HandleProps & Omit, 'id'>; const selector = (s: ReactFlowState) => ({ - connectionStartHandle: s.connectionStartHandle, connectOnClick: s.connectOnClick, noPanClassName: s.noPanClassName, + rfId: s.rfId, }); const connectingSelector = @@ -70,7 +70,7 @@ const HandleComponent = forwardRef( const isTarget = type === 'target'; const store = useStoreApi(); const nodeId = useNodeId(); - const { connectOnClick, noPanClassName } = useStore(selector, shallow); + const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow); const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow); if (!nodeId) { @@ -116,6 +116,7 @@ const HandleComponent = forwardRef( isTarget, handleId, nodeId, + flowId: currentStore.rfId, panBy: currentStore.panBy, cancelConnection: currentStore.cancelConnection, onConnectStart: currentStore.onConnectStart, @@ -142,6 +143,7 @@ const HandleComponent = forwardRef( connectionMode, isValidConnection: isValidConnectionStore, lib, + rfId: flowId, } = store.getState(); if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) { @@ -167,6 +169,7 @@ const HandleComponent = forwardRef( fromHandleId: connectionClickStartHandle.handleId || null, fromType: connectionClickStartHandle.type, isValidConnection: isValidConnectionHandler, + flowId, doc, lib, }); @@ -185,7 +188,7 @@ const HandleComponent = forwardRef( data-handleid={handleId} data-nodeid={nodeId} data-handlepos={position} - data-id={`${nodeId}-${handleId}-${type}`} + data-id={`${rfId}-${nodeId}-${handleId}-${type}`} className={cc([ 'react-flow__handle', `react-flow__handle-${position}`, diff --git a/packages/react/src/components/Panel/index.tsx b/packages/react/src/components/Panel/index.tsx index cde20716..eeb01a15 100644 --- a/packages/react/src/components/Panel/index.tsx +++ b/packages/react/src/components/Panel/index.tsx @@ -6,6 +6,9 @@ import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; export type PanelProps = HTMLAttributes & { + /** Set position of the panel + * @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' + */ position?: PanelPosition; children: ReactNode; }; diff --git a/packages/react/src/hooks/useNodesEdgesState.ts b/packages/react/src/hooks/useNodesEdgesState.ts index c134472b..766ee049 100644 --- a/packages/react/src/hooks/useNodesEdgesState.ts +++ b/packages/react/src/hooks/useNodesEdgesState.ts @@ -12,9 +12,12 @@ import type { Node, NodeChange, Edge, EdgeChange } from '../types'; */ export function useNodesState( initialNodes: NodeType[] -): [NodeType[], Dispatch>, (changes: NodeChange[]) => void] { +): [NodeType[], Dispatch>, (changes: NodeChange[]) => void] { const [nodes, setNodes] = useState(initialNodes); - const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), []); + const onNodesChange = useCallback( + (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), + [] + ); return [nodes, setNodes, onNodesChange]; } @@ -28,9 +31,12 @@ export function useNodesState( */ export function useEdgesState( initialEdges: EdgeType[] -): [EdgeType[], Dispatch>, (changes: EdgeChange[]) => void] { +): [EdgeType[], Dispatch>, (changes: EdgeChange[]) => void] { const [edges, setEdges] = useState(initialEdges); - const onEdgesChange = useCallback((changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), []); + const onEdgesChange = useCallback( + (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), + [] + ); return [edges, setEdges, onEdgesChange]; } diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 481bae15..39320459 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -86,8 +86,8 @@ const useViewportHelper = (): ViewportHelperFunctions => { panZoom?.setViewport(viewport, { duration: options?.duration }); }, - screenToFlowPosition: (position: XYPosition) => { - const { transform, snapToGrid, snapGrid, domNode } = store.getState(); + screenToFlowPosition: (position: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { + const { transform, snapGrid, domNode } = store.getState(); if (!domNode) { return position; @@ -100,7 +100,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { y: position.y - domY, }; - return pointToRendererPoint(correctedPosition, transform, snapToGrid, snapGrid || [1, 1]); + return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); }, flowToScreenPosition: (position: XYPosition) => { const { transform, domNode } = store.getState(); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 20573392..3cabb813 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -29,7 +29,7 @@ export { useConnection } from './hooks/useConnection'; export { useNodeId } from './contexts/NodeIdContext'; export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes'; -export { isNode, isEdge, getIncomers, getOutgoers, addEdge, updateEdge, getConnectedEdges } from './utils/general'; +export { isNode, isEdge } from './utils/general'; export * from './additional-components'; @@ -102,5 +102,10 @@ export { getStraightPath, getViewportForBounds, getNodesBounds, + getIncomers, + getOutgoers, + addEdge, + updateEdge, + getConnectedEdges, internalsSymbol, } from '@xyflow/system'; diff --git a/packages/react/src/types/changes.ts b/packages/react/src/types/changes.ts index 3dfdffa2..9a5e8fe5 100644 --- a/packages/react/src/types/changes.ts +++ b/packages/react/src/types/changes.ts @@ -44,13 +44,13 @@ export type NodeReplaceChange = { * Union type of all possible node changes. * @public */ -export type NodeChange = +export type NodeChange = | NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange - | NodeAddChange - | NodeReplaceChange; + | NodeAddChange + | NodeReplaceChange; export type EdgeSelectionChange = NodeSelectionChange; export type EdgeRemoveChange = NodeRemoveChange; @@ -64,4 +64,9 @@ export type EdgeReplaceChange = { item: EdgeType; type: 'replace'; }; -export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeReplaceChange; + +export type EdgeChange = + | EdgeSelectionChange + | EdgeRemoveChange + | EdgeAddChange + | EdgeReplaceChange; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index bdb6e529..213b9d8c 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -51,118 +51,456 @@ import type { * @public */ export type ReactFlowProps = Omit, 'onError'> & { + /** An array of nodes to render in a controlled flow. + * @example + * const nodes = [ + * { + * id: 'node-1', + * type: 'input', + * data: { label: 'Node 1' }, + * position: { x: 250, y: 50 } + * } + * ]; + */ nodes?: Node[]; + /** An array of edges to render in a controlled flow. + * @example + * const edges = [ + * { + * id: 'edge-1-2', + * source: 'node-1', + * target: 'node-2', + * } + * ]; + */ edges?: Edge[]; + /** The initial nodes to render in an uncontrolled flow. */ defaultNodes?: Node[]; + /** The initial edges to render in an uncontrolled flow. */ defaultEdges?: Edge[]; + /** Defaults to be applied to all new edges that are added to the flow. + * + * Properties on a new edge will override these defaults if they exist. + * @example + * const defaultEdgeOptions = { + * type: 'customEdgeType', + * animated: true, + * interactionWidth: 10, + * data: { label: 'custom label' }, + * hidden: false, + * deletable: true, + * selected: false, + * focusable: true, + * markerStart: EdgeMarker.ArrowClosed, + * markerEnd: EdgeMarker.ArrowClosed, + * zIndex: 12, + * ariaLabel: 'custom aria label' + * } + */ defaultEdgeOptions?: DefaultEdgeOptions; + /** This event handler is called when a user clicks on a node */ onNodeClick?: NodeMouseHandler; + /** This event handler is called when a user double clicks on a node */ onNodeDoubleClick?: NodeMouseHandler; + /** This event handler is called when mouse of a user enters a node */ onNodeMouseEnter?: NodeMouseHandler; + /** This event handler is called when mouse of a user moves over a node */ onNodeMouseMove?: NodeMouseHandler; + /** This event handler is called when mouse of a user leaves a node */ onNodeMouseLeave?: NodeMouseHandler; + /** This event handler is called when a user right clicks on a node */ onNodeContextMenu?: NodeMouseHandler; + /** This event handler is called when a user starts to drag a node */ onNodeDragStart?: NodeDragHandler; + /** This event handler is called when a user drags a node */ onNodeDrag?: NodeDragHandler; + /** This event handler is called when a user stops dragging a node */ onNodeDragStop?: NodeDragHandler; + /** This event handler is called when a user clicks on an edge */ onEdgeClick?: (event: ReactMouseEvent, edge: Edge) => void; - onEdgeUpdate?: OnEdgeUpdateFunc; + /** This event handler is called when a user right clicks on an edge */ onEdgeContextMenu?: EdgeMouseHandler; + /** This event handler is called when mouse of a user enters an edge */ onEdgeMouseEnter?: EdgeMouseHandler; + /** This event handler is called when mouse of a user moves over an edge */ onEdgeMouseMove?: EdgeMouseHandler; + /** This event handler is called when mouse of a user leaves an edge */ onEdgeMouseLeave?: EdgeMouseHandler; + /** This event handler is called when a user double clicks on an edge */ onEdgeDoubleClick?: EdgeMouseHandler; onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; + onEdgeUpdate?: OnEdgeUpdateFunc; + /** This event handler is called when a Node is updated + * @example // Use NodesState hook to create edges and get onNodesChange handler + * import ReactFlow, { useNodesState } from '@xyflow/react'; + * const [edges, setNodes, onNodesChange] = useNodesState(initialNodes); + * + * return () + * @example // Use helper function to update edge + * import ReactFlow, { applyNodeChanges } from '@xyflow/react'; + * + * const onNodeChange = useCallback( + * (changes) => setNode((eds) => applyNodeChanges(changes, eds)), + * [], + * ); + * + * return () + */ onNodesChange?: OnNodesChange; + /** This event handler is called when a Edge is updated + * @example // Use EdgesState hook to create edges and get onEdgesChange handler + * import ReactFlow, { useEdgesState } from '@xyflow/react'; + * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + * + * return () + * @example // Use helper function to update edge + * import ReactFlow, { applyEdgeChanges } from '@xyflow/react'; + * + * const onEdgesChange = useCallback( + * (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), + * [], + * ); + * + * return () + */ onEdgesChange?: OnEdgesChange; + /** This event handler gets called when a Node is deleted */ onNodesDelete?: OnNodesDelete; + /** This event handler gets called when a Edge is deleted */ onEdgesDelete?: OnEdgesDelete; + /** This event handler gets called when a Node or Edge is deleted */ onDelete?: OnDelete; + /** This event handler gets called when a user starts to drag a selection box */ onSelectionDragStart?: SelectionDragHandler; + /** This event handler gets called when a user drags a selection box */ onSelectionDrag?: SelectionDragHandler; + /** This event handler gets called when a user stops dragging a selection box */ onSelectionDragStop?: SelectionDragHandler; onSelectionStart?: (event: ReactMouseEvent) => void; onSelectionEnd?: (event: ReactMouseEvent) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void; + /** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. + * + * You can use the addEdge utility to convert the connection to a complete edge. + * @example // Use helper function to update edges onConnect + * import ReactFlow, { addEdge } from '@xyflow/react'; + * + * const onConnect = useCallback( + * (params) => setEdges((eds) => addEdge(params, eds)), + * [], + * ); + * + * return () + */ onConnect?: OnConnect; + /** This event handler gets called when a user starts to drag a connection line */ onConnectStart?: OnConnectStart; + /** This event handler gets called when a user stops dragging a connection line */ onConnectEnd?: OnConnectEnd; onClickConnectStart?: OnConnectStart; onClickConnectEnd?: OnConnectEnd; + /** This event handler gets called when a flow has finished initializing */ onInit?: OnInit; + /** This event handler is called while the user is either panning or zooming the viewport. */ onMove?: OnMove; + /** This event handler gets called when a user starts to pan or zoom the viewport */ onMoveStart?: OnMoveStart; + /** This event handler gets called when a user stops panning or zooming the viewport */ onMoveEnd?: OnMoveEnd; + /** This event handler gets called when a user changes group of selected elements in the flow */ onSelectionChange?: OnSelectionChangeFunc; + /** This event handler gets called when user scroll inside the pane */ onPaneScroll?: (event?: WheelEvent) => void; + /** This event handler gets called when user clicks inside the pane */ onPaneClick?: (event: ReactMouseEvent) => void; + /** This event handler gets called when user right clicks inside the pane */ onPaneContextMenu?: (event: ReactMouseEvent | MouseEvent) => void; + /** This event handler gets called when mouse enters the pane */ onPaneMouseEnter?: (event: ReactMouseEvent) => void; + /** This event handler gets called when mouse moves over the pane */ onPaneMouseMove?: (event: ReactMouseEvent) => void; + /** This event handler gets called when mouse leaves the pane */ onPaneMouseLeave?: (event: ReactMouseEvent) => void; + /** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */ onBeforeDelete?: OnBeforeDelete; + /** Custom node types to be available in a flow. + * + * React Flow matches a node's type to a component in the nodeTypes object. + * @example + * import CustomNode from './CustomNode'; + * + * const nodeTypes = { nameOfNodeType: CustomNode }; + */ nodeTypes?: NodeTypes; + /** Custom edge types to be available in a flow. + * + * React Flow matches an edge's type to a component in the edgeTypes object. + * @example + * import CustomEdge from './CustomEdge'; + * + * const edgeTypes = { nameOfEdgeType: CustomEdge }; + */ edgeTypes?: EdgeTypes; + /** The type of edge path to use for connection lines. + * + * Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created! + */ connectionLineType?: ConnectionLineType; + /** Styles to be applied to the connection line */ connectionLineStyle?: CSSProperties; + /** React Component to be used as a connection line */ connectionLineComponent?: ConnectionLineComponent; + /** Styles to be applied to the container of the connection line */ connectionLineContainerStyle?: CSSProperties; + /** 'strict' connection mode will only allow you to connect source handles to target handles. + * + * 'loose' connection mode will allow you to connect handles of any type to one another. + * @default 'strict' + */ connectionMode?: ConnectionMode; + /** Pressing down this key deletes all selected nodes & edges. + * @default 'Backspace' + */ deleteKeyCode?: KeyCode | null; + /** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. + * + * By setting this prop to null you can disable this functionality. + * @default 'Space' + */ selectionKeyCode?: KeyCode | null; + /** Select multiple elements with a selection box, without pressing down selectionKey */ selectionOnDrag?: boolean; + /** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected. + * @default 'full' + */ selectionMode?: SelectionMode; + /** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. + * + * By setting this prop to null you can disable this functionality. + * @default 'Space' + */ panActivationKeyCode?: KeyCode | null; + /** Pressing down this key you can select multiple elements by clicking. + * @default 'Meta' for macOS, "Ctrl" for other systems + */ multiSelectionKeyCode?: KeyCode | null; + /** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false. + * + * By setting this prop to null you can disable this functionality. + * @default 'Meta' for macOS, "Ctrl" for other systems + * */ zoomActivationKeyCode?: KeyCode | null; + /** Set this prop to make the flow snap to the grid */ snapToGrid?: boolean; + /** Grid all nodes will snap to + * @example [20, 20] + */ snapGrid?: SnapGrid; + /** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport. + * + * This might improve performance when you have a large number of nodes and edges but also adds an overhead. + * @default false + */ onlyRenderVisibleElements?: boolean; + /** Controls if all nodes should be draggable + * @default true + */ nodesDraggable?: boolean; + /** Controls if all nodes should be connectable to each other + * @default true + */ nodesConnectable?: boolean; + /** Controls if all nodes should be focusable + * @default true + */ nodesFocusable?: boolean; + /** Defines nodes relative position to its coordinates + * @example + * [0, 0] // default, top left + * [0.5, 0.5] // center + * [1, 1] // bottom right + */ nodeOrigin?: NodeOrigin; + /** Controls if all edges should be focusable + * @default true + */ edgesFocusable?: boolean; + /** Controls if all edges should be updateable + * @default true + */ edgesUpdatable?: boolean; + /** Controls if all elements should (nodes & edges) be selectable + * @default true + */ elementsSelectable?: boolean; + /** If true, nodes get selected on drag + * @default true + */ selectNodesOnDrag?: boolean; + /** Enableing this prop allows users to pan the viewport by clicking and dragging. + * + * You can also set this prop to an array of numbers to limit which mouse buttons can activate panning. + * @example [0, 2] // allows panning with the left and right mouse buttons + * [0, 1, 2, 3, 4] // allows panning with all mouse buttons + */ panOnDrag?: boolean | number[]; + /** Minimum zoom level + * @default 0.1 + */ minZoom?: number; + /** Maximum zoom level + * @default 1 + */ maxZoom?: number; + /** Controlled viewport to be used instead of internal one */ viewport?: Viewport; + /** Sets the initial position and zoom of the viewport. + * + * If a default viewport is provided but fitView is enabled, the default viewport will be ignored. + * @example + * const initialViewport = { + * zoom: 0.5, + * position: { x: 0, y: 0 } + * }; + */ defaultViewport?: Viewport; + /** + * Gets called when the viewport changes. + */ onViewportChange?: (viewport: Viewport) => void; + /** By default the viewport extends infinitely. You can use this prop to set a boundary. + * + * The first pair of coordinates is the top left boundary and the second pair is the bottom right. + * @example [[-1000, -10000], [1000, 1000]] + */ translateExtent?: CoordinateExtent; + /** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. + * @default true + */ preventScrolling?: boolean; + /** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. + * + * The first pair of coordinates is the top left boundary and the second pair is the bottom right. + * @example [[-1000, -10000], [1000, 1000]] + */ nodeExtent?: CoordinateExtent; + /** Color of edge markers + * @example "#b1b1b7" + */ defaultMarkerColor?: string; + /** Controls if the viewport should zoom by scrolling inside the container */ zoomOnScroll?: boolean; + /** Controls if the viewport should zoom by pinching on a touch screen */ zoomOnPinch?: boolean; + /** Controls if the viewport should pan by scrolling inside the container + * + * Can be limited to a specific direction with panOnScrollMode + */ panOnScroll?: boolean; + /** Controls how fast viewport should be panned on scroll. + * + * Use togther with panOnScroll prop. + */ panOnScrollSpeed?: number; + /** This prop is used to limit the direction of panning when panOnScroll is enabled. + * + * The "free" option allows panning in any direction. + * @default "free" + * @example "horizontal" | "vertical" + */ panOnScrollMode?: PanOnScrollMode; + /** Controls if the viewport should zoom by double clicking somewhere on the flow */ zoomOnDoubleClick?: boolean; edgeUpdaterRadius?: number; noDragClassName?: string; noWheelClassName?: string; noPanClassName?: string; + /** If set, initial viewport will show all nodes & edges */ fitView?: boolean; + /** Options to be used in combination with fitView + * @example + * const fitViewOptions = { + * padding: 0.1, + * includeHiddenNodes: false, + * minZoom: 0.1, + * maxZoom: 1, + * duration: 200, + * nodes: [{id: 'node-1'}, {id: 'node-2'}], // nodes to fit + * }; + */ fitViewOptions?: FitViewOptions; + /**The connectOnClick option lets you click or tap on a source handle to start a connection + * and then click on a target handle to complete the connection. + * + * If you set this option to false, users will need to drag the connection line to the target + * handle to create a connection. + */ connectOnClick?: boolean; + /** Set position of the attribution + * @default 'bottom-right' + * @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' + */ attributionPosition?: PanelPosition; + /** By default, we render a small attribution in the corner of your flows that links back to the project. + * + * Anyone is free to remove this attribution whether they're a Pro subscriber or not + * but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide} + * before doing so. + */ proOptions?: ProOptions; + /** Enabling this option will raise the z-index of nodes when they are selected. + * @default true + */ elevateNodesOnSelect?: boolean; + /** Enabling this option will raise the z-index of edges when they are selected. + * @default true + */ elevateEdgesOnSelect?: boolean; + /** + * Can be set true if built-in keyboard controls should be disabled. + * @default false + */ disableKeyboardA11y?: boolean; + /** You can enable this prop to automatically pan the viewport while dragging a node. + * @default true + */ autoPanOnNodeDrag?: boolean; + /** You can enable this prop to automatically pan the viewport while dragging a node. + * @default true + */ autoPanOnConnect?: boolean; + /** You can enable this prop to automatically pan the viewport while making a new connection. + * @default true + */ connectionRadius?: number; + /** Ocassionally something may happen that causes Svelte Flow to throw an error. + * + * Instead of exploding your application, we log a message to the console and then call this event handler. + * You might use it for additional logging or to show a message to the user. + */ onError?: OnError; + /** This callback can be used to validate a new connection + * + * If you return false, the edge will not be added to your flow. + * If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons. + * @default (connection: Connection) => true + */ isValidConnection?: IsValidConnection; + /** With a threshold greater than zero you can control the distinction between node drag and click events. + * + * If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired. + * @default 1 + */ nodeDragThreshold?: number; + /** Sets a fixed width for the flow */ width?: number; + /** Sets a fixed height for the flow */ height?: number; + /** Controls color scheme used for styling the flow + * @default 'system' + * @example 'system' | 'light' | 'dark' + */ colorMode?: ColorMode; }; diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index b65b78c3..2b514c96 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -114,13 +114,24 @@ export type EdgeProps = Pick< * @public */ export type BaseEdgeProps = EdgeLabelOptions & { + /** Unique id of edge */ id?: string; + /** Additional padding where interacting with an edge is still possible */ interactionWidth?: number; className?: string; + /** The x position of edge label */ labelX?: number; + /** The y position of edge label */ labelY?: number; + /** Marker at start of edge + * @example 'url(#arrow)' + */ markerStart?: string; + /** Marker at end of edge + * @example 'url(#arrow)' + */ markerEnd?: string; + /** SVG path of the edge */ path: string; style?: CSSProperties; }; diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index a1d76983..9a36e41f 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -16,8 +16,8 @@ import { import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.'; import { ComponentType } from 'react'; -export type OnNodesChange = (changes: NodeChange[]) => void; -export type OnEdgesChange = (changes: EdgeChange[]) => void; +export type OnNodesChange = (changes: NodeChange[]) => void; +export type OnEdgesChange = (changes: EdgeChange[]) => void; export type OnNodesDelete = (nodes: Node[]) => void; export type OnEdgesDelete = (edges: Edge[]) => void; @@ -55,7 +55,7 @@ export type ViewportHelperFunctions = { fitView: FitView; setCenter: SetCenter; fitBounds: FitBounds; - screenToFlowPosition: (position: XYPosition) => XYPosition; + screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; flowToScreenPosition: (position: XYPosition) => XYPosition; viewportInitialized: boolean; }; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 0bf01b20..6b37fd14 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -45,9 +45,6 @@ export namespace Instance { area: Rect, partially?: boolean ) => boolean; - export type getConnectedEdges = (id: string | (Node | { id: Node['id'] })[]) => Edge[]; - export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[]; - export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[]; export type UpdateNode = ( id: string, diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index cd148a6e..6bcddabd 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -188,7 +188,10 @@ function applyChange(change: any, element: any, elements: any[] = []): any { ); */ -export function applyNodeChanges(changes: NodeChange[], nodes: NodeType[]): NodeType[] { +export function applyNodeChanges( + changes: NodeChange[], + nodes: NodeType[] +): NodeType[] { return applyChanges(changes, nodes) as NodeType[]; } @@ -212,7 +215,10 @@ export function applyNodeChanges(changes: NodeChan ); */ -export function applyEdgeChanges(changes: EdgeChange[], edges: EdgeType[]): EdgeType[] { +export function applyEdgeChanges( + changes: EdgeChange[], + edges: EdgeType[] +): EdgeType[] { return applyChanges(changes, edges) as EdgeType[]; } @@ -261,7 +267,7 @@ export function getElementsDiffChanges({ lookup, }: { items: Node[] | undefined; - lookup: NodeLookup; + lookup: NodeLookup; }): NodeChange[]; export function getElementsDiffChanges({ items, diff --git a/packages/react/src/utils/general.ts b/packages/react/src/utils/general.ts index 5de3c3b2..2c2387fc 100644 --- a/packages/react/src/utils/general.ts +++ b/packages/react/src/utils/general.ts @@ -1,12 +1,4 @@ -import { - isNodeBase, - isEdgeBase, - addEdgeBase, - getOutgoersBase, - getIncomersBase, - updateEdgeBase, - getConnectedEdgesBase, -} from '@xyflow/system'; +import { isNodeBase, isEdgeBase } from '@xyflow/system'; import type { Edge, Node } from '../types'; @@ -17,7 +9,8 @@ import type { Edge, Node } from '../types'; * @param element - The element to test * @returns A boolean indicating whether the element is an Node */ -export const isNode = isNodeBase; +export const isNode = (element: unknown): element is NodeType => + isNodeBase(element); /** * Test whether an object is useable as an Edge @@ -26,52 +19,5 @@ export const isNode = isNodeBase; * @param element - The element to test * @returns A boolean indicating whether the element is an Edge */ -export const isEdge = isEdgeBase; - -/** - * Pass in a node, and get connected nodes where edge.source === node.id - * @public - * @param node - The node to get the connected nodes from - * @param nodes - The array of all nodes - * @param edges - The array of all edges - * @returns An array of nodes that are connected over eges where the source is the given node - */ -export const getOutgoers = getOutgoersBase; - -/** - * Pass in a node, and get connected nodes where edge.target === node.id - * @public - * @param node - The node to get the connected nodes from - * @param nodes - The array of all nodes - * @param edges - The array of all edges - * @returns An array of nodes that are connected over eges where the target is the given node - */ -export const getIncomers = getIncomersBase; - -/** - * This util is a convenience function to add a new Edge to an array of edges - * @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. - * @public - * @param edgeParams - Either an Edge or a Connection you want to add - * @param edges - The array of all current edges - * @returns A new array of edges with the new edge added - */ -export const addEdge = addEdgeBase; - -/** - * A handy utility to update an existing Edge with new properties - * @param oldEdge - The edge you want to update - * @param newConnection - The new connection you want to update the edge with - * @param edges - The array of all current edges - * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id - * @returns the updated edges array - */ -export const updateEdge = updateEdgeBase; - -/** - * Get all connecting edges for a given set of nodes - * @param nodes - Nodes you want to get the connected edges for - * @param edges - All edges - * @returns Array of edges that connect any of the given nodes with each other - */ -export const getConnectedEdges = getConnectedEdgesBase; +export const isEdge = (element: unknown): element is EdgeType => + isEdgeBase(element); diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b2fef4b0..b7557fff 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,16 @@ # @xyflow/svelte +## 0.0.34 + +## Minor changes + +- add second option param to `screenToFlowPosition` for configuring if `snapToGrid` should be used +- add slot to `Controls` +- cleanup `ControlButton` types +- infer types for `getIncomers`, `getOutgoers`, `updateEdge`, `addEdge` and `getConnectedEdges` thanks @joeyballentine +- refactor handles: prefix with flow id for handling nested flows +- add comments for types like `SvelteFlowProps` or `Node` for a better developer experience + ## 0.0.33 ### Bugfix diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f47dc169..5095441e 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.0.33", + "version": "0.0.34", "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/BaseEdge/types.ts b/packages/svelte/src/lib/components/BaseEdge/types.ts index c4135c1c..41a1189b 100644 --- a/packages/svelte/src/lib/components/BaseEdge/types.ts +++ b/packages/svelte/src/lib/components/BaseEdge/types.ts @@ -5,10 +5,19 @@ export type BaseEdgeProps = Pick< 'interactionWidth' | 'label' | 'labelStyle' | 'style' > & { id?: string; + /** SVG path of the edge */ path: string; + /** The x coordinate of the label */ labelX?: number; + /** The y coordinate of the label */ labelY?: number; + /** Marker at start of edge + * @example 'url(#arrow)' + */ markerStart?: string; + /** Marker at end of edge + * @example 'url(#arrow)' + */ markerEnd?: string; class?: string; }; diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 807594e0..212efe56 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -55,7 +55,8 @@ connectionLookup, onconnect: onConnectAction, onconnectstart: onConnectStartAction, - onconnectend: onConnectEndAction + onconnectend: onConnectEndAction, + flowId } = store; function onPointerDown(event: MouseEvent | TouchEvent) { @@ -72,6 +73,7 @@ connectionMode: $connectionMode, lib: $lib, autoPanOnConnect: $autoPanOnConnect, + flowId: $flowId, isValidConnection: $isValidConnection, updateConnection, cancelConnection, @@ -128,7 +130,7 @@ data-handleid={handleId} data-nodeid={nodeId} data-handlepos={position} - data-id="{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/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index 21ed2a61..cce95636 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -30,10 +30,14 @@ diff --git a/packages/svelte/src/lib/plugins/Controls/Controls.svelte b/packages/svelte/src/lib/plugins/Controls/Controls.svelte index a82c7c03..042b2be5 100644 --- a/packages/svelte/src/lib/plugins/Controls/Controls.svelte +++ b/packages/svelte/src/lib/plugins/Controls/Controls.svelte @@ -74,6 +74,7 @@ data-testid="svelte-flow__controls" aria-label={ariaLabel ?? 'Svelte Flow controls'} > + {#if showZoom} {:else}{/if} {/if} + + diff --git a/packages/svelte/src/lib/plugins/Controls/types.ts b/packages/svelte/src/lib/plugins/Controls/types.ts index 2aad9b39..fca86b00 100644 --- a/packages/svelte/src/lib/plugins/Controls/types.ts +++ b/packages/svelte/src/lib/plugins/Controls/types.ts @@ -1,9 +1,17 @@ +import type { HTMLButtonAttributes } from 'svelte/elements'; import type { PanelPosition } from '@xyflow/system'; export type ControlsProps = { + /** Position of the controls on the pane + * @example PanelPosition.TopLeft, PanelPosition.TopRight, + * PanelPosition.BottomLeft, PanelPosition.BottomRight + */ position?: PanelPosition; + /** Show button for zoom in/out */ showZoom?: boolean; + /** Show button for fit view */ showFitView?: boolean; + /** Show button for toggling interactivity */ showLock?: boolean; buttonBgColor?: string; buttonBgColorHover?: string; @@ -11,3 +19,12 @@ export type ControlsProps = { buttonColorHover?: string; 'aria-label'?: string; }; + +export type ControlButtonProps = HTMLButtonAttributes & { + class?: string; + bgColor?: string; + bgColorHover?: string; + color?: string; + colorHover?: string; + borderColor?: string; +}; diff --git a/packages/svelte/src/lib/plugins/Minimap/types.ts b/packages/svelte/src/lib/plugins/Minimap/types.ts index d39aab7a..d66577a8 100644 --- a/packages/svelte/src/lib/plugins/Minimap/types.ts +++ b/packages/svelte/src/lib/plugins/Minimap/types.ts @@ -5,25 +5,45 @@ import type { Node } from '$lib/types'; export type GetMiniMapNodeAttribute = (node: Node) => string; export type MiniMapProps = { + /** Background color of minimap */ bgColor?: string; + /** Color of nodes on the minimap */ nodeColor?: string | GetMiniMapNodeAttribute; + /** Stroke color of nodes on the minimap */ nodeStrokeColor?: string | GetMiniMapNodeAttribute; + /** Class applied to nodes on the minimap */ nodeClass?: string | GetMiniMapNodeAttribute; + /** Border radius of nodes on the minimap */ nodeBorderRadius?: number; + /** Stroke width of nodes on the minimap */ nodeStrokeWidth?: number; + /** Color of the mask representing viewport */ maskColor?: string; + /** Stroke color of the mask representing viewport */ maskStrokeColor?: string; + /** Stroke width of the mask representing viewport */ maskStrokeWidth?: number; + /** Position of the minimap on the pane + * @example PanelPosition.TopLeft, PanelPosition.TopRight, + * PanelPosition.BottomLeft, PanelPosition.BottomRight + */ position?: PanelPosition; + /** Class applied to container */ class?: string; + /** Style applied to container */ style?: string; + /** The aria-label applied to container */ ariaLabel?: string | null; + /** Width of minimap */ width?: number; + /** Height of minimap */ height?: number; // onClick?: (event: MouseEvent, position: XYPosition) => void; // onNodeClick?: (event: MouseEvent, node: Node) => void; pannable?: boolean; zoomable?: boolean; + /** Invert the direction when panning the minimap viewport */ inversePan?: boolean; + /** Step size for zooming in/out */ zoomStep?: number; }; diff --git a/packages/svelte/src/lib/plugins/NodeResizer/types.ts b/packages/svelte/src/lib/plugins/NodeResizer/types.ts index b37e55c1..ebe44b8b 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/types.ts +++ b/packages/svelte/src/lib/plugins/NodeResizer/types.ts @@ -8,21 +8,39 @@ import type { } from '@xyflow/system'; export type NodeResizerProps = { + /** Id of the node it is resizing + * @remarks optional if used inside custom node + */ nodeId?: string; + /** Color of the resize handle */ color?: string; + /** Class applied to handle */ handleClass?: string; + /** Style applied to handle */ handleStyle?: string; + /** Class applied to line */ lineClass?: string; + /** Style applied to line */ lineStyle?: string; + /** Are the controls visible */ isVisible?: boolean; + /** Minimum width of node */ minWidth?: number; + /** Minimum height of node */ minHeight?: number; + /** Maximum width of node */ maxWidth?: number; + /** Maximum height of node */ maxHeight?: number; + /** Keep aspect ratio when resizing */ keepAspectRatio?: boolean; + /** Callback to determine if node should resize */ shouldResize?: ShouldResize; + /** Callback called when resizing starts */ onResizeStart?: OnResizeStart; + /** Callback called when resizing */ onResize?: OnResize; + /** Callback called when resizing ends */ onResizeEnd?: OnResizeEnd; }; @@ -40,7 +58,14 @@ export type ResizeControlProps = Pick< | 'onResize' | 'onResizeEnd' > & { + /** Position of control + * @example ControlPosition.TopLeft, ControlPosition.TopRight, + * ControlPosition.BottomLeft, ControlPosition.BottomRight + */ position?: ControlPosition; + /** Variant of control + * @example ResizeControlVariant.Handle, ResizeControlVariant.Line + */ variant?: ResizeControlVariant; class?: string; style?: string; diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/types.ts b/packages/svelte/src/lib/plugins/NodeToolbar/types.ts index 6c812de2..aec506a3 100644 --- a/packages/svelte/src/lib/plugins/NodeToolbar/types.ts +++ b/packages/svelte/src/lib/plugins/NodeToolbar/types.ts @@ -1,9 +1,19 @@ import type { Position, Align } from '@xyflow/system'; export type NodeToolbarProps = { + /** The id of the node, or array of ids the toolbar should be displayed at */ nodeId?: string | string[]; + /** Position of the toolbar relative to the node + * @example Position.TopLeft, Position.TopRight, + * Position.BottomLeft, Position.BottomRight + */ position?: Position; + /** Align the toolbar relative to the node + * @example Align.Start, Align.Center, Align.End + */ align?: Align; + /** Offset the toolbar from the node */ offset?: number; + /** If true, node toolbar is visible even if node is not selected */ isVisible?: boolean; }; diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index a13cc7f1..18941bc6 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -7,6 +7,7 @@ import { getElementsToRemove, panBy as panBySystem, updateNodeDimensions as updateNodeDimensionsSystem, + addEdge as addEdgeUtil, type UpdateNodePositions, type NodeDimensionUpdate, type ViewportHelperFunctionOptions, @@ -19,7 +20,6 @@ import { errorMessages } from '@xyflow/system'; -import { addEdge as addEdgeUtil } from '$lib/utils'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import type { SvelteFlowStore } from './types'; diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 90c59b2d..26c57c86 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -99,7 +99,7 @@ export const getInitialStore = ({ return { flowId: writable(null), nodes: createNodesStore(nextNodes, nodeLookup), - nodeLookup: readable(nodeLookup), + nodeLookup: readable>(nodeLookup), edgeLookup: readable(edgeLookup), visibleNodes: readable([]), edges: createEdgesStore(edges, connectionLookup, edgeLookup), diff --git a/packages/svelte/src/lib/types/general.ts b/packages/svelte/src/lib/types/general.ts index 5d33a97c..4f5fa30b 100644 --- a/packages/svelte/src/lib/types/general.ts +++ b/packages/svelte/src/lib/types/general.ts @@ -23,13 +23,26 @@ export type ConnectionData = { }; export type HandleComponentProps = { + /** Type of the handle + * @example HandleType.Source, HandleType.Target + */ type: HandleType; + /** Position of the handle + * @example Position.TopLeft, Position.TopRight, + * Position.BottomLeft, Position.BottomRight + */ position?: Position; + /** Id of the handle + * @remarks optional if there is only one handle of this type + */ id?: string; class?: string; style?: string; + /** Should you be able to connect from/to this handle */ isConnectable?: boolean; + /** Shoould you be able to connect from this handle */ isConnectableStart?: boolean; + /** Should you be able to connect to this handle */ isConnectableEnd?: boolean; onconnect?: (connections: Connection[]) => void; ondisconnect?: (connections: Connection[]) => void; diff --git a/packages/svelte/src/lib/utils/index.ts b/packages/svelte/src/lib/utils/index.ts index bad572dc..10f205ca 100644 --- a/packages/svelte/src/lib/utils/index.ts +++ b/packages/svelte/src/lib/utils/index.ts @@ -1,12 +1,4 @@ -import { - isNodeBase, - isEdgeBase, - addEdgeBase, - getOutgoersBase, - getIncomersBase, - updateEdgeBase, - getConnectedEdgesBase -} from '@xyflow/system'; +import { isNodeBase, isEdgeBase } from '@xyflow/system'; import type { Edge, Node } from '$lib/types'; @@ -17,7 +9,8 @@ import type { Edge, Node } from '$lib/types'; * @param element - The element to test * @returns A boolean indicating whether the element is an Node */ -export const isNode = isNodeBase; +export const isNode = (element: unknown): element is NodeType => + isNodeBase(element); /** * Test whether an object is useable as an Edge @@ -26,52 +19,5 @@ export const isNode = isNodeBase; * @param element - The element to test * @returns A boolean indicating whether the element is an Edge */ -export const isEdge = isEdgeBase; - -/** - * Pass in a node, and get connected nodes where edge.source === node.id - * @public - * @param node - The node to get the connected nodes from - * @param nodes - The array of all nodes - * @param edges - The array of all edges - * @returns An array of nodes that are connected over eges where the source is the given node - */ -export const getOutgoers = getOutgoersBase; - -/** - * Pass in a node, and get connected nodes where edge.target === node.id - * @public - * @param node - The node to get the connected nodes from - * @param nodes - The array of all nodes - * @param edges - The array of all edges - * @returns An array of nodes that are connected over eges where the target is the given node - */ -export const getIncomers = getIncomersBase; - -/** - * This util is a convenience function to add a new Edge to an array of edges - * @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. - * @public - * @param edgeParams - Either an Edge or a Connection you want to add - * @param edges - The array of all current edges - * @returns A new array of edges with the new edge added - */ -export const addEdge = addEdgeBase; - -/** - * A handy utility to update an existing Edge with new properties - * @param oldEdge - The edge you want to update - * @param newConnection - The new connection you want to update the edge with - * @param edges - The array of all current edges - * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id - * @returns the updated edges array - */ -export const updateEdge = updateEdgeBase; - -/** - * Get all connecting edges for a given set of nodes - * @param nodes - Nodes you want to get the connected edges for - * @param edges - All edges - * @returns Array of edges that connect any of the given nodes with each other - */ -export const getConnectedEdges = getConnectedEdgesBase; +export const isEdge = (element: unknown): element is EdgeType => + isEdgeBase(element); diff --git a/packages/system/package.json b/packages/system/package.json index 6cc84fd4..4eecdb57 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/system", - "version": "0.0.14", + "version": "0.0.15", "description": "xyflow core system that powers React Flow and Svelte Flow.", "keywords": [ "node-based UI", diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index c2318f0c..e28ad058 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -2,22 +2,40 @@ import { Position } from './utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type EdgeBase = { + /** Unique id of an edge */ id: string; + /** Type of an edge defined in edgeTypes */ type?: string; + /** Id of source node */ source: string; + /** Id of target node */ target: string; + /** Id of source handle + * only needed if there are multiple handles per node + */ sourceHandle?: string | null; + /** Id of target handle + * only needed if there are multiple handles per node + */ targetHandle?: string | null; animated?: boolean; hidden?: boolean; deletable?: boolean; selectable?: boolean; + /** Arbitrary data passed to an edge */ data?: EdgeData; selected?: boolean; + /** Set the marker on the beginning of an edge + * @example 'arrow', 'arrowclosed' or custom marker + */ markerStart?: EdgeMarkerType; + /** Set the marker on the end of an edge + * @example 'arrow', 'arrowclosed' or custom marker + */ markerEnd?: EdgeMarkerType; zIndex?: number; ariaLabel?: string; + /** Padding around the edge where interaction is still possible */ interactionWidth?: number; }; diff --git a/packages/system/src/types/handles.ts b/packages/system/src/types/handles.ts index c1ea7d0e..32732935 100644 --- a/packages/system/src/types/handles.ts +++ b/packages/system/src/types/handles.ts @@ -27,12 +27,29 @@ export type ConnectionHandle = { }; export type HandleProps = { + /** Type of the handle + * @example HandleType.Source, HandleType.Target + */ type: HandleType; + /** Position of the handle + * @example Position.TopLeft, Position.TopRight, + * Position.BottomLeft, Position.BottomRight + */ position: Position; + /** Should you be able to connect to/from this handle */ isConnectable?: boolean; + /** Should you be able to connect from this handle */ isConnectableStart?: boolean; + /** Should you be able to connect to this handle */ isConnectableEnd?: boolean; + /** Callback called when connection is made */ onConnect?: OnConnect; + /** Callback if connection is valid + * @remarks connection becomes an edge if isValidConnection returns true + */ isValidConnection?: IsValidConnection; + /** Id of the handle + * @remarks optional if there is only one handle of this type + */ id?: string; }; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index c96ee085..40aa1c16 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -3,16 +3,34 @@ import { internalsSymbol } from '../constants'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import { Optional } from '../utils/types'; -// this is stuff that all nodes share independent of the framework +/** + * Framework independent node data structure. + * + * @typeParam T - type of the node data + * @typeParam U - type of the node + */ export type NodeBase = { + /** Unique id of a node */ id: string; + /** Position of a node on the pane + * @example { x: 0, y: 0 } + */ position: XYPosition; + /** Arbitrary data passed to a node */ data: T; + /** Type of node defined in nodeTypes */ type?: U; + /** Only relevant for default, source, target nodeType. controls source position + * @example 'right', 'left', 'top', 'bottom' + */ sourcePosition?: Position; + /** Only relevant for default, source, target nodeType. controls target position + * @example 'right', 'left', 'top', 'bottom' + */ targetPosition?: Position; hidden?: boolean; selected?: boolean; + /** True, if node is being dragged */ dragging?: boolean; draggable?: boolean; selectable?: boolean; @@ -21,11 +39,21 @@ export type NodeBase dragHandle?: string; width?: number | null; height?: number | null; + /** Parent node id, used for creating sub-flows */ parentNode?: string; zIndex?: number; + /** Boundary a node can be moved in + * @example 'parent' or [[0, 0], [100, 100]] + */ extent?: 'parent' | CoordinateExtent; expandParent?: boolean; ariaLabel?: string; + /** Origin of the node relative to it's position + * @example + * [0.5, 0.5] // centers the node + * [0, 0] // top left + * [1, 1] // bottom right + */ origin?: NodeOrigin; handles?: NodeHandle[]; computed?: { @@ -34,7 +62,7 @@ export type NodeBase positionAbsolute?: XYPosition; }; - // only used internally + // Only used internally [internalsSymbol]?: { z?: number; handleBounds?: NodeHandleBounds; @@ -47,7 +75,14 @@ export type NodeBase }; // props that get passed to a custom node +/** + * The node data structure that gets used for the nodes prop. + * + * @public + * @param id - The id of the node. + */ export type NodeProps = { + /** Id of the node */ id: NodeBase['id']; data: T; dragHandle: NodeBase['dragHandle']; diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 14e73318..f53db23a 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -98,7 +98,7 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { * @param edges - The array of all current edges * @returns A new array of edges with the new edge added */ -export const addEdgeBase = ( +export const addEdge = ( edgeParams: EdgeType | Connection, edges: EdgeType[] ): EdgeType[] => { @@ -145,7 +145,7 @@ export type UpdateEdgeOptions = { * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id * @returns the updated edges array */ -export const updateEdgeBase = ( +export const updateEdge = ( oldEdge: EdgeType, newConnection: Connection, edges: EdgeType[], diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 68024ce5..fa56a987 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -54,7 +54,7 @@ export const isNodeBase = (element: any): * @param edges - The array of all edges * @returns An array of nodes that are connected over eges where the source is the given node */ -export const getOutgoersBase = ( +export const getOutgoers = ( node: NodeType | { id: string }, nodes: NodeType[], edges: EdgeType[] @@ -81,7 +81,7 @@ export const getOutgoersBase = ( +export const getIncomers = ( node: NodeType | { id: string }, nodes: NodeType[], edges: EdgeType[] @@ -148,7 +148,7 @@ export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0 const box = nodes.reduce( (currBox, node) => { - const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute; + const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); return getBoundsOfBoxes( currBox, rectToBox({ @@ -212,7 +212,7 @@ export const getNodesInside = ( * @param edges - All edges * @returns Array of edges that connect any of the given nodes with each other */ -export const getConnectedEdgesBase = ( +export const getConnectedEdges = ( nodes: NodeType[], edges: EdgeType[] ): EdgeType[] => { @@ -370,7 +370,7 @@ export async function getElementsToRemove edge.id); const deletableEdges = edges.filter((edge) => edge.deletable !== false); - const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges); + const connectedEdges = getConnectedEdges(matchingNodes, deletableEdges); const matchingEdges: EdgeType[] = connectedEdges; for (const edge of deletableEdges) { diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 826a1c80..239e6356 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -27,6 +27,7 @@ export type OnPointerDownParams = { isTarget: boolean; nodes: NodeBase[]; lib: string; + flowId: string | null; edgeUpdaterType?: HandleType; updateConnection: UpdateConnection; panBy: PanBy; @@ -48,6 +49,7 @@ export type IsValidParams = { isValidConnection?: IsValidConnection; doc: Document | ShadowRoot; lib: string; + flowId: string | null; }; export type XYHandleInstance = { @@ -79,6 +81,7 @@ function onPointerDown( nodes, lib, autoPanOnConnect, + flowId, panBy, cancelConnection, onConnectStart, @@ -169,6 +172,7 @@ function onPointerDown( isValidConnection, doc, lib, + flowId, }); handleDomNode = result.handleDomNode; @@ -251,12 +255,13 @@ function isValidHandle( fromType, doc, lib, + flowId, isValidConnection = alwaysValid, }: IsValidParams ) { const isTarget = fromType === 'target'; const handleDomNode = doc.querySelector( - `.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]` + `.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]` ); const { x, y } = getEventPosition(event); const handleBelow = doc.elementFromPoint(x, y);