refactor(svelte): cleanup store usage

This commit is contained in:
moklick
2023-02-28 12:43:29 +01:00
parent 7a879f3c54
commit 62a6278682
25 changed files with 420 additions and 397 deletions

View File

@@ -25,13 +25,13 @@ type UseDragParams = {
handleSelector?: string;
nodeId?: string;
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
nodesStore: Writable<Node[]>;
transformStore: Writable<Transform>;
nodes: Writable<Node[]>;
transform: Writable<Transform>;
};
export default function drag(
nodeRef: Element,
{ handleSelector, nodeId, updateNodePositions, nodesStore, transformStore }: UseDragParams
{ handleSelector, nodeId, updateNodePositions, nodes, transform: transformStore }: UseDragParams
) {
let dragging = false;
let dragItems: NodeDragItem[] = [];
@@ -62,7 +62,7 @@ export default function drag(
dragItems = dragItems.map((n) => {
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
const updatedPos = calcNextPosition(n, nextPosition, get(nodesStore) as RFNode[]);
const updatedPos = calcNextPosition(n, nextPosition, get(nodes) as RFNode[]);
// we want to make sure that we only fire a change event when there is a changes
hasChange =
@@ -89,7 +89,7 @@ export default function drag(
const pointerPos = getPointerPosition(event);
console.log(pointerPos);
lastPos = pointerPos;
dragItems = getDragItems(get(nodesStore) as RFNode[], pointerPos, nodeId);
dragItems = getDragItems(get(nodes) as RFNode[], pointerPos, nodeId);
})
.on('drag', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event);

View File

@@ -78,13 +78,13 @@ function filter(event: any, params: ZoomParams): boolean {
}
type ZoomParams = {
transformStore: Writable<Transform>;
transform: Writable<Transform>;
selecting: boolean;
d3Store: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>;
d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>;
};
export default function zoom(domNode: Element, params: ZoomParams) {
const { transformStore, d3Store } = params;
const { transform, d3 } = params;
const d3ZoomInstance = d3Zoom();
const selection = select(domNode).call(d3ZoomInstance);
const d3ZoomHandler = selection.on('wheel.zoom');
@@ -100,13 +100,13 @@ export default function zoom(domNode: Element, params: ZoomParams) {
d3ZoomHandler!.call(this, event, d);
});
d3Store.set({
d3.set({
zoom: d3ZoomInstance,
selection
});
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transformStore.set([event.transform.x, event.transform.y, event.transform.k]);
transform.set([event.transform.x, event.transform.y, event.transform.k]);
});
d3ZoomInstance.filter((event: any) => filter(event, params));

View File

@@ -6,17 +6,17 @@
type $$Props = ConnectionLineProps;
const { connectionPathStore, widthStore, heightStore, connectionStore } = useStore();
const { connectionPath, width, height, connection } = useStore();
</script>
{#if $connectionPathStore}
{#if $connectionPath}
<svg
width={$widthStore}
height={$heightStore}
width={$width}
height={$height}
class="react-flow__connectionline"
>
<g class={cc(['react-flow__connection', $connectionStore.status])}>
<path d={$connectionPathStore} fill="none" class="react-flow__connection-path" />
<g class={cc(['react-flow__connection', $connection.status])}>
<path d={$connectionPath} fill="none" class="react-flow__connection-path" />
</g>
</svg>
{/if}

View File

@@ -38,7 +38,7 @@ export function handlePointerDown({
connectionMode,
connectionRadius,
isTarget,
transformStore,
transform: transformStore,
panBy,
updateConnection,
cancelConnection,
@@ -56,7 +56,7 @@ export function handlePointerDown({
nodes: Node[];
connectionRadius: number;
isValidConnection: ValidConnectionFunc;
transformStore: Writable<Transform>;
transform: Writable<Transform>;
updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void;
panBy: (delta: XYPosition) => void;

View File

@@ -22,11 +22,11 @@
const handleId = id || null;
const {
connectionModeStore,
domNodeStore,
nodesStore,
connectionRadiusStore,
transformStore,
connectionMode,
domNode,
nodes,
connectionRadius,
transform,
addEdge,
panBy,
cancelConnection,
@@ -49,11 +49,11 @@
handleId,
nodeId,
isTarget,
connectionRadius: $connectionRadiusStore,
domNode: $domNodeStore,
nodes: $nodesStore,
connectionMode: $connectionModeStore,
transformStore,
connectionRadius: $connectionRadius,
domNode: $domNode,
nodes: $nodes,
connectionMode: $connectionMode,
transform,
isValidConnection: isValidConnection!,
onConnect: onConnectExtended,
updateConnection,

View File

@@ -5,7 +5,7 @@
import type { KeyHandlerProps } from './KeyHandler'
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
const { selectionKeyPressedStore, deleteKeyPressedStore } = useStore();
const { selectionKeyPressed, deleteKeyPressed } = useStore();
type $$Props = KeyHandlerProps;
@@ -25,19 +25,19 @@
<svelte:window
use:shortcut={{
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressedStore.set(true) }],
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(true) }],
type: 'keydown'
}}
use:shortcut={{
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressedStore.set(false) }],
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(false) }],
type: 'keyup'
}}
use:shortcut={{
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(true) }],
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(true) }],
type: 'keydown'
}}
use:shortcut={{
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(false) }],
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(false) }],
type: 'keyup'
}}
/>

