feat(svelte): add subflows

This commit is contained in:
moklick
2023-03-15 18:34:07 +01:00
parent 9e961809ee
commit 77d14f9ad0
15 changed files with 347 additions and 84 deletions

View File

@@ -89,14 +89,16 @@ export function calcNextPosition(
if (node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
const parentOrigin = parent?.origin || nodeOrigin;
const currNodeOrigin = node.origin || nodeOrigin;
const { x: parentX, y: parentY } = getNodePositionWithOrigin(parent, parentOrigin).positionAbsolute;
currentExtent =
parent && isNumeric(parentX) && isNumeric(parentY) && isNumeric(parent.width) && isNumeric(parent.height)
? [
[parentX + node.width * parentOrigin[0], parentY + node.height * parentOrigin[1]],
[parentX + node.width * currNodeOrigin[0], parentY + node.height * currNodeOrigin[1]],
[
parentX + parent.width - node.width + node.width * parentOrigin[0],
parentY + parent.height - node.height + node.height * parentOrigin[1],
parentX + parent.width - node.width + node.width * currNodeOrigin[0],
parentY + parent.height - node.height + node.height * currNodeOrigin[1],
],
]
: currentExtent;

View File

@@ -5,6 +5,7 @@
'drag-n-drop',
'overview',
'stress',
'subflows',
'usesvelteflow',
'validation'
];

View File

@@ -1,10 +1,9 @@
import { get, type Writable } from 'svelte/store';
import { get } from 'svelte/store';
import { drag as d3Drag, type D3DragEvent, type SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import type { XYPosition, CoordinateExtent, Transform } from '@reactflow/system';
import type { XYPosition, CoordinateExtent } from '@reactflow/system';
import { getDragItems, hasSelector, calcNextPosition } from './utils';
import type { Node } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
export type UseDragData = { dx: number; dy: number };

View File

@@ -1,5 +1,11 @@
import type { CoordinateExtent, NodeDragItem, NodeOrigin, XYPosition } from '@reactflow/system';
import { clampPosition, isNumeric } from '@reactflow/utils';
import {
errorMessages,
type CoordinateExtent,
type NodeDragItem,
type NodeOrigin,
type XYPosition
} from '@reactflow/system';
import { clampPosition, devWarn, getNodePositionWithOrigin, isNumeric } from '@reactflow/utils';
import type { Node } from '$lib/types';
@@ -65,16 +71,28 @@ export function calcNextPosition(
node: NodeDragItem | Node,
nextPosition: XYPosition,
nodes: Node[],
nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0]
nodeExtent?: CoordinateExtent
): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
const parentOrigin = parent.origin || nodeOrigin;
const { x: parentX, y: parentY } = parent.positionAbsolute!;
const parent = nodes.find((n) => n.id === node.parentNode);
const parentOrigin = parent?.origin || [0, 0];
const nodeOrigin = node.origin || [0, 0];
const { x: parentX, y: parentY } = getNodePositionWithOrigin(
parent,
parentOrigin
).positionAbsolute;
console.log({
parentX,
parentY,
parentW: parent?.width,
parentH: parent?.height,
nodeW: node.width,
nodeH: node.height,
parentOrigin: parentOrigin[0]
});
currentExtent =
parent &&
isNumeric(parentX) &&
@@ -82,19 +100,23 @@ export function calcNextPosition(
isNumeric(parent.width) &&
isNumeric(parent.height)
? [
[parentX + node.width * parentOrigin[0], parentY + node.height * parentOrigin[1]],
[parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]],
[
parentX + parent.width! - node.width + node.width * parentOrigin[0],
parentY + parent.height! - node.height + node.height * parentOrigin[1]
parentX + parent.width - node.width + node.width * nodeOrigin[0],
parentY + parent.height - node.height + node.height * nodeOrigin[1]
]
]
: currentExtent;
} else {
devWarn('005', errorMessages['005']());
currentExtent = nodeExtent;
}
} else if (node.extent && node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
const { x: parentX, y: parentY } = parent.positionAbsolute!;
const parent = nodes.find((n) => n.id === node.parentNode);
const { x: parentX, y: parentY } = getNodePositionWithOrigin(
parent,
parent?.origin || [0, 0]
).positionAbsolute;
currentExtent = [
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
[node.extent[1][0] + parentX, node.extent[1][1] + parentY]
@@ -104,8 +126,8 @@ export function calcNextPosition(
let parentPosition = { x: 0, y: 0 };
if (node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
parentPosition = parent.positionAbsolute!;
const parent = nodes.find((n) => n.id === node.parentNode);
parentPosition = getNodePositionWithOrigin(parent, parent?.origin || [0, 0]).positionAbsolute;
}
const positionAbsolute = currentExtent

View File

@@ -22,11 +22,11 @@
export let width: NodeWrapperProps['width'] = undefined;
export let height: NodeWrapperProps['height'] = undefined;
export let type: NodeWrapperProps['type'] = 'default';
export let isParent: NodeWrapperProps['isParent'] = false;
export let positionAbsolute: NodeWrapperProps['positionAbsolute'] = undefined;
export let positionOrigin: NodeWrapperProps['positionOrigin'] = undefined;
export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined;
export let targetPosition: NodeWrapperProps['targetPosition'] = undefined;
let className: string = '';
export { className as class };
@@ -77,12 +77,13 @@
use:drag={{ nodeId: id, nodes, snapGrid, transform, updateNodePositions }}
bind:this={nodeRef}
data-id={id}
class={cc(['svelte-flow__node', `svelte-flow__node-${type}`, className])}
class={cc(['svelte-flow__node', `svelte-flow__node-${type || 'default'}`, className])}
class:initializing={!width && !height}
class:dragging
class:selected
class:draggable
class:connectable
class:parent={isParent}
style:transform={`translate(${positionOrigin?.x ?? 0}px, ${positionOrigin?.y ?? 0}px)`}
{style}
on:click={onSelectNodeHandler}

View File

@@ -21,4 +21,5 @@ export type NodeWrapperProps = Pick<
positionOrigin?: XYPosition;
'on:nodeclick'?: (event: MouseEvent) => void;
resizeObserver?: ResizeObserver | null;
isParent?: boolean;
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { internalsSymbol } from '@reactflow/system';
import { getPositionWithOrigin } from '@reactflow/utils';
import { NodeWrapper } from '$lib/components/NodeWrapper';
@@ -41,6 +42,7 @@
connectable={node.connectable || node.connectable === undefined}
positionAbsolute={node.positionAbsolute}
positionOrigin={posOrigin}
isParent={!!node[internalsSymbol]?.isParent}
width={node.width}
height={node.height}
style={node.style}

View File

@@ -8,55 +8,55 @@ import type { SvelteFlowStoreState } from './types';
export function getEdgesLayouted(store: SvelteFlowStoreState) {
return derived([store.edges, store.nodes], ([edges, nodes]) => {
return edges
.map((edge) => {
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode);
return edges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode);
if (!sourceIsValid || !targetIsValid) {
return null;
}
if (!sourceIsValid || !targetIsValid) {
return res;
}
const edgeType = edge.type || 'default';
const edgeType = edge.type || 'default';
const targetNodeHandles = targetHandleBounds!.target;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const targetNodeHandles = targetHandleBounds!.target;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
return null;
}
if (!sourceHandle || !targetHandle) {
return res;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
// we nee to do this to match the types
const sourceHandleId = edge.sourceHandle;
const targetHandleId = edge.targetHandle;
// we nee to do this to match the types
const sourceHandleId = edge.sourceHandle;
const targetHandleId = edge.targetHandle;
return {
...edge,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
sourceHandleId,
targetHandleId
};
})
.filter((e) => e !== null) as EdgeLayouted[];
res.push({
...edge,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
sourceHandleId,
targetHandleId
} as EdgeLayouted);
return res;
}, []);
});
}

