Merge pull request #3700 from xyflow/refactor/tsdoc

Refactor: Cleanup types + TSDoc integration
This commit is contained in:
Moritz Klack
2023-12-19 13:16:03 +01:00
committed by GitHub
49 changed files with 771 additions and 222 deletions

View File

@@ -4,12 +4,12 @@ import type { PanelPosition, XYPosition } from '@xyflow/system';
import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeData = any> = (node: Node<NodeData>) => string;
export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
nodeColor?: string | GetMiniMapNodeAttribute<NodeData>;
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeData>;
nodeClassName?: string | GetMiniMapNodeAttribute<NodeData>;
export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>;
nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>;
nodeBorderRadius?: number;
nodeStrokeWidth?: number;
nodeComponent?: ComponentType<MiniMapNodeProps>;
@@ -18,7 +18,7 @@ export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, '
maskStrokeWidth?: number;
position?: PanelPosition;
onClick?: (event: MouseEvent, position: XYPosition) => void;
onNodeClick?: (event: MouseEvent, node: Node<NodeData>) => void;
onNodeClick?: (event: MouseEvent, node: NodeType) => void;
pannable?: boolean;
zoomable?: boolean;
ariaLabel?: string | null;
@@ -27,8 +27,8 @@ export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, '
offsetScale?: number;
};
export type MiniMapNodes = Pick<
MiniMapProps,
export type MiniMapNodes<NodeType extends Node = Node> = Pick<
MiniMapProps<NodeType>,
'nodeColor' | 'nodeStrokeColor' | 'nodeClassName' | 'nodeBorderRadius' | 'nodeStrokeWidth' | 'nodeComponent'
> & {
onClick?: (event: MouseEvent, nodeId: string) => void;

View File

@@ -9,6 +9,12 @@ function getMediaQuery() {
return window.matchMedia('(prefers-color-scheme: dark)');
}
/**
* Hook for receiving the current color mode class 'dark' or 'light'.
*
* @internal
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
*/
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
colorMode === 'system' ? null : colorMode

View File

@@ -13,6 +13,11 @@ type UseDragParams = {
isSelectable?: boolean;
};
/**
* Hook for calling XYDrag helper from @xyflow/system.
*
* @internal
*/
function useDrag({ nodeRef, disabled = false, noDragClassName, handleSelector, nodeId, isSelectable }: UseDragParams) {
const store = useStoreApi();
const [dragging, setDragging] = useState<boolean>(false);

View File

@@ -5,6 +5,12 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges;
/**
* Hook for getting the current edges from the store.
*
* @public
* @returns An array of edges
*/
function useEdges<EdgeData>(): Edge<EdgeData>[] {
const edges = useStore(edgesSelector, shallow);

View File

@@ -10,6 +10,11 @@ const selected = (item: Node | Edge) => item.selected;
const deleteKeyOptions: UseKeyPressOptions = { actInsideInputWithModifier: false };
/**
* Hook for handling global key events.
*
* @internal
*/
export default ({
deleteKeyCode,
multiSelectionKeyCode,

View File

@@ -17,8 +17,8 @@ type useHandleConnectionsParams = {
*
* @public
* @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections

View File

@@ -12,11 +12,19 @@ export type UseKeyPressOptions = {
const defaultDoc = typeof document !== 'undefined' ? document : null;
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
/**
* Hook for handling key events.
*
* @public
* @param param.keyCode - The key code (string or array of strings) to use
* @param param.options - Options
* @returns boolean
*/
export default (
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean => {

View File

@@ -5,8 +5,14 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes;
function useNodes<NodeData>(): Node<NodeData>[] {
const nodes = useStore(nodesSelector, shallow);
/**
* Hook for getting the current nodes from the store.
*
* @public
* @returns An array of nodes
*/
function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];
return nodes;
}

View File

@@ -4,6 +4,14 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
/**
* Hook for receiving data of one or multiple nodes
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns An array od data objects
*/
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
export function useNodesData<NodeType extends Node = Node>(

View File

@@ -7,18 +7,16 @@ import type { Node, NodeChange, Edge, EdgeChange } from '../types';
type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[];
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
// returns a hook that can be used liked this:
// const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
function createUseItemsState(
applyChanges: ApplyChanges<Node, NodeChange>
): <NodeData = any>(
initialItems: Node<NodeData>[]
) => [Node<NodeData>[], Dispatch<SetStateAction<Node<NodeData>[]>>, OnChange<NodeChange>];
): <NodeType extends Node = Node>(
initialItems: NodeType[]
) => [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnChange<NodeChange>];
function createUseItemsState(
applyChanges: ApplyChanges<Edge, EdgeChange>
): <EdgeData = any>(
initialItems: Edge<EdgeData>[]
) => [Edge<EdgeData>[], Dispatch<SetStateAction<Edge<EdgeData>[]>>, OnChange<EdgeChange>];
): <EdgeType extends Edge = Edge>(
initialItems: EdgeType[]
) => [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnChange<EdgeChange>];
function createUseItemsState(
applyChanges: ApplyChanges<any, any>
): (initialItems: any[]) => [any[], Dispatch<SetStateAction<any[]>>, OnChange<any>] {
@@ -31,5 +29,20 @@ function createUseItemsState(
};
}
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
*/
export const useNodesState = createUseItemsState(applyNodeChanges);
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
*/
export const useEdgesState = createUseItemsState(applyEdgeChanges);

View File

@@ -21,6 +21,13 @@ const defaultOptions = {
includeHiddenNodes: false,
};
/**
* Hook which returns true when all nodes are initialized.
*
* @public
* @param options.includeHiddenNodes - defaults to false
* @returns boolean indicating whether all nodes are initialized
*/
function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
const initialized = useStore(selector(options));

View File

@@ -3,6 +3,11 @@ import { useEffect, useRef } from 'react';
import useReactFlow from './useReactFlow';
import type { OnInit } from '../types';
/**
* Hook for calling onInit handler.
*
* @internal
*/
function useOnInitHandler(onInit: OnInit | undefined) {
const rfInstance = useReactFlow();
const isInitialized = useRef<boolean>(false);

View File

@@ -7,6 +7,12 @@ export type UseOnSelectionChangeOptions = {
onChange: OnSelectionChangeFunc;
};
/**
* Hook for registering an onSelectionChange handler.
*
* @public
* @params params.onChange - The handler to register
*/
function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();

View File

@@ -9,6 +9,14 @@ export type UseOnViewportChangeOptions = {
onEnd?: OnViewportChange;
};
/**
* Hook for registering an onViewportChange handler.
*
* @public
* @param params.onStart - gets called when the viewport starts changing
* @param params.onChange - gets called when the viewport changes
* @param params.onEnd - gets called when the viewport stops changing
*/
function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi();

View File

@@ -1,13 +1,5 @@
import { useCallback, useMemo } from 'react';
import {
getElementsToRemove,
getIncomersBase,
getOutgoersBase,
getOverlappingArea,
isRectObject,
nodeToRect,
type Rect,
} from '@xyflow/system';
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from './useStore';
@@ -26,32 +18,40 @@ import type {
} from '../types';
import { isNode } from '../utils';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
/**
* Hook for accessing the ReactFlow instance.
*
* @public
* @returns ReactFlowInstance
*/
export default function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,
EdgeType
> {
const viewportHelper = useViewportHelper();
const store = useStoreApi();
const getNodes = useCallback<Instance.GetNodes<NodeData>>(() => {
return store.getState().nodes.map((n) => ({ ...n }));
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
}, []);
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => {
return store.getState().nodeLookup.get(id);
const getNode = useCallback<Instance.GetNode<NodeType>>((id) => {
return store.getState().nodeLookup.get(id) as NodeType;
}, []);
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => {
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e }));
return edges.map((e) => ({ ...e })) as EdgeType[];
}, []);
const getEdge = useCallback<Instance.GetEdge<EdgeData>>((id) => {
const getEdge = useCallback<Instance.GetEdge<EdgeType>>((id) => {
const { edges = [] } = store.getState();
return edges.find((e) => e.id === id);
return edges.find((e) => e.id === id) as EdgeType;
}, []);
const setNodes = useCallback<Instance.SetNodes<NodeData>>((payload) => {
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
const nextNodes = typeof payload === 'function' ? payload(nodes) : payload;
const nextNodes = typeof payload === 'function' ? payload(nodes as NodeType[]) : payload;
if (hasDefaultNodes) {
setNodes(nextNodes);
@@ -59,14 +59,14 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const changes =
nextNodes.length === 0
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeData>));
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeType>));
onNodesChange(changes);
}
}, []);
const setEdges = useCallback<Instance.SetEdges<EdgeData>>((payload) => {
const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
const nextEdges = typeof payload === 'function' ? payload(edges) : payload;
const nextEdges = typeof payload === 'function' ? payload(edges as EdgeType[]) : payload;
if (hasDefaultEdges) {
setEdges(nextEdges);
@@ -74,12 +74,12 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const changes =
nextEdges.length === 0
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeData>));
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeType>));
onEdgesChange(changes);
}
}, []);
const addNodes = useCallback<Instance.AddNodes<NodeData>>((payload) => {
const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
const nodes = Array.isArray(payload) ? payload : [payload];
const { nodes: currentNodes, hasDefaultNodes, onNodesChange, setNodes } = store.getState();
@@ -87,29 +87,29 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const nextNodes = [...currentNodes, ...nodes];
setNodes(nextNodes);
} else if (onNodesChange) {
const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeData>));
const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeType>));
onNodesChange(changes);
}
}, []);
const addEdges = useCallback<Instance.AddEdges<EdgeData>>((payload) => {
const addEdges = useCallback<Instance.AddEdges<EdgeType>>((payload) => {
const nextEdges = Array.isArray(payload) ? payload : [payload];
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
if (hasDefaultEdges) {
setEdges([...edges, ...nextEdges]);
} else if (onEdgesChange) {
const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeData>));
const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeType>));
onEdgesChange(changes);
}
}, []);
const toObject = useCallback<Instance.ToObject<NodeData, EdgeData>>(() => {
const toObject = useCallback<Instance.ToObject<NodeType, EdgeType>>(() => {
const { nodes = [], edges = [], transform } = store.getState();
const [x, y, zoom] = transform;
return {
nodes: nodes.map((n) => ({ ...n })),
edges: edges.map((e) => ({ ...e })),
nodes: nodes.map((n) => ({ ...n })) as NodeType[],
edges: edges.map((e) => ({ ...e })) as EdgeType[],
viewport: {
x,
y,
@@ -181,11 +181,9 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
}, []);
const getNodeRect = useCallback(
(
nodeOrRect: Node<NodeData> | { id: Node['id'] } | Rect
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
(nodeOrRect: NodeType | { id: Node['id'] } | Rect): [Rect | null, NodeType | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id);
const node = isRect ? null : (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
if (!isRect && !node) {
[null, null, isRect];
@@ -198,7 +196,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[]
);
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeData>>(
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
(nodeOrRect, partially = true, nodes) => {
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
@@ -216,12 +214,12 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
});
}) as NodeType[];
},
[]
);
const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeData>>(
const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeType>>(
(nodeOrRect, area, partially = true) => {
const [nodeRect] = getNodeRect(nodeOrRect);
@@ -237,48 +235,13 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[]
);
const getConnectedEdges = useCallback<Instance.getConnectedEdges>((node) => {
const { edges } = store.getState();
const nodeIds = new Set();
if (typeof node === 'string') {
nodeIds.add(node);
} else if (node.length >= 1) {
node.forEach((n) => {
nodeIds.add(n.id);
});
}
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
}, []);
const getIncomers = useCallback<Instance.getIncomers>((node) => {
const { nodes, edges } = store.getState();
if (typeof node === 'string') {
return getIncomersBase({ id: node }, nodes, edges);
}
return getIncomersBase(node, nodes, edges);
}, []);
const getOutgoers = useCallback<Instance.getOutgoers>((node) => {
const { nodes, edges } = store.getState();
if (typeof node == 'string') {
return getOutgoersBase({ id: node }, nodes, edges);
}
return getOutgoersBase(node, nodes, edges);
}, []);
const updateNode = useCallback<Instance.UpdateNode>(
const updateNode = useCallback<Instance.UpdateNode<NodeType>>(
(id, nodeUpdate, options = { replace: true }) => {
setNodes((prevNodes) =>
prevNodes.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as NodeType) : nodeUpdate;
return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode };
}
return node;
@@ -288,7 +251,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[setNodes]
);
const updateNodeData = useCallback<Instance.UpdateNodeData>(
const updateNodeData = useCallback<Instance.UpdateNodeData<NodeType>>(
(id, dataUpdate, options = { replace: false }) => {
updateNode(
id,
@@ -317,9 +280,6 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
deleteElements,
getIntersectingNodes,
isNodeIntersecting,
getConnectedEdges,
getIncomers,
getOutgoers,
updateNode,
updateNodeData,
};
@@ -337,8 +297,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
deleteElements,
getIntersectingNodes,
isNodeIntersecting,
getConnectedEdges,
getIncomers,
getOutgoers,
updateNode,
updateNodeData,
]);
}

View File

@@ -3,6 +3,11 @@ import { errorMessages, getDimensions } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
/**
* Hook for handling resize events.
*
* @internal
*/
function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi();

View File

@@ -10,6 +10,14 @@ const zustandErrorMessage = errorMessages['error001']();
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
/**
* Hook for accessing the internal store. Should only be used in rare cases.
*
* @public
* @param selector
* @param equalityFn
* @returns The selected state slice
*/
function useStore<StateSlice = ExtractState>(
selector: (state: ReactFlowState) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean

View File

@@ -3,6 +3,12 @@ import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
/**
* Hook for updating node internals.
*
* @public
* @returns function for updating node internals
*/
function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi();

View File

@@ -7,6 +7,12 @@ import { useStoreApi } from '../hooks/useStore';
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'));
/**
* Hook for updating node positions.
*
* @internal
* @returns function for updating node positions
*/
function useUpdateNodePositions() {
const store = useStoreApi();

View File

@@ -10,6 +10,12 @@ const viewportSelector = (state: ReactFlowState) => ({
zoom: state.transform[2],
});
/**
* Hook for getting the current viewport from the store.
*
* @public
* @returns The current viewport
*/
function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow);

View File

@@ -12,6 +12,12 @@ import type { ViewportHelperFunctions, ReactFlowState } from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom;
/**
* Hook for getting viewport helper functions.
*
* @internal
* @returns viewport helper functions
*/
const useViewportHelper = (): ViewportHelperFunctions => {
const store = useStoreApi();
const panZoomInitialized = useStore(selector);

View File

@@ -6,6 +6,12 @@ import type { ReactFlowState } from '../types';
const selector = (state: ReactFlowState) => state.panZoom?.syncViewport;
/**
* Hook for syncing the viewport with the panzoom instance.
*
* @internal
* @param viewport
*/
export default function useViewportSync(viewport?: Viewport) {
const syncViewport = useStore(selector);
const store = useStoreApi();

View File

@@ -5,6 +5,13 @@ import { isEdgeVisible } from '@xyflow/system';
import { useStore } from './useStore';
import { type ReactFlowState } from '../types';
/**
* Hook for getting the visible edge ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible edge ids
*/
function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] {
const edgeIds = useStore(
useCallback(

View File

@@ -13,6 +13,13 @@ const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
: Array.from(s.nodeLookup.keys());
};
/**
* Hook for getting the visible node ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible node ids
*/
function useVisibleNodeIds(onlyRenderVisible: boolean) {
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);

View File

@@ -30,16 +30,20 @@ export type NodeRemoveChange = {
type: 'remove';
};
export type NodeAddChange<NodeData = any> = {
item: Node<NodeData>;
export type NodeAddChange<NodeType extends Node = Node> = {
item: NodeType;
type: 'add';
};
export type NodeResetChange<NodeData = any> = {
item: Node<NodeData>;
export type NodeResetChange<NodeType extends Node = Node> = {
item: NodeType;
type: 'reset';
};
/**
* Union type of all possible node changes.
* @public
*/
export type NodeChange =
| NodeDimensionChange
| NodePositionChange
@@ -50,12 +54,12 @@ export type NodeChange =
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeData = any> = {
item: Edge<EdgeData>;
export type EdgeAddChange<EdgeType extends Edge = Edge> = {
item: EdgeType;
type: 'add';
};
export type EdgeResetChange<EdgeData = any> = {
item: Edge<EdgeData>;
export type EdgeResetChange<EdgeType extends Edge = Edge> = {
item: EdgeType;
type: 'reset';
};
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;

View File

@@ -20,6 +20,7 @@ import type {
OnError,
IsValidConnection,
ColorMode,
SnapGrid,
} from '@xyflow/system';
import type {
@@ -44,6 +45,10 @@ import type {
EdgeMouseHandler,
} from '.';
/**
* ReactFlow component props.
* @public
*/
export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
nodes?: Node[];
edges?: Edge[];
@@ -110,7 +115,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
multiSelectionKeyCode?: KeyCode | null;
zoomActivationKeyCode?: KeyCode | null;
snapToGrid?: boolean;
snapGrid?: [number, number];
snapGrid?: SnapGrid;
onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean;
nodesConnectable?: boolean;

View File

@@ -13,7 +13,6 @@ import type {
HandleElement,
ConnectionStatus,
EdgePosition,
Optional,
StepPathOptions,
OnError,
} from '@xyflow/system';
@@ -31,12 +30,13 @@ export type EdgeLabelOptions = {
export type EdgeUpdatable = boolean | HandleType;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
style?: CSSProperties;
className?: string;
updatable?: EdgeUpdatable;
focusable?: boolean;
} & EdgeLabelOptions;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> &
EdgeLabelOptions & {
style?: CSSProperties;
className?: string;
updatable?: EdgeUpdatable;
focusable?: boolean;
};
type SmoothStepEdgeType<T> = DefaultEdge<T> & {
type: 'smoothstep';
@@ -53,6 +53,10 @@ type StepEdgeType<T> = DefaultEdge<T> & {
pathOptions?: StepPathOptions;
};
/**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* @public
*/
export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T> | StepEdgeType<T>;
export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void;
@@ -88,14 +92,18 @@ export type EdgeTextProps = HTMLAttributes<SVGElement> &
y: number;
};
// props that get passed to a custom edge
/**
* Custom edge component props
* @public
*/
export type EdgeProps<T = any> = Pick<
Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> &
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId'> &
EdgePosition &
EdgeLabelOptions & {
sourceHandleId?: string | null;
targetHandleId?: string | null;
markerStart?: string;
markerEnd?: string;
// @TODO: how can we get better types for pathOptions?
@@ -103,31 +111,69 @@ export type EdgeProps<T = any> = Pick<
interactionWidth?: number;
};
export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd' | 'interactionWidth'> &
/**
* BaseEdge component props
* @public
*/
export type BaseEdgeProps = EdgeLabelOptions & {
id?: string;
interactionWidth?: number;
labelX?: number;
labelY?: number;
markerStart?: string;
markerEnd?: string;
path: string;
style?: CSSProperties;
};
/**
* Helper type for edge components that get exported by the library
* @public
*/
export type EdgeComponentProps = EdgePosition &
EdgeLabelOptions & {
id?: string;
labelX?: number;
labelY?: number;
path: string;
id?: EdgeProps['id'];
markerStart?: EdgeProps['markerStart'];
markerEnd?: EdgeProps['markerEnd'];
interactionWidth?: EdgeProps['interactionWidth'];
style?: EdgeProps['style'];
sourceHandleId?: EdgeProps['sourceHandleId'];
targetHandleId?: EdgeProps['targetHandleId'];
};
export type EdgeComponentProps<T = any> = Optional<Omit<EdgeProps<T>, 'source' | 'target'>, 'id'>;
export type StraightEdgeProps<T = any> = Omit<EdgeComponentProps<T>, 'sourcePosition' | 'targetPosition'>;
export type SmoothStepEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: SmoothStepPathOptions;
export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
pathOptions?: PathOptions;
};
export type BezierEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: BezierPathOptions;
};
/**
* BezierEdge component props
* @public
*/
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
export type StepEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: StepPathOptions;
};
/**
* SmoothStepEdge component props
* @public
*/
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
export type SimpleBezierEdgeProps<T = any> = EdgeComponentProps<T>;
/**
* StepEdge component props
* @public
*/
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/**
* StraightEdge component props
* @public
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
/**
* SimpleBezier component props
* @public
*/
export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;

View File

@@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType } from 'react';
import {
FitViewParamsBase,
FitViewOptionsBase,
NodeProps,
ZoomInOut,
ZoomTo,
SetViewport,
@@ -12,9 +10,11 @@ import {
SetCenter,
FitBounds,
XYPosition,
NodeProps,
} from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, EdgeProps, ReactFlowInstance } from '.';
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;
@@ -41,7 +41,9 @@ export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type FitViewParams = FitViewParamsBase<Node>;
export type FitViewOptions = FitViewOptionsBase<Node>;
export type FitView = (fitViewOptions?: FitViewOptions) => boolean;
export type OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void;
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void;
export type ViewportHelperFunctions = {
zoomIn: ZoomInOut;

View File

@@ -3,9 +3,9 @@
import type { Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions } from '.';
export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
nodes: Node<NodeData>[];
edges: Edge<EdgeData>[];
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: NodeType[];
edges: EdgeType[];
viewport: Viewport;
};
@@ -15,30 +15,33 @@ export type DeleteElementsOptions = {
};
export namespace Instance {
export type GetNodes<NodeData> = () => Node<NodeData>[];
export type SetNodes<NodeData> = (
payload: Node<NodeData>[] | ((nodes: Node<NodeData>[]) => Node<NodeData>[])
export type GetNodes<NodeType extends Node = Node> = () => NodeType[];
export type SetNodes<NodeType extends Node = Node> = (
payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])
) => void;
export type AddNodes<NodeData> = (payload: Node<NodeData>[] | Node<NodeData>) => void;
export type GetNode<NodeData> = (id: string) => Node<NodeData> | undefined;
export type GetEdges<EdgeData> = () => Edge<EdgeData>[];
export type SetEdges<EdgeData> = (
payload: Edge<EdgeData>[] | ((edges: Edge<EdgeData>[]) => Edge<EdgeData>[])
export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
export type SetEdges<EdgeType extends Edge = Edge> = (
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
) => void;
export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined;
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
export type GetEdge<EdgeType extends Edge = Edge> = (id: string) => EdgeType | undefined;
export type AddEdges<EdgeType extends Edge = Edge> = (payload: EdgeType[] | EdgeType) => void;
export type ToObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = () => ReactFlowJsonObject<
NodeType,
EdgeType
>;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => {
deletedNodes: Node[];
deletedEdges: Edge[];
};
export type GetIntersectingNodes<NodeData> = (
node: Node<NodeData> | { id: Node['id'] } | Rect,
export type GetIntersectingNodes<NodeType extends Node = Node> = (
node: NodeType | { id: Node['id'] } | Rect,
partially?: boolean,
nodes?: Node<NodeData>[]
) => Node<NodeData>[];
export type IsNodeIntersecting<NodeData> = (
node: Node<NodeData> | { id: Node['id'] } | Rect,
nodes?: NodeType[]
) => NodeType[];
export type IsNodeIntersecting<NodeType extends Node = Node> = (
node: NodeType | { id: Node['id'] } | Rect,
area: Rect,
partially?: boolean
) => boolean;
@@ -46,32 +49,32 @@ export namespace Instance {
export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[];
export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[];
export type UpdateNode = (
export type UpdateNode<NodeType extends Node = Node> = (
id: string,
dataUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
options?: { replace: boolean }
) => void;
export type UpdateNodeData = (
export type UpdateNodeData<NodeType extends Node = Node> = (
id: string,
dataUpdate: object | ((node: Node) => object),
dataUpdate: object | ((node: NodeType) => object),
options?: { replace: boolean }
) => void;
}
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
getNodes: Instance.GetNodes<NodeData>;
setNodes: Instance.SetNodes<NodeData>;
addNodes: Instance.AddNodes<NodeData>;
getNode: Instance.GetNode<NodeData>;
getEdges: Instance.GetEdges<EdgeData>;
setEdges: Instance.SetEdges<EdgeData>;
addEdges: Instance.AddEdges<EdgeData>;
getEdge: Instance.GetEdge<EdgeData>;
toObject: Instance.ToObject<NodeData, EdgeData>;
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
getNodes: Instance.GetNodes<NodeType>;
setNodes: Instance.SetNodes<NodeType>;
addNodes: Instance.AddNodes<NodeType>;
getNode: Instance.GetNode<NodeType>;
getEdges: Instance.GetEdges<EdgeType>;
setEdges: Instance.SetEdges<EdgeType>;
addEdges: Instance.AddEdges<EdgeType>;
getEdge: Instance.GetEdge<EdgeType>;
toObject: Instance.ToObject<NodeType, EdgeType>;
deleteElements: Instance.DeleteElements;
getIntersectingNodes: Instance.GetIntersectingNodes<NodeData>;
isNodeIntersecting: Instance.IsNodeIntersecting<NodeData>;
updateNode: Instance.UpdateNode;
updateNodeData: Instance.UpdateNodeData;
getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
updateNode: Instance.UpdateNode<NodeType>;
updateNodeData: Instance.UpdateNodeData<NodeType>;
viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>;

View File

@@ -1,8 +1,13 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* The node data structure that gets used for the nodes prop.
* @public
*/
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData,
NodeType
@@ -10,6 +15,7 @@ export type Node<NodeData = any, NodeType extends string | undefined = string |
style?: CSSProperties;
className?: string;
resizing?: boolean;
focusable?: boolean;
};
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;

View File

@@ -142,12 +142,52 @@ function applyChanges(changes: any[], elements: any[]): any[] {
return updatedElements;
}
export function applyNodeChanges<NodeData = any>(changes: NodeChange[], nodes: Node<NodeData>[]): Node<NodeData>[] {
return applyChanges(changes, nodes) as Node<NodeData>[];
/**
* Drop in function that applies node changes to an array of nodes.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @example
* const onNodesChange = useCallback(
(changes) => {
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
},
[setNodes],
);
return (
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
);
*/
export function applyNodeChanges<NodeType extends Node = Node>(changes: NodeChange[], nodes: NodeType[]): NodeType[] {
return applyChanges(changes, nodes) as NodeType[];
}
export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: Edge<EdgeData>[]): Edge<EdgeData>[] {
return applyChanges(changes, edges) as Edge<EdgeData>[];
/**
* Drop in function that applies edge changes to an array of edges.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @example
* const onEdgesChange = useCallback(
(changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
},
[setEdges],
);
return (
<ReactFLow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
);
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(changes: EdgeChange[], edges: EdgeType[]): EdgeType[] {
return applyChanges(changes, edges) as EdgeType[];
}
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({

View File

@@ -10,10 +10,68 @@ import {
import type { Edge, Node } from '../types';
/**
* Test whether an object is useable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
*/
export const isNode = isNodeBase<Node>;
/**
* Test whether an object is useable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
*/
export const isEdge = isEdgeBase<Edge>;
/**
* 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<Node, Edge>;
/**
* 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<Node, Edge>;
/**
* 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<Edge>;
/**
* 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<Edge>;
/**
* 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<Node, Edge>;

View File

@@ -9,6 +9,12 @@ function getMediaQuery() {
return window.matchMedia('(prefers-color-scheme: dark)');
}
/**
* Hook for receiving the current color mode class 'dark' or 'light'.
*
* @internal
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
*/
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
const colorModeClass = readable<ColorModeClass>('light', (set) => {
if (colorMode !== 'system') {

View File

@@ -3,6 +3,12 @@ import type { Readable } from 'svelte/store';
import { useStore } from '$lib/store';
import type { ConnectionProps } from '$lib/store/derived-connection-props';
/**
* Hook for receiving the current connection.
*
* @public
* @returns current connection as a readable store
*/
export function useConnection(): Readable<ConnectionProps> {
const { connection } = useStore();

View File

@@ -11,6 +11,15 @@ export type useHandleConnectionsParams = {
const initialConnections: Connection[] = [];
/**
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
*
* @public
* @param param.nodeId
* @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @returns an array with connections
*/
export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) {
const { edges, connectionLookup } = useStore();
let prevConnections: Map<string, Connection> | undefined = undefined;

View File

@@ -21,6 +21,14 @@ function areNodesDataEqual(a: Node['data'][] | null, b: Node['data'][] | null) {
return true;
}
/**
* Hook for receiving data of one or multiple nodes
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns A readable store with an array of data objects
*/
export function useNodesData<NodeType extends Node = Node>(
nodeId: string
): Readable<NodeType['data'] | null>;

View File

@@ -1,10 +1,22 @@
import { useStore } from '$lib/store';
/**
* Hook for getting the current nodes from the store.
*
* @public
* @returns store with an array of nodes
*/
export function useNodes() {
const { nodes } = useStore();
return nodes;
}
/**
* Hook for getting the current edges from the store.
*
* @public
* @returns store with an array of edges
*/
export function useEdges() {
const { edges } = useStore();
return edges;

View File

@@ -20,6 +20,12 @@ import { useStore } from '$lib/store';
import type { Edge, FitViewOptions, Node } from '$lib/types';
import { isNode } from '$lib/utils';
/**
* Hook for accessing the ReactFlow instance.
*
* @public
* @returns helper functions
*/
export function useSvelteFlow(): {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;

View File

@@ -3,6 +3,12 @@ import type { UpdateNodeInternals } from '@xyflow/system';
import { useStore } from '$lib/store';
/**
* Hook for updating node internals.
*
* @public
* @returns function for updating node internals
*/
export function useUpdateNodeInternals(): UpdateNodeInternals {
const { domNode, updateNodeDimensions } = useStore();

View File

@@ -6,7 +6,6 @@ import type {
DefaultEdgeOptionsBase,
EdgePosition,
SmoothStepPathOptions,
Optional,
StepPathOptions
} from '@xyflow/system';
@@ -34,12 +33,18 @@ type StepEdgeType<T> = DefaultEdge<T> & {
pathOptions?: StepPathOptions;
};
/**
* The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component.
*/
export type Edge<T = any> =
| DefaultEdge<T>
| SmoothStepEdgeType<T>
| BezierEdgeType<T>
| StepEdgeType<T>;
/**
* Custom edge component props.
*/
export type EdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle' | 'type'> &
EdgePosition & {
markerStart?: string;
@@ -48,30 +53,48 @@ export type EdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle' |
targetHandleId?: string | null;
};
export type EdgeComponentProps<T = any> = Optional<
Omit<
EdgeProps<T>,
'source' | 'target' | 'sourceHandleId' | 'targetHandleId' | 'animated' | 'selected' | 'data'
>,
'id'
>;
export type BezierEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: BezierPathOptions;
/**
* Helper type for edge components that get exported by the library.
*/
export type EdgeComponentProps = EdgePosition & {
id?: EdgeProps['id'];
hidden?: EdgeProps['hidden'];
deletable?: EdgeProps['deletable'];
selectable?: EdgeProps['selectable'];
markerStart?: EdgeProps['markerStart'];
markerEnd?: EdgeProps['markerEnd'];
zIndex?: EdgeProps['zIndex'];
ariaLabel?: EdgeProps['ariaLabel'];
interactionWidth?: EdgeProps['interactionWidth'];
label?: EdgeProps['label'];
labelStyle?: EdgeProps['labelStyle'];
style?: EdgeProps['style'];
class?: EdgeProps['class'];
};
export type SmoothStepEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: SmoothStepPathOptions;
export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
pathOptions?: PathOptions;
};
export type StepEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: StepPathOptions;
};
/**
* BezierEdge component props
*/
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
export type StraightEdgeProps<T = any> = Omit<
EdgeComponentProps<T>,
'sourcePosition' | 'targetPosition'
>;
/**
* SmoothStepEdge component props
*/
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
/**
* StepEdge component props
*/
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/**
* StraightEdge component props
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;

View File

@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType, SvelteComponent } from 'svelte';
import type { NodeBase, NodeProps } from '@xyflow/system';
// @todo: currently the helper function only like Node from '@reactflow/core'
// we need a base node type or helpes that accept Node like types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* The node data structure that gets used for the nodes prop.
* @public
*/
export type Node<
NodeData = any,
NodeType extends string | undefined = string | undefined

View File

@@ -10,10 +10,68 @@ import {
import type { Edge, Node } from '$lib/types';
/**
* Test whether an object is useable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
*/
export const isNode = isNodeBase<Node>;
/**
* Test whether an object is useable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
*/
export const isEdge = isEdgeBase<Edge>;
/**
* 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<Node, Edge>;
/**
* 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<Node, Edge>;
/**
* 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<Edge>;
/**
* 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<Edge>;
/**
* 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<Node, Edge>;

View File

@@ -26,7 +26,6 @@ export type NodeBase<T = any, U extends string | undefined = string | undefined>
extent?: 'parent' | CoordinateExtent;
expandParent?: boolean;
ariaLabel?: string;
focusable?: boolean;
origin?: NodeOrigin;
handles?: NodeHandle[];
computed?: {

View File

@@ -69,6 +69,29 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva
}
}
/**
* Get a bezier path from source to target handle
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
* @param params.targetX - The x position of the target handle
* @param params.targetY - The y position of the target handle
* @param params.targetPosition - The position of the target handle (default: Position.Top)
* @param params.curvature - The curvature of the bezier edge
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
*/
export function getBezierPath({
sourceX,
sourceY,

View File

@@ -90,6 +90,14 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
);
};
/**
* 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 addEdgeBase = <EdgeType extends EdgeBase>(
edgeParams: EdgeType | Connection,
edges: EdgeType[]
@@ -129,6 +137,14 @@ export type UpdateEdgeOptions = {
shouldReplaceId?: boolean;
};
/**
* 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 updateEdgeBase = <EdgeType extends EdgeBase>(
oldEdge: EdgeType,
newConnection: Connection,

View File

@@ -190,6 +190,28 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
}
/**
* Get a smooth step path from source to target handle
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
* @param params.targetX - The x position of the target handle
* @param params.targetY - The y position of the target handle
* @param params.targetPosition - The position of the target handle (default: Position.Top)
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
*/
export function getSmoothStepPath({
sourceX,
sourceY,

View File

@@ -7,6 +7,26 @@ export type GetStraightPathParams = {
targetY: number;
};
/**
* Get a straight path from source to target handle
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.targetX - The x position of the target handle
* @param params.targetY - The y position of the target handle
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
*/
export function getStraightPath({
sourceX,
sourceY,

View File

@@ -8,8 +8,8 @@ import type {
NodeOrigin,
SnapGrid,
Transform,
Viewport,
} from '../types';
import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph';
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
@@ -19,8 +19,14 @@ export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: Coo
y: clamp(position.y, extent[0][1], extent[1][1]),
});
// returns a number between 0 and 1 that represents the velocity of the movement
// when the mouse is close to the edge of the canvas
/**
* Calculates the velocity of panning when the mouse is close to the edge of the canvas
* @internal
* @param value - One dimensional poition of the mouse (x or y)
* @param min - Minimal position on canvas before panning starts
* @param max - Maximal position on canvas before panning starts
* @returns - A number between 0 and 1 that represents the velocity of panning
*/
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
if (value < min) {
return clamp(Math.abs(value - min), 1, 50) / 50;
@@ -127,12 +133,12 @@ export const getPositionWithOrigin = ({
};
};
export function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition {
export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => {
return {
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
};
}
};
export const pointToRendererPoint = (
{ x, y }: XYPosition,
@@ -155,6 +161,22 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
};
};
/**
* Returns a viewport that encloses the given bounds with optional padding.
* @public
* @remarks You can determine bounds of nodes with {@link getNodesBounds} and {@link getBoundsOfRects}
* @param bounds - Bounds to fit inside viewport
* @param width - Width of the viewport
* @param height - Height of the viewport
* @param minZoom - Minimum zoom level of the resulting viewport
* @param maxZoom - Maximum zoom level of the resulting viewport
* @param padding - Optional padding around the bounds
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
* @example
* const { x, y, zoom } = getViewportForBounds(
{ x: 0, y: 0, width: 100, height: 100},
1200, 800, 0.5, 2);
*/
export const getViewportForBounds = (
bounds: Rect,
width: number,

View File

@@ -25,12 +25,34 @@ import {
} from '../types';
import { errorMessages } from '../constants';
/**
* Test whether an object is useable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
*/
export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any): element is EdgeType =>
'id' in element && 'source' in element && 'target' in element;
/**
* Test whether an object is useable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
*/
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
'id' in element && !('source' in element) && !('target' in element);
/**
* 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 getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: NodeType | { id: string },
nodes: NodeType[],
@@ -50,6 +72,14 @@ export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType e
return nodes.filter((n) => outgoerIds.has(n.id));
};
/**
* 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 getIncomersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: NodeType | { id: string },
nodes: NodeType[],
@@ -102,6 +132,14 @@ export const getNodePositionWithOrigin = (
};
};
/**
* Determines a bounding box that contains all given nodes in an array
* @public
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
* @param nodes - Nodes to calculate the bounds for
* @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
* @returns Bounding box enclosing all nodes
*/
export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0]): Rect => {
if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
@@ -167,6 +205,12 @@ export const getNodesInside = <NodeType extends NodeBase>(
return visibleNodes;
};
/**
* 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 getConnectedEdgesBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
nodes: NodeType[],
edges: EdgeType[]
@@ -281,9 +325,15 @@ export function calcNextPosition<NodeType extends NodeBase>(
};
}
// helper function to get arrays of nodes and edges that can be deleted
// you can pass in a list of nodes and edges that should be deleted
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
/**
* Pass in nodes & edges to delete, get arrays of nodes and edges that actually can be deleted
* @internal
* @param param.nodesToRemove - The nodes to remove
* @param param.edgesToRemove - The edges to remove
* @param param.nodes - All nodes
* @param param.edges - All edges
* @returns matchingNodes: nodes that can be deleted, matchingEdges: edges that can be deleted
*/
export function getElementsToRemove<NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>({
nodesToRemove,
edgesToRemove,