Merge pull request #3482 from wbkd/feat/ssr
feat(react/svelte): add ssr support
This commit is contained in:
21
examples/astro-xyflow/.gitignore
vendored
Normal file
21
examples/astro-xyflow/.gitignore
vendored
Normal 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
examples/astro-xyflow/README.md
Normal file
10
examples/astro-xyflow/README.md
Normal 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
examples/astro-xyflow/astro.config.mjs
Normal file
8
examples/astro-xyflow/astro.config.mjs
Normal 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
examples/astro-xyflow/package.json
Normal file
25
examples/astro-xyflow/package.json
Normal 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
examples/astro-xyflow/public/.gitkeep
Normal file
0
examples/astro-xyflow/public/.gitkeep
Normal 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);
|
||||
130
examples/astro-xyflow/src/components/ReactFlowExample/index.tsx
Normal file
130
examples/astro-xyflow/src/components/ReactFlowExample/index.tsx
Normal 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;
|
||||
@@ -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
1
examples/astro-xyflow/src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
27
examples/astro-xyflow/src/pages/index.astro
Normal file
27
examples/astro-xyflow/src/pages/index.astro
Normal 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>
|
||||
7
examples/astro-xyflow/tsconfig.json
Normal file
7
examples/astro-xyflow/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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']());
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -40,4 +40,6 @@ export type WrapNodeProps<NodeData = any> = Pick<
|
||||
noPanClassName: string;
|
||||
rfId: string;
|
||||
disableKeyboardA11y: boolean;
|
||||
sizeWidth?: number;
|
||||
sizeHeight?: number;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -27,4 +27,5 @@ export type NodeWrapperProps = Pick<
|
||||
isParent?: boolean;
|
||||
zIndex: number;
|
||||
node: Node;
|
||||
initialized: boolean;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -72,6 +72,8 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
attributionPosition?: PanelPosition;
|
||||
proOptions?: ProOptions;
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
||||
class?: string;
|
||||
style?: string;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
1
packages/system/src/utils/types.ts
Normal file
1
packages/system/src/utils/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
6645
pnpm-lock.yaml
generated
6645
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ export const esmConfig = defineConfig({
|
||||
output: {
|
||||
file: pkg.module,
|
||||
format: 'esm',
|
||||
banner: '"use client"',
|
||||
},
|
||||
onwarn,
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user