View File

@@ -5,20 +5,20 @@
import Selection from '$lib/components/Selection/index.svelte';
import drag from '$lib/actions/drag'
const { selectionRectModeStore, nodesStore, nodeOriginStore, transformStore, updateNodePositions } = useStore();
const { selectionRectMode, nodes, nodeOrigin, transform, updateNodePositions } = useStore();
$: selectedNodes = $nodesStore.filter(n => n.selected);
$: rect = getRectOfNodes(selectedNodes, $nodeOriginStore);
$: selectedNodes = $nodes.filter(n => n.selected);
$: rect = getRectOfNodes(selectedNodes, $nodeOrigin);
</script>
{#if selectedNodes}
<div
class="selection-wrapper nopan"
style={`width: ${rect.width}px; height: ${rect.height}px; transform: translate(${rect.x}px, ${rect.y}px)`}
use:drag={{ nodesStore, transformStore, updateNodePositions }}
use:drag={{ nodes, transform, updateNodePositions }}
/>
<Selection
isVisible={$selectionRectModeStore === 'nodes'}
isVisible={$selectionRectMode === 'nodes'}
width={rect.width}
height={rect.height}
x={rect.x}

View File

@@ -2,13 +2,13 @@
import { useStore } from '$lib/store';
import Selection from '$lib/components/Selection/index.svelte';
const { selectionRectStore, selectionRectModeStore } = useStore();
const { selectionRect, selectionRectMode } = useStore();
</script>
<Selection
isVisible={!!($selectionRectStore && $selectionRectModeStore === 'user')}
width={$selectionRectStore?.width}
height={$selectionRectStore?.height}
x={$selectionRectStore?.x}
y={$selectionRectStore?.y}
isVisible={!!($selectionRect && $selectionRectMode === 'user')}
width={$selectionRect?.width}
height={$selectionRect?.height}
x={$selectionRect?.x}
y={$selectionRect?.y}
/>

View File

@@ -22,8 +22,8 @@
export let selected: $$Props['selected'] = false;
export let label: $$Props['label'] = undefined;
const { edgeTypesStore } = useStore();
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypesStore[type!] || BezierEdge;
const { edgeTypes } = useStore();
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge;
</script>
<g

View File

@@ -28,8 +28,8 @@
let nodeRef: HTMLDivElement;
const { nodesStore, transformStore, nodeTypesStore, updateNodePositions, addSelectedNodes } = useStore();
const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> = $nodeTypesStore[type] || DefaultNode;
const { nodes, transform, nodeTypes, updateNodePositions, addSelectedNodes } = useStore();
const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> = $nodeTypes[type] || DefaultNode;
const isSelectable = true;
const selectNodesOnDrag = false;
const isDraggable = true;
@@ -58,7 +58,7 @@
</script>
<div
use:drag={{ nodeId: id, nodesStore, transformStore, updateNodePositions }}
use:drag={{ nodeId: id, nodes, transform, updateNodePositions }}
class={cc(['react-flow__node', `react-flow__node-${type}`, className])}
class:initializing={!width && !height}
class:dragging={dragging}

View File

@@ -2,15 +2,15 @@
import EdgeWrapper from '$lib/components/edges/EdgeWrapper.svelte';
import { useStore } from '$lib/store';
const { edgesWithDataStore, widthStore, heightStore } = useStore();
const { edgesLayouted, width, height } = useStore();
</script>
<svg
width={$widthStore}
<svg
width={$width}
height={$height}
class="react-flow__edges"
>
>
{#each $edgesLayouted as edge (edge.id)}
<EdgeWrapper {...edge} />
{/each}

View File

@@ -4,7 +4,7 @@
import NodeWrapper from '$lib/components/nodes/NodeWrapper.svelte';
import { useStore } from '$lib/store';
const { nodesStore, updateNodeDimensions } = useStore();
const { nodes, updateNodeDimensions } = useStore();
const resizeObserver: ResizeObserver | null = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = entries.map((entry: ResizeObserverEntry) => ({
id: entry.target.getAttribute('data-id') as string,
@@ -21,7 +21,7 @@
</script>
<div class="react-flow__nodes">
{#each $nodesStore as node (node.id)}
{#each $nodes as node (node.id)}
<NodeWrapper {...node} {resizeObserver} />
{/each}
</div>

View File

@@ -33,7 +33,7 @@
import { SelectionMode, type Node, type Edge } from '@reactflow/system';
import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils';
const { nodesStore, edgesStore, transformStore, nodeOriginStore, draggingStore, selectionRectStore, selectionRectModeStore, selectionKeyPressedStore, resetSelectedElements } = useStore();
const { nodes, edges, transform, nodeOrigin, dragging, selectionRect, selectionRectMode, selectionKeyPressed, resetSelectedElements } = useStore();
// @todo take from props
const elementsSelectable = true;
@@ -43,15 +43,15 @@
let containerBounds: DOMRect | null = null
let selectedNodes: Node[] = [];
$: isSelecting = $selectionKeyPressedStore;
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectModeStore === 'user');
$: isSelecting = $selectionKeyPressed;
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick (event: MouseEvent) {
// onPaneClick?.(event);
resetSelectedElements();
selectionRectModeStore.set(null)
selectionRectMode.set(null)
}
function onMouseDown(event: MouseEvent) {
@@ -71,7 +71,7 @@
resetSelectedElements();
selectionRectStore.set({
selectionRect.set({
width: 0,
height: 0,
startX: x,
@@ -87,14 +87,14 @@
function onMouseMove(event: MouseEvent) {
if (!isSelecting || !containerBounds || !$selectionRectStore) {
if (!isSelecting || !containerBounds || !$selectionRect) {
return;
}
const mousePos = getEventPosition(event, containerBounds);
const startX = $selectionRectStore.startX ?? 0;
const startY = $selectionRectStore.startY ?? 0;
const startX = $selectionRect.startX ?? 0;
const startY = $selectionRect.startY ?? 0;
const nextUserSelectRect = {
...$selectionRectStore,
...$selectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
@@ -102,21 +102,21 @@
};
selectedNodes = getNodesInside(
new Map($nodesStore.map(node => [node.id, node])),
new Map($nodes.map(node => [node.id, node])),
nextUserSelectRect,
$transformStore,
$transform,
selectionMode === SelectionMode.Partial,
true,
$nodeOriginStore
$nodeOrigin
);
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edgesStore).map((e) => e.id);
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
nodesStore.update(nodes => nodes.map(toggleSelected(selectedNodeIds)));
edgesStore.update(edges => edges.map(toggleSelected(selectedEdgeIds)));
nodes.update(nodes => nodes.map(toggleSelected(selectedNodeIds)));
edges.update(edges => edges.map(toggleSelected(selectedEdgeIds)));
selectionRectModeStore.set('user');
selectionRectStore.set(nextUserSelectRect);
selectionRectMode.set('user');
selectionRect.set(nextUserSelectRect);
}
function onMouseUp(event: MouseEvent) {
@@ -129,10 +129,10 @@
// if (!userSelectionActive && userSelectionRect && event.target === container.current) {
// onClick?.(event);
// }
selectionRectStore.set(null);
selectionRect.set(null);
if (selectedNodes.length > 0) {
selectionRectModeStore.set('nodes');
selectionRectMode.set('nodes');
}
@@ -140,19 +140,19 @@
}
const onMouseLeave = (event: MouseEvent) => {
if ($selectionRectModeStore === 'user') {
selectionRectModeStore.set(selectedNodes.length > 0 ? 'nodes' : null);
if ($selectionRectMode === 'user') {
selectionRectMode.set(selectedNodes.length > 0 ? 'nodes' : null);
// onSelectionEnd?.(event);
}
selectionRectStore.set(null);
selectionRect.set(null);
};
</script>
<div
bind:this={container}
class="react-flow__pane"
class:dragging={$draggingStore}
class:dragging={$dragging}
class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
on:mousedown={hasActiveSelection ? onMouseDown : undefined}

View File

@@ -23,7 +23,8 @@
export let nodeTypes: $$Props['nodeTypes'] = undefined;
export let selectionKey: $$Props['selectionKey'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
@@ -40,16 +41,38 @@
onMount(() => {
const { width, height } = domNode.getBoundingClientRect();
store.widthStore.set(width);
store.heightStore.set(height);
store.domNodeStore.set(domNode);
store.width.set(width);
store.height.set(height);
store.domNode.set(domNode);
// @todo: is this a svelte way for two way binding?
store.nodesStore.subscribe((ns) => {
store.nodes.subscribe((ns) => {
nodes = ns;
});
});
// $: {
// const updatableProps = {
// defaultEdgeOptions,
// connectionMode,
// snapToGrid,
// snapGrid,
// nodesDraggable,
// connectOnClick,
// fitViewOnInit: fitView,
// fitViewOnInitOptions: fitViewOptions,
// };
// Object.keys(updatableProps).forEach((key) => {
// store.update((state) => ({
// ...state,
// [key]: valuesToUpdate[key],
// }));
// });
// }
$: {
store.setNodes(nodes);
}

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { useStore } from '$lib/store';
const { transformStore} = useStore();
const { transform } = useStore();
</script>
<div
class="react-flow__viewport"
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
style="transform: translate({$transform[0]}px, {$transform[1]}px) scale({$transform[2]})"
>
<slot />
</div>

View File

@@ -2,12 +2,12 @@
import { useStore } from '$lib/store';
import zoom from '$lib/actions/zoom';
const { transformStore, d3Store, selectionKeyPressedStore, selectionRectModeStore } = useStore();
const { transform, d3, selectionKeyPressed, selectionRectMode } = useStore();
$: selecting = $selectionKeyPressedStore || $selectionRectModeStore === 'user';
$: selecting = $selectionKeyPressed || $selectionRectMode === 'user';
</script>
<div class="react-flow__zoom" use:zoom={{ transformStore, d3Store, selecting }}>
<div class="react-flow__zoom" use:zoom={{ transform, d3, selecting }}>
<slot />
</div>

View File

@@ -27,16 +27,16 @@
[BackgroundVariant.Cross]: 6,
};
const { transformStore, idStore } = useStore();
const { transform, id } = useStore();
const patternColor = color || defaultColor[variant];
const patternSize = size || defaultSize[variant];
const isDots = variant === BackgroundVariant.Dots;
const isCross = variant === BackgroundVariant.Cross;
const gapXY: number[] = Array.isArray(gap) ? gap : [gap, gap];
$: patternId = `background-pattern-${$idStore}`;
$: scaledGap = [gapXY[0] * $transformStore[2] || 1, gapXY[1] * $transformStore[2] || 1];
$: scaledSize = patternSize * $transformStore[2];
$: patternId = `background-pattern-${$id}`;
$: scaledGap = [gapXY[0] * $transform[2] || 1, gapXY[1] * $transform[2] || 1];
$: scaledSize = patternSize * $transform[2];
$: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number];
$: patternOffset = isDots
? [scaledSize / 2, scaledSize / 2]
@@ -46,8 +46,8 @@
<svg class={cc(['react-flow__background', className])} >
<pattern
id={patternId}
x={$transformStore[0] % scaledGap[0]}
y={$transformStore[1] % scaledGap[1]}
x={$transform[0] % scaledGap[0]}
y={$transform[1] % scaledGap[1]}
width={scaledGap[0]}
height={scaledGap[1]}
patternUnits="userSpaceOnUse"

View File

@@ -24,7 +24,7 @@
const defaultWidth = 200;
const defaultHeight = 150;
const { nodesStore, transformStore, widthStore, heightStore, nodeOriginStore, idStore } = useStore();
const { nodes, transform, width, height, nodeOrigin, id } = useStore();
type GetMiniMapNodeAttribute = (node: Node) => string;
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
@@ -32,15 +32,15 @@
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName);
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `react-flow__minimap-desc-${$idStore}`;
const labelledBy = `react-flow__minimap-desc-${$id}`;
$: viewBB = {
x: -$transformStore[0] / $transformStore[2],
y: -$transformStore[1] / $transformStore[2],
width: $widthStore / $transformStore[2],
height: $heightStore / $transformStore[2],
x: -$transform[0] / $transform[2],
y: -$transform[1] / $transform[2],
width: $width / $transform[2],
height: $height / $transform[2],
} as Rect;
$: boundingRect = $nodesStore.length > 0 ? getBoundsOfRects(getRectOfNodes($nodesStore, $nodeOriginStore), viewBB) : viewBB
$: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes, $nodeOrigin), viewBB) : viewBB
$: elementWidth = (style?.width as number) ?? defaultWidth;
$: elementHeight = (style?.height as number) ?? defaultHeight;
$: scaledWidth = boundingRect.width / elementWidth;
@@ -51,23 +51,23 @@
$: offset = 5 * viewScale;
$: x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
$: y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
$: width = viewWidth + offset * 2;
$: height = viewHeight + offset * 2;
$: viewboxWidth = viewWidth + offset * 2;
$: viewboxHeight = viewHeight + offset * 2;
</script>
<Panel position={position} class={cc(['react-flow__minimap', className])} style={`background-color: ${bgColor};`}>
<svg
width={elementWidth}
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
viewBox={`${x} ${y} ${viewboxWidth} ${viewboxHeight}`}
role="img"
aria-labelledby={labelledBy}
>
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
{#each $nodesStore as node}
{#each $nodes as node(node.id)}
{#if node.width && node.height}
{@const pos = getNodePositionWithOrigin(node, $nodeOriginStore).positionAbsolute}
{@const pos = getNodePositionWithOrigin(node, $nodeOrigin).positionAbsolute}
<MinimapNode
x={pos.x}
y={pos.y}
@@ -85,7 +85,7 @@
{/each}
<path
class="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${-viewboxWidth - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fill={maskColor}
fill-rule="evenodd"

View File

@@ -10,9 +10,8 @@
export let shapeRendering: string;
export let strokeColor: string;
export let strokeWidth: number;
export let className: string = '';
export let style: Record<string, string>;
let className: string = '';
export { className as class };
const { background, backgroundColor } = style || {};

View File

@@ -0,0 +1,76 @@
import { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils';
import { ConnectionLineType, ConnectionMode, Position } from '@reactflow/system';
import type { SvelteFlowStoreState } from './types';
import { derived } from 'svelte/store';
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top
};
export function getConnectionPath(store: SvelteFlowStoreState) {
return derived(
[
store.connection,
store.connectionLineType,
store.connectionMode,
store.nodes,
store.transform
],
([$connection, $connectionLineType, $connectionMode, $nodes, $transform]) => {
if (!$connection.nodeId) {
return null;
}
const fromNode = $nodes.find((n) => n.id === $connection.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const handleBoundsStrict = fromHandleBounds?.[$connection.handleType || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[$connection.handleType === 'source' ? 'target' : 'source']!;
const handleBounds =
$connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = $connection.handleId
? handleBounds.find((d) => d.id === $connection.handleId)
: handleBounds[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
: (fromNode?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: (($connection.position?.x ?? 0) - $transform[0]) / $transform[2],
targetY: (($connection.position?.y ?? 0) - $transform[1]) / $transform[2],
targetPosition: toPosition
};
let path = '';
if ($connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if ($connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if ($connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
}
return path;
}
);
}

View File

@@ -0,0 +1,61 @@
import { derived } from 'svelte/store';
import { Position } from '@reactflow/system';
import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils';
import type { WrapEdgeProps } from '$lib/types';
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);
if (!sourceIsValid || !targetIsValid) {
return null;
}
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;
if (!sourceHandle || !targetHandle) {
return null;
}
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;
return {
...edge,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
sourceHandleId,
targetHandleId
};
})
.filter((e) => e !== null) as WrapEdgeProps[];
});
}

View File

@@ -1,23 +1,16 @@
import { getContext } from 'svelte';
import { derived, get, writable, type Readable, type Writable } from 'svelte/store';
import { get } from 'svelte/store';
import {
type Transform,
type NodeDragItem,
type NodeDimensionUpdate,
Position,
internalsSymbol,
SelectionMode,
type NodeOrigin,
type D3ZoomInstance,
type D3SelectionInstance,
type ViewportHelperFunctionOptions,
type SelectionRect,
type Node as RFNode,
type Connection,
ConnectionMode,
type XYPosition,
type CoordinateExtent,
ConnectionLineType
type CoordinateExtent
} from '@reactflow/system';
import {
fitView as fitViewUtil,
@@ -29,15 +22,12 @@ import {
import { zoomIdentity } from 'd3-zoom';
import { getHandleBounds } from '../../utils';
import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import InputNode from '$lib/components/nodes/InputNode.svelte';
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import type { EdgeTypes, NodeTypes, Node, Edge, WrapEdgeProps, ConnectionData } from '$lib/types';
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 { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils';
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
import { getEdgesLayouted } from './edges-layouted';
import { getConnectionPath } from './connection-path';
import { initConnectionData, initialStoreState } from './initial-store';
import type { SvelteFlowStore } from './types';
export const key = Symbol();
@@ -50,59 +40,6 @@ type CreateStoreProps = {
id?: string;
};
type SvelteFlowStore = {
nodesStore: Writable<Node[]>;
edgesStore: Writable<Edge[]>;
heightStore: Writable<number>;
widthStore: Writable<number>;
d3Store: Writable<{
zoom: D3ZoomInstance | null;
selection: D3SelectionInstance | null;
}>;
transformStore: Writable<Transform>;
edgesWithDataStore: Readable<WrapEdgeProps[]>;
idStore: Writable<string>;
nodeOriginStore: Writable<NodeOrigin>;
draggingStore: Writable<boolean>;
selectionRectStore: Writable<SelectionRect | null>;
selectionRectModeStore: Writable<string | null>;
selectionMode: Writable<SelectionMode>;
selectionKeyPressedStore: Writable<boolean>;
deleteKeyPressedStore: Writable<boolean>;
nodeTypesStore: Writable<NodeTypes>;
edgeTypesStore: Writable<EdgeTypes>;
domNodeStore: Writable<HTMLDivElement | null>;
connectionRadiusStore: Writable<number>;
connectionModeStore: Writable<ConnectionMode>;
connectionStore: Writable<ConnectionData>;
connectionPathStore: Readable<string | null>;
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
addEdge: (edge: Edge | Connection) => void;
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
updateNodePositions: (
nodeDragItems: NodeDragItem[],
positionChanged?: boolean,
dragging?: boolean
) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
resetSelectedElements: () => void;
addSelectedNodes: (ids: string[]) => void;
panBy: (delta: XYPosition) => void;
updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void;
};
const initConnectionData = {
nodeId: null,
handleId: null,
handleType: null,
position: null,
status: null
};
export function createStore({
transform = [0, 0, 1],
nodeOrigin = [0, 0],
@@ -111,182 +48,23 @@ export function createStore({
edgeTypes = {},
id = '1'
}: CreateStoreProps): SvelteFlowStore {
const nodesStore = writable([] as Node[]);
const edgesStore = writable([] as Edge[]);
const heightStore = writable(500);
const widthStore = writable(500);
const nodeOriginStore = writable(nodeOrigin);
const d3Store = writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
zoom: null,
selection: null
});
const idStore = writable(id);
const draggingStore = writable(false);
const selectionRectStore = writable(null);
const selectionKeyPressedStore = writable(false);
const multiselectionKeyPressedStore = writable(false);
const deleteKeyPressedStore = writable(false);
const selectionRectModeStore = writable(null);
const selectionMode = writable(SelectionMode.Partial);
const nodeTypesStore = writable({
...nodeTypes,
input: nodeTypes.input || InputNode,
output: nodeTypes.output || OutputNode,
default: nodeTypes.default || DefaultNode
});
const edgeTypesStore = writable({
...edgeTypes,
straight: edgeTypes.straight || StraightEdge,
smoothstep: edgeTypes.smoothstep || SmoothStepEdge,
default: edgeTypes.default || BezierEdge
});
const transformStore = writable(transform);
const connectionModeStore = writable(ConnectionMode.Strict);
const domNodeStore = writable(null);
const connectionStore = writable<ConnectionData>(initConnectionData);
const connectionRadiusStore = writable(25);
const connectionLineTypeStore = writable(ConnectionLineType.Bezier);
const store = {
...initialStoreState
};
let fitViewOnInitDone = false;
const edgesWithDataStore = derived([edgesStore, nodesStore], ([$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 as RFNode
);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(
targetNode as RFNode
);
if (!sourceIsValid || !targetIsValid) {
return null;
}
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;
if (!sourceHandle || !targetHandle) {
return null;
}
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;
return {
...edge,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
sourceHandleId,
targetHandleId
};
})
.filter((e) => e !== null) as WrapEdgeProps[];
});
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top
};
const connectionPathStore = derived(
[connectionStore, connectionLineTypeStore, connectionModeStore, nodesStore, transformStore],
([
$connectionStore,
$connectionLineTypeStore,
$connectionModeStore,
$nodesStore,
$transformStore
]) => {
if (!$connectionStore.nodeId) {
return null;
}
const fromNode = $nodesStore.find((n) => n.id === $connectionStore.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const handleBoundsStrict = fromHandleBounds?.[$connectionStore.handleType || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[$connectionStore.handleType === 'source' ? 'target' : 'source']!;
const handleBounds =
$connectionModeStore === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = $connectionStore.handleId
? handleBounds.find((d) => d.id === $connectionStore.handleId)
: handleBounds[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
: (fromNode?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: (($connectionStore.position?.x ?? 0) - $transformStore[0]) / $transformStore[2],
targetY: (($connectionStore.position?.y ?? 0) - $transformStore[1]) / $transformStore[2],
targetPosition: toPosition
};
let path = '';
if ($connectionLineTypeStore === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if ($connectionLineTypeStore === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if ($connectionLineTypeStore === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
}
return path;
}
);
function setEdges(edges: Edge[]) {
edgesStore.set(edges);
store.edges.set(edges);
}
function addEdge(edgeParams: Edge | Connection) {
const edges = get(edgesStore);
edgesStore.set(addEdgeUtil(edgeParams, edges));
const edges = get(store.edges);
store.edges.set(addEdgeUtil(edgeParams, edges));
}
function setNodes(nodes: Node[]) {
nodesStore.update((currentNodes) => {
store.nodes.update((currentNodes) => {
const nextNodes = nodes.map((n) => {
const currentNode = currentNodes.find((cn) => cn.id === n.id) || {};
@@ -302,7 +80,7 @@ export function createStore({
}
function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) {
nodesStore.update((nds) => {
store.nodes.update((nds) => {
return nds.map((n) => {
const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id);
@@ -330,7 +108,7 @@ export function createStore({
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nextNodes = get(nodesStore).map((node) => {
const nextNodes = get(store.nodes).map((node) => {
const update = updates.find((u) => u.id === node.id);
if (update) {
@@ -360,16 +138,16 @@ export function createStore({
return node;
});
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
fitViewOnInitDone =
fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView());
nodesStore.set(nextNodes);
store.nodes.set(nextNodes);
}
function zoomIn(options?: ViewportHelperFunctionOptions) {
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (d3Zoom && d3Selection) {
d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2);
@@ -377,14 +155,14 @@ export function createStore({
}
function zoomOut(options?: ViewportHelperFunctionOptions) {
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (d3Zoom && d3Selection) {
d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2);
}
}
function fitView() {
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (!d3Zoom || !d3Selection) {
return false;
@@ -392,14 +170,14 @@ export function createStore({
return fitViewUtil(
{
nodes: get(nodesStore) as RFNode[],
width: get(widthStore),
height: get(heightStore),
nodes: get(store.nodes) as RFNode[],
width: get(store.width),
height: get(store.height),
minZoom: 0.2,
maxZoom: 2,
d3Selection,
d3Zoom,
nodeOrigin: get(nodeOriginStore)
nodeOrigin: get(store.nodeOrigin)
},
{}
);
@@ -417,14 +195,14 @@ export function createStore({
}
function resetSelectedElements() {
nodesStore.update((ns) => ns.map(resetSelectedItem));
edgesStore.update((es) => es.map(resetSelectedItem));
store.nodes.update((ns) => ns.map(resetSelectedItem));
store.edges.update((es) => es.map(resetSelectedItem));
}
deleteKeyPressedStore.subscribe((deleteKeyPressed) => {
store.deleteKeyPressed.subscribe((deleteKeyPressed) => {
if (deleteKeyPressed) {
const nodes = get(nodesStore);
const edges = get(edgesStore);
const nodes = get(store.nodes);
const edges = get(store.edges);
const selectedNodes = nodes.filter((node) => node.selected);
const selectedEdges = edges.filter((edge) => edge.selected);
@@ -458,21 +236,21 @@ export function createStore({
return res;
}, []);
nodesStore.update((nds) => nds.filter((node) => !nodeIds.includes(node.id)));
edgesStore.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id)));
store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id)));
store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id)));
}
}
});
function addSelectedNodes(ids: string[]) {
selectionRectStore.set(null);
selectionRectModeStore.set(null);
store.selectionRect.set(null);
store.selectionRectMode.set(null);
if (get(multiselectionKeyPressedStore)) {
if (get(store.multiselectionKeyPressed)) {
// @todo handle multiselection key
}
nodesStore.update((ns) =>
store.nodes.update((ns) =>
ns.map((node) => {
return {
...node,
@@ -483,10 +261,10 @@ export function createStore({
}
function panBy(delta: XYPosition) {
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
const transform = get(transformStore);
const width = get(widthStore);
const height = get(heightStore);
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
const transform = get(store.transform);
const width = get(store.width);
const height = get(store.height);
if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) {
return;
@@ -509,7 +287,7 @@ export function createStore({
}
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) {
const currentConnectionData = get(connectionStore);
const currentConnectionData = get(store.connection);
const nextConnectionData = currentConnectionData
? {
...initConnectionData,
@@ -521,7 +299,7 @@ export function createStore({
...connectionUpdate
};
connectionStore.set(nextConnectionData);
store.connection.set(nextConnectionData);
}
function cancelConnection() {
@@ -529,28 +307,14 @@ export function createStore({
}
return {
nodesStore,
edgesStore,
transformStore,
d3Store,
heightStore,
widthStore,
edgesWithDataStore,
idStore,
nodeOriginStore,
draggingStore,
selectionRectStore,
selectionKeyPressedStore,
deleteKeyPressedStore,
selectionRectModeStore,
selectionMode,
nodeTypesStore,
edgeTypesStore,
connectionModeStore,
domNodeStore,
connectionStore,
connectionRadiusStore,
connectionPathStore,
// state
...store,
// derived state
edgesLayouted: getEdgesLayouted(store),
connectionPath: getConnectionPath(store),
// actions
setNodes,
setEdges,
addEdge,

View File

@@ -0,0 +1,65 @@
import { readable, writable } from 'svelte/store';
import {
SelectionMode,
type D3ZoomInstance,
type D3SelectionInstance,
ConnectionMode,
ConnectionLineType,
type Transform,
type NodeOrigin,
type Rect
} from '@reactflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import InputNode from '$lib/components/nodes/InputNode.svelte';
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes } from '$lib/types';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
export const initConnectionData = {
nodeId: null,
handleId: null,
handleType: null,
position: null,
status: null
};
export const initialStoreState = {
nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]),
edgesLayouted: readable<Edge[]>([]),
height: writable<number>(500),
width: writable<number>(500),
nodeOrigin: writable<NodeOrigin>([0.5, 0.5]),
d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
zoom: null,
selection: null
}),
id: writable<string | null>(null),
dragging: writable<boolean>(false),
selectionRect: writable<(Rect & { startX: number; startY: number }) | null>(null),
selectionKeyPressed: writable<boolean>(false),
multiselectionKeyPressed: writable<boolean>(false),
deleteKeyPressed: writable<boolean>(false),
selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
nodeTypes: writable<NodeTypes>({
input: InputNode,
output: OutputNode,
default: DefaultNode
}),
edgeTypes: writable<EdgeTypes>({
straight: StraightEdge,
smoothstep: SmoothStepEdge,
default: BezierEdge
}),
transform: writable<Transform>([0, 0, 1]),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null),
connectionPath: readable<string | null>(null),
connection: writable<ConnectionData>(initConnectionData),
connectionRadius: writable<number>(25),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier)
};

View File

@@ -0,0 +1,34 @@
import type {
NodeDimensionUpdate,
XYPosition,
ViewportHelperFunctionOptions,
Connection,
NodeDragItem
} from '@reactflow/system';
import { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData } from '$lib/types';
export type SvelteFlowStoreActions = {
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
addEdge: (edge: Edge | Connection) => void;
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
updateNodePositions: (
nodeDragItems: NodeDragItem[],
positionChanged?: boolean,
dragging?: boolean
) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
resetSelectedElements: () => void;
addSelectedNodes: (ids: string[]) => void;
panBy: (delta: XYPosition) => void;
updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void;
};
export type SvelteFlowStoreState = typeof initialStoreState;
export type SvelteFlowStore = SvelteFlowStoreState & SvelteFlowStoreActions;

View File

@@ -1,6 +1,6 @@
import type { Node, NodeTypes } from './nodes';
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { Edge, HandleType, XYPosition } from '@reactflow/system';
import type { ConnectionLineType, Edge, HandleType, XYPosition } from '@reactflow/system';
export type KeyModifier = ShortcutModifierDefinition;
export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
@@ -20,6 +20,7 @@ export type SvelteFlowProps = {
nodes: Node[];
edges: Edge[];
connectionLineType?: ConnectionLineType;
selectionKey?: KeyDefinition;
deleteKey?: KeyDefinition;
nodeTypes?: NodeTypes;