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
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
+10
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
```
+8
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()],
});
+25
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"
}
}
@@ -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);
@@ -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;
@@ -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
View File
@@ -0,0 +1 @@
/// <reference types="astro/client" />
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
@@ -66,7 +66,7 @@ const DnDFlow = () => {
return ( return (
<div className={styles.dndflow}> <div className={styles.dndflow}>
<ReactFlowProvider> <ReactFlowProvider nodes={initialNodes} edges={[]}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -77,7 +77,6 @@ const DnDFlow = () => {
onInit={onInit} onInit={onInit}
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
+3 -1
View File
@@ -17,9 +17,11 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec
const data = { label: `Node ${nodeId}` }; const data = { label: `Node ${nodeId}` };
const node = { const node = {
id: nodeId.toString(), id: nodeId.toString(),
style: { width: 50, fontSize: 11 }, style: { width: 50, height: 30, fontSize: 11 },
data, data,
position, position,
width: 50,
height: 30,
}; };
initialNodes.push(node); initialNodes.push(node);
@@ -204,9 +204,6 @@ const Subflow = () => {
onNodeDrag={onNodeDrag} onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example" className="react-flow-basic-example"
defaultViewport={defaultViewport}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false} onlyRenderVisibleElements={false}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView fitView
+1 -1
View File
@@ -7,7 +7,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "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:svelte": "turbo run dev --filter=svelte --filter=system",
"dev:react": "turbo run dev --filter=react", "dev:react": "turbo run dev --filter=react",
"build": "turbo run build", "build": "turbo run build",
@@ -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 cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
@@ -37,18 +37,9 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
position = 'bottom-left', position = 'bottom-left',
}) => { }) => {
const store = useStoreApi(); const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow); const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
const { zoomIn, zoomOut, fitView } = useReactFlow(); const { zoomIn, zoomOut, fitView } = useReactFlow();
useEffect(() => {
setIsVisible(true);
}, []);
if (!isVisible) {
return null;
}
const onZoomInHandler = () => { const onZoomInHandler = () => {
zoomIn(); zoomIn();
onZoomIn?.(); onZoomIn?.();
@@ -52,6 +52,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
disableKeyboardA11y, disableKeyboardA11y,
ariaLabel, ariaLabel,
rfId, rfId,
sizeWidth,
sizeHeight,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const store = useStoreApi(); const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
@@ -183,6 +185,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`, transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none', pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden', visibility: initialized ? 'visible' : 'hidden',
width: sizeWidth,
height: sizeHeight,
...style, ...style,
}} }}
data-id={id} data-id={id}
@@ -1,20 +1,40 @@
import { useRef, type FC, type PropsWithChildren } from 'react'; import { useRef, type ReactNode } from 'react';
import { type StoreApi } from 'zustand'; import { type StoreApi } from 'zustand';
import { UseBoundStoreWithEqualityFn } from 'zustand/traditional'; import { UseBoundStoreWithEqualityFn } from 'zustand/traditional';
import { Provider } from '../../contexts/RFStoreContext'; import { Provider } from '../../contexts/RFStoreContext';
import { createRFStore } from '../../store'; 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); const storeRef = useRef<UseBoundStoreWithEqualityFn<StoreApi<ReactFlowState>> | null>(null);
if (!storeRef.current) { 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>; return <Provider value={storeRef.current}>{children}</Provider>;
}; }
ReactFlowProvider.displayName = 'ReactFlowProvider'; ReactFlowProvider.displayName = 'ReactFlowProvider';
@@ -1,7 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { StoreApi } from 'zustand'; import { StoreApi } from 'zustand';
import { shallow } from 'zustand/shallow'; 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 { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types'; import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types';
@@ -201,7 +201,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('rfId', rfId, store.setState); useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState); useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState); useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState); useDirectStoreUpdater('onError', onError || devWarn, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState); useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState); useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState); useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
@@ -62,23 +62,13 @@ const EdgeRenderer = ({
onEdgeUpdateEnd, onEdgeUpdateEnd,
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { width, height, edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
if (!width) {
return null;
}
return ( return (
<> <>
{edgeTree.map(({ level, edges, isMaxLevel }) => ( {edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg <svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />} {isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g> <g>
{edges.map((edge) => { {edges.map((edge) => {
@@ -17,6 +17,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
const typesParsed = useMemo(() => { const typesParsed = useMemo(() => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes); const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) { if (shallow(typesKeysRef.current, typeKeys)) {
store.getState().onError?.('002', errorMessages['error002']()); store.getState().onError?.('002', errorMessages['error002']());
} }
@@ -98,6 +98,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
height: node.height ?? 0, height: node.height ?? 0,
origin: node.origin || props.nodeOrigin, origin: node.origin || props.nodeOrigin,
}); });
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
return ( return (
<NodeComponent <NodeComponent
@@ -105,6 +106,8 @@ const NodeRenderer = (props: NodeRendererProps) => {
id={node.id} id={node.id}
className={node.className} className={node.className}
style={node.style} style={node.style}
sizeWidth={node.size?.width}
sizeHeight={node.size?.height}
type={nodeType} type={nodeType}
data={node.data} data={node.data}
sourcePosition={node.sourcePosition || Position.Bottom} sourcePosition={node.sourcePosition || Position.Bottom}
@@ -131,7 +134,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
isParent={!!node[internalsSymbol]?.isParent} isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName} noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName} noPanClassName={props.noPanClassName}
initialized={!!node.width && !!node.height} initialized={initialized}
rfId={props.rfId} rfId={props.rfId}
disableKeyboardA11y={props.disableKeyboardA11y} disableKeyboardA11y={props.disableKeyboardA11y}
ariaLabel={node.ariaLabel} ariaLabel={node.ariaLabel}
@@ -1,9 +1,24 @@
import { useContext, type FC, type PropsWithChildren } from 'react'; import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/RFStoreContext'; import StoreContext from '../../contexts/RFStoreContext';
import ReactFlowProvider from '../../components/ReactFlowProvider'; 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); const isWrapped = useContext(StoreContext);
if (isWrapped) { if (isWrapped) {
@@ -12,8 +27,18 @@ const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
return <>{children}</>; return <>{children}</>;
} }
return <ReactFlowProvider>{children}</ReactFlowProvider>; return (
}; <ReactFlowProvider
initialNodes={nodes}
initialEdges={edges}
initialWidth={width}
initialHeight={height}
fitView={fitView}
>
{children}
</ReactFlowProvider>
);
}
Wrapper.displayName = 'ReactFlowWrapper'; Wrapper.displayName = 'ReactFlowWrapper';
@@ -166,6 +166,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
nodeDragThreshold, nodeDragThreshold,
viewport, viewport,
onViewportChange, onViewportChange,
width,
height,
...rest ...rest
}, },
ref ref
@@ -181,7 +183,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
data-testid="rf__wrapper" data-testid="rf__wrapper"
id={id} id={id}
> >
<Wrapper> <Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView}>
<GraphView <GraphView
onInit={onInit} onInit={onInit}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
+3 -3
View File
@@ -178,7 +178,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const getNodeRect = useCallback( 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] => { ): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect); const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : store.getState().nodes.find((n) => n.id === nodeOrRect.id); 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 overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0; 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 overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
}, },
[] []
); );
+45 -15
View File
@@ -11,7 +11,7 @@ import {
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { updateNodesAndEdgesSelections } from './utils'; import { updateNodesAndEdgesSelections } from './utils';
import initialState from './initialState'; import getInitialState from './initialState';
import type { import type {
ReactFlowState, ReactFlowState,
Node, Node,
@@ -24,10 +24,22 @@ import type {
FitViewOptions, FitViewOptions,
} from '../types'; } from '../types';
const createRFStore = () => const createRFStore = ({
nodes,
edges,
width,
height,
fitView,
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) =>
createWithEqualityFn<ReactFlowState>( createWithEqualityFn<ReactFlowState>(
(set, get) => ({ (set, get) => ({
...initialState, ...getInitialState({ nodes, edges, width, height, fitView }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get(); const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect }); const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
@@ -45,15 +57,27 @@ const createRFStore = () =>
const hasDefaultNodes = typeof nodes !== 'undefined'; const hasDefaultNodes = typeof nodes !== 'undefined';
const hasDefaultEdges = typeof edges !== 'undefined'; const hasDefaultEdges = typeof edges !== 'undefined';
const nextNodes = hasDefaultNodes const nextState: {
? updateNodes(nodes, [], { nodes?: Node[];
nodeOrigin: get().nodeOrigin, edges?: Edge[];
elevateNodesOnSelect: get().elevateNodesOnSelect, hasDefaultNodes: boolean;
}) hasDefaultEdges: boolean;
: []; } = {
const nextEdges = hasDefaultEdges ? edges : []; 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) => { updateNodeDimensions: (updates) => {
const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } = const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } =
@@ -263,9 +287,9 @@ const createRFStore = () =>
}, },
cancelConnection: () => cancelConnection: () =>
set({ set({
connectionStatus: initialState.connectionStatus, connectionStatus: null,
connectionStartHandle: initialState.connectionStartHandle, connectionStartHandle: null,
connectionEndHandle: initialState.connectionEndHandle, connectionEndHandle: null,
}), }),
updateConnection: (params) => { updateConnection: (params) => {
const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get(); const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get();
@@ -279,7 +303,13 @@ const createRFStore = () =>
set(currentConnection); 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 Object.is
); );
+90 -55
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 = { const getInitialState = ({
rfId: '1', nodes = [],
width: 0, edges = [],
height: 0, width,
transform: [0, 0, 1], height,
nodes: [], fitView,
edges: [], }: {
onNodesChange: null, nodes?: Node[];
onEdgesChange: null, edges?: Edge[];
hasDefaultNodes: false, width?: number;
hasDefaultEdges: false, height?: number;
panZoom: null, fitView?: boolean;
minZoom: 0.5, } = {}): ReactFlowStore => {
maxZoom: 2, const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
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,
snapGrid: [15, 15], let transform: Transform = [0, 0, 1];
snapToGrid: false,
nodesDraggable: true, if (fitView && width && height) {
nodesConnectable: true, const nodesWithDimensions = nextNodes.map((node) => ({
nodesFocusable: true, ...node,
edgesFocusable: true, width: node.size?.width,
edgesUpdatable: true, height: node.size?.height,
elementsSelectable: true, }));
elevateNodesOnSelect: true, const bounds = getRectOfNodes(nodesWithDimensions, [0, 0]);
fitViewOnInit: false, transform = getTransformForBounds(bounds, width, height, 0.5, 2, 0.1);
fitViewDone: false, }
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true,
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, snapGrid: [15, 15],
connectionEndHandle: null, snapToGrid: false,
connectionClickStartHandle: null,
connectOnClick: true,
ariaLiveMessage: '', nodesDraggable: true,
autoPanOnConnect: true, nodesConnectable: true,
autoPanOnNodeDrag: true, nodesFocusable: true,
connectionRadius: 20, edgesFocusable: true,
onError: devWarn, edgesUpdatable: true,
isValidConnection: undefined, 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;
+3 -1
View File
@@ -42,7 +42,7 @@ import type {
EdgeMouseHandler, EdgeMouseHandler,
} from '.'; } from '.';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & { export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
defaultNodes?: Node[]; defaultNodes?: Node[];
@@ -151,6 +151,8 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
onError?: OnError; onError?: OnError;
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
nodeDragThreshold?: number; nodeDragThreshold?: number;
width?: number;
height?: number;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+7 -7
View File
@@ -10,8 +10,8 @@ export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
}; };
export type DeleteElementsOptions = { export type DeleteElementsOptions = {
nodes?: (Partial<Node> & { id: Node['id'] })[]; nodes?: (Node | { id: Node['id'] })[];
edges?: (Partial<Edge> & { id: Edge['id'] })[]; edges?: (Edge | { id: Edge['id'] })[];
}; };
export namespace Instance { export namespace Instance {
@@ -33,18 +33,18 @@ export namespace Instance {
deletedEdges: Edge[]; deletedEdges: Edge[];
}; };
export type GetIntersectingNodes<NodeData> = ( export type GetIntersectingNodes<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect, node: Node<NodeData> | { id: Node['id'] } | Rect,
partially?: boolean, partially?: boolean,
nodes?: Node<NodeData>[] nodes?: Node<NodeData>[]
) => Node<NodeData>[]; ) => Node<NodeData>[];
export type IsNodeIntersecting<NodeData> = ( export type IsNodeIntersecting<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect, node: Node<NodeData> | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => boolean; ) => boolean;
export type getConnectedEdges = (id: string | (Partial<Node> & { id: Node['id'] })[]) => Edge[]; export type getConnectedEdges = (id: string | (Node | { id: Node['id'] })[]) => Edge[];
export type getIncomers = (node: string | (Partial<Node> & { id: Node['id'] })) => Node[]; export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[];
export type getOutgoers = (node: string | (Partial<Node> & { id: Node['id'] })) => Node[]; export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[];
} }
export type ReactFlowInstance<NodeData = any, EdgeData = any> = { export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
+2
View File
@@ -40,4 +40,6 @@ export type WrapNodeProps<NodeData = any> = Pick<
noPanClassName: string; noPanClassName: string;
rfId: string; rfId: string;
disableKeyboardA11y: boolean; disableKeyboardA11y: boolean;
sizeWidth?: number;
sizeHeight?: number;
}; };
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import cc from 'classcat';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer'; import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
import type { BaseEdgeProps } from './types'; import type { BaseEdgeProps } from './types';
@@ -21,7 +22,7 @@
<path <path
d={path} d={path}
{id} {id}
class="svelte-flow__edge-path" class={cc(['svelte-flow__edge-path', className])}
marker-start={markerStart} marker-start={markerStart}
marker-end={markerEnd} marker-end={markerEnd}
fill="none" fill="none"
@@ -39,6 +39,7 @@
export let targetPosition: NodeWrapperProps['targetPosition'] = undefined; export let targetPosition: NodeWrapperProps['targetPosition'] = undefined;
export let zIndex: NodeWrapperProps['zIndex']; export let zIndex: NodeWrapperProps['zIndex'];
export let dragHandle: NodeWrapperProps['dragHandle'] = undefined; export let dragHandle: NodeWrapperProps['dragHandle'] = undefined;
export let initialized: NodeWrapperProps['initialized'] = false;
let className: string = ''; let className: string = '';
export { className as class }; export { className as class };
@@ -157,6 +158,9 @@
class:parent={isParent} class:parent={isParent}
style:z-index={zIndex} style:z-index={zIndex}
style:transform="translate({positionOrigin?.x ?? 0}px, {positionOrigin?.y ?? 0}px)" 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} {style}
on:click={onSelectNodeHandler} on:click={onSelectNodeHandler}
on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })} on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })}
@@ -27,4 +27,5 @@ export type NodeWrapperProps = Pick<
isParent?: boolean; isParent?: boolean;
zIndex: number; zIndex: number;
node: Node; node: Node;
initialized: boolean;
}; };
@@ -2,8 +2,23 @@
import { onDestroy, setContext } from 'svelte'; import { onDestroy, setContext } from 'svelte';
import { createStore, key } from '$lib/store'; 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, { setContext(key, {
getStore: () => store getStore: () => store
@@ -0,0 +1,9 @@
import type { Edge, Node } from '$lib/types';
export type SvelteFlowProviderProps = {
initialNodes?: Node[];
initialEdges?: Edge[];
initialWidth?: number;
initialHeight?: number;
fitView?: boolean;
};
@@ -8,8 +8,6 @@
export let defaultEdgeOptions: DefaultEdgeOptions | undefined; export let defaultEdgeOptions: DefaultEdgeOptions | undefined;
const { const {
width,
height,
elementsSelectable, elementsSelectable,
edgeTree, edgeTree,
edges: { setDefaultOptions } edges: { setDefaultOptions }
@@ -21,7 +19,7 @@
</script> </script>
{#each $edgeTree as group (group.level)} {#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} {#if group.isMaxLevel} <MarkerDefinition />{/if}
<g> <g>
{#each group.edges as edge (edge.id)} {#each group.edges as edge (edge.id)}
@@ -35,8 +35,8 @@
{@const posOrigin = getPositionWithOrigin({ {@const posOrigin = getPositionWithOrigin({
x: node.positionAbsolute?.x ?? 0, x: node.positionAbsolute?.x ?? 0,
y: node.positionAbsolute?.y ?? 0, y: node.positionAbsolute?.y ?? 0,
width: node.width ?? 0, width: (node.size?.width || node.width) ?? 0,
height: node.height ?? 0, height: (node.size?.height || node.height) ?? 0,
origin: node.origin origin: node.origin
})} })}
<NodeWrapper <NodeWrapper
@@ -65,6 +65,7 @@
dragging={node.dragging} dragging={node.dragging}
zIndex={node[internalsSymbol]?.z ?? 0} zIndex={node[internalsSymbol]?.z ?? 0}
dragHandle={node.dragHandle} dragHandle={node.dragHandle}
initialized={(!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height)}
{resizeObserver} {resizeObserver}
on:nodeclick on:nodeclick
on:nodemouseenter on:nodemouseenter
@@ -16,6 +16,7 @@
import { key, useStore, createStoreContext } from '$lib/store'; import { key, useStore, createStoreContext } from '$lib/store';
import type { SvelteFlowProps } from './types'; import type { SvelteFlowProps } from './types';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils'; import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { get } from 'svelte/store';
type $$Props = SvelteFlowProps; type $$Props = SvelteFlowProps;
@@ -63,6 +64,8 @@
export let attributionPosition: $$Props['attributionPosition'] = undefined; export let attributionPosition: $$Props['attributionPosition'] = undefined;
export let proOptions: $$Props['proOptions'] = undefined; export let proOptions: $$Props['proOptions'] = undefined;
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined; export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let defaultMarkerColor = '#b1b1b7'; export let defaultMarkerColor = '#b1b1b7';
@@ -74,7 +77,9 @@
let clientWidth: number; let clientWidth: number;
let clientHeight: 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(() => { onMount(() => {
store.width.set(clientWidth); store.width.set(clientWidth);
@@ -72,6 +72,8 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
attributionPosition?: PanelPosition; attributionPosition?: PanelPosition;
proOptions?: ProOptions; proOptions?: ProOptions;
defaultEdgeOptions?: DefaultEdgeOptions; defaultEdgeOptions?: DefaultEdgeOptions;
width?: number;
height?: number;
class?: string; class?: string;
style?: string; style?: string;
+14 -14
View File
@@ -31,26 +31,26 @@ export function useSvelteFlow(): {
getViewport: () => Viewport; getViewport: () => Viewport;
fitView: (options?: FitViewOptions) => void; fitView: (options?: FitViewOptions) => void;
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
partially?: boolean, partially?: boolean,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => Node[]; ) => Node[];
isNodeIntersecting: ( isNodeIntersecting: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => boolean; ) => boolean;
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
deleteElements: ( deleteElements: (
nodesToRemove?: Partial<Node> & { id: string }[], nodesToRemove?: (Node | { id: Node['id'] })[],
edgesToRemove?: Partial<Edge> & { id: string }[] edgesToRemove?: (Edge | { id: Edge['id'] })[]
) => { deletedNodes: Node[]; deletedEdges: Edge[] }; ) => { deletedNodes: Node[]; deletedEdges: Edge[] };
screenToFlowCoordinate: (position: XYPosition) => XYPosition; screenToFlowCoordinate: (position: XYPosition) => XYPosition;
flowToScreenCoordinate: (position: XYPosition) => XYPosition; flowToScreenCoordinate: (position: XYPosition) => XYPosition;
viewport: Writable<Viewport>; viewport: Writable<Viewport>;
getConnectedEdges: (id: string | (Partial<Node> & { id: Node['id'] })[]) => Edge[]; getConnectedEdges: (id: string | (Node | { id: Node['id'] })[]) => Edge[];
getIncomers: (node: string | (Partial<Node> & { id: Node['id'] })) => Node[]; getIncomers: (node: string | Node | { id: Node['id'] }) => Node[];
getOutgoers: (node: string | (Partial<Node> & { id: Node['id'] })) => Node[]; getOutgoers: (node: string | Node | { id: Node['id'] }) => Node[];
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
} { } {
const { const {
@@ -70,7 +70,7 @@ export function useSvelteFlow(): {
} = useStore(); } = useStore();
const getNodeRect = ( const getNodeRect = (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect nodeOrRect: Node | { id: Node['id'] } | Rect
): [Rect | null, Node | null | undefined, boolean] => { ): [Rect | null, Node | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect); const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id); const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id);
@@ -137,7 +137,7 @@ export function useSvelteFlow(): {
); );
}, },
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
partially = true, partially = true,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => { ) => {
@@ -156,11 +156,11 @@ export function useSvelteFlow(): {
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
}); });
}, },
isNodeIntersecting: ( isNodeIntersecting: (
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect, nodeOrRect: Node | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially = true partially = true
) => { ) => {
@@ -173,11 +173,11 @@ export function useSvelteFlow(): {
const overlappingArea = getOverlappingArea(nodeRect, area); const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
}, },
deleteElements: ( deleteElements: (
nodesToRemove: Partial<Node> & { id: string }[] = [], nodesToRemove: (Node | { id: Node['id'] })[] = [],
edgesToRemove: Partial<Edge> & { id: string }[] = [] edgesToRemove: (Edge | { id: Edge['id'] })[] = []
) => { ) => {
const _nodes = get(nodes); const _nodes = get(nodes);
const _edges = get(edges); const _edges = get(edges);
+28 -4
View File
@@ -29,8 +29,20 @@ import { getDerivedConnectionProps } from './derived-connection-props';
export const key = Symbol(); export const key = Symbol();
export function createStore(): SvelteFlowStore { export function createStore({
const store = getInitialStore(); 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) { function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({ store.nodeTypes.set({
@@ -333,8 +345,20 @@ export function useStore(): SvelteFlowStore {
return store.getStore(); return store.getStore();
} }
export function createStoreContext() { export function createStoreContext({
const store = createStore(); 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, { setContext(key, {
getStore: () => store getStore: () => store
+82 -50
View File
@@ -14,7 +14,10 @@ import {
type NodeOrigin, type NodeOrigin,
type OnError, type OnError,
devWarn, devWarn,
type Viewport type Viewport,
updateNodes,
getRectOfNodes,
getTransformForBounds
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; 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 StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import StepEdge from '$lib/components/edges/StepEdge.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 { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props'; import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
@@ -43,51 +46,80 @@ export const initialEdgeTypes = {
step: StepEdge step: StepEdge
}; };
export const getInitialStore = () => ({ export const getInitialStore = ({
flowId: writable<string | null>(null), nodes = [],
nodes: createNodesStore([]), edges = [],
visibleNodes: readable<Node[]>([]), width,
edges: createEdgesStore([]), height,
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]), fitView
height: writable<number>(500), }: {
width: writable<number>(500), nodes?: Node[];
minZoom: writable<number>(0.5), edges?: Edge[];
maxZoom: writable<number>(2), width?: number;
nodeOrigin: writable<NodeOrigin>([0, 0]), height?: number;
nodeDragThreshold: writable<number>(0), fitView?: boolean;
nodeExtent: writable<CoordinateExtent>(infiniteExtent), }) => {
translateExtent: writable<CoordinateExtent>(infiniteExtent), const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
autoPanOnNodeDrag: writable<boolean>(true),
autoPanOnConnect: writable<boolean>(true), let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
fitViewOnInit: writable<boolean>(false),
fitViewOnInitDone: writable<boolean>(false), if (fitView && width && height) {
fitViewOptions: writable<FitViewOptions>(undefined), const nodesWithDimensions = nextNodes.map((node) => ({
panZoom: writable<PanZoomInstance | null>(null), ...node,
snapGrid: writable<SnapGrid | null>(null), width: node.size?.width,
dragging: writable<boolean>(false), height: node.size?.height
selectionRect: writable<SelectionRect | null>(null), }));
selectionKeyPressed: writable<boolean>(false), const bounds = getRectOfNodes(nodesWithDimensions, [0, 0]);
multiselectionKeyPressed: writable<boolean>(false), const transform = getTransformForBounds(bounds, width, height, 0.5, 2, 0.1);
deleteKeyPressed: writable<boolean>(false), viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
panActivationKeyPressed: writable<boolean>(false), }
selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial), return {
nodeTypes: writable<NodeTypes>(initialNodeTypes), flowId: writable<string | null>(null),
edgeTypes: writable<EdgeTypes>(initialEdgeTypes), nodes: createNodesStore(nextNodes),
viewport: writable<Viewport>({ x: 0, y: 0, zoom: 1 }), visibleNodes: readable<Node[]>([]),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict), edges: createEdgesStore(edges),
domNode: writable<HTMLDivElement | null>(null), edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
connection: readable<ConnectionProps>(initConnectionProps), height: writable<number>(500),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier), width: writable<number>(500),
connectionRadius: writable<number>(20), minZoom: writable<number>(0.5),
isValidConnection: writable<IsValidConnection>(() => true), maxZoom: writable<number>(2),
nodesDraggable: writable<boolean>(true), nodeOrigin: writable<NodeOrigin>([0, 0]),
nodesConnectable: writable<boolean>(true), nodeDragThreshold: writable<number>(0),
elementsSelectable: writable<boolean>(true), nodeExtent: writable<CoordinateExtent>(infiniteExtent),
selectNodesOnDrag: writable<boolean>(true), translateExtent: writable<CoordinateExtent>(infiniteExtent),
markers: readable<MarkerProps[]>([]), autoPanOnNodeDrag: writable<boolean>(true),
defaultMarkerColor: writable<string>('#b1b1b7'), autoPanOnConnect: writable<boolean>(true),
lib: readable<string>('svelte'), fitViewOnInit: writable<boolean>(false),
onlyRenderVisibleElements: writable<boolean>(false), fitViewOnInitDone: writable<boolean>(false),
onError: writable<OnError>(devWarn) 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)
};
};
+1 -1
View File
@@ -6,7 +6,7 @@ import {
type Writable, type Writable,
get get
} from 'svelte/store'; } 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'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -9,6 +9,7 @@ export function getVisibleNodes(store: SvelteFlowStoreState) {
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport], [store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport],
([nodes, onlyRenderVisibleElements, width, height, viewport]) => { ([nodes, onlyRenderVisibleElements, width, height, viewport]) => {
const transform: Transform = [viewport.x, viewport.y, viewport.zoom]; const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
return onlyRenderVisibleElements return onlyRenderVisibleElements
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true) ? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes; : nodes;
+2
View File
@@ -39,6 +39,8 @@ export type Edge<T = any> =
export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> & export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> &
EdgePosition & { EdgePosition & {
markerStart?: string;
markerEnd?: string;
sourceHandleId?: string | null; sourceHandleId?: string | null;
targetHandleId?: string | null; targetHandleId?: string | null;
}; };
+1 -1
View File
@@ -63,7 +63,7 @@ export type FitViewOptionsBase<NodeType extends NodeBase> = {
minZoom?: number; minZoom?: number;
maxZoom?: number; maxZoom?: number;
duration?: number; duration?: number;
nodes?: (Partial<NodeType> & { id: NodeType['id'] })[]; nodes?: (NodeType | { id: NodeType['id'] })[];
}; };
export type Viewport = { export type Viewport = {
+10 -6
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 HandleType = 'source' | 'target';
export type HandleElement = XYPosition & export type HandleElement = {
Dimensions & { id?: string | null;
id?: string | null; x: number;
position: Position; y: number;
}; width: number;
height: number;
position: Position;
type?: HandleType;
};
export type ConnectingHandle = { export type ConnectingHandle = {
nodeId: string; nodeId: string;
+8
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { internalsSymbol } from '../constants'; import { internalsSymbol } from '../constants';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
import { Optional } from '../utils/types';
// this is stuff that all nodes share independent of the framework // this is stuff that all nodes share independent of the framework
export type NodeBase<T = any, U extends string | undefined = string | undefined> = { 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; ariaLabel?: string;
focusable?: boolean; focusable?: boolean;
origin?: NodeOrigin; origin?: NodeOrigin;
handles?: NodeHandle[];
size?: {
width?: number;
height?: number;
};
// only used internally // only used internally
[internalsSymbol]?: { [internalsSymbol]?: {
@@ -89,3 +95,5 @@ export type NodeOrigin = [number, number];
export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void; export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void;
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
+38 -9
View File
@@ -1,6 +1,6 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; 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 { Position, Rect, XYPosition } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants'; import { errorMessages, internalsSymbol } from '../../constants';
import { HandleElement } from '../../types'; import { HandleElement } from '../../types';
@@ -16,10 +16,10 @@ export type GetEdgePositionParams = {
}; };
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null { export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getHandleDataByNode(params.sourceNode); const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getHandleDataByNode(params.targetNode); const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
if (!sourceIsValid || !targetIsValid) { if (!isSourceValid || !isTargetValid) {
return null; 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] { 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 = const isValid =
handleBounds && handleBounds &&
node?.width && nodeWidth &&
node?.height && nodeHeight &&
typeof node?.positionAbsolute?.x !== 'undefined' && typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined'; typeof node?.positionAbsolute?.y !== 'undefined';
@@ -73,8 +102,8 @@ function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, b
{ {
x: node?.positionAbsolute?.x || 0, x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0, y: node?.positionAbsolute?.y || 0,
width: node?.width || 0, width: nodeWidth || 0,
height: node?.height || 0, height: nodeHeight || 0,
}, },
handleBounds, handleBounds,
!!isValid, !!isValid,
+2 -2
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); ): element is NodeType => 'id' in element && !('source' in element) && !('target' in element);
export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>( export const getOutgoersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: Partial<NodeType> & { id: string }, node: NodeType | { id: string },
nodes: NodeType[], nodes: NodeType[],
edges: EdgeType[] edges: EdgeType[]
): NodeType[] => { ): 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>( export const getIncomersBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: Partial<NodeType> & { id: string }, node: NodeType | { id: string },
nodes: NodeType[], nodes: NodeType[],
edges: EdgeType[] edges: EdgeType[]
): NodeType[] => { ): NodeType[] => {
+1
View File
@@ -0,0 +1 @@
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
+4890 -1755
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -30,6 +30,7 @@ export const esmConfig = defineConfig({
output: { output: {
file: pkg.module, file: pkg.module,
format: 'esm', format: 'esm',
banner: '"use client"',
}, },
onwarn, onwarn,
plugins: [ plugins: [