View File

@@ -19,7 +19,7 @@ import {
} from '@reactflow/utils';
import { addEdge as addEdgeUtil } from '$lib/utils';
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData, FitViewOptions } from '$lib/types';
import { getEdgesLayouted } from './edges-layouted';
import { getConnectionPath } from './connection-path';
import {
@@ -102,15 +102,20 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
);
if (doUpdate) {
node[internalsSymbol] = {
...node[internalsSymbol],
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin)
const newNode = {
...node,
width: dimensions.width,
height: dimensions.height,
[internalsSymbol]: {
...node[internalsSymbol],
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin)
}
}
};
node.width = dimensions.width;
node.height = dimensions.height;
return newNode;
}
}
@@ -121,7 +126,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
const fitViewOnInitDone =
get(store.fitViewOnInitDone) ||
(get(store.fitViewOnInit) && !!d3Zoom && !!d3Selection && fitView());
(get(store.fitViewOnInit) && !!d3Zoom && !!d3Selection && fitView({ nodes: nextNodes }));
store.fitViewOnInitDone.set(fitViewOnInitDone);
store.nodes.set(nextNodes);
@@ -162,16 +167,18 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
}
}
function fitView() {
function fitView(options?: FitViewOptions) {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (!d3Zoom || !d3Selection) {
return false;
}
const fitViewNodes = options?.nodes || get(store.nodes);
return fitViewUtil(
{
nodes: get(store.nodes),
nodes: fitViewNodes as Node[],
width: get(store.width),
height: get(store.height),
minZoom: 0.2,

View File

@@ -7,7 +7,7 @@ import type {
} from '@reactflow/system';
import type { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes } from '$lib/types';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/types';
import type { Writable } from 'svelte/store';
export type SvelteFlowStoreActions = {
@@ -18,7 +18,7 @@ export type SvelteFlowStoreActions = {
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
fitView: (options?: FitViewOptions) => boolean;
updateNodePositions: (
nodeDragItems: NodeDragItem[],
positionChanged?: boolean,

View File

@@ -1,3 +1,4 @@
import { writable, type Writable } from 'svelte/store';
import {
isNodeBase,
isEdgeBase,
@@ -5,10 +6,12 @@ import {
getOutgoersBase,
getIncomersBase,
updateEdgeBase,
getConnectedEdgesBase
getConnectedEdgesBase,
isNumeric,
getNodePositionWithOrigin
} from '@reactflow/utils';
import { internalsSymbol, type XYZPosition } from '@reactflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
import { writable, type Writable } from 'svelte/store';
export const isNode = isNodeBase<Node, Edge>;
export const isEdge = isEdgeBase<Node, Edge>;
@@ -27,11 +30,53 @@ export const createNodes = (
let defaults = defaultOptions || {};
const _set: typeof set = (nds: Node[]) => {
const nextNodes = defaults ? nds.map((node) => ({ ...defaults, ...node })) : nds;
// @todo calculate absolute position based on parent / child relation
const nextNodesLayouted = nextNodes.map((n) => ({ ...n, positionAbsolute: n.position }));
const parentNodes: Record<string, boolean> = {};
value = nextNodesLayouted;
const nextNodes = nds.map((n) => {
const node: Node = { ...defaults, ...n, positionAbsolute: n.position };
const z = (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? 1 : 0);
if (node.parentNode) {
parentNodes[node.parentNode] = true;
}
Object.defineProperty(node, internalsSymbol, {
value: {
handleBounds: node?.[internalsSymbol]?.handleBounds,
z
}
});
return node;
});
const nodesWithPositions = nextNodes.map((node) => {
if (node.parentNode && !parentNodes[node.parentNode]) {
throw new Error(`Parent node ${node.parentNode} not found`);
}
if (node.parentNode || parentNodes?.[node.id]) {
const { x, y, z } = calculateXYZPosition(node, nextNodes, {
...node.position,
z: node[internalsSymbol]?.z ?? 0
});
node.positionAbsolute = {
x,
y
};
node[internalsSymbol]!.z = z;
if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true;
}
}
return node;
});
value = nodesWithPositions;
set(value);
};
@@ -81,3 +126,20 @@ export const createEdges = (
setDefaultOptions
};
};
function calculateXYZPosition(node: Node, nodes: Node[], result: XYZPosition): XYZPosition {
if (!node.parentNode) {
return result;
}
const parentNode = nodes.find((n) => n.id === node.parentNode)!;
const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin);
return calculateXYZPosition(parentNode, nodes, {
x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y,
z:
(parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0)
? parentNode[internalsSymbol]?.z ?? 0
: result.z ?? 0
});
}

View File

@@ -0,0 +1,141 @@
<script lang="ts">
import SvelteFlow, {
SvelteFlowProvider,
Controls,
Background,
BackgroundVariant,
Minimap,
createNodes,
createEdges,
type NodeTypes,
} from '../../lib/index';
import { DebugNode } from './DebugNode';
const nodeTypes: NodeTypes = {
default: DebugNode,
};
const nodes = createNodes([
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
origin: [0.5, 0.5],
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 100, y: 200 },
style: "width:500px; height:300px;"
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
parentNode: '4',
extent: [
[0, 0],
[100, 100],
],
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 100, y: 60 },
style: "width: 300px; height: 200px;",
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 40, y: 20 },
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 20, y: 100 },
parentNode: '4b',
},
{
id: '5',
type: 'group',
data: { label: 'Node 5' },
position: { x: 650, y: 250 },
style: "width: 400px; height: 150px",
zIndex: 1000,
},
{
id: '5a',
data: { label: 'Node 5a' },
position: { x: 0, y: 0 },
parentNode: '5',
extent: 'parent',
},
{
id: '5b',
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
parentNode: '5',
expandParent: true,
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
}
]);
const edges = createEdges([
{
id: 'e1-2',
source: '1',
target: '2',
// markerEnd: {
// type: MarkerType.Arrow,
// strokeWidth: 2,
// width: 15,
// height: 15,
// color: '#f00',
// },
},
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
], { animated: true });
$: {
console.log($nodes)
}
</script>
<SvelteFlowProvider
{nodes}
{edges}
>
<SvelteFlow
{nodeTypes}
fitView
minZoom={0.1}
maxZoom={2.5}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<Minimap />
</SvelteFlow>
</SvelteFlowProvider>
<style>
:global(.svelte-flow .svelte-flow__node.parent) {
background-color: rgba(220,220,255,0.4)
}
</style>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { Handle, Position } from '../../../lib/index';
export let id: string;
export let xPos: number = 0;
export let yPos: number = 0;
export let zIndex: number = 0;
</script>
<Handle type="target" position={Position.Top} />
<div>{id}</div>
<div>
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
</div>
<Handle type="source" position={Position.Bottom} />
<style>
.custom {
background: #ffcc00;
padding: 10px;
}
</style>

View File

@@ -0,0 +1 @@
export { default as DebugNode } from './DebugNode.svelte';

View File

@@ -80,6 +80,7 @@ export type NodeDragItem = {
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
dragging?: boolean;
origin?: NodeOrigin;
};
export type NodeOrigin = [number, number];