Merge pull request #4129 from xyflow/refactor/svelte-internal

Svelte: Refactor internals
This commit is contained in:
Moritz Klack
2024-04-10 17:03:08 +02:00
committed by GitHub
32 changed files with 235 additions and 175 deletions
@@ -13,7 +13,7 @@
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable([ const nodes = writable<Node[]>([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -55,8 +55,6 @@
const onDragOver = (event: DragEvent) => { const onDragOver = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
console.log(event);
if (event.dataTransfer) { if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'; event.dataTransfer.dropEffect = 'move';
} }
@@ -81,7 +79,8 @@
data: { label: `${type} node` } data: { label: `${type} node` }
}; };
nodes.update((nds) => nds.concat(newNode)); $nodes.push(newNode);
$nodes = $nodes;
}; };
$: { $: {
@@ -12,8 +12,8 @@
const { getIntersectingNodes } = useSvelteFlow(); const { getIntersectingNodes } = useSvelteFlow();
function onNodeDrag({ detail: { node } }) { function onNodeDrag({ detail: { targetNode } }) {
const intersections = getIntersectingNodes(node).map((n) => n.id); const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
$nodes.forEach((n) => { $nodes.forEach((n) => {
n.class = intersections.includes(n.id) ? 'highlight' : ''; n.class = intersections.includes(n.id) ? 'highlight' : '';
@@ -1,28 +1,28 @@
import type { Node, Edge } from '@xyflow/svelte'; import type { Node, Edge } from '@xyflow/svelte';
export const initialNodes: Node[] = [ export const initialNodes: Node[] = [
{ {
id: '1', id: '1',
data: { label: 'Node 1' }, data: { label: 'Node 1' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
style: 'width: 200px; height: 100px;' style: 'width: 200px; height: 100px;'
}, },
{ {
id: '2', id: '2',
data: { label: 'Node 2' }, data: { label: 'Node 2' },
position: { x: 0, y: 150 } position: { x: 0, y: 150 }
}, },
{ {
id: '3', id: '3',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 250, y: 0 } position: { x: 250, y: 0 }
}, },
{ {
id: '4', id: '4',
data: { label: 'Node' }, data: { label: 'Node' },
position: { x: 350, y: 150 }, position: { x: 350, y: 150 },
style: 'width: 50px; height: 50px;' style: 'width: 50px; height: 50px;'
} }
]; ];
export const initialEdges: Edge[] = []; export const initialEdges: Edge[] = [];
@@ -11,6 +11,7 @@
type $$Props = NodeProps; type $$Props = NodeProps;
export let id: $$Props['id']; export let id: $$Props['id'];
$$restProps;
const connections = useHandleConnections({ const connections = useHandleConnections({
nodeId: id, nodeId: id,
@@ -7,6 +7,7 @@
export let data: $$Props['data']; export let data: $$Props['data'];
const { updateNodeData } = useSvelteFlow(); const { updateNodeData } = useSvelteFlow();
$$restProps;
</script> </script>
<div class="custom"> <div class="custom">
@@ -12,6 +12,8 @@
type $$Props = NodeProps; type $$Props = NodeProps;
export let id: $$Props['id']; export let id: $$Props['id'];
export let data: $$Props['data'];
$$restProps;
const { updateNodeData } = useSvelteFlow(); const { updateNodeData } = useSvelteFlow();
const connections = useHandleConnections({ const connections = useHandleConnections({
@@ -22,8 +24,12 @@
$: nodeData = useNodesData<MyNode>($connections[0]?.source); $: nodeData = useNodesData<MyNode>($connections[0]?.source);
$: textNode = isTextNode($nodeData) ? $nodeData : null; $: textNode = isTextNode($nodeData) ? $nodeData : null;
$: console.log(textNode?.data, data);
$: { $: {
updateNodeData(id, { text: textNode?.data.text.toUpperCase() || '' }); const input = textNode?.data.text.toUpperCase() ?? '';
updateNodeData(id, { text: input });
console.log('updatedNodeData with', input);
} }
</script> </script>
@@ -74,7 +74,7 @@ export function NodeToolbar({
} }
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
const zIndex: number = Math.max(...nodes.map((node) => (node.internals?.z || 1) + 1)); const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
@@ -52,7 +52,7 @@ const ConnectionLine = ({
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.internals?.handleBounds; const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType]; let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) { if (connectionMode === ConnectionMode.Loose) {
@@ -66,8 +66,8 @@ const ConnectionLine = ({
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX; const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY; const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null; const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
+6
View File
@@ -24,6 +24,12 @@ export type Node<
focusable?: boolean; focusable?: boolean;
}; };
/**
* The node data structure that gets used for internal nodes.
* There are some data structures added under node.internal
* that are needed for tracking some properties
* @public
*/
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>; export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void; export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
+1
View File
@@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/build /build
/dist
/.svelte-kit /.svelte-kit
/package /package
.env .env
@@ -42,7 +42,7 @@
const { const {
connectionMode, connectionMode,
domNode, domNode,
nodes, nodeLookup,
connectionRadius, connectionRadius,
viewport, viewport,
isValidConnection, isValidConnection,
@@ -72,7 +72,7 @@
isTarget, isTarget,
connectionRadius: $connectionRadius, connectionRadius: $connectionRadius,
domNode: $domNode, domNode: $domNode,
nodes: $nodes, nodeLookup: $nodeLookup,
connectionMode: $connectionMode, connectionMode: $connectionMode,
lib: $lib, lib: $lib,
autoPanOnConnect: $autoPanOnConnect, autoPanOnConnect: $autoPanOnConnect,
@@ -35,8 +35,8 @@
export let sourcePosition: $$Props['sourcePosition'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined;
export let zIndex: $$Props['zIndex']; export let zIndex: $$Props['zIndex'];
export let computedWidth: $$Props['computedWidth'] = undefined; export let measuredWidth: $$Props['measuredWidth'] = undefined;
export let computedHeight: $$Props['computedHeight'] = undefined; export let measuredHeight: $$Props['measuredHeight'] = undefined;
export let initialWidth: $$Props['initialWidth'] = undefined; export let initialWidth: $$Props['initialWidth'] = undefined;
export let initialHeight: $$Props['initialHeight'] = undefined; export let initialHeight: $$Props['initialHeight'] = undefined;
export let width: $$Props['width'] = undefined; export let width: $$Props['width'] = undefined;
@@ -79,8 +79,8 @@
height, height,
initialWidth, initialWidth,
initialHeight, initialHeight,
computedWidth, measuredWidth,
computedHeight measuredHeight
}); });
$: { $: {
@@ -21,8 +21,8 @@ export type NodeWrapperProps = Pick<
| 'initialWidth' | 'initialWidth'
| 'initialHeight' | 'initialHeight'
> & { > & {
computedWidth?: number; measuredWidth?: number;
computedHeight?: number; measuredHeight?: number;
type: string; type: string;
positionX: number; positionX: number;
positionY: number; positionY: number;
@@ -3,20 +3,20 @@ export function getNodeInlineStyleDimensions({
height, height,
initialWidth, initialWidth,
initialHeight, initialHeight,
computedWidth, measuredWidth,
computedHeight measuredHeight
}: { }: {
width?: number; width?: number;
height?: number; height?: number;
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
computedWidth?: number; measuredWidth?: number;
computedHeight?: number; measuredHeight?: number;
}): { }): {
width: string | undefined; width: string | undefined;
height: string | undefined; height: string | undefined;
} { } {
if (computedWidth === undefined && computedHeight === undefined) { if (measuredWidth === undefined && measuredHeight === undefined) {
const styleWidth = width ?? initialWidth; const styleWidth = width ?? initialWidth;
const styleHeight = height ?? initialHeight; const styleHeight = height ?? initialHeight;
@@ -1,11 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { import { getPositionWithOrigin, getNodeDimensions, nodeHasDimensions } from '@xyflow/system';
internalsSymbol,
getPositionWithOrigin,
getNodeDimensions,
nodeHasDimensions
} from '@xyflow/system';
import { NodeWrapper } from '$lib/components/NodeWrapper'; import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -46,8 +41,8 @@
{#each $visibleNodes as node (node.id)} {#each $visibleNodes as node (node.id)}
{@const nodeDimesions = getNodeDimensions(node)} {@const nodeDimesions = getNodeDimensions(node)}
{@const posOrigin = getPositionWithOrigin({ {@const posOrigin = getPositionWithOrigin({
x: node.computed?.positionAbsolute?.x ?? 0, x: node.internals.positionAbsolute.x,
y: node.computed?.positionAbsolute?.y ?? 0, y: node.internals.positionAbsolute.y,
...nodeDimesions, ...nodeDimesions,
origin: node.origin origin: node.origin
})} })}
@@ -66,26 +61,26 @@
node.connectable || node.connectable ||
($nodesConnectable && typeof node.connectable === 'undefined') ($nodesConnectable && typeof node.connectable === 'undefined')
)} )}
positionX={node.computed?.positionAbsolute?.x ?? 0} positionX={node.internals.positionAbsolute.x}
positionY={node.computed?.positionAbsolute?.y ?? 0} positionY={node.internals.positionAbsolute.y}
positionOriginX={posOrigin.x ?? 0} positionOriginX={posOrigin.x ?? 0}
positionOriginY={posOrigin.y ?? 0} positionOriginY={posOrigin.y ?? 0}
isParent={!!node[internalsSymbol]?.isParent} isParent={!!node.internals.isParent}
style={node.style} style={node.style}
class={node.class} class={node.class}
type={node.type ?? 'default'} type={node.type ?? 'default'}
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition} targetPosition={node.targetPosition}
dragging={node.dragging} dragging={node.dragging}
zIndex={node[internalsSymbol]?.z ?? 0} zIndex={node.internals.z ?? 0}
dragHandle={node.dragHandle} dragHandle={node.dragHandle}
initialized={nodeHasDimensions(node)} initialized={nodeHasDimensions(node)}
width={node.width} width={node.width}
height={node.height} height={node.height}
initialWidth={node.initialWidth} initialWidth={node.initialWidth}
initialHeight={node.initialHeight} initialHeight={node.initialHeight}
computedWidth={node.computed?.width} measuredWidth={node.measured.width}
computedHeight={node.computed?.height} measuredHeight={node.measured.height}
{resizeObserver} {resizeObserver}
on:nodeclick on:nodeclick
on:nodemouseenter on:nodemouseenter
@@ -17,10 +17,7 @@
const isSelected = ids.includes(item.id); const isSelected = ids.includes(item.id);
if (item.selected !== isSelected) { if (item.selected !== isSelected) {
return { item.selected = isSelected;
...item,
selected: isSelected
};
} }
return item; return item;
@@ -56,6 +53,7 @@
}>(); }>();
const { const {
nodes, nodes,
nodeLookup,
edges, edges,
viewport, viewport,
dragging, dragging,
@@ -130,8 +128,8 @@
const prevSelectedNodeIds = selectedNodes.map((n) => n.id); const prevSelectedNodeIds = selectedNodes.map((n) => n.id);
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
selectedNodes = getNodesInside<Node>( selectedNodes = getNodesInside(
$nodes, $nodeLookup,
nextUserSelectRect, nextUserSelectRect,
[$viewport.x, $viewport.y, $viewport.zoom], [$viewport.x, $viewport.y, $viewport.zoom],
$selectionMode === SelectionMode.Partial, $selectionMode === SelectionMode.Partial,
@@ -172,7 +170,7 @@
selectionRect.set(null); selectionRect.set(null);
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
selectionRectMode.set('nodes'); $selectionRectMode = 'nodes';
} }
// onSelectionEnd?.(event); // onSelectionEnd?.(event);
@@ -29,7 +29,7 @@ export function useNodesData(nodeIds: any): any {
const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds]; const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds];
for (const nodeId of _nodeIds) { for (const nodeId of _nodeIds) {
const node = nodeLookup.get(nodeId); const node = nodeLookup.get(nodeId)?.internals.userNode;
if (node) { if (node) {
nextNodesData.push({ nextNodesData.push({
id: node.id, id: node.id,
+33 -17
View File
@@ -256,17 +256,28 @@ export function useSvelteFlow(): {
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>), nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
options: { replace: boolean } = { replace: false } options: { replace: boolean } = { replace: false }
) => { ) => {
nodes.update((nds) => const node = get(nodeLookup).get(id)?.internals.userNode;
nds.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode }; if (!node) {
} return;
}
return node; const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
})
); if (options.replace) {
nodes.update((nds) =>
nds.map((node) => {
if (node.id === id) {
return isNode(nextNode) ? nextNode : { ...node, ...nextNode };
}
return node;
})
);
} else {
Object.assign(node, nextNode);
nodes.update((nds) => nds);
}
}; };
return { return {
@@ -331,11 +342,12 @@ export function useSvelteFlow(): {
} }
return (nodesToIntersect || get(nodes)).filter((n) => { return (nodesToIntersect || get(nodes)).filter((n) => {
if (!isRect && (n.id === nodeOrRect.id || !n.computed?.positionAbsolute)) { const internalNode = get(nodeLookup).get(n.id);
if (!internalNode || (!isRect && n.id === nodeOrRect.id)) {
return false; return false;
} }
const currNodeRect = nodeToRect(n); const currNodeRect = nodeToRect(internalNode);
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
@@ -447,13 +459,17 @@ export function useSvelteFlow(): {
}, },
updateNode, updateNode,
updateNodeData: (id, dataUpdate, options) => { updateNodeData: (id, dataUpdate, options) => {
updateNode(id, (node) => { const node = get(nodeLookup).get(id)?.internals.userNode;
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
return options?.replace if (!node) {
? { ...node, data: nextData } return;
: { ...node, data: { ...node.data, ...nextData } }; }
});
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
node.data = options?.replace ? nextData : { ...node.data, ...nextData };
nodes.update((nds) => nds);
}, },
viewport viewport
}; };
+1 -2
View File
@@ -116,6 +116,5 @@ export {
getOutgoers, getOutgoers,
getConnectedEdges, getConnectedEdges,
addEdge, addEdge,
updateEdge, updateEdge
internalsSymbol
} from '@xyflow/system'; } from '@xyflow/system';
@@ -69,7 +69,7 @@
}; };
}, },
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const node = $nodeLookup.get(id); const node = $nodeLookup.get(id)?.internals.userNode;
if (node) { if (node) {
node.height = change.isHeightChange ? change.height : node.height; node.height = change.isHeightChange ? change.height : node.height;
node.width = change.isWidthChange ? change.width : node.width; node.width = change.isWidthChange ? change.width : node.width;
@@ -79,7 +79,7 @@
: node.position; : node.position;
for (const childChange of childChanges) { for (const childChange of childChanges) {
const childNode = $nodeLookup.get(childChange.id); const childNode = $nodeLookup.get(childChange.id)?.internals.userNode;
if (childNode) { if (childNode) {
childNode.position = childChange.position; childNode.position = childChange.position;
} }
@@ -1,14 +1,8 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { import { getNodesBounds, Position, type Rect, getNodeToolbarTransform } from '@xyflow/system';
getNodesBounds,
Position,
type Rect,
internalsSymbol,
getNodeToolbarTransform
} from '@xyflow/system';
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
import type { Node } from '$lib/types'; import type { InternalNode } from '$lib/types';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { NodeToolbarProps } from './types'; import type { NodeToolbarProps } from './types';
@@ -25,26 +19,26 @@
const contextNodeId = getContext<string>('svelteflow__node_id'); const contextNodeId = getContext<string>('svelteflow__node_id');
let transform: string; let transform: string;
let toolbarNodes: Node[] = []; let toolbarNodes: InternalNode[] = [];
let _offset = offset !== undefined ? offset : 10; let _offset = offset !== undefined ? offset : 10;
let _position = position !== undefined ? position : Position.Top; let _position = position !== undefined ? position : Position.Top;
let _align = align !== undefined ? align : 'center'; let _align = align !== undefined ? align : 'center';
$: { $: {
// $nodes only needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates // nly needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates
if ($nodes) { $nodes;
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
toolbarNodes = nodeIds.reduce<Node[]>((res, nodeId) => { const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
const node = $nodeLookup.get(nodeId);
if (node) { toolbarNodes = nodeIds.reduce<InternalNode[]>((res, nodeId) => {
res.push(node); const node = $nodeLookup.get(nodeId);
}
return res; if (node) {
}, []); res.push(node);
} }
return res;
}, []);
} }
$: { $: {
@@ -54,8 +48,8 @@
const toolbarNode = toolbarNodes[0]; const toolbarNode = toolbarNodes[0];
nodeRect = { nodeRect = {
...toolbarNode.position, ...toolbarNode.position,
width: toolbarNode.computed?.width ?? toolbarNode.width ?? 0, width: toolbarNode.measured.width ?? toolbarNode.width ?? 0,
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0 height: toolbarNode.measured.height ?? toolbarNode.height ?? 0
}; };
} else if (toolbarNodes.length > 1) { } else if (toolbarNodes.length > 1) {
nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin }); nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin });
@@ -69,7 +63,7 @@
$: zIndex = $: zIndex =
toolbarNodes.length === 0 toolbarNodes.length === 0
? 1 ? 1
: Math.max(...toolbarNodes.map((node) => (node[internalsSymbol]?.z || 5) + 1)); : Math.max(...toolbarNodes.map((node) => (node.internals.z || 5) + 1));
//FIXME: Possible performance bottleneck //FIXME: Possible performance bottleneck
$: selectedNodesCount = $nodes.filter((node) => node.selected).length; $: selectedNodesCount = $nodes.filter((node) => node.selected).length;
@@ -6,7 +6,6 @@ import {
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
Position, Position,
internalsSymbol,
type HandleElement type HandleElement
} from '@xyflow/system'; } from '@xyflow/system';
@@ -65,8 +64,9 @@ export function getDerivedConnectionProps(
return initConnectionProps; return initConnectionProps;
} }
// TODO: it should bail out if the node is not found
const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId); const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.internals.handleBounds;
const handleBoundsStrict = const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || []; fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict
@@ -81,12 +81,12 @@ export function getDerivedConnectionProps(
: handleBounds?.[0]; : handleBounds?.[0];
const fromHandleX = fromHandle const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2 ? fromHandle.x + fromHandle.width / 2
: (fromNode?.computed?.width ?? 0) / 2; : (fromNode?.measured.width ?? 0) / 2;
const fromHandleY = fromHandle const fromHandleY = fromHandle
? fromHandle.y + fromHandle.height / 2 ? fromHandle.y + fromHandle.height / 2
: fromNode?.computed?.height ?? 0; : fromNode?.measured.height ?? 0;
const fromX = (fromNode?.computed?.positionAbsolute?.x ?? 0) + fromHandleX; const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode?.computed?.positionAbsolute?.y ?? 0) + fromHandleY; const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
+42 -23
View File
@@ -64,52 +64,71 @@ export function createStore({
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
const nodeLookup = get(store.nodeLookup); const nodeLookup = get(store.nodeLookup);
nodeDragItems.forEach((nodeDragItem) => { for (const nodeDragItem of nodeDragItems) {
const node = nodeLookup.get(nodeDragItem.id); const node = nodeLookup.get(nodeDragItem.id)?.internals.userNode;
if (node) { if (!node) {
node.position = nodeDragItem.position; continue;
node.dragging = dragging;
node.computed = {
...node.computed,
positionAbsolute: nodeDragItem.computed?.positionAbsolute
};
} }
});
store.nodes.set(get(store.nodes)); node.position = nodeDragItem.position;
node.dragging = dragging;
}
store.nodes.update((nds) => nds);
}; };
function updateNodeDimensions(updates: Map<string, NodeDimensionUpdate>) { function updateNodeDimensions(updates: Map<string, NodeDimensionUpdate>) {
const nextNodes = updateNodeDimensionsSystem( const nodeLookup = get(store.nodeLookup);
const changes = updateNodeDimensionsSystem(
updates, updates,
get(store.nodes), nodeLookup,
get(store.nodeLookup),
get(store.domNode), get(store.domNode),
get(store.nodeOrigin) get(store.nodeOrigin)
); );
if (!nextNodes) { if (!changes) {
return; return;
} }
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) {
const fitViewOptions = get(store.fitViewOptions); const fitViewOptions = get(store.fitViewOptions);
const fitViewOnInitDone = fitView(nextNodes, { const fitViewOnInitDone = fitView({
...fitViewOptions, ...fitViewOptions,
nodes: fitViewOptions?.nodes || nextNodes nodes: fitViewOptions?.nodes
}); });
store.fitViewOnInitDone.set(fitViewOnInitDone); store.fitViewOnInitDone.set(fitViewOnInitDone);
} }
store.nodes.set(nextNodes); for (const change of changes) {
const node = nodeLookup.get(change.id)?.internals.userNode;
if (!node) {
continue;
}
switch (change.type) {
case 'dimensions': {
const measured = { ...node.measured, ...change.dimensions };
node.width = change.dimensions?.width ?? node.width;
node.height = change.dimensions?.height ?? node.height;
node.measured = measured;
break;
}
case 'position':
node.position = change.position ?? node.position;
break;
}
}
store.nodes.update((nds) => nds);
if (!get(store.nodesInitialized)) { if (!get(store.nodesInitialized)) {
store.nodesInitialized.set(true); store.nodesInitialized.set(true);
} }
} }
function fitView(nodes: Node[], options?: FitViewOptions) { function fitView(options?: FitViewOptions) {
const panZoom = get(store.panZoom); const panZoom = get(store.panZoom);
if (!panZoom) { if (!panZoom) {
@@ -118,7 +137,7 @@ export function createStore({
return fitViewUtil( return fitViewUtil(
{ {
nodes, nodeLookup: get(store.nodeLookup),
width: get(store.width), width: get(store.width),
height: get(store.height), height: get(store.height),
minZoom: get(store.minZoom), minZoom: get(store.minZoom),
@@ -186,10 +205,10 @@ export function createStore({
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) { function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes)); const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
if (resetNodes) store.nodes.set(get(store.nodes)); if (resetNodes) store.nodes.update((nds) => nds);
const resetEdges = resetSelectedElements(params?.edges || get(store.edges)); const resetEdges = resetSelectedElements(params?.edges || get(store.edges));
if (resetEdges) store.edges.set(get(store.edges)); if (resetEdges) store.edges.update((nds) => nds);
} }
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => { store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
@@ -384,7 +403,7 @@ export function createStore({
updateNodeDimensions, updateNodeDimensions,
zoomIn, zoomIn,
zoomOut, zoomOut,
fitView: (options?: FitViewOptions) => fitView(get(store.nodes), options), fitView: (options?: FitViewOptions) => fitView(options),
setMinZoom, setMinZoom,
setMaxZoom, setMaxZoom,
setTranslateExtent, setTranslateExtent,
+11 -8
View File
@@ -5,7 +5,7 @@ import {
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
devWarn, devWarn,
adoptUserProvidedNodes, adoptUserNodes,
getNodesBounds, getNodesBounds,
getViewportForBounds, getViewportForBounds,
updateConnectionLookup, updateConnectionLookup,
@@ -47,7 +47,8 @@ import type {
OnDelete, OnDelete,
OnEdgeCreate, OnEdgeCreate,
OnBeforeDelete, OnBeforeDelete,
IsValidConnection IsValidConnection,
InternalNode
} from '$lib/types'; } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils'; import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props'; import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
@@ -80,9 +81,10 @@ export const getInitialStore = ({
fitView?: boolean; fitView?: boolean;
}) => { }) => {
const nodeLookup: NodeLookup = new Map(); const nodeLookup: NodeLookup = new Map();
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { adoptUserNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false elevateNodesOnSelect: false,
checkEquality: false
}); });
const connectionLookup = new Map(); const connectionLookup = new Map();
const edgeLookup = new Map(); const edgeLookup = new Map();
@@ -91,9 +93,10 @@ export const getInitialStore = ({
let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter( const nodesWithDimensions = nodes.filter(
(node) => (node.width && node.height) || (node.initialWidth && node.initialHeight) (node) => (node.width && node.height) || (node.initialWidth && node.initialHeight)
); );
// @todo users nodeOrigin should be used here // @todo users nodeOrigin should be used here
const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] }); const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
@@ -101,10 +104,10 @@ export const getInitialStore = ({
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes, nodeLookup), nodes: createNodesStore(nodes, nodeLookup),
nodeLookup: readable<NodeLookup<Node>>(nodeLookup), nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup), edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<InternalNode[]>([]),
edges: createEdgesStore(edges, connectionLookup, edgeLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
visibleEdges: readable<EdgeLayouted[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
connectionLookup: readable<ConnectionLookup>(connectionLookup), connectionLookup: readable<ConnectionLookup>(connectionLookup),
+6 -5
View File
@@ -16,7 +16,7 @@ import {
type NodeLookup type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
// we need to sync the user nodes and the internal nodes so that the user can receive the updates // we need to sync the user nodes and the internal nodes so that the user can receive the updates
// made by Svelte Flow (like dragging or selecting a node). // made by Svelte Flow (like dragging or selecting a node).
@@ -127,7 +127,7 @@ export type NodeStoreOptions = {
// The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: NodeLookup<Node> nodeLookup: NodeLookup<InternalNode>
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -141,12 +141,13 @@ export const createNodesStore = (
let elevateNodesOnSelect = true; let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
const nextNodes = adoptUserNodes(nds, nodeLookup, { adoptUserNodes(nds, nodeLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
defaults defaults,
checkEquality: false
}); });
value = nextNodes; value = nds;
set(value); set(value);
+11 -6
View File
@@ -1,18 +1,23 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { getNodesInside, type Transform } from '@xyflow/system'; import { getNodesInside, type Transform } from '@xyflow/system';
import type { Node } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
export function getVisibleNodes(store: SvelteFlowStoreState) { export function getVisibleNodes(store: SvelteFlowStoreState) {
return derived( return derived(
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport], [
([nodes, onlyRenderVisibleElements, width, height, viewport]) => { store.nodeLookup,
store.onlyRenderVisibleElements,
store.width,
store.height,
store.viewport,
store.nodes
],
([nodeLookup, onlyRenderVisibleElements, width, height, viewport]) => {
const transform: Transform = [viewport.x, viewport.y, viewport.zoom]; const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
return onlyRenderVisibleElements return onlyRenderVisibleElements
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true) ? getNodesInside(nodeLookup, { x: 0, y: 0, width, height }, transform, true)
: nodes; : Array.from(nodeLookup.values());
} }
); );
} }
+9 -1
View File
@@ -1,5 +1,13 @@
import type { ComponentType, SvelteComponent } from 'svelte'; import type { ComponentType, SvelteComponent } from 'svelte';
import type { NodeBase, NodeProps as NodePropsBase } from '@xyflow/system'; import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
/**
* The node data structure that gets used for internal nodes.
* There are some data structures added under node.internal
* that are needed for tracking some properties
* @public
*/
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
/** /**
* The node data structure that gets used for the nodes prop. * The node data structure that gets used for the nodes prop.
+2 -2
View File
@@ -97,8 +97,8 @@ function toHandleBounds(handles?: NodeHandle[]) {
} }
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] { function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0); const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0); const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
switch (position) { switch (position) {
+3 -2
View File
@@ -230,14 +230,14 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes = false, excludeNonSelectableNodes = false,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): NodeType[] => { ): InternalNodeBase<NodeType>[] => {
const paneRect = { const paneRect = {
...pointToRendererPoint(rect, [tx, ty, tScale]), ...pointToRendererPoint(rect, [tx, ty, tScale]),
width: rect.width / tScale, width: rect.width / tScale,
height: rect.height / tScale, height: rect.height / tScale,
}; };
const visibleNodes: NodeType[] = []; const visibleNodes: InternalNodeBase<NodeType>[] = [];
for (const [, node] of nodeLookup) { for (const [, node] of nodeLookup) {
const { measured, selectable = true, hidden = false } = node; const { measured, selectable = true, hidden = false } = node;
@@ -290,6 +290,7 @@ export function fitView<Params extends FitViewParamsBase<NodeBase>, Options exte
nodeLookup.forEach((n) => { nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
// TODO: this remove options.nodes.some with a Set
if ( if (
isVisible && isVisible &&
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id))) (!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
+13 -6
View File
@@ -14,6 +14,8 @@ import {
NodeChange, NodeChange,
NodeLookup, NodeLookup,
Rect, Rect,
NodeDimensionChange,
NodePositionChange,
} from '../types'; } from '../types';
import { getDimensions, getHandleBounds } from './dom'; import { getDimensions, getHandleBounds } from './dom';
import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
@@ -68,6 +70,7 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
elevateNodesOnSelect?: boolean; elevateNodesOnSelect?: boolean;
defaults?: Partial<NodeType>; defaults?: Partial<NodeType>;
checkEquality?: boolean;
}; };
export function adoptUserNodes<NodeType extends NodeBase>( export function adoptUserNodes<NodeType extends NodeBase>(
@@ -77,6 +80,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
nodeOrigin: [0, 0] as NodeOrigin, nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
defaults: {}, defaults: {},
checkEquality: true,
} }
) { ) {
const tmpLookup = new Map(nodeLookup); const tmpLookup = new Map(nodeLookup);
@@ -91,7 +95,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
parentNodeIds.add(userNode.parentId); parentNodeIds.add(userNode.parentId);
} }
if (userNode === currentStoreNode?.internals.userNode) { if (options.checkEquality && userNode === currentStoreNode?.internals.userNode) {
nodeLookup.set(userNode.id, currentStoreNode); nodeLookup.set(userNode.id, currentStoreNode);
} else { } else {
nodeLookup.set(userNode.id, { nodeLookup.set(userNode.id, {
@@ -103,7 +107,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
}, },
internals: { internals: {
positionAbsolute: userNode.position, positionAbsolute: userNode.position,
handleBounds: currentStoreNode?.internals?.handleBounds, handleBounds: currentStoreNode?.internals.handleBounds,
z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0),
userNode, userNode,
isParent: false, isParent: false,
@@ -142,8 +146,11 @@ function calculateXYZPosition<NodeType extends NodeBase>(
); );
} }
export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] { export function handleParentExpand(
const changes: NodeChange[] = []; nodes: InternalNodeBase[],
nodeLookup: NodeLookup
): (NodeDimensionChange | NodePositionChange)[] {
const changes: (NodeDimensionChange | NodePositionChange)[] = [];
const chilNodeRects = new Map<string, Rect>(); const chilNodeRects = new Map<string, Rect>();
nodes.forEach((node) => { nodes.forEach((node) => {
@@ -211,14 +218,14 @@ export function updateNodeDimensions<NodeType extends InternalNodeBase>(
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null, domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin nodeOrigin?: NodeOrigin
): NodeChange[] { ): (NodeDimensionChange | NodePositionChange)[] {
const viewportNode = domNode?.querySelector('.xyflow__viewport'); const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) { if (!viewportNode) {
return []; return [];
} }
const changes: NodeChange[] = []; const changes: (NodeDimensionChange | NodePositionChange)[] = [];
const style = window.getComputedStyle(viewportNode); const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
// in this array we collect nodes, that might trigger changes (like expanding parent) // in this array we collect nodes, that might trigger changes (like expanding parent)
+2 -2
View File
@@ -55,8 +55,8 @@ export function getDragItems<NodeType extends NodeBase>(
id: internalNode.id, id: internalNode.id,
position: internalNode.position || { x: 0, y: 0 }, position: internalNode.position || { x: 0, y: 0 },
distance: { distance: {
x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0), x: mousePos.x - internalNode.internals.positionAbsolute.x,
y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0), y: mousePos.y - internalNode.internals.positionAbsolute.y,
}, },
extent: internalNode.extent, extent: internalNode.extent,
parentId: internalNode.parentId, parentId: internalNode.parentId,
+2 -2
View File
@@ -22,8 +22,8 @@ export function getHandles(
id: h.id || null, id: h.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2, x: node.internals.positionAbsolute.x + h.x + h.width / 2,
y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2, y: node.internals.positionAbsolute.y + h.y + h.height / 2,
}); });
} }
return res; return res;