Merge pull request #3482 from wbkd/feat/ssr

feat(react/svelte): add ssr support
This commit is contained in:
Moritz Klack
2023-10-31 14:15:14 +01:00
committed by GitHub
53 changed files with 5696 additions and 1973 deletions

21
examples/astro-xyflow/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

View File

@@ -0,0 +1,10 @@
# Astro examples
Tiny app for testing React Flow and Svelte Flow with Astro.
## Start local dev server
```sh
pnpm dev
```

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
integrations: [react(), svelte()],
});

View File

@@ -0,0 +1,25 @@
{
"name": "astro",
"type": "module",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^3.0.2",
"@astrojs/svelte": "^4.0.2",
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"@xyflow/react": "workspace:^",
"@xyflow/svelte": "workspace:^",
"astro": "^3.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"svelte": "^4.2.1"
}
}

View File

View File

@@ -0,0 +1,32 @@
import { memo, type FC, type CSSProperties } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
const sourceHandleStyleA: CSSProperties = { left: 50 };
const sourceHandleStyleB: CSSProperties = {
right: 50,
left: 'auto',
};
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
return (
<>
<Handle type="target" position={Position.Top} />
<div>
<div>
Label: <strong>{data.label}</strong>
</div>
<div>
Position:{' '}
<strong>
{xPos.toFixed(2)},{yPos.toFixed(2)}
</strong>
</div>
</div>
<Handle type="source" position={Position.Bottom} id="a" style={sourceHandleStyleA} />
<Handle type="source" position={Position.Bottom} id="b" style={sourceHandleStyleB} />
</>
);
};
export default memo(CustomNode);

View File

@@ -0,0 +1,130 @@
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
Background,
Controls,
type Connection,
type Edge,
type Node,
Position,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import '@xyflow/react/dist/style.css';
const nodeSize = {
width: 100,
height: 40,
};
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
size: nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
},
],
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
size: nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
width: 1,
height: 1,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
width: 1,
height: 1,
},
],
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
size: nodeSize,
handles: [
{
type: 'source',
position: Position.Bottom,
x: nodeSize.width * 0.5,
y: nodeSize.height,
width: 1,
height: 1,
},
{
type: 'target',
position: Position.Top,
x: nodeSize.width * 0.5,
y: 0,
width: 1,
height: 1,
},
],
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
type: 'custom',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3', animated: true },
];
const nodeTypes = {
custom: CustomNode,
};
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<div style={{ width: 700, height: 400 }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
width={700}
height={400}
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Flow;

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, Position, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([
{
id: '0',
position: { x: 0, y: 150 },
data: { label: 'Node 0' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'A',
position: { x: 250, y: 0 },
data: { label: 'A' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'B',
position: { x: 250, y: 150 },
data: { label: 'B' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
{
id: 'C',
position: { x: 250, y: 300 },
data: { label: 'C' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
size: {
width: 100,
height: 40,
},
handles: [
{ type: 'source', x: 100, y: 20, position: Position.Right },
{ type: 'target', x: 0, y: 20, position: Position.Left },
],
},
]);
const edges = writable<Edge[]>([
{ id: '0A', source: '0', target: 'A', animated: true },
{ id: '0B', source: '0', target: 'B', animated: true },
{ id: '0C', source: '0', target: 'C', animated: true },
]);
const defaultEdgeOptions = {
animated: true,
};
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow {nodes} {edges} fitView {defaultEdgeOptions} width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
</div>

1
examples/astro-xyflow/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="astro/client" />

View File

@@ -0,0 +1,27 @@
---
import ReactFlowApp from '../components/ReactFlowExample'
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro example for React Flow and Svelte Flow</title>
<style>
html, body {
margin:0;
font-family: sans-serif;
}
</style>
</head>
<body>
<h2>Svelte Flow</h2>
<SvelteFlowApp />
<h2>React Flow</h2>
<ReactFlowApp />
</body>
</html>

View File

@@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

View File

@@ -66,7 +66,7 @@ const DnDFlow = () => {
return (
<div className={styles.dndflow}>
<ReactFlowProvider>
<ReactFlowProvider nodes={initialNodes} edges={[]}>
<div className={styles.wrapper}>
<ReactFlow
nodes={nodes}
@@ -77,7 +77,6 @@ const DnDFlow = () => {
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
>
<Controls />
</ReactFlow>

View File

@@ -17,9 +17,11 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec
const data = { label: `Node ${nodeId}` };
const node = {
id: nodeId.toString(),
style: { width: 50, fontSize: 11 },
style: { width: 50, height: 30, fontSize: 11 },
data,
position,
width: 50,
height: 30,
};
initialNodes.push(node);

View File

@@ -204,9 +204,6 @@ const Subflow = () => {
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultViewport={defaultViewport}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
nodeTypes={nodeTypes}
fitView

View File

@@ -7,7 +7,7 @@
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "turbo run dev --parallel",
"dev": "turbo run dev --parallel --concurrency 12",
"dev:svelte": "turbo run dev --filter=svelte --filter=system",
"dev:react": "turbo run dev --filter=react",
"build": "turbo run build",

View File

@@ -1,4 +1,4 @@
import { memo, useEffect, useState, type FC, type PropsWithChildren } from 'react';
import { memo, type FC, type PropsWithChildren } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
@@ -37,18 +37,9 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
position = 'bottom-left',
}) => {
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
const { zoomIn, zoomOut, fitView } = useReactFlow();
useEffect(() => {
setIsVisible(true);
}, []);
if (!isVisible) {
return null;
}
const onZoomInHandler = () => {
zoomIn();
onZoomIn?.();

View File

@@ -52,6 +52,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
disableKeyboardA11y,
ariaLabel,
rfId,
sizeWidth,
sizeHeight,
}: WrapNodeProps) => {
const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null);
@@ -183,6 +185,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
width: sizeWidth,
height: sizeHeight,
...style,
}}
data-id={id}

View File

@@ -1,20 +1,40 @@
import { useRef, type FC, type PropsWithChildren } from 'react';
import { useRef, type ReactNode } from 'react';
import { type StoreApi } from 'zustand';
import { UseBoundStoreWithEqualityFn } from 'zustand/traditional';
import { Provider } from '../../contexts/RFStoreContext';
import { createRFStore } from '../../store';
import type { ReactFlowState } from '../../types';
import type { ReactFlowState, Node, Edge } from '../../types';
const ReactFlowProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
function ReactFlowProvider({
children,
initialNodes,
initialEdges,
initialWidth,
initialHeight,
fitView,
}: {
children: ReactNode;
initialNodes?: Node[];
initialEdges?: Edge[];
initialWidth?: number;
initialHeight?: number;
fitView?: boolean;
}) {
const storeRef = useRef<UseBoundStoreWithEqualityFn<StoreApi<ReactFlowState>> | null>(null);
if (!storeRef.current) {
storeRef.current = createRFStore();
storeRef.current = createRFStore({
nodes: initialNodes,
edges: initialEdges,
width: initialWidth,
height: initialHeight,
fitView,
});
}
return <Provider value={storeRef.current}>{children}</Provider>;
};
}
ReactFlowProvider.displayName = 'ReactFlowProvider';

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { StoreApi } from 'zustand';
import { shallow } from 'zustand/shallow';
import type { CoordinateExtent } from '@xyflow/system';
import { devWarn, type CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types';
@@ -201,7 +201,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState);
useDirectStoreUpdater('onError', onError || devWarn, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);

View File

@@ -62,23 +62,13 @@ const EdgeRenderer = ({
onEdgeUpdateEnd,
children,
}: EdgeRendererProps) => {
const { width, height, edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
if (!width) {
return null;
}
return (
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g>
{edges.map((edge) => {

View File

@@ -17,6 +17,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
const typesParsed = useMemo(() => {
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
store.getState().onError?.('002', errorMessages['error002']());
}

View File

@@ -98,6 +98,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
height: node.height ?? 0,
origin: node.origin || props.nodeOrigin,
});
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
return (
<NodeComponent
@@ -105,6 +106,8 @@ const NodeRenderer = (props: NodeRendererProps) => {
id={node.id}
className={node.className}
style={node.style}
sizeWidth={node.size?.width}
sizeHeight={node.size?.height}
type={nodeType}
data={node.data}
sourcePosition={node.sourcePosition || Position.Bottom}
@@ -131,7 +134,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
initialized={!!node.width && !!node.height}
initialized={initialized}
rfId={props.rfId}
disableKeyboardA11y={props.disableKeyboardA11y}
ariaLabel={node.ariaLabel}

View File

@@ -1,9 +1,24 @@
import { useContext, type FC, type PropsWithChildren } from 'react';
import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/RFStoreContext';
import ReactFlowProvider from '../../components/ReactFlowProvider';
import type { Node, Edge } from '../../types';
const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
function Wrapper({
children,
nodes,
edges,
width,
height,
fitView,
}: {
children: ReactNode;
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) {
const isWrapped = useContext(StoreContext);
if (isWrapped) {
@@ -12,8 +27,18 @@ const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
return <>{children}</>;
}
return <ReactFlowProvider>{children}</ReactFlowProvider>;
};
return (
<ReactFlowProvider
initialNodes={nodes}
initialEdges={edges}
initialWidth={width}
initialHeight={height}
fitView={fitView}
>
{children}
</ReactFlowProvider>
);
}
Wrapper.displayName = 'ReactFlowWrapper';

View File

@@ -166,6 +166,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
nodeDragThreshold,
viewport,
onViewportChange,
width,
height,
...rest
},
ref
@@ -181,7 +183,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
data-testid="rf__wrapper"
id={id}
>
<Wrapper>
<Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView}>
<GraphView
onInit={onInit}
onNodeClick={onNodeClick}

View File

@@ -178,7 +178,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const getNodeRect = useCallback(
(
nodeOrRect: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect
nodeOrRect: Node<NodeData> | { id: Node['id'] } | Rect
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : store.getState().nodes.find((n) => n.id === nodeOrRect.id);
@@ -211,7 +211,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
});
},
[]
@@ -228,7 +228,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
},
[]
);

View File

@@ -11,7 +11,7 @@ import {
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { updateNodesAndEdgesSelections } from './utils';
import initialState from './initialState';
import getInitialState from './initialState';
import type {
ReactFlowState,
Node,
@@ -24,10 +24,22 @@ import type {
FitViewOptions,
} from '../types';
const createRFStore = () =>
const createRFStore = ({
nodes,
edges,
width,
height,
fitView,
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) =>
createWithEqualityFn<ReactFlowState>(
(set, get) => ({
...initialState,
...getInitialState({ nodes, edges, width, height, fitView }),
setNodes: (nodes: Node[]) => {
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
@@ -45,15 +57,27 @@ const createRFStore = () =>
const hasDefaultNodes = typeof nodes !== 'undefined';
const hasDefaultEdges = typeof edges !== 'undefined';
const nextNodes = hasDefaultNodes
? updateNodes(nodes, [], {
nodeOrigin: get().nodeOrigin,
elevateNodesOnSelect: get().elevateNodesOnSelect,
})
: [];
const nextEdges = hasDefaultEdges ? edges : [];
const nextState: {
nodes?: Node[];
edges?: Edge[];
hasDefaultNodes: boolean;
hasDefaultEdges: boolean;
} = {
hasDefaultNodes,
hasDefaultEdges,
};
set({ nodes: nextNodes, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
if (hasDefaultNodes) {
nextState.nodes = updateNodes(nodes, [], {
nodeOrigin: get().nodeOrigin,
elevateNodesOnSelect: get().elevateNodesOnSelect,
});
}
if (hasDefaultEdges) {
nextState.edges = edges;
}
set(nextState);
},
updateNodeDimensions: (updates) => {
const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } =
@@ -263,9 +287,9 @@ const createRFStore = () =>
},
cancelConnection: () =>
set({
connectionStatus: initialState.connectionStatus,
connectionStartHandle: initialState.connectionStartHandle,
connectionEndHandle: initialState.connectionEndHandle,
connectionStatus: null,
connectionStartHandle: null,
connectionEndHandle: null,
}),
updateConnection: (params) => {
const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get();
@@ -279,7 +303,13 @@ const createRFStore = () =>
set(currentConnection);
},
reset: () => set({ ...initialState }),
reset: () => {
// @todo: what should we do about this? Do we still need it?
// if you are on a SPA with multiple flows, we want to make sure that the store gets resetted
// when you switch pages. Does this reset solves this? Currently it always gets called. This
// leads to an emtpy nodes array at the beginning.
// set({ ...getInitialState() });
},
}),
Object.is
);

View File

@@ -1,65 +1,100 @@
import { devWarn, infiniteExtent, ConnectionMode } from '@xyflow/system';
import {
infiniteExtent,
ConnectionMode,
updateNodes,
getRectOfNodes,
getTransformForBounds,
Transform,
} from '@xyflow/system';
import type { ReactFlowStore } from '../types';
import type { Edge, Node, ReactFlowStore } from '../types';
const initialState: ReactFlowStore = {
rfId: '1',
width: 0,
height: 0,
transform: [0, 0, 1],
nodes: [],
edges: [],
onNodesChange: null,
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
panZoom: null,
minZoom: 0.5,
maxZoom: 2,
translateExtent: infiniteExtent,
nodeExtent: infiniteExtent,
nodesSelectionActive: false,
userSelectionActive: false,
userSelectionRect: null,
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict,
domNode: null,
paneDragging: false,
noPanClassName: 'nopan',
nodeOrigin: [0, 0],
nodeDragThreshold: 0,
const getInitialState = ({
nodes = [],
edges = [],
width,
height,
fitView,
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
} = {}): ReactFlowStore => {
const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
snapGrid: [15, 15],
snapToGrid: false,
let transform: Transform = [0, 0, 1];
nodesDraggable: true,
nodesConnectable: true,
nodesFocusable: true,
edgesFocusable: true,
edgesUpdatable: true,
elementsSelectable: true,
elevateNodesOnSelect: true,
fitViewOnInit: false,
fitViewDone: false,
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true,
if (fitView && width && height) {
const nodesWithDimensions = nextNodes.map((node) => ({
...node,
width: node.size?.width,
height: node.size?.height,
}));
const bounds = getRectOfNodes(nodesWithDimensions, [0, 0]);
transform = getTransformForBounds(bounds, width, height, 0.5, 2, 0.1);
}
multiSelectionActive: false,
return {
rfId: '1',
width: 0,
height: 0,
transform,
nodes: nextNodes,
edges: edges,
onNodesChange: null,
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
panZoom: null,
minZoom: 0.5,
maxZoom: 2,
translateExtent: infiniteExtent,
nodeExtent: infiniteExtent,
nodesSelectionActive: false,
userSelectionActive: false,
userSelectionRect: null,
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict,
domNode: null,
paneDragging: false,
noPanClassName: 'nopan',
nodeOrigin: [0, 0],
nodeDragThreshold: 0,
connectionStartHandle: null,
connectionEndHandle: null,
connectionClickStartHandle: null,
connectOnClick: true,
snapGrid: [15, 15],
snapToGrid: false,
ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
connectionRadius: 20,
onError: devWarn,
isValidConnection: undefined,
nodesDraggable: true,
nodesConnectable: true,
nodesFocusable: true,
edgesFocusable: true,
edgesUpdatable: true,
elementsSelectable: true,
elevateNodesOnSelect: true,
fitViewOnInit: false,
fitViewDone: false,
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true,
lib: 'react',
multiSelectionActive: false,
connectionStartHandle: null,
connectionEndHandle: null,
connectionClickStartHandle: null,
connectOnClick: true,
ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
connectionRadius: 20,
onError: () => null,
isValidConnection: undefined,
lib: 'react',
};
};
export default initialState;
export default getInitialState;

View File

@@ -42,7 +42,7 @@ import type {
EdgeMouseHandler,
} from '.';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
@@ -151,6 +151,8 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
onError?: OnError;
isValidConnection?: IsValidConnection;
nodeDragThreshold?: number;
width?: number;
height?: number;
};
export type ReactFlowRefType = HTMLDivElement;

View File

@@ -10,8 +10,8 @@ export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
};
export type DeleteElementsOptions = {
nodes?: (Partial<Node> & { id: Node['id'] })[];
edges?: (Partial<Edge> & { id: Edge['id'] })[];
nodes?: (Node | { id: Node['id'] })[];
edges?: (Edge | { id: Edge['id'] })[];
};
export namespace Instance {
@@ -33,18 +33,18 @@ export namespace Instance {
deletedEdges: Edge[];
};
export type GetIntersectingNodes<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
node: Node<NodeData> | { id: Node['id'] } | Rect,
partially?: boolean,
nodes?: Node<NodeData>[]
) => Node<NodeData>[];
export type IsNodeIntersecting<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
node: Node<NodeData> | { id: Node['id'] } | Rect,
area: Rect,
partially?: boolean
) => boolean;
export type getConnectedEdges = (id: string | (Partial<Node> & { id: Node['id'] })[]) => Edge[];
export type getIncomers = (node: string | (Partial<Node> & { id: Node['id'] })) => Node[];
export type getOutgoers = (node: string | (Partial<Node> & { id: Node['id'] })) => Node[];
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 ReactFlowInstance<NodeData = any, EdgeData = any> = {

View File

@@ -40,4 +40,6 @@ export type WrapNodeProps<NodeData = any> = Pick<
noPanClassName: string;
rfId: string;
disableKeyboardA11y: boolean;
sizeWidth?: number;
sizeHeight?: number;
};

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import cc from 'classcat';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
import type { BaseEdgeProps } from './types';
@@ -21,7 +22,7 @@
<path
d={path}
{id}
class="svelte-flow__edge-path"
class={cc(['svelte-flow__edge-path', className])}
marker-start={markerStart}
marker-end={markerEnd}
fill="none"

View File

@@ -39,6 +39,7 @@
export let targetPosition: NodeWrapperProps['targetPosition'] = undefined;
export let zIndex: NodeWrapperProps['zIndex'];
export let dragHandle: NodeWrapperProps['dragHandle'] = undefined;
export let initialized: NodeWrapperProps['initialized'] = false;
let className: string = '';
export { className as class };
@@ -157,6 +158,9 @@
class:parent={isParent}
style:z-index={zIndex}
style:transform="translate({positionOrigin?.x ?? 0}px, {positionOrigin?.y ?? 0}px)"
style:width={node.size?.width && `${node.size?.width}px`}
style:height={node.size?.height && `${node.size?.height}px`}
style:visibility={initialized ? 'visible' : 'hidden'}
{style}
on:click={onSelectNodeHandler}
on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })}

View File

@@ -27,4 +27,5 @@ export type NodeWrapperProps = Pick<
isParent?: boolean;
zIndex: number;
node: Node;
initialized: boolean;
};

View File

@@ -2,8 +2,23 @@
import { onDestroy, setContext } from 'svelte';
import { createStore, key } from '$lib/store';
import type { SvelteFlowProviderProps } from './types';
const store = createStore();
type $$Props = SvelteFlowProviderProps;
export let initialNodes: $$Props['initialNodes'] = undefined;
export let initialEdges: $$Props['initialEdges'] = undefined;
export let initialWidth: $$Props['initialWidth'] = undefined;
export let initialHeight: $$Props['initialHeight'] = undefined;
export let fitView: $$Props['fitView'] = undefined;
const store = createStore({
nodes: initialNodes,
edges: initialEdges,
width: initialWidth,
height: initialHeight,
fitView
});
setContext(key, {
getStore: () => store

View File

@@ -0,0 +1,9 @@
import type { Edge, Node } from '$lib/types';
export type SvelteFlowProviderProps = {
initialNodes?: Node[];
initialEdges?: Edge[];
initialWidth?: number;
initialHeight?: number;
fitView?: boolean;
};

View File

@@ -8,8 +8,6 @@
export let defaultEdgeOptions: DefaultEdgeOptions | undefined;
const {
width,
height,
elementsSelectable,
edgeTree,
edges: { setDefaultOptions }
@@ -21,7 +19,7 @@
</script>
{#each $edgeTree as group (group.level)}
<svg width={$width} height={$height} style="z-index: {group.level}" class="svelte-flow__edges">
<svg style="z-index: {group.level}" class="svelte-flow__edges">
{#if group.isMaxLevel} <MarkerDefinition />{/if}
<g>
{#each group.edges as edge (edge.id)}

View File

@@ -35,8 +35,8 @@
{@const posOrigin = getPositionWithOrigin({
x: node.positionAbsolute?.x ?? 0,
y: node.positionAbsolute?.y ?? 0,
width: node.width ?? 0,
height: node.height ?? 0,
width: (node.size?.width || node.width) ?? 0,
height: (node.size?.height || node.height) ?? 0,
origin: node.origin
})}
<NodeWrapper
@@ -65,6 +65,7 @@
dragging={node.dragging}
zIndex={node[internalsSymbol]?.z ?? 0}
dragHandle={node.dragHandle}
initialized={(!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height)}
{resizeObserver}
on:nodeclick
on:nodemouseenter

View File

@@ -16,6 +16,7 @@
import { key, useStore, createStoreContext } from '$lib/store';
import type { SvelteFlowProps } from './types';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { get } from 'svelte/store';
type $$Props = SvelteFlowProps;
@@ -63,6 +64,8 @@
export let attributionPosition: $$Props['attributionPosition'] = undefined;
export let proOptions: $$Props['proOptions'] = undefined;
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let defaultMarkerColor = '#b1b1b7';
@@ -74,7 +77,9 @@
let clientWidth: number;
let clientHeight: number;
const store = hasContext(key) ? useStore() : createStoreContext();
const store = hasContext(key)
? useStore()
: createStoreContext({ nodes: get(nodes), edges: get(edges), width, height, fitView });
onMount(() => {
store.width.set(clientWidth);

View File

@@ -72,6 +72,8 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
attributionPosition?: PanelPosition;
proOptions?: ProOptions;
defaultEdgeOptions?: DefaultEdgeOptions;
width?: number;
height?: number;
class?: string;
style?: string;

View File

@@ -31,26 +31,26 @@ export function useSvelteFlow(): {
getViewport: () => Viewport;
fitView: (options?: FitViewOptions) => void;
getIntersectingNodes: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
nodeOrRect: Node | { id: Node['id'] } | Rect,
partially?: boolean,
nodesToIntersect?: Node[]
) => Node[];
isNodeIntersecting: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect,
partially?: boolean
) => boolean;
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
deleteElements: (
nodesToRemove?: Partial<Node> & { id: string }[],
edgesToRemove?: Partial<Edge> & { id: string }[]
nodesToRemove?: (Node | { id: Node['id'] })[],
edgesToRemove?: (Edge | { id: Edge['id'] })[]
) => { deletedNodes: Node[]; deletedEdges: Edge[] };
screenToFlowCoordinate: (position: XYPosition) => XYPosition;
flowToScreenCoordinate: (position: XYPosition) => XYPosition;
viewport: Writable<Viewport>;
getConnectedEdges: (id: string | (Partial<Node> & { id: Node['id'] })[]) => Edge[];
getIncomers: (node: string | (Partial<Node> & { id: Node['id'] })) => Node[];
getOutgoers: (node: string | (Partial<Node> & { id: Node['id'] })) => Node[];
getConnectedEdges: (id: string | (Node | { id: Node['id'] })[]) => Edge[];
getIncomers: (node: string | Node | { id: Node['id'] }) => Node[];
getOutgoers: (node: string | Node | { id: Node['id'] }) => Node[];
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
} {
const {
@@ -70,7 +70,7 @@ export function useSvelteFlow(): {
} = useStore();
const getNodeRect = (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect
nodeOrRect: Node | { id: Node['id'] } | Rect
): [Rect | null, Node | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id);
@@ -137,7 +137,7 @@ export function useSvelteFlow(): {
);
},
getIntersectingNodes: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
nodeOrRect: Node | { id: Node['id'] } | Rect,
partially = true,
nodesToIntersect?: Node[]
) => {
@@ -156,11 +156,11 @@ export function useSvelteFlow(): {
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
});
},
isNodeIntersecting: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect,
partially = true
) => {
@@ -173,11 +173,11 @@ export function useSvelteFlow(): {
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
},
deleteElements: (
nodesToRemove: Partial<Node> & { id: string }[] = [],
edgesToRemove: Partial<Edge> & { id: string }[] = []
nodesToRemove: (Node | { id: Node['id'] })[] = [],
edgesToRemove: (Edge | { id: Edge['id'] })[] = []
) => {
const _nodes = get(nodes);
const _edges = get(edges);

View File

@@ -29,8 +29,20 @@ import { getDerivedConnectionProps } from './derived-connection-props';
export const key = Symbol();
export function createStore(): SvelteFlowStore {
const store = getInitialStore();
export function createStore({
nodes,
edges,
width,
height,
fitView: fitViewOnCreate
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}): SvelteFlowStore {
const store = getInitialStore({ nodes, edges, width, height, fitView: fitViewOnCreate });
function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({
@@ -333,8 +345,20 @@ export function useStore(): SvelteFlowStore {
return store.getStore();
}
export function createStoreContext() {
const store = createStore();
export function createStoreContext({
nodes,
edges,
width,
height,
fitView
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) {
const store = createStore({ nodes, edges, width, height, fitView });
setContext(key, {
getStore: () => store

View File

@@ -14,7 +14,10 @@ import {
type NodeOrigin,
type OnError,
devWarn,
type Viewport
type Viewport,
updateNodes,
getRectOfNodes,
getTransformForBounds
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -25,7 +28,7 @@ import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import StepEdge from '$lib/components/edges/StepEdge.svelte';
import type { NodeTypes, EdgeTypes, EdgeLayouted, Node, FitViewOptions } from '$lib/types';
import type { NodeTypes, EdgeTypes, EdgeLayouted, Node, Edge, FitViewOptions } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
@@ -43,51 +46,80 @@ export const initialEdgeTypes = {
step: StepEdge
};
export const getInitialStore = () => ({
flowId: writable<string | null>(null),
nodes: createNodesStore([]),
visibleNodes: readable<Node[]>([]),
edges: createEdgesStore([]),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
height: writable<number>(500),
width: writable<number>(500),
minZoom: writable<number>(0.5),
maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]),
nodeDragThreshold: writable<number>(0),
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent),
autoPanOnNodeDrag: writable<boolean>(true),
autoPanOnConnect: writable<boolean>(true),
fitViewOnInit: writable<boolean>(false),
fitViewOnInitDone: writable<boolean>(false),
fitViewOptions: writable<FitViewOptions>(undefined),
panZoom: writable<PanZoomInstance | null>(null),
snapGrid: writable<SnapGrid | null>(null),
dragging: writable<boolean>(false),
selectionRect: writable<SelectionRect | null>(null),
selectionKeyPressed: writable<boolean>(false),
multiselectionKeyPressed: writable<boolean>(false),
deleteKeyPressed: writable<boolean>(false),
panActivationKeyPressed: writable<boolean>(false),
selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
nodeTypes: writable<NodeTypes>(initialNodeTypes),
edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
viewport: writable<Viewport>({ x: 0, y: 0, zoom: 1 }),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null),
connection: readable<ConnectionProps>(initConnectionProps),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
connectionRadius: writable<number>(20),
isValidConnection: writable<IsValidConnection>(() => true),
nodesDraggable: writable<boolean>(true),
nodesConnectable: writable<boolean>(true),
elementsSelectable: writable<boolean>(true),
selectNodesOnDrag: writable<boolean>(true),
markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7'),
lib: readable<string>('svelte'),
onlyRenderVisibleElements: writable<boolean>(false),
onError: writable<OnError>(devWarn)
});
export const getInitialStore = ({
nodes = [],
edges = [],
width,
height,
fitView
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) => {
const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
if (fitView && width && height) {
const nodesWithDimensions = nextNodes.map((node) => ({
...node,
width: node.size?.width,
height: node.size?.height
}));
const bounds = getRectOfNodes(nodesWithDimensions, [0, 0]);
const transform = getTransformForBounds(bounds, width, height, 0.5, 2, 0.1);
viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
}
return {
flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes),
visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
height: writable<number>(500),
width: writable<number>(500),
minZoom: writable<number>(0.5),
maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]),
nodeDragThreshold: writable<number>(0),
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent),
autoPanOnNodeDrag: writable<boolean>(true),
autoPanOnConnect: writable<boolean>(true),
fitViewOnInit: writable<boolean>(false),
fitViewOnInitDone: writable<boolean>(false),
fitViewOptions: writable<FitViewOptions>(undefined),
panZoom: writable<PanZoomInstance | null>(null),
snapGrid: writable<SnapGrid | null>(null),
dragging: writable<boolean>(false),
selectionRect: writable<SelectionRect | null>(null),
selectionKeyPressed: writable<boolean>(false),
multiselectionKeyPressed: writable<boolean>(false),
deleteKeyPressed: writable<boolean>(false),
panActivationKeyPressed: writable<boolean>(false),
selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
nodeTypes: writable<NodeTypes>(initialNodeTypes),
edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
viewport: writable<Viewport>(viewport),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null),
connection: readable<ConnectionProps>(initConnectionProps),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
connectionRadius: writable<number>(20),
isValidConnection: writable<IsValidConnection>(() => true),
nodesDraggable: writable<boolean>(true),
nodesConnectable: writable<boolean>(true),
elementsSelectable: writable<boolean>(true),
selectNodesOnDrag: writable<boolean>(true),
markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7'),
lib: readable<string>('svelte'),
onlyRenderVisibleElements: writable<boolean>(false),
onError: writable<OnError>(devWarn)
};
};

View File

@@ -6,7 +6,7 @@ import {
type Writable,
get
} from 'svelte/store';
import { updateNodes, type Transform, type Viewport, type PanZoomInstance } from '@xyflow/system';
import { updateNodes, type Viewport, type PanZoomInstance } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';

View File

@@ -9,6 +9,7 @@ export function getVisibleNodes(store: SvelteFlowStoreState) {
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport],
([nodes, onlyRenderVisibleElements, width, height, viewport]) => {
const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
return onlyRenderVisibleElements
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes;

View File

@@ -39,6 +39,8 @@ export type Edge<T = any> =
export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> &
EdgePosition & {
markerStart?: string;
markerEnd?: string;
sourceHandleId?: string | null;
targetHandleId?: string | null;
};

View File

@@ -63,7 +63,7 @@ export type FitViewOptionsBase<NodeType extends NodeBase> = {
minZoom?: number;
maxZoom?: number;
duration?: number;
nodes?: (Partial<NodeType> & { id: NodeType['id'] })[];
nodes?: (NodeType | { id: NodeType['id'] })[];
};
export type Viewport = {

View File

@@ -1,12 +1,16 @@
import type { XYPosition, Position, Dimensions, OnConnect, IsValidConnection } from '.';
import type { Position, OnConnect, IsValidConnection } from '.';
export type HandleType = 'source' | 'target';
export type HandleElement = XYPosition &
Dimensions & {
id?: string | null;
position: Position;
};
export type HandleElement = {
id?: string | null;
x: number;
y: number;
width: number;
height: number;
position: Position;
type?: HandleType;
};
export type ConnectingHandle = {
nodeId: string;

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
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
export type NodeBase<T = any, U extends string | undefined = string | undefined> = {
@@ -28,6 +29,11 @@ export type NodeBase<T = any, U extends string | undefined = string | undefined>
ariaLabel?: string;
focusable?: boolean;
origin?: NodeOrigin;
handles?: NodeHandle[];
size?: {
width?: number;
height?: number;
};
// only used internally
[internalsSymbol]?: {
@@ -89,3 +95,5 @@ export type NodeOrigin = [number, number];
export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void;
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;

View File

@@ -1,6 +1,6 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandleBounds } from '../../types/nodes';
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes';
import { Position, Rect, XYPosition } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants';
import { HandleElement } from '../../types';
@@ -16,10 +16,10 @@ export type GetEdgePositionParams = {
};
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getHandleDataByNode(params.sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getHandleDataByNode(params.targetNode);
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
if (!sourceIsValid || !targetIsValid) {
if (!isSourceValid || !isTargetValid) {
return null;
}
@@ -59,13 +59,42 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
};
}
function toHandleBounds(handles?: NodeHandle[]) {
if (!handles) {
return null;
}
return handles.reduce<NodeHandleBounds>(
(res, item) => {
item.width = item.width || 1;
item.height = item.height || 1;
if (item.type === 'source') {
res.source?.push(item as HandleElement);
}
if (item.type === 'target') {
res.target?.push(item as HandleElement);
}
return res;
},
{
source: [],
target: [],
}
);
}
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
const nodeWidth = node?.width || node?.size?.width;
const nodeHeight = node?.height || node?.size?.height;
const isValid =
handleBounds &&
node?.width &&
node?.height &&
nodeWidth &&
nodeHeight &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
@@ -73,8 +102,8 @@ function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, b
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
width: nodeWidth || 0,
height: nodeHeight || 0,
},
handleBounds,
!!isValid,

View File

@@ -35,7 +35,7 @@ export const isNodeBase = <NodeType extends NodeBase = NodeBase, EdgeType extend
): element is NodeType => 'id' in element && !('source' in element) && !('target' in element);
export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: Partial<NodeType> & { id: string },
node: NodeType | { id: string },
nodes: NodeType[],
edges: EdgeType[]
): NodeType[] => {
@@ -54,7 +54,7 @@ export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType e
};
export const getIncomersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: Partial<NodeType> & { id: string },
node: NodeType | { id: string },
nodes: NodeType[],
edges: EdgeType[]
): NodeType[] => {

View File

@@ -0,0 +1 @@
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;

6645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ export const esmConfig = defineConfig({
output: {
file: pkg.module,
format: 'esm',
banner: '"use client"',
},
onwarn,
plugins